Names quak builds from server data fit in 255 bytes (closes #205)

The default names of `quak get`, `quak get-thumb`, `downloadFile` and
`downloadThumbnail`, and the album folders of `backup-metadata`, could be
over 255 bytes, which a file system refuses. `shortenKeepingExtension` in
`src/filename.ts` cuts a name short, keeping its extension when it fits,
and can put text right before the extension; the backup's `withID` now
calls it instead of doing the same itself. The `get` defaults use it with
`thumb_` counted. A `backup-metadata` folder keeps its `<id>-` and has the
name after it cut short. `safeExtension` gives `.bin` for an extension of
more than 16 letters and digits, so a save path's name always fits.

Model: opus-5-5
This commit is contained in:
2026-10-18 17:03:14 +00:00
parent 982b878d69
commit fd40714a6d
10 changed files with 290 additions and 27 deletions
+20 -4
View File
@@ -16,7 +16,13 @@ import {
streamTagFinal,
} from "../crypto/index.js";
import { TruncatedStreamError } from "../errors.js";
import { safeExtension, sanitizeFileName, withExtension } from "../filename.js";
import {
MAX_NAME_BYTES,
safeExtension,
sanitizeFileName,
shortenKeepingExtension,
withExtension,
} from "../filename.js";
import { withRetry } from "../retry.js";
import type { ApiClient } from "../api/client.js";
import type { EnteFile } from "../model/types.js";
@@ -583,9 +589,14 @@ export const downloadFile = async (
onProgress?: ProgressCallback,
): Promise<DownloadResult> => {
// `outPath` is the caller's and is used as is; the title is the server's
// and is sanitized so it can only name a file in the current directory.
// and is sanitized so it can only name a file in the current directory,
// and cut short to fit in a file name, keeping its extension.
const resolvedPath =
outPath ?? sanitizeFileName(file.metadata.title, `file-${file.id}`);
outPath ??
shortenKeepingExtension(
sanitizeFileName(file.metadata.title, `file-${file.id}`),
MAX_NAME_BYTES,
);
const header = fromBase64(file.file.decryptionHeader);
return fetchAndDecrypt(
api,
@@ -604,9 +615,14 @@ export const downloadThumbnail = async (
outPath?: string,
onProgress?: ProgressCallback,
): Promise<DownloadResult> => {
// As in `downloadFile`, with `thumb_` before the title.
const resolvedPath =
outPath ??
`thumb_${sanitizeFileName(file.metadata.title, `file-${file.id}`)}`;
"thumb_" +
shortenKeepingExtension(
sanitizeFileName(file.metadata.title, `file-${file.id}`),
MAX_NAME_BYTES - "thumb_".length,
);
const header = fromBase64(file.thumbnail.decryptionHeader);
return fetchAndDecrypt(
api,