testsAndMisc/python_pkg/wsg_grabber/files.py
Krzysztof kuhy Rudnicki 33f0fee342
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
wsg_grabber: keep to the cloud folder, cap trash at 200 passes
Kept videos land in ~/data/cloud/Media/<year>/wsg (~/Downloads without the
cloud folder); trash/ keeps only the newest 200 passes and marks the rest
purged. Legacy keep/ is migrated once. Undo follows a keep through every
place it can end up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8G3BNETGmbr6aM5cc18g4
2026-09-28 10:44:00 +02:00

81 lines
2.4 KiB
Python

"""The only code in this package that moves bytes around on disk.
Deliberately tiny, because everything interesting about a verdict was already
decided in :mod:`python_pkg.wsg_grabber.verdict`. The one ``unlink`` lives
here too, so that "what touches the disk" stays a one-module question; only
:mod:`python_pkg.wsg_grabber._prune` and the keep migration call it.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from python_pkg.wsg_grabber import logs
if TYPE_CHECKING:
from pathlib import Path
from python_pkg.wsg_grabber.models import FileMove
def existing_names(directory: Path) -> set[str]:
"""Return the filenames already present in *directory*.
Args:
directory: Directory to inspect; need not exist.
Returns:
set[str]: Names currently in the directory.
"""
if not directory.is_dir():
return set()
return {entry.name for entry in directory.iterdir()}
def apply_move(move: FileMove) -> bool:
"""Carry out a planned relocation.
Refuses to overwrite an existing destination. ``Path.replace`` clobbers
silently, so without this check a caller that forgot to pick a
collision-free name would destroy a video the user had already decided to
keep -- exactly the thing this package promises never to do. Callers pick
the name with :func:`verdict.unique_destination`; this is the backstop that
makes the guarantee structural rather than a convention.
Args:
move: Where the file is and where it should go.
Returns:
bool: True when the file was moved. False when the source had already
vanished, or when the destination is occupied -- the caller treats
either as "this file is not where I thought", never as success.
"""
if not move.src.exists():
return False
if move.dst.exists():
logs.error(
"files.destination_occupied",
src=str(move.src),
dst=str(move.dst),
)
return False
move.dst.parent.mkdir(parents=True, exist_ok=True)
move.src.replace(move.dst)
return True
def remove(path: Path) -> bool:
"""Delete a file for good.
Args:
path: File to delete.
Returns:
bool: True when a file was removed; False when nothing was there.
"""
try:
path.unlink()
except FileNotFoundError:
return False
return True