check / check (push) Successful in 41s
A live photo, which Ente stores as one ZIP, is unpacked as it downloads into its image and its video, each `<fileID>.<ext>` with its extension from the ZIP, beside `<fileID>.livephoto.json`, which names the two. Both are checked against the recorded hash and renamed into place only when both are complete. A ZIP with a second image or video, or whose parts come to more than 20 times its size plus 16 MiB, is refused. The backup and the content cache count a live photo as stored only with both files, album folders link both, `quak get` writes both, and the content result gives the video as `videoPath`. A ZIP an earlier version stored is replaced. Model: opus-5-5
42 lines
1.9 KiB
TypeScript
42 lines
1.9 KiB
TypeScript
// File names built from server-supplied metadata.
|
|
//
|
|
// A file's title and a collection's name are decrypted from data the server
|
|
// hands us, and quak does not trust the server. Any name taken from them and
|
|
// used on disk goes through here, so it can only ever name one file inside the
|
|
// directory the caller chose: never a path, never `..`, never hidden, never a
|
|
// Windows device name.
|
|
//
|
|
// A path the user typed (`--out`, `outPath`) is not passed through here: the
|
|
// caller is trusted, the server is not.
|
|
|
|
import { extname } from "node:path";
|
|
|
|
// Path separators, characters Windows forbids in file names, and control
|
|
// characters (NUL included).
|
|
// eslint-disable-next-line no-control-regex
|
|
const UNSAFE_CHARACTERS = /[/\\:*?"<>|\x00-\x1f\x7f]/g;
|
|
|
|
// Names Windows reserves for devices, with or without an extension.
|
|
const RESERVED_DEVICE_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i;
|
|
|
|
// `name` made safe to use as a single file name. Each unsafe character becomes
|
|
// `_`, a leading run of dots becomes one `_`, and a device name gets a leading
|
|
// `_`. A name with none of these comes back unchanged. An empty name becomes
|
|
// `fallback`, which the caller derives from the record's ID.
|
|
export const sanitizeFileName = (name: string, fallback: string): string => {
|
|
if (name === "") return fallback;
|
|
const cleaned = name.replace(UNSAFE_CHARACTERS, "_").replace(/^\.+/, "_");
|
|
return RESERVED_DEVICE_NAME.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
};
|
|
|
|
// The extension of `title` (".jpg"), or ".bin" when it has none or it holds
|
|
// anything but letters and digits.
|
|
export const safeExtension = (title: string): string => {
|
|
const ext = extname(title);
|
|
return /^\.[A-Za-z0-9]+$/.test(ext) ? ext : ".bin";
|
|
};
|
|
|
|
// `name` with its extension, if it has one, replaced by `ext` (".mov").
|
|
export const withExtension = (name: string, ext: string): string =>
|
|
name.slice(0, name.length - extname(name).length) + ext;
|