diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 12db786f5..21104028f 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -62,9 +62,15 @@ const handleRead = async (path: string) => { // this is binary data. res.headers.set("Content-Type", "application/octet-stream"); + const stat = await fs.stat(path); + // Add the file's size as the Content-Length header. - const fileSize = (await fs.stat(path)).size; + const fileSize = stat.size; res.headers.set("Content-Length", `${fileSize}`); + + // Add the file's last modified time (as epoch milliseconds). + const mtimeMs = stat.mtimeMs; + res.headers.set("X-Last-Modified-Ms", `${mtimeMs}`); } return res; } catch (e) { diff --git a/web/apps/photos/src/services/exif.ts b/web/apps/photos/src/services/exif.ts index 0ceab6d28..56c20a1c2 100644 --- a/web/apps/photos/src/services/exif.ts +++ b/web/apps/photos/src/services/exif.ts @@ -34,7 +34,7 @@ type RawEXIFData = Record & ImageHeight: number; }>; -const EXIF_TAGS_NEEDED = [ +const exifTagsNeededForParsingImageMetadata = [ "DateTimeOriginal", "CreateDate", "ModifyDate", @@ -53,31 +53,19 @@ const EXIF_TAGS_NEEDED = [ ]; /** - * Read EXIF data from an image and use that to construct and return an - * {@link ParsedExtractedMetadata}. This function is tailored for use when we - * upload files. + * Read EXIF data from an image {@link file} and use that to construct and + * return an {@link ParsedExtractedMetadata}. * - * @param fileOrData The image {@link File}, or its contents. + * This function is tailored for use when we upload files. */ export const parseImageMetadata = async ( - fileOrData: File | Uint8Array, + file: File, fileTypeInfo: FileTypeInfo, ): Promise => { - /* - if (!(receivedFile instanceof File)) { - receivedFile = new File( - [await receivedFile.blob()], - receivedFile.name, - { - lastModified: receivedFile.lastModified, - }, - ); - } - */ const exifData = await getParsedExifData( - fileOrData, + file, fileTypeInfo, - EXIF_TAGS_NEEDED, + exifTagsNeededForParsingImageMetadata, ); return { diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 6dc8d1da7..1fc0a190b 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -375,21 +375,32 @@ export const assetName = ({ */ const readFileOrPath = async ( fileOrPath: File | string, -): Promise<{ dataOrStream: Uint8Array | DataStream; fileSize: number }> => { +): Promise<{ + dataOrStream: Uint8Array | DataStream; + fileSize: number; + lastModifiedMs: number; +}> => { let dataOrStream: Uint8Array | DataStream; let fileSize: number; + let lastModifiedMs: number; if (fileOrPath instanceof File) { const file = fileOrPath; fileSize = file.size; + lastModifiedMs = file.lastModified; dataOrStream = fileSize > MULTIPART_PART_SIZE ? getFileStream(file, FILE_READER_CHUNK_SIZE) : new Uint8Array(await file.arrayBuffer()); } else { const path = fileOrPath; - const { response, size } = await readStream(ensureElectron(), path); + const { + response, + size, + lastModifiedMs: lm, + } = await readStream(ensureElectron(), path); fileSize = size; + lastModifiedMs = lm; if (size > MULTIPART_PART_SIZE) { const chunkCount = Math.ceil(size / FILE_READER_CHUNK_SIZE); dataOrStream = { stream: response.body, chunkCount }; @@ -398,7 +409,7 @@ const readFileOrPath = async ( } } - return { dataOrStream, fileSize }; + return { dataOrStream, fileSize, lastModifiedMs }; }; /** diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index a9a76b41b..c1033545b 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -19,18 +19,21 @@ import type { Electron } from "@/next/types/ipc"; * @param path The path on the file on the user's local filesystem whose * contents we want to stream. * - * @return A ({@link Response}, size) tuple. + * @return A ({@link Response}, size, lastModifiedMs) triple. * * * The response contains the contents of the file. In particular, the `body` * {@link ReadableStream} property of this response can be used to read the * files contents in a streaming manner. * * * The size is the size of the file that we'll be reading from disk. + * + * * The lastModifiedMs value is the last modified time of the file that we're + * reading, expressed as epoch milliseconds. */ export const readStream = async ( _: Electron, path: string, -): Promise<{ response: Response; size: number }> => { +): Promise<{ response: Response; size: number; lastModifiedMs: number }> => { const req = new Request(`stream://read${path}`, { method: "GET", }); @@ -41,13 +44,19 @@ export const readStream = async ( `Failed to read stream from ${path}: HTTP ${res.status}`, ); - const size = +res.headers["Content-Length"]; - if (isNaN(size)) - throw new Error( - `Got a numeric Content-Length when reading a stream. The response was ${res}`, - ); + const size = readNumericHeader(res, "Content-Length"); + const lastModifiedMs = readNumericHeader(res, "X-Last-Modified-Ms"); - return { response: res, size }; + return { response: res, size, lastModifiedMs }; +}; + +const readNumericHeader = (res: Response, key: string) => { + const value = +res.headers[key]; + if (isNaN(value)) + throw new Error( + `Expected a numeric ${key} when reading a stream response: ${res}`, + ); + return value; }; /**