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.
114 lines
4.2 KiB
Python
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
|