better trash; no favicon

This commit is contained in:
Ludwig Lehnert
2026-08-19 10:07:25 +00:00
parent 1c4e07c713
commit d618957b68
14 changed files with 263 additions and 54 deletions
+53 -35
View File
@@ -27,6 +27,10 @@ DEFAULT_BACKUP_STATUS_FILE = "/state/backup-status.json"
DEFAULT_ARCHIVE_TEMP_DIR = "/tmp"
GROUPS_SOURCE_PATH = "/data/groups"
GROUP_ARCHIVE_CATEGORIES = ("data", "archive")
TRASH_DIRECTORY = ".trash"
TRASH_EXCLUDED_DESTINATIONS = frozenset(
{"data/private", "data/groups", "data/fslogix"}
)
DEFAULT_PROGRESS_MODE = "auto"
DEFAULT_PROGRESS_INTERVAL_SECONDS = 10
PROGRESS_BAR_WIDTH = 28
@@ -422,17 +426,23 @@ def parse_data_size(raw_value: str) -> Optional[int]:
return int(round(normalize_size_number(match.group("value")) * multiplier))
def measure_path_bytes(path: str) -> int:
def measure_path_bytes(path: str, *, exclude_root_trash: bool = False) -> int:
total = 0
stack = [path]
stack = [(path, True)]
while stack:
current = stack.pop()
current, is_root = stack.pop()
try:
with os.scandir(current) as entries:
for entry in entries:
try:
if (
exclude_root_trash
and is_root
and entry.name == TRASH_DIRECTORY
):
continue
if entry.is_dir(follow_symlinks=False):
stack.append(entry.path)
stack.append((entry.path, False))
elif entry.is_file(follow_symlinks=False):
total += entry.stat(follow_symlinks=False).st_size
except OSError as exc:
@@ -445,8 +455,11 @@ def measure_path_bytes(path: str) -> int:
def measure_backup_payload(sources: List[Tuple[str, str]]) -> Tuple[Dict[str, int], int]:
source_sizes: Dict[str, int] = {}
total = 0
for source_path, _destination_path in sources:
size = measure_path_bytes(source_path)
for source_path, destination_path in sources:
size = measure_path_bytes(
source_path,
exclude_root_trash=destination_path in TRASH_EXCLUDED_DESTINATIONS,
)
source_sizes[source_path] = size
total += size
return source_sizes, total
@@ -1082,6 +1095,9 @@ def prepare_group_archives(
key=lambda entry: entry.name.casefold(),
)
for entry in members:
if entry.name == TRASH_DIRECTORY:
log(f"Excluding retained trash from backup source {category}")
continue
target = os.path.join(target_category, entry.name)
if entry.is_dir(follow_symlinks=False) and not entry.is_symlink():
relative_name = f"{category}/{entry.name}"
@@ -1407,25 +1423,25 @@ class RcloneBackend:
interval_seconds=progress_interval_seconds,
interactive=interactive_progress,
)
run_streaming_command(
[
"rclone",
"sync",
f"{source_path}/",
f"backup:{remote_path}",
"--create-empty-src-dirs",
"--progress",
"--stats",
f"{progress_interval_seconds}s",
"--transfers",
str(MAX_PARALLEL_FILE_UPLOADS),
"--log-level",
"INFO",
"--config",
self.config_path,
],
progress=progress,
)
command = [
"rclone",
"sync",
f"{source_path}/",
f"backup:{remote_path}",
"--create-empty-src-dirs",
"--progress",
"--stats",
f"{progress_interval_seconds}s",
"--transfers",
str(MAX_PARALLEL_FILE_UPLOADS),
"--log-level",
"INFO",
"--config",
self.config_path,
]
if destination_path in TRASH_EXCLUDED_DESTINATIONS:
command.extend(["--exclude", f"/{TRASH_DIRECTORY}/**"])
run_streaming_command(command, progress=progress)
def write_marker(self, snapshot_name: str) -> None:
marker_path = join_path(self._snapshot_root(snapshot_name), ".backup_complete")
@@ -1532,17 +1548,19 @@ class RsyncBackend:
interval_seconds=progress_interval_seconds,
interactive=interactive_progress,
)
command = [
"rsync",
"-a",
"--delete",
"--mkpath",
"--progress",
"--outbuf=L",
]
if destination_path in TRASH_EXCLUDED_DESTINATIONS:
command.append(f"--exclude=/{TRASH_DIRECTORY}/")
command.extend([f"{source_path}/", f"{target}/"])
run_streaming_command(
[
"rsync",
"-a",
"--delete",
"--mkpath",
"--progress",
"--outbuf=L",
f"{source_path}/",
f"{target}/",
],
command,
env=self.command_env,
progress=progress,
)
+13 -2
View File
@@ -15,6 +15,13 @@ DEFAULT_RETENTION_DAYS = 7
VERSION_PREFIX_RE = re.compile(r"^Copy #\d+ of ", re.IGNORECASE)
def excluded_filename(filename: str) -> bool:
"""Return whether a transient file must never be retained."""
unversioned = VERSION_PREFIX_RE.sub("", filename, count=1)
folded = unversioned.casefold()
return folded.endswith(".tmp") or folded.startswith("~$")
def retention_days() -> int:
try:
value = int(os.getenv("TRASH_RETENTION_DAYS", str(DEFAULT_RETENTION_DAYS)))
@@ -147,6 +154,8 @@ def _resolved_item(item_id: str) -> Tuple[str, str, str, List[str], os.stat_resu
share_root = share_roots()[share]
repository = trash_root(share_root)
parts = _relative_parts(relative_path)
if excluded_filename(parts[-1]):
raise FileNotFoundError("Papierkorbeintrag ist ausgeschlossen")
parent_fd = _open_directory_chain(repository, parts[:-1])
try:
info = os.stat(parts[-1], dir_fd=parent_fd, follow_symlinks=False)
@@ -217,6 +226,8 @@ def list_items(
continue
subdirectories[:] = safe_subdirectories
for filename in filenames:
if excluded_filename(filename):
continue
candidate = os.path.join(directory, filename)
try:
info = os.lstat(candidate)
@@ -370,7 +381,7 @@ def cleanup_expired(now: Optional[dt.datetime] = None) -> Dict[str, int]:
candidate = os.path.join(directory, filename)
try:
info = os.lstat(candidate)
if info.st_mtime >= cutoff:
if info.st_mtime >= cutoff and not excluded_filename(filename):
continue
if not (
stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode)
@@ -405,7 +416,7 @@ def main() -> int:
parser.error("--cleanup is required")
result = cleanup_expired()
print(
f"[trash] Removed {result['removed']} expired item(s) "
f"[trash] Removed {result['removed']} expired or excluded item(s) "
f"({result['removedBytes']} bytes)",
flush=True,
)
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#205c46"/><path fill="#fff" d="M15 16h35v8H25v8h21v8H25v16H15zm31 28h8v8h-8z"/></svg>

Before

Width:  |  Height:  |  Size: 189 B

-1
View File
@@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>Dateiserver-Verwaltung</title>
<link rel="icon" href="/favicon.svg">
<link rel="stylesheet" href="/assets/styles.css">
</head>
<body>
-1
View File
@@ -1221,7 +1221,6 @@ class Handler(BaseHTTPRequestHandler):
"/assets/vendor/typst/0.6.0-csp1/compiler.wasm": ("vendor/typst/compiler.wasm", "application/wasm"),
"/assets/vendor/typst/0.6.0-csp1/LibertinusSerif-Regular.otf": ("vendor/typst/LibertinusSerif-Regular.otf", "font/otf"),
"/assets/vendor/typst/0.6.0-csp1/LibertinusSerif-Semibold.otf": ("vendor/typst/LibertinusSerif-Semibold.otf", "font/otf"),
"/favicon.svg": ("favicon.svg", "image/svg+xml"),
}
if path in files:
filename, content_type = files[path]