aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/uploads.py
Commit message (Collapse)AuthorAgeFilesLines
* fix(node): index, serve and write files past Windows' MAX_PATHHEADmainChristophe Besson15 hours1-4/+9
| | | | | | | | | | | | | | | | | | | | | | | | | | | With the system's default setting (LongPathsEnabled off) every call given a plain path of 260 characters or more refuses it, so a deep library was partly invisible: the walk could not list such a directory and its files never reached the members. The node now hands each call that touches a group's disk the extended form (paths.long_path), and keeps the plain path in the index, the hash cache and the log. A directory is listed under its long form and its entries named from the plain one (indexer.walk_files, wire._walk_dirs). Covered: the walk, stats and hashing, reconcile and the watcher, the folder list members see, serving over WebRTC and QUIC, deletion, uploads and their recovery, folder creation and removal, audio tags and the cover beside a track, photos, and a show's episode numbering. ffmpeg extends a long path itself and keeps the plain one. long_path now normalises before prefixing (the extended form skips the system's normalisation) and starts at 248 characters, where creating a directory is already refused. test_long_paths.py runs with the setting off: 15 of its 17 tests fail without the change. Checked by hand in the installed app with files at 312 to 380 characters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): the reaper deletes only the .part files the node wroteChristophe Besson14 days1-1/+12
| | | | | | | | Any *.part older than a day in a writable root was deleted — a browser's download in progress in a shared folder included. Only names carrying the node's tag (name.<8 hex>.part) are reaped now (F-28). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(node): an upload never replaces a file, nor shares a part with anotherChristophe Besson14 days1-0/+20
| | | | | | | | | | | A name an upload in flight will take is reserved; each upload writes its own `name.<tag>.part`; the finished file is published by a hard link, which refuses an existing target, and takes the next free name if one appeared meanwhile — the last ack names it. Two members sending one name at once wrote one part and published it twice; a file copied in during an upload was replaced (F-09). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* style: ruff's own fixes, mechanically appliedChristophe Besson2026-09-191-1/+1
| | | | | | | | | | | | | | | | | | | | `ruff check .` had gone unrun long enough to report 568 errors, which is the same as having no linter: the next real finding would have been invisible in the noise. This is the 521 it fixes by itself, in 173 files, and nothing else — the 98 it cannot fix are the next commit. What actually changed: import sorting (225), imports nobody used (87, none of them a re-export — no `__init__.py` is touched, which was the one way this could have broken an import elsewhere), `datetime.timezone.utc` to `datetime.UTC` (69) and `asyncio.TimeoutError` to `TimeoutError` (18), both plain aliases on the 3.12 this project requires, `Optional[X]` to `X | None` (24), and f-strings with nothing to interpolate (19). Checked rather than assumed: every module in the three packages still imports, and the suite is 2893 passed — the same count, test for test, as the merge before it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(node): uploads outlive their connection, and their leftovers are reapedChristophe Besson2026-09-091-0/+190
Stage 8 of ~/next/improve-downloads.md, first half. Two defects that are the same defect seen from two sides. An upload's progress lived on the session, keyed by `rel_dir/filename`. A dropped connection threw it away and the client's next chunk was refused with `not_started`: an upload interrupted at 99% could only be started again from zero, on a link flaky enough to have interrupted it once. It now lives in the group context, keyed by member as well -- a shared directory means two people can be sending IMG_1234.jpg at the same moment and neither may inherit, or overwrite the position of, the other's. What the lost state left behind was a `.part` nothing would ever finish, delete or look at again. It is not an index entry, so it is invisible to every member and to the operator's own file list: one abandoned film is a gigabyte of their disk, kept for ever. That leak predates this branch. A `.part` is deleted only when **both** hold: no upload is writing it, and nothing has been written to it for 24 hours. Waiting costs disk; being wrong costs somebody their upload, and is not reversible -- so a read-only root is never walked (it cannot have received an upload), an unavailable one is never walked (an unmounted drive reporting "nothing found" is how a careless janitor deletes a library), and a file whose mtime is in the future is left alone (a clock that went backwards is not evidence). The reaper matches whole paths and the state records the path it is writing, rather than both sides rebuilding one from a root name -- two implementations of one rule whose failure mode is deleting a live upload. The rules are in `uploads.py`, pure logic with no asyncio and no transport, the same shape as `transfers.py` and for the same reason. 23 cases, four of them checked against the unfixed source. Node suite 1195 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST