14
14
python YOURLS-diff_CreatePackage.py --old 1.8.10
15
15
16
16
Options:
17
- --old Tag of the starting release (required, e.g. 1.8.10)
18
- --new Tag of the target release (default: latest)
19
- --output Output ZIP filename (default: YOURLS-update-OLD-to-NEW.zip)
20
- --no-verify Disable SSL certificate verification (not recommended)
21
- --summary Generate a summary text file with patch details (e.g. for use in release notes)
17
+ --old Tag of the starting release (required, e.g. 1.8.10)
18
+ --new Tag of the target release (default: latest)
19
+ --output Output ZIP filename (default: YOURLS-update-OLD-to-NEW.zip)
20
+ --no-verify Disable SSL certificate verification (not recommended)
21
+ --summary Generate a summary text file with patch details (e.g. for use in release notes)
22
+ --only-removed Only generate the .removed.txt file (if any). Skip all other outputs. Also generates a deployment script to remove the files from the server.
23
+ --winscp Generate a .winscp.txt script to download and delete the removed files (requires --only-removed)
24
+ --help Show this help message and exit
22
25
23
26
Author: Gioxx
24
27
Repo: https://github.com/gioxx/YOURLS-diff
@@ -116,15 +119,6 @@ def write_manifest(changed_files, new_root, manifest_path):
116
119
mf .write (rel + "\n " )
117
120
print (f"→ Manifest saved to { manifest_path } " )
118
121
119
- # def create_diff_zip(changed_files, new_root, zip_output):
120
- # """Create a ZIP archive containing only the changed files."""
121
- # print(f"→ Creating package {zip_output}")
122
- # with zipfile.ZipFile(zip_output, "w", compression=zipfile.ZIP_DEFLATED) as z:
123
- # for full_path in changed_files:
124
- # rel = os.path.relpath(full_path, new_root)
125
- # z.write(full_path, rel)
126
- # print("→ Done.")
127
-
128
122
def create_diff_zip (changed_files , new_root , zip_output ):
129
123
"""Create a ZIP archive containing only the changed files."""
130
124
print (f"→ Creating package { zip_output } " )
@@ -136,36 +130,28 @@ def create_diff_zip(changed_files, new_root, zip_output):
136
130
count += 1
137
131
print (f"→ Done. ZIP contains { count } file." )
138
132
139
- def generate_deploy_script (old_tag , new_tag , zip_name , manifest_name , removed_manifest_name = None ):
140
- """Generate a Bash deployment script to upload changed files using rsync."""
133
+ def generate_deploy_script (old_tag , new_tag , zip_name , manifest_name ,
134
+ removed_manifest_name = None , only_removed = False ):
135
+ """
136
+ Generate a Bash deployment script. If only_removed is True, include only the file removal logic.
137
+ """
141
138
script_filename = f"YOURLS-deploy-{ old_tag } -to-{ new_tag } .sh"
142
- temp_dir = "__deploy_temp"
143
-
144
139
lines = [
145
140
"#!/bin/bash" ,
146
141
"" ,
147
142
"# Deployment script generated by YOURLS-diff" ,
148
143
"# Update the variables below before running." ,
149
144
"" ,
150
- "# Check if rsync is installed" ,
151
- "if ! command -v rsync >/dev/null 2>&1; then" ,
152
- " echo \" Error: rsync is not installed or not found in PATH.\" " ,
153
- " exit 1" ,
154
- "fi" ,
155
- "" ,
156
145
"# Check if ssh is installed" ,
157
146
"if ! command -v ssh >/dev/null 2>&1; then" ,
158
147
" echo \" Error: ssh is not installed or not found in PATH.\" " ,
159
148
" exit 1" ,
160
149
"fi" ,
161
150
"" ,
162
- f"MANIFEST=\" { manifest_name } \" " ,
163
- f"REMOVED_MANIFEST=\" { removed_manifest_name } \" " if removed_manifest_name else "# REMOVED_MANIFEST=\" \" " ,
164
- f"ZIP_FILE=\" { zip_name } \" " ,
151
+ f"REMOVED_MANIFEST=\" { removed_manifest_name } \" " ,
165
152
"TARGET_DIR=\" /var/www/yourls\" # <-- Update this with your server's path" ,
166
153
"REMOTE_USER=\" user\" # <-- Update with your SSH user" ,
167
154
"REMOTE_HOST=\" yourserver.com\" # <-- Update with your server hostname or IP" ,
168
- f"TEMP_DIR=\" ./{ temp_dir } \" " ,
169
155
"" ,
170
156
"# Pass --dry-run as first argument to simulate the deploy" ,
171
157
"DRYRUN=\" \" " ,
@@ -174,36 +160,100 @@ def generate_deploy_script(old_tag, new_tag, zip_name, manifest_name, removed_ma
174
160
" echo \" Running in DRY-RUN mode. No files will be copied or deleted.\" " ,
175
161
"fi" ,
176
162
"" ,
177
- "# Clean and unzip the patch" ,
178
- "rm -rf \" $TEMP_DIR\" " ,
179
- "mkdir -p \" $TEMP_DIR\" " ,
180
- "unzip -q \" $ZIP_FILE\" -d \" $TEMP_DIR\" " ,
181
- "echo \" → Files extracted into $TEMP_DIR\" " ,
182
- "" ,
183
- "# Upload changed/added files" ,
184
- "echo \" → Uploading changed files...\" " ,
185
- "while IFS= read -r file; do" ,
186
- " rsync -avz $DRYRUN \" $TEMP_DIR/$file\" \" $REMOTE_USER@$REMOTE_HOST:$TARGET_DIR/$file\" " ,
187
- "done < \" $MANIFEST\" " ,
188
- "" ,
189
- "# Remove deleted files from remote (if any)" ,
190
- "if [[ -f \" $REMOVED_MANIFEST\" ]]; then" ,
191
- " echo \" → Removing obsolete files...\" " ,
192
- " while IFS= read -r file; do" ,
193
- " ssh \" $REMOTE_USER@$REMOTE_HOST\" \" rm -f '$TARGET_DIR/$file'\" " ,
194
- " done < \" $REMOVED_MANIFEST\" " ,
195
- "fi" ,
196
- "" ,
197
- "# Clean up temp directory" ,
198
- "rm -rf \" $TEMP_DIR\" " ,
199
- "echo \" Deployment completed!\" "
200
163
]
201
164
165
+ if only_removed :
166
+ lines += [
167
+ "# Remove deleted files from remote (if any)" ,
168
+ "if [[ -f \" $REMOVED_MANIFEST\" ]]; then" ,
169
+ " echo \" → Removing obsolete files...\" " ,
170
+ " while IFS= read -r file; do" ,
171
+ " ssh \" $REMOTE_USER@$REMOTE_HOST\" \" rm -f '$TARGET_DIR/$file'\" " ,
172
+ " done < \" $REMOVED_MANIFEST\" " ,
173
+ "fi" ,
174
+ ]
175
+ else :
176
+ lines += [
177
+ f"ZIP_FILE=\" { zip_name } \" " ,
178
+ f"MANIFEST=\" { manifest_name } \" " ,
179
+ "TEMP_DIR=\" ./__deploy_temp\" " ,
180
+ "" ,
181
+ "# Clean and unzip the patch" ,
182
+ "rm -rf \" $TEMP_DIR\" " ,
183
+ "mkdir -p \" $TEMP_DIR\" " ,
184
+ "unzip -q \" $ZIP_FILE\" -d \" $TEMP_DIR\" " ,
185
+ "echo \" → Files extracted into $TEMP_DIR\" " ,
186
+ "" ,
187
+ "# Upload changed/added files" ,
188
+ "echo \" → Uploading changed files...\" " ,
189
+ "while IFS= read -r file; do" ,
190
+ " rsync -avz $DRYRUN \" $TEMP_DIR/$file\" \" $REMOTE_USER@$REMOTE_HOST:$TARGET_DIR/$file\" " ,
191
+ "done < \" $MANIFEST\" " ,
192
+ "" ,
193
+ "# Remove deleted files" ,
194
+ "if [[ -f \" $REMOVED_MANIFEST\" ]]; then" ,
195
+ " echo \" → Removing obsolete files...\" " ,
196
+ " while IFS= read -r file; do" ,
197
+ " ssh \" $REMOTE_USER@$REMOTE_HOST\" \" rm -f '$TARGET_DIR/$file'\" " ,
198
+ " done < \" $REMOVED_MANIFEST\" " ,
199
+ "fi" ,
200
+ "" ,
201
+ "# Clean up" ,
202
+ "rm -rf \" $TEMP_DIR\" " ,
203
+ ]
204
+
205
+ lines += ["echo \" Deployment completed!\" " ]
206
+
202
207
with open (script_filename , "w" , encoding = "utf-8" ) as f :
203
208
f .write ("\n " .join (lines ))
204
209
os .chmod (script_filename , 0o755 )
205
210
print (f"→ Deployment script generated: { script_filename } " )
206
211
212
+ def generate_winscp_script (removed_manifest_path , remote_base_path , host , user ):
213
+ """
214
+ Generate a WinSCP script to download and delete files listed in the removed manifest,
215
+ preserving folder structure locally, under a 'removed_backup' directory near the Python script.
216
+ """
217
+ import pathlib
218
+
219
+ # Directory of this Python script
220
+ script_dir = os .path .dirname (os .path .abspath (__file__ ))
221
+ local_backup_dir = os .path .join (script_dir , "removed_backup" )
222
+
223
+ # Make sure the base local backup dir exists
224
+ os .makedirs (local_backup_dir , exist_ok = True )
225
+
226
+ # WinSCP script filename (next to manifest)
227
+ script_name = os .path .splitext (removed_manifest_path )[0 ] + ".winscp.txt"
228
+
229
+ # Load removed files
230
+ with open (removed_manifest_path , "r" , encoding = "utf-8" ) as f :
231
+ files = [line .strip () for line in f if line .strip ()]
232
+
233
+ # Generate WinSCP script
234
+ with open (script_name , "w" , encoding = "utf-8" ) as wsc :
235
+ wsc .write ("option batch on\n " )
236
+ wsc .write ("option confirm off\n " )
237
+ wsc .write (f"open sftp://{ user } @{ host } /\n " )
238
+ wsc .write (f"cd { remote_base_path } \n " )
239
+ wsc .write (f"lcd { local_backup_dir } \n " )
240
+
241
+ for rel_path in files :
242
+ unix_path = rel_path .replace ("\\ " , "/" )
243
+ local_path = os .path .join (local_backup_dir , rel_path )
244
+ os .makedirs (os .path .dirname (local_path ), exist_ok = True )
245
+ wsc .write (f"get \" { unix_path } \" \" { rel_path } \" \n " )
246
+
247
+ for rel_path in files :
248
+ unix_path = rel_path .replace ("\\ " , "/" )
249
+ wsc .write (f"rm \" { unix_path } \" \n " )
250
+
251
+ wsc .write ("close\n " )
252
+ wsc .write ("exit\n " )
253
+
254
+ print (f"→ WinSCP script generated: { script_name } " )
255
+ print (f"→ Backup folder prepared at: { local_backup_dir } " )
256
+
207
257
def main ():
208
258
parser = argparse .ArgumentParser (
209
259
description = "Prepare a ZIP package with differences between two YOURLS releases and an external manifest file."
@@ -214,16 +264,14 @@ def main():
214
264
help = "Tag of the target release (if omitted, 'latest' is used)." )
215
265
parser .add_argument ("--output" , default = None ,
216
266
help = "Output ZIP filename (default: YOURLS-update-OLD-to-NEW.zip)." )
217
- parser .add_argument (
218
- "--no-verify" ,
219
- action = "store_true" ,
220
- help = "Disable SSL certificate verification (not recommended)."
221
- )
222
- parser .add_argument (
223
- "--summary" ,
224
- action = "store_true" ,
225
- help = "Generate a summary text file with patch details (e.g. for use in release notes)."
226
- )
267
+ parser .add_argument ("--no-verify" ,action = "store_true" ,
268
+ help = "Disable SSL certificate verification (not recommended)." )
269
+ parser .add_argument ("--summary" ,action = "store_true" ,
270
+ help = "Generate a summary text file with patch details (e.g. for use in release notes)." )
271
+ parser .add_argument ("--only-removed" ,action = "store_true" ,
272
+ help = "Only generate the .removed.txt file (if any). Skip all other outputs. Also generates a deployment script to remove the files from the server." )
273
+ parser .add_argument ("--winscp" , action = "store_true" ,
274
+ help = "Generate a .winscp.txt script to download and delete the removed files (requires --only-removed)" )
227
275
args = parser .parse_args ()
228
276
229
277
# Determine SSL verification setting
@@ -247,7 +295,10 @@ def main():
247
295
248
296
# Determine output names
249
297
zip_name = args .output or f"YOURLS-update-{ old_tag } -to-{ new_tag } .zip"
250
- manifest_name = os .path .splitext (zip_name )[0 ] + ".txt"
298
+ base_name = os .path .splitext (zip_name )[0 ]
299
+ manifest_name = base_name + ".txt"
300
+ removed_manifest = base_name + ".removed.txt"
301
+ release_body_path = base_name + ".summary.txt"
251
302
252
303
with tempfile .TemporaryDirectory () as tmp :
253
304
old_zip = os .path .join (tmp , f"{ old_tag } .zip" )
@@ -258,16 +309,42 @@ def main():
258
309
old_dir = extract_zip (old_zip , os .path .join (tmp , "old" ))
259
310
new_dir = extract_zip (new_zip , os .path .join (tmp , "new" ))
260
311
261
- # print("→ Comparing directories…")
262
- # changed = collect_changed(old_dir, new_dir)
263
- # if not changed:
264
- # print("No differences found. Exiting.")
265
- # sys.exit(0)
266
-
267
312
print ("→ Comparing directories…" )
268
- changed = collect_changed (old_dir , new_dir )
269
313
removed = collect_removed (old_dir , new_dir )
270
314
315
+ if args .only_removed :
316
+ if removed :
317
+ with open (removed_manifest , "w" , encoding = "utf-8" ) as rmf :
318
+ for full in sorted (removed ):
319
+ rel = os .path .relpath (full , old_dir )
320
+ rmf .write (rel + "\n " )
321
+ print (f"→ Removed files found, list saved to { removed_manifest } " )
322
+
323
+ # Always generate deploy.sh in --only-removed mode
324
+ generate_deploy_script (
325
+ old_tag = old_tag ,
326
+ new_tag = new_tag ,
327
+ zip_name = zip_name ,
328
+ manifest_name = manifest_name ,
329
+ removed_manifest_name = removed_manifest ,
330
+ only_removed = True
331
+ )
332
+ print ("→ You can use the generated script to remove the files from the server." )
333
+
334
+ if args .winscp :
335
+ generate_winscp_script (
336
+ removed_manifest_path = removed_manifest ,
337
+ remote_base_path = "/var/www/yourls" ,
338
+ host = "yourserver.com" ,
339
+ user = "youruser"
340
+ )
341
+
342
+ sys .exit (0 )
343
+ else :
344
+ print ("→ No files to remove from OLD to NEW. Exiting." )
345
+ sys .exit (0 )
346
+
347
+ changed = collect_changed (old_dir , new_dir )
271
348
total_old = count_all_files (old_dir )
272
349
total_new = count_all_files (new_dir )
273
350
@@ -280,17 +357,12 @@ def main():
280
357
print ("No differences found. Exiting." )
281
358
sys .exit (0 )
282
359
283
- # # Generate external manifest
284
- # manifest_path = os.path.join(os.getcwd(), manifest_name)
285
- # write_manifest(changed, new_dir, manifest_path)
286
-
287
360
# Generate external manifest
288
361
manifest_path = os .path .join (os .getcwd (), manifest_name )
289
362
write_manifest (changed , new_dir , manifest_path )
290
363
291
364
# Create .removed.txt if needed
292
365
if removed :
293
- removed_manifest = os .path .splitext (zip_name )[0 ] + ".removed.txt"
294
366
with open (removed_manifest , "w" , encoding = "utf-8" ) as rmf :
295
367
for full in sorted (removed ):
296
368
rel = os .path .relpath (full , old_dir )
@@ -305,7 +377,6 @@ def main():
305
377
306
378
# Create a summary file if requested
307
379
if args .summary :
308
- release_body_path = os .path .splitext (zip_name )[0 ] + ".summary.txt"
309
380
with open (release_body_path , "w" , encoding = "utf-8" ) as rb :
310
381
rb .write (f"# YOURLS Patch Summary (from { old_tag } version to { new_tag } )\n \n " )
311
382
rb .write (f"Number of files in OLD: { total_old } \n " )
0 commit comments