Skip to content

Commit

Permalink
[web] Retain original's file creation time on edits (#4037)
Browse files Browse the repository at this point in the history
  • Loading branch information
mnvr authored Nov 15, 2024
2 parents e645081 + 057bd3a commit 80710d2
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -475,15 +475,10 @@ 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);
setFileURL(null);
props.onClose();
props.closePhotoViewer();
Expand Down
35 changes: 32 additions & 3 deletions web/apps/photos/src/services/upload/upload-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -889,6 +909,7 @@ const extractAssetMetadata = async (
)
: await extractImageOrVideoMetadata(
uploadItem,
externalParsedMetadata,
fileTypeInfo,
lastModifiedMs,
collectionID,
Expand All @@ -911,6 +932,7 @@ const extractLivePhotoMetadata = async (
const { metadata: imageMetadata, publicMagicMetadata } =
await extractImageOrVideoMetadata(
livePhotoAssets.image,
undefined,
imageFileTypeInfo,
lastModifiedMs,
collectionID,
Expand All @@ -935,6 +957,7 @@ const extractLivePhotoMetadata = async (

const extractImageOrVideoMetadata = async (
uploadItem: UploadItem,
externalParsedMetadata: ParsedMetadata | undefined,
fileTypeInfo: FileTypeInfo,
lastModifiedMs: number,
collectionID: number,
Expand All @@ -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`.
Expand Down
55 changes: 43 additions & 12 deletions web/apps/photos/src/services/upload/uploadManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -38,6 +39,7 @@ import UploadService, {
uploadItemFileName,
uploader,
type PotentialLivePhotoAsset,
type UploadAsset,
} from "./upload-service";

export type FileID = number;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -450,6 +449,43 @@ 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 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,
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 },
};

return this.uploadItems([item], [collection]);
}

private abortIfCancelled = () => {
if (uploadCancelService.isUploadCancelationRequested()) {
throw Error(CustomError.UPLOAD_CANCELLED);
Expand Down Expand Up @@ -705,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. */
Expand All @@ -716,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 = (
Expand All @@ -737,6 +767,7 @@ const makeUploadItemWithCollectionIDAndName = (
isLivePhoto: f.isLivePhoto,
uploadItem: f.uploadItem,
livePhotoAssets: f.livePhotoAssets,
externalParsedMetadata: f.externalParsedMetadata,
});

/**
Expand Down
4 changes: 4 additions & 0 deletions web/packages/media/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*
Expand Down

0 comments on commit 80710d2

Please sign in to comment.