Skip to content

Commit fd51bda

Browse files
committed
Update YOURLS-diff_CreatePackage.py
1 parent a72e1e3 commit fd51bda

File tree

1 file changed

+146
-75
lines changed

1 file changed

+146
-75
lines changed

YOURLS-diff_CreatePackage.py

Lines changed: 146 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@
1414
python YOURLS-diff_CreatePackage.py --old 1.8.10
1515
1616
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
2225
2326
Author: Gioxx
2427
Repo: https://github.com/gioxx/YOURLS-diff
@@ -116,15 +119,6 @@ def write_manifest(changed_files, new_root, manifest_path):
116119
mf.write(rel + "\n")
117120
print(f"→ Manifest saved to {manifest_path}")
118121

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-
128122
def create_diff_zip(changed_files, new_root, zip_output):
129123
"""Create a ZIP archive containing only the changed files."""
130124
print(f"→ Creating package {zip_output}")
@@ -136,36 +130,28 @@ def create_diff_zip(changed_files, new_root, zip_output):
136130
count += 1
137131
print(f"→ Done. ZIP contains {count} file.")
138132

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+
"""
141138
script_filename = f"YOURLS-deploy-{old_tag}-to-{new_tag}.sh"
142-
temp_dir = "__deploy_temp"
143-
144139
lines = [
145140
"#!/bin/bash",
146141
"",
147142
"# Deployment script generated by YOURLS-diff",
148143
"# Update the variables below before running.",
149144
"",
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-
"",
156145
"# Check if ssh is installed",
157146
"if ! command -v ssh >/dev/null 2>&1; then",
158147
" echo \"Error: ssh is not installed or not found in PATH.\"",
159148
" exit 1",
160149
"fi",
161150
"",
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}\"",
165152
"TARGET_DIR=\"/var/www/yourls\" # <-- Update this with your server's path",
166153
"REMOTE_USER=\"user\" # <-- Update with your SSH user",
167154
"REMOTE_HOST=\"yourserver.com\" # <-- Update with your server hostname or IP",
168-
f"TEMP_DIR=\"./{temp_dir}\"",
169155
"",
170156
"# Pass --dry-run as first argument to simulate the deploy",
171157
"DRYRUN=\"\"",
@@ -174,36 +160,100 @@ def generate_deploy_script(old_tag, new_tag, zip_name, manifest_name, removed_ma
174160
" echo \"Running in DRY-RUN mode. No files will be copied or deleted.\"",
175161
"fi",
176162
"",
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!\""
200163
]
201164

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+
202207
with open(script_filename, "w", encoding="utf-8") as f:
203208
f.write("\n".join(lines))
204209
os.chmod(script_filename, 0o755)
205210
print(f"→ Deployment script generated: {script_filename}")
206211

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+
207257
def main():
208258
parser = argparse.ArgumentParser(
209259
description="Prepare a ZIP package with differences between two YOURLS releases and an external manifest file."
@@ -214,16 +264,14 @@ def main():
214264
help="Tag of the target release (if omitted, 'latest' is used).")
215265
parser.add_argument("--output", default=None,
216266
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)")
227275
args = parser.parse_args()
228276

229277
# Determine SSL verification setting
@@ -247,7 +295,10 @@ def main():
247295

248296
# Determine output names
249297
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"
251302

252303
with tempfile.TemporaryDirectory() as tmp:
253304
old_zip = os.path.join(tmp, f"{old_tag}.zip")
@@ -258,16 +309,42 @@ def main():
258309
old_dir = extract_zip(old_zip, os.path.join(tmp, "old"))
259310
new_dir = extract_zip(new_zip, os.path.join(tmp, "new"))
260311

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-
267312
print("→ Comparing directories…")
268-
changed = collect_changed(old_dir, new_dir)
269313
removed = collect_removed(old_dir, new_dir)
270314

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)
271348
total_old = count_all_files(old_dir)
272349
total_new = count_all_files(new_dir)
273350

@@ -280,17 +357,12 @@ def main():
280357
print("No differences found. Exiting.")
281358
sys.exit(0)
282359

283-
# # Generate external manifest
284-
# manifest_path = os.path.join(os.getcwd(), manifest_name)
285-
# write_manifest(changed, new_dir, manifest_path)
286-
287360
# Generate external manifest
288361
manifest_path = os.path.join(os.getcwd(), manifest_name)
289362
write_manifest(changed, new_dir, manifest_path)
290363

291364
# Create .removed.txt if needed
292365
if removed:
293-
removed_manifest = os.path.splitext(zip_name)[0] + ".removed.txt"
294366
with open(removed_manifest, "w", encoding="utf-8") as rmf:
295367
for full in sorted(removed):
296368
rel = os.path.relpath(full, old_dir)
@@ -305,7 +377,6 @@ def main():
305377

306378
# Create a summary file if requested
307379
if args.summary:
308-
release_body_path = os.path.splitext(zip_name)[0] + ".summary.txt"
309380
with open(release_body_path, "w", encoding="utf-8") as rb:
310381
rb.write(f"# YOURLS Patch Summary (from {old_tag} version to {new_tag})\n\n")
311382
rb.write(f"Number of files in OLD: {total_old}\n")

0 commit comments

Comments
 (0)