From 708109f5e411ab6b52645cd46cdc8cc0891a1099 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 15 Nov 2024 08:42:58 +0530 Subject: [PATCH 1/2] [web] Retain original's file creation time on edits --- .../PhotoViewer/ImageEditorOverlay/index.tsx | 11 ++++---- .../src/services/upload/uploadManager.ts | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx index de5afca30b..848bf2e462 100644 --- a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx @@ -475,15 +475,14 @@ const ImageEditorOverlay = (props: IProps) => { ); const editedFile = await getEditedFile(); - const file = { - uploadItem: editedFile, - localID: 1, - collectionID: props.file.collectionID, - }; uploadManager.prepareForNewUpload(); uploadManager.showUploadProgressDialog(); - uploadManager.uploadItems([file], [collection]); + uploadManager.uploadFile( + editedFile, + collection, + props.file.metadata.creationTime, + ); setFileURL(null); props.onClose(); props.closePhotoViewer(); diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 700dbc9738..1a4dcb3908 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -450,6 +450,33 @@ class UploadManager { return this.uiService.hasFilesInResultList(); } + /** + * Upload a single file to the given collection. + * + * @param file A web {@link File} object representing the file to upload. + * + * @param collection The {@link Collection} in which the file should be added. + * + * @param creationTime The timestamp (unix epoch microseconds) to use as the + * `creationTime` of the newly created {@link EnteFile}. + */ + public async uploadFile( + file: File, + collection: Collection, + creationTime: number, + ) { + const item = { + uploadItem: file, + localID: 1, + collectionID: collection.id, + }; + this.parsedMetadataJSONMap.set( + getMetadataJSONMapKeyForJSON(collection.id, file.name), + { creationTime }, + ); + return this.uploadItems([item], [collection]); + } + private abortIfCancelled = () => { if (uploadCancelService.isUploadCancelationRequested()) { throw Error(CustomError.UPLOAD_CANCELLED); From 057bd3a4d2eb00a632b615df1caee14f14ebb0ce Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 15 Nov 2024 10:21:12 +0530 Subject: [PATCH 2/2] Retain more info Also invent a new scheme for passing around this data instead of piggy backing on the JSON route, since the JSON route has other complications (e.g. it strips off the "-edited" prefix) that we'd anyways would've needed to workaround. --- .../PhotoViewer/ImageEditorOverlay/index.tsx | 6 +-- .../src/services/upload/upload-service.ts | 35 +++++++++++++-- .../src/services/upload/uploadManager.ts | 44 ++++++++++--------- web/packages/media/file.ts | 4 ++ 4 files changed, 61 insertions(+), 28 deletions(-) diff --git a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx index 848bf2e462..248976a57b 100644 --- a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay/index.tsx @@ -478,11 +478,7 @@ const ImageEditorOverlay = (props: IProps) => { uploadManager.prepareForNewUpload(); uploadManager.showUploadProgressDialog(); - uploadManager.uploadFile( - editedFile, - collection, - props.file.metadata.creationTime, - ); + uploadManager.uploadFile(editedFile, collection, props.file); setFileURL(null); props.onClose(); props.closePhotoViewer(); diff --git a/web/apps/photos/src/services/upload/upload-service.ts b/web/apps/photos/src/services/upload/upload-service.ts index 35a1937c60..c08a08ad60 100644 --- a/web/apps/photos/src/services/upload/upload-service.ts +++ b/web/apps/photos/src/services/upload/upload-service.ts @@ -197,10 +197,25 @@ export const uploadItemFileName = (uploadItem: UploadItem) => { /* -- Various intermediate type used during upload -- */ -interface UploadAsset { +export interface UploadAsset { + /** `true` if this is a live photo. */ isLivePhoto?: boolean; - uploadItem?: UploadItem; + /* Valid for live photos */ livePhotoAssets?: LivePhotoAssets; + /* Valid for non-live photos */ + uploadItem?: UploadItem; + /** + * Metadata we know about a file externally. Valid for non-live photos. + * + * This is metadata that is not present within the file, but we have + * available from external sources. There is also a parsed metadata we + * obtain from JSON files. So together with the metadata present within the + * file itself, there are three places where the file's initial metadata can + * be filled in from. + * + * This will not be present for live photos. + */ + externalParsedMetadata?: ParsedMetadata; } interface ThumbnailedFile { @@ -871,7 +886,12 @@ interface ExtractAssetMetadataResult { * {@link parsedMetadataJSONMap} for the assets. Return the resultant metadatum. */ const extractAssetMetadata = async ( - { isLivePhoto, uploadItem, livePhotoAssets }: UploadAsset, + { + isLivePhoto, + uploadItem, + externalParsedMetadata, + livePhotoAssets, + }: UploadAsset, fileTypeInfo: FileTypeInfo, lastModifiedMs: number, collectionID: number, @@ -889,6 +909,7 @@ const extractAssetMetadata = async ( ) : await extractImageOrVideoMetadata( uploadItem, + externalParsedMetadata, fileTypeInfo, lastModifiedMs, collectionID, @@ -911,6 +932,7 @@ const extractLivePhotoMetadata = async ( const { metadata: imageMetadata, publicMagicMetadata } = await extractImageOrVideoMetadata( livePhotoAssets.image, + undefined, imageFileTypeInfo, lastModifiedMs, collectionID, @@ -935,6 +957,7 @@ const extractLivePhotoMetadata = async ( const extractImageOrVideoMetadata = async ( uploadItem: UploadItem, + externalParsedMetadata: ParsedMetadata | undefined, fileTypeInfo: FileTypeInfo, lastModifiedMs: number, collectionID: number, @@ -956,6 +979,12 @@ const extractImageOrVideoMetadata = async ( throw new Error(`Unexpected file type ${fileType} for ${uploadItem}`); } + // The `UploadAsset` itself might have metadata associated with a-priori, if + // so, merge the data we read from the file's contents into it. + if (externalParsedMetadata) { + parsedMetadata = { ...externalParsedMetadata, ...parsedMetadata }; + } + const hash = await computeHash(uploadItem, worker); // Some of this logic is duplicated in `uploadItemCreationDate`. diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 1a4dcb3908..12ddef9ee2 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -7,6 +7,7 @@ import { ComlinkWorker } from "@/base/worker/comlink-worker"; import { shouldDisableCFUploadProxy } from "@/gallery/upload"; import type { Collection } from "@/media/collection"; import { EncryptedEnteFile, EnteFile } from "@/media/file"; +import type { ParsedMetadata } from "@/media/file-metadata"; import { FileType } from "@/media/file-type"; import { potentialFileTypeFromExtension } from "@/media/live-photo"; import { getLocalFiles } from "@/new/photos/services/files"; @@ -38,6 +39,7 @@ import UploadService, { uploadItemFileName, uploader, type PotentialLivePhotoAsset, + type UploadAsset, } from "./upload-service"; export type FileID = number; @@ -85,13 +87,10 @@ export interface ProgressUpdater { /** The number of uploads to process in parallel. */ const maxConcurrentUploads = 4; -export interface UploadItemWithCollection { +export type UploadItemWithCollection = UploadAsset & { localID: number; collectionID: number; - isLivePhoto?: boolean; - uploadItem?: UploadItem; - livePhotoAssets?: LivePhotoAssets; -} +}; export interface LivePhotoAssets { image: UploadItem; @@ -455,25 +454,35 @@ class UploadManager { * * @param file A web {@link File} object representing the file to upload. * - * @param collection The {@link Collection} in which the file should be added. + * @param collection The {@link Collection} in which the file should be + * added. * - * @param creationTime The timestamp (unix epoch microseconds) to use as the - * `creationTime` of the newly created {@link EnteFile}. + * @param sourceEnteFile The {@link EnteFile} from which the file being + * uploaded has been derived. This is used to extract and reassociated + * relevant metadata to the newly uploaded file. */ public async uploadFile( file: File, collection: Collection, - creationTime: number, + sourceEnteFile: EnteFile, ) { + const timestamp = sourceEnteFile.metadata.creationTime; + const dateTime = sourceEnteFile.pubMagicMetadata.data.dateTime; + const offset = sourceEnteFile.pubMagicMetadata.data.offsetTime; + + const creationDate: ParsedMetadata["creationDate"] = { + timestamp, + dateTime, + offset, + }; + const item = { uploadItem: file, localID: 1, collectionID: collection.id, + externalParsedMetadata: { creationDate }, }; - this.parsedMetadataJSONMap.set( - getMetadataJSONMapKeyForJSON(collection.id, file.name), - { creationTime }, - ); + return this.uploadItems([item], [collection]); } @@ -732,7 +741,7 @@ export default new UploadManager(); * {@link collection}, giving us {@link UploadableUploadItem}. This is what * gets queued and then passed to the {@link uploader}. */ -type UploadItemWithCollectionIDAndName = { +type UploadItemWithCollectionIDAndName = UploadAsset & { /** A unique ID for the duration of the upload */ localID: number; /** The ID of the collection to which this file should be uploaded. */ @@ -743,12 +752,6 @@ type UploadItemWithCollectionIDAndName = { * In case of live photos, this'll be the name of the image part. */ fileName: string; - /** `true` if this is a live photo. */ - isLivePhoto?: boolean; - /* Valid for non-live photos */ - uploadItem?: UploadItem; - /* Valid for live photos */ - livePhotoAssets?: LivePhotoAssets; }; const makeUploadItemWithCollectionIDAndName = ( @@ -764,6 +767,7 @@ const makeUploadItemWithCollectionIDAndName = ( isLivePhoto: f.isLivePhoto, uploadItem: f.uploadItem, livePhotoAssets: f.livePhotoAssets, + externalParsedMetadata: f.externalParsedMetadata, }); /** diff --git a/web/packages/media/file.ts b/web/packages/media/file.ts index 1ec99b8a71..2be0a0dd1c 100644 --- a/web/packages/media/file.ts +++ b/web/packages/media/file.ts @@ -163,6 +163,10 @@ export interface FilePublicMagicMetadataProps { * Epoch microseconds. */ editedTime?: number; + /** See {@link PublicMagicMetadata} in file-metadata.ts */ + dateTime?: string; + /** See {@link PublicMagicMetadata} in file-metadata.ts */ + offsetTime?: string; /** * Edited name of the {@link EnteFile}. *