Files
pxe-server/webapp/services/deploy.py
cproudlock c16a4f23b4 webapp: extract service layer (config.py + services/) from app.py
Phase 1a of a multi-session refactor toward a clean blueprint
structure. Pulls the helper code that lived alongside the routes in
the 1621-line app.py into focused modules. app.py is now 625 lines
of mostly routes plus a small Flask wiring header. Behaviour is
unchanged: smoke-tested against the 8 main GET routes (200 OK).

New modules:

- config.py            env vars + IMAGE_TYPES + FRIENDLY_NAMES +
                       SHARED_DEPLOY_* taxonomy + unattend XML
                       namespaces.
- services/audit.py    audit log file handler + audit() helper.
- services/csrf.py     session CSRF token + before_request validator
                       wired via init_csrf(app).
- services/fs.py       image_root / deploy_path / unattend_path /
                       control_path / tools_path + load_json /
                       save_json + resolve_destination.
- services/system.py   service_status / find_usb_mounts /
                       find_upload_sources.
- services/images.py   image_status + load_image_config.
- services/deploy.py   import_deploy + _merge_tree +
                       _replace_with_symlink + allowed_import_source.
- services/unattend.py parse_unattend / build_unattend_xml /
                       extract_form_data and the qn / qwcm / settings
                       pass helpers.
- services/wim.py      extract_startnet / update_startnet / list_files
                       wrapping wimextract / wimupdate / wimdir.

Endpoint names kept stable (dashboard, clonezilla_backups, etc.) so
existing url_for(...) calls in templates are unchanged. Phase 1b
(Flask blueprints with ".endpoint" naming) deferred to a future
session because it requires updating ~30 url_for sites in templates
and is mostly cosmetic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:25:32 -04:00

97 lines
3.3 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 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 _merge_tree(src, dst, move=False):
"""Recursively merge src tree into dst, overwriting existing files.
When move=True, files are moved instead of copied (saves disk space
on imports from the local upload-dir).
"""
_transfer = shutil.move if move else shutil.copy2
_transfer_tree = shutil.move if move else shutil.copytree
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
if os.path.isdir(d):
_merge_tree(s, d, move=move)
else:
if os.path.exists(d):
os.remove(d)
_transfer_tree(s, d)
else:
os.makedirs(os.path.dirname(d), exist_ok=True)
_transfer(s, d)
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