better trash; no favicon
This commit is contained in:
@@ -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
@@ -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 +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 |
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user