Sanitize file names taken from server metadata (closes #9)
check / check (push) Successful in 37s

A file title or album name decrypted from server data could name a path
outside the chosen directory (`../../.ssh/authorized_keys`). One module,
src/filename.ts, now makes such names safe for `quak get`/`get-thumb`
without `--out`, downloadFile/downloadThumbnail without outPath, and the
backup and metadata backup trees. Originals-cache extensions are limited to
letters and digits. A user-supplied path is still used as is. decryptFile
reads a missing or non-string title as "" and rejects metadata that is not
a JSON object.

Model: opus-5-5
This commit was merged in pull request #78.
This commit is contained in:
2026-09-23 02:04:31 +02:00
parent fe952d3e62
commit 3871d6228e
15 changed files with 381 additions and 44 deletions
+8 -11
View File
@@ -40,8 +40,9 @@ import {
symlinkSync,
writeFileSync,
} from "node:fs";
import { basename, dirname, extname, join, relative } from "node:path";
import { basename, dirname, join, relative } from "node:path";
import { safeExtension, sanitizeFileName } from "./filename.js";
import type { Collection, EnteFile } from "./model/types.js";
export type ProgressCallback = (message: string) => void;
@@ -108,16 +109,11 @@ interface FailureEntry {
const LEDGER_VERSION = 1;
const sanitizePath = (name: string): string =>
name.replace(/[/\\:*?"<>|]/g, "_").replace(/^\.+/, "_");
// The originals/ filename for a file: `<id><ext>`, the extension taken from the
// title (or `.bin`). Matches the content cache's own naming so a present check
// lines up with what a fetch would write.
const originalName = (file: EnteFile): string => {
const ext = extname(file.metadata.title || "") || ".bin";
return `${file.id}${ext}`;
};
const originalName = (file: EnteFile): string =>
`${file.id}${safeExtension(file.metadata.title)}`;
// A regular file with content is treated as complete. A zero-byte file is not:
// it is the shape an aborted write leaves and must be re-fetched.
@@ -370,7 +366,7 @@ export const runBackup = async (
// Then the per-collection symlink trees and JSON.
for (const c of collections) {
const colDirName = sanitizePath(c.name || `collection-${c.id}`);
const colDirName = sanitizeFileName(c.name, `collection-${c.id}`);
const colDir = join(collectionsDir, colDirName);
mkdirSync(colDir, { recursive: true });
@@ -381,8 +377,9 @@ export const runBackup = async (
if (!includeOriginals) continue;
const orig = join(originalsDir, originalName(file));
if (!isPresent(orig)) continue;
const linkName = sanitizePath(
file.metadata.title || `file-${file.id}`,
const linkName = sanitizeFileName(
file.metadata.title,
`file-${file.id}`,
);
const linkPath = join(colDir, linkName);
try {