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>
This commit is contained in:
96
webapp/services/deploy.py
Normal file
96
webapp/services/deploy.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user