@@ -41,19 +41,32 @@ def backup_repository(self, repository):
41
41
repo_backup_dir = user_backup_dir / repository .name
42
42
repo_backup_dir .mkdir (exist_ok = True )
43
43
44
- # Generate timestamp for this backup
45
- timestamp = datetime .utcnow ().strftime ('%Y%m%d_%H%M%S ' )
46
- backup_name = f"{ repository .name } _{ timestamp } "
44
+ # Generate timestamp for this backup with microseconds for uniqueness
45
+ timestamp = datetime .utcnow ().strftime ('%Y%m%d_%H%M%S_%f ' )
46
+ backup_name = f"{ repository .name } _{ timestamp [: 19 ] } " # Keep readable format for backup name
47
47
48
48
# Create unique temporary directory and ensure it's clean
49
49
temp_clone_dir = repo_backup_dir / f"temp_{ timestamp } "
50
50
51
- # Remove temp directory if it already exists
52
- if temp_clone_dir .exists ():
51
+ # Ensure temp directory doesn't exist and create it
52
+ retry_count = 0
53
+ max_retries = 5
54
+ while temp_clone_dir .exists () and retry_count < max_retries :
53
55
logger .warning (f"Temp directory already exists, removing: { temp_clone_dir } " )
54
- shutil .rmtree (temp_clone_dir )
56
+ try :
57
+ shutil .rmtree (temp_clone_dir )
58
+ break
59
+ except (OSError , PermissionError ) as e :
60
+ retry_count += 1
61
+ if retry_count >= max_retries :
62
+ raise Exception (f"Unable to clean temp directory after { max_retries } attempts: { e } " )
63
+ # Add a small delay and try with a new timestamp
64
+ import time
65
+ time .sleep (0.1 )
66
+ timestamp = datetime .utcnow ().strftime ('%Y%m%d_%H%M%S_%f' )
67
+ temp_clone_dir = repo_backup_dir / f"temp_{ timestamp } "
55
68
56
- temp_clone_dir .mkdir (exist_ok = True )
69
+ temp_clone_dir .mkdir (parents = True , exist_ok = False )
57
70
58
71
self ._clone_repository (repository , temp_clone_dir )
59
72
@@ -84,6 +97,12 @@ def backup_repository(self, repository):
84
97
backup_job .status = 'failed'
85
98
backup_job .error_message = str (e )
86
99
backup_job .completed_at = datetime .utcnow ()
100
+
101
+ # Ensure we commit the failed status immediately
102
+ try :
103
+ db .session .commit ()
104
+ except Exception as commit_error :
105
+ logger .error (f"Failed to commit backup job failure status: { commit_error } " )
87
106
88
107
finally :
89
108
# Always clean up temporary directory
@@ -93,8 +112,30 @@ def backup_repository(self, repository):
93
112
shutil .rmtree (temp_clone_dir )
94
113
except Exception as cleanup_error :
95
114
logger .error (f"Failed to cleanup temp directory { temp_clone_dir } : { cleanup_error } " )
115
+ # Try force cleanup
116
+ try :
117
+ import stat
118
+ def handle_remove_readonly (func , path , exc ):
119
+ if exc [1 ].errno == 13 : # Permission denied
120
+ os .chmod (path , stat .S_IWRITE )
121
+ func (path )
122
+ else :
123
+ raise
124
+ shutil .rmtree (temp_clone_dir , onerror = handle_remove_readonly )
125
+ logger .info (f"Force cleaned temp directory: { temp_clone_dir } " )
126
+ except Exception as force_error :
127
+ logger .error (f"Could not force clean temp directory: { force_error } " )
96
128
97
- db .session .commit ()
129
+ # Final commit to ensure all changes are saved
130
+ try :
131
+ db .session .commit ()
132
+ except Exception as final_commit_error :
133
+ logger .error (f"Failed final commit for backup job: { final_commit_error } " )
134
+ # Try to rollback to prevent session issues
135
+ try :
136
+ db .session .rollback ()
137
+ except :
138
+ pass
98
139
99
140
def _clone_repository (self , repository , clone_dir ):
100
141
"""Clone a repository to the specified directory"""
@@ -109,28 +150,97 @@ def _clone_repository(self, repository, clone_dir):
109
150
# Clean up any existing temp directories for this repository first
110
151
self ._cleanup_temp_directories (clone_dir .parent )
111
152
153
+ # Ensure the clone directory is completely clean before starting
154
+ if clone_dir .exists ():
155
+ logger .warning (f"Clone directory exists before cloning, removing: { clone_dir } " )
156
+ try :
157
+ shutil .rmtree (clone_dir )
158
+ except Exception as e :
159
+ logger .error (f"Failed to remove existing clone directory: { e } " )
160
+ raise Exception (f"Cannot clean clone directory: { e } " )
161
+
162
+ # Recreate the directory to ensure it's empty
163
+ clone_dir .mkdir (parents = True , exist_ok = False )
164
+
112
165
# Clone the repository with error handling
113
166
try :
114
- git .Repo .clone_from (clone_url , clone_dir , depth = 1 )
115
- logger .info (f"Repository cloned to: { clone_dir } " )
116
- except git .GitCommandError as e :
117
- if "already exists and is not an empty directory" in str (e ):
118
- logger .warning (f"Directory exists, cleaning and retrying: { clone_dir } " )
119
- shutil .rmtree (clone_dir )
120
- clone_dir .mkdir (exist_ok = True )
121
- git .Repo .clone_from (clone_url , clone_dir , depth = 1 )
122
- else :
123
- raise e
167
+ # Use git command directly for better error handling
168
+ import subprocess
169
+ git_cmd = [
170
+ 'git' , 'clone' ,
171
+ '--depth=1' ,
172
+ '--verbose' ,
173
+ '--config' , 'core.autocrlf=false' , # Prevent line ending issues
174
+ '--config' , 'core.filemode=false' , # Prevent permission issues
175
+ clone_url ,
176
+ str (clone_dir )
177
+ ]
178
+
179
+ result = subprocess .run (
180
+ git_cmd ,
181
+ capture_output = True ,
182
+ text = True ,
183
+ timeout = 300 , # 5 minute timeout
184
+ cwd = str (clone_dir .parent )
185
+ )
186
+
187
+ if result .returncode != 0 :
188
+ error_msg = f"Git clone failed with exit code { result .returncode } \n "
189
+ error_msg += f"stdout: { result .stdout } \n "
190
+ error_msg += f"stderr: { result .stderr } "
191
+ logger .error (error_msg )
192
+ raise Exception (f"Git clone failed: { result .stderr } " )
193
+
194
+ logger .info (f"Repository cloned successfully to: { clone_dir } " )
195
+
196
+ except subprocess .TimeoutExpired :
197
+ logger .error (f"Git clone timed out for repository: { repository .url } " )
198
+ raise Exception ("Git clone operation timed out" )
199
+ except Exception as e :
200
+ logger .error (f"Git clone failed for { repository .url } : { str (e )} " )
201
+ # Clean up on failure
202
+ if clone_dir .exists ():
203
+ try :
204
+ shutil .rmtree (clone_dir )
205
+ except :
206
+ pass
207
+ raise e
124
208
125
209
def _cleanup_temp_directories (self , repo_backup_dir ):
126
210
"""Clean up old temporary directories that might be left behind"""
127
211
try :
212
+ if not repo_backup_dir .exists ():
213
+ return
214
+
128
215
temp_dirs = [d for d in repo_backup_dir .iterdir () if d .is_dir () and d .name .startswith ('temp_' )]
216
+ current_time = datetime .utcnow ().timestamp ()
217
+
129
218
for temp_dir in temp_dirs :
130
- # Remove temp directories older than 1 hour
131
- if datetime .utcnow ().timestamp () - temp_dir .stat ().st_mtime > 3600 :
132
- logger .info (f"Cleaning up old temp directory: { temp_dir } " )
133
- shutil .rmtree (temp_dir )
219
+ try :
220
+ # Remove temp directories older than 10 minutes or any that exist from failed jobs
221
+ dir_age = current_time - temp_dir .stat ().st_mtime
222
+ if dir_age > 600 : # 10 minutes
223
+ logger .info (f"Cleaning up old temp directory: { temp_dir } " )
224
+ shutil .rmtree (temp_dir )
225
+ elif not any (temp_dir .iterdir ()): # Empty directory
226
+ logger .info (f"Cleaning up empty temp directory: { temp_dir } " )
227
+ shutil .rmtree (temp_dir )
228
+ except (OSError , PermissionError ) as e :
229
+ logger .warning (f"Failed to remove temp directory { temp_dir } : { e } " )
230
+ # Try to force remove if it's a permission issue
231
+ try :
232
+ import stat
233
+ def handle_remove_readonly (func , path , exc ):
234
+ if exc [1 ].errno == 13 : # Permission denied
235
+ os .chmod (path , stat .S_IWRITE )
236
+ func (path )
237
+ else :
238
+ raise
239
+ shutil .rmtree (temp_dir , onerror = handle_remove_readonly )
240
+ logger .info (f"Force removed temp directory: { temp_dir } " )
241
+ except Exception as force_error :
242
+ logger .error (f"Could not force remove temp directory { temp_dir } : { force_error } " )
243
+
134
244
except Exception as e :
135
245
logger .warning (f"Failed to cleanup temp directories: { e } " )
136
246
0 commit comments