A live photo's default names fit, and no cut name starts with a dot
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
`defaultFileName` in `src/filename.ts` now gives the name a file is saved under when the caller gives no path, and `originalName`, `thumbnailName`, `downloadFile` and `downloadThumbnail` all call it. For a live photo's image and video it puts the part's own extension in before cutting the name short, so those names fit too; a path from `--out` or `outPath` is kept as given. `shortenKeepingExtension` keeps the first character before the extension, cutting a long extension short to leave room for it, so a cut name no longer starts with a dot. The backup's links for such a title change to match. TODO.md names the open bug and describes what the code does. Model: opus-5-5
This commit is contained in:
+27
-26
@@ -16,13 +16,7 @@ import {
|
||||
streamTagFinal,
|
||||
} from "../crypto/index.js";
|
||||
import { TruncatedStreamError } from "../errors.js";
|
||||
import {
|
||||
MAX_NAME_BYTES,
|
||||
safeExtension,
|
||||
sanitizeFileName,
|
||||
shortenKeepingExtension,
|
||||
withExtension,
|
||||
} from "../filename.js";
|
||||
import { defaultFileName, safeExtension, withExtension } from "../filename.js";
|
||||
import { withRetry } from "../retry.js";
|
||||
import type { ApiClient } from "../api/client.js";
|
||||
import type { EnteFile } from "../model/types.js";
|
||||
@@ -371,10 +365,14 @@ const openPart = async (
|
||||
// name starts with `image` as the image and the one whose name starts with
|
||||
// `video` as the video, and refuses a ZIP holding a second of either. It is
|
||||
// written unpacked: each part is named `destination` with the extension
|
||||
// replaced by its own entry's, and the two must differ ignoring case. When the
|
||||
// file records a hash, `<imageHash>:<videoHash>` must match it, each over that
|
||||
// part's own bytes. Only then are the image, then the video, renamed into
|
||||
// place; on any failure neither is stored.
|
||||
// replaced by its own entry's, and the two must differ ignoring case. When
|
||||
// `defaultName` is set, `destination` is the name `defaultFileName` made from
|
||||
// the title rather than a path the caller gave, and each part is named as
|
||||
// `defaultFileName` names it with its own extension, so that the name is cut
|
||||
// short to fit with that extension. When the file records a hash,
|
||||
// `<imageHash>:<videoHash>` must match it, each over that part's own bytes.
|
||||
// Only then are the image, then the video, renamed into place; on any failure
|
||||
// neither is stored.
|
||||
//
|
||||
// The ZIP is chosen by its uploader and may expand enormously, so each part is
|
||||
// written as it decompresses and never held, and the ZIP is refused once the
|
||||
@@ -394,6 +392,7 @@ const decryptLivePhoto = async (
|
||||
key: Uint8Array,
|
||||
onProgress: ProgressCallback | undefined,
|
||||
file: EnteFile,
|
||||
defaultName: boolean,
|
||||
): Promise<DownloadResult> => {
|
||||
const sliceSize = 4096;
|
||||
const dir = dirname(destination);
|
||||
@@ -492,8 +491,12 @@ const decryptLivePhoto = async (
|
||||
`live photo's image and video have the same extension, ${video.ext}`,
|
||||
);
|
||||
}
|
||||
const path = withExtension(destination, image.ext);
|
||||
const videoPath = withExtension(destination, video.ext);
|
||||
const partPath = (ext: string): string =>
|
||||
defaultName
|
||||
? defaultFileName(file.metadata.title, file.id, { ext })
|
||||
: withExtension(destination, ext);
|
||||
const path = partPath(image.ext);
|
||||
const videoPath = partPath(video.ext);
|
||||
for (const part of parts) {
|
||||
await part.handle.sync();
|
||||
await part.handle.close();
|
||||
@@ -537,6 +540,9 @@ const decryptLivePhoto = async (
|
||||
// and then removes its own temp file, and only the attempt that reaches
|
||||
// TAG_FINAL renames one into place, so a download that needed three tries still
|
||||
// performs exactly one rename over the destination.
|
||||
//
|
||||
// `defaultName` says `destination` is the name `defaultFileName` made, not a
|
||||
// path the caller gave (see `decryptLivePhoto`).
|
||||
const fetchAndDecrypt = async (
|
||||
api: ApiClient,
|
||||
openStream: () => Promise<ReadableStream<Uint8Array>>,
|
||||
@@ -545,6 +551,7 @@ const fetchAndDecrypt = async (
|
||||
destination: string,
|
||||
onProgress?: ProgressCallback,
|
||||
original?: EnteFile,
|
||||
defaultName = false,
|
||||
): Promise<DownloadResult> =>
|
||||
withRetry(async () => {
|
||||
const stream = await openStream();
|
||||
@@ -557,6 +564,7 @@ const fetchAndDecrypt = async (
|
||||
key,
|
||||
onProgress,
|
||||
original,
|
||||
defaultName,
|
||||
);
|
||||
}
|
||||
const bytesWritten = await decryptToTemp(
|
||||
@@ -588,15 +596,11 @@ export const downloadFile = async (
|
||||
outPath?: string,
|
||||
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 cut short to fit in a file name, keeping its extension.
|
||||
// `outPath` is the caller's and is used as is. Without it, the file is
|
||||
// written in the current directory under the name `defaultFileName` makes
|
||||
// from the title, which is the server's.
|
||||
const resolvedPath =
|
||||
outPath ??
|
||||
shortenKeepingExtension(
|
||||
sanitizeFileName(file.metadata.title, `file-${file.id}`),
|
||||
MAX_NAME_BYTES,
|
||||
);
|
||||
outPath ?? defaultFileName(file.metadata.title, file.id);
|
||||
const header = fromBase64(file.file.decryptionHeader);
|
||||
return fetchAndDecrypt(
|
||||
api,
|
||||
@@ -606,6 +610,7 @@ export const downloadFile = async (
|
||||
resolvedPath,
|
||||
onProgress,
|
||||
file,
|
||||
outPath === undefined,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -618,11 +623,7 @@ export const downloadThumbnail = async (
|
||||
// As in `downloadFile`, with `thumb_` before the title.
|
||||
const resolvedPath =
|
||||
outPath ??
|
||||
"thumb_" +
|
||||
shortenKeepingExtension(
|
||||
sanitizeFileName(file.metadata.title, `file-${file.id}`),
|
||||
MAX_NAME_BYTES - "thumb_".length,
|
||||
);
|
||||
defaultFileName(file.metadata.title, file.id, { prefix: "thumb_" });
|
||||
const header = fromBase64(file.thumbnail.decryptionHeader);
|
||||
return fetchAndDecrypt(
|
||||
api,
|
||||
|
||||
Reference in New Issue
Block a user