mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-05 13:41:36 +02:00
@Carter3DP's report on 0.2.4b1: a file moved into an external (NAS)
folder showed up in Bambuddy under that folder but was never written
to the mount. Traced to move_files only updating file.folder_id in
the DB while leaving the bytes in library_files_dir/. Direct upload
to a writable external folder was already fixed in 0.2.4b1; the move
path was not.
Cross-boundary moves now physically relocate the bytes through a new
_move_file_bytes helper. Same-boundary moves (managed -> managed)
keep the existing DB-only fast path because a managed file's on-disk
location doesn't depend on which managed folder owns it.
Four flows:
- managed -> external: copy to <mount>/<filename>, set
is_external=True, store the absolute path,
unlink the managed source
- external -> managed: copy to internal storage with a fresh UUID
name, set is_external=False, store the
relative path, unlink the external source,
recompute file_hash (scan-tracked rows
carry file_hash=None)
- external -> external: same shape as managed -> external
- managed -> managed: DB-only
Copy-then-unlink ordering means a partial copy followed by a failed
unlink leaves both copies on disk rather than losing the source if
the target write fails halfway through on a flaky NAS mount. Failed
shutil.copy2 cleans up partial dest before raising.
Defence-in-depth skips:
- source on a read-only external mount (move = delete-on-source
which a RO mount can't fulfil)
- filename collision on the target mount
- traversal-style filenames after Path.resolve()
- missing source on disk
- os.access(W_OK) on the target mount
Each skip carries a structured {file_id, code, reason} entry in a new
skipped_reasons field on the response so the UI can surface "5 of 10
skipped: 3 collisions, 2 missing on disk" instead of a blank number.
The {moved, skipped} numeric counters are preserved so existing
frontend code keeps working.
6 new integration tests in test_external_folders_api.py::
TestCrossBoundaryMove covering: managed -> external relocates bytes
(the actual fix), external -> managed relocates bytes including hash
recompute, name collision skip with the pre-existing target file
intact, source-readonly skip, managed -> managed stays DB-only, and
skipped_reasons always present.