Files
pxe-server/webapp/services/deploy.py
cproudlock 0cb6b26c27 webapp import: checksum-aware sync (skip unchanged, update only changed)
Image import previously full-copied everything and rmtree'd existing target dirs on every run. Replaced the shutil copy/move/copytree/rmtree with deploy.sync_tree(), which shells out to rsync -a --checksum: files whose content already matches the target are skipped, only new or changed files are written, and existing target files not in the source are left untouched (merge, not mirror). move=True uses --remove-source-files (frees the SMB upload dir) and prunes emptied source dirs. Applies to the Deploy import, the _shared redirections, and the root-level items. Big re-imports now only rewrite what actually changed.
2026-07-23 10:45:58 -04:00

114 lines
4.2 KiB
Python

"""Image deploy import logic: copy/move from a USB or upload-dir source
into ``SAMBA_SHARE/<image_type>/Deploy/`` while merging shared subdirs
(``Out-of-box Drivers`` etc.) into ``SAMBA_SHARE/_shared/`` and replacing
the per-image copies with symlinks. This is what lets two image types
re-use the same multi-GB driver tree without doubling disk usage.
"""
import os
import shutil
import subprocess
import config
from services.system import find_usb_mounts
def _replace_with_symlink(link_path, target_path):
"""Replace a file/dir/symlink at link_path with a symlink to target_path."""
if os.path.islink(link_path):
os.remove(link_path)
elif os.path.isdir(link_path):
shutil.rmtree(link_path)
os.symlink(target_path, link_path)
def sync_tree(src, dst, move=False, checksum=True):
"""Copy src -> dst with rsync, SKIPPING files whose content already
matches and transferring only new or changed files.
src may be a file or a directory (its contents merge into dst). With
checksum=True rsync compares by hash (--checksum), so a re-import only
rewrites files that actually differ instead of replacing everything;
set checksum=False for the faster size+mtime comparison. move=True
removes source files after a successful transfer (frees the upload dir).
Existing dst files not present in src are left untouched (merge, not
mirror - no --delete)."""
flags = ["-a"] # recurse, preserve perms/times/symlinks
if checksum:
flags.append("--checksum") # compare by content hash, not size+mtime
if move:
flags.append("--remove-source-files")
if os.path.isdir(src):
os.makedirs(dst, exist_ok=True)
src_arg = src.rstrip("/") + "/" # trailing slash = merge CONTENTS into dst
else:
parent = os.path.dirname(dst)
if parent:
os.makedirs(parent, exist_ok=True)
src_arg = src
subprocess.run(["rsync", *flags, src_arg, dst], check=True)
if move and os.path.isdir(src):
# --remove-source-files empties files but leaves the dir skeleton; prune it
for root, _dirs, _files in os.walk(src, topdown=False):
try:
os.rmdir(root)
except OSError:
pass
def _merge_tree(src, dst, move=False):
"""Backwards-compatible alias: checksum-aware merge of src into dst."""
sync_tree(src, dst, move=move)
def import_deploy(src_deploy, dst_deploy, target="", move=False):
"""Import Deploy/ contents, redirecting shared subdirs into _shared/."""
scoped_shared = []
prefix_key = ""
for prefix, dirs in config.SHARED_DEPLOY_SCOPED.items():
if target.startswith(prefix):
scoped_shared = dirs
prefix_key = prefix
break
_transfer = shutil.move if move else shutil.copy2
_transfer_tree = shutil.move if move else shutil.copytree
os.makedirs(dst_deploy, exist_ok=True)
for item in os.listdir(src_deploy):
src_item = os.path.join(src_deploy, item)
dst_item = os.path.join(dst_deploy, item)
if not os.path.isdir(src_item):
_transfer(src_item, dst_item)
continue
if item in config.SHARED_DEPLOY_GLOBAL:
shared_dest = os.path.join(config.SHARED_DIR, item)
os.makedirs(shared_dest, exist_ok=True)
_merge_tree(src_item, shared_dest, move=move)
_replace_with_symlink(dst_item, shared_dest)
continue
if item in scoped_shared:
shared_dest = os.path.join(config.SHARED_DIR, f"{prefix_key}{item}")
os.makedirs(shared_dest, exist_ok=True)
_merge_tree(src_item, shared_dest, move=move)
_replace_with_symlink(dst_item, shared_dest)
continue
if os.path.isdir(dst_item):
_merge_tree(src_item, dst_item, move=move)
else:
_transfer_tree(src_item, dst_item)
def allowed_import_source(source):
"""True if source is a USB mount or under the upload dir."""
usb = find_usb_mounts()
if any(source == m or source.startswith(m + "/") for m in usb):
return True
if source == config.UPLOAD_DIR or source.startswith(config.UPLOAD_DIR + "/"):
return os.path.isdir(source)
return False