Ver código fonte

[web] Upload refactoring - Part x/x (#1531)

Manav Rathi 1 ano atrás
pai
commit
c684b5fd69
46 arquivos alterados com 202 adições e 312 exclusões
  1. 0 42
      web/apps/cast/src/constants/upload.ts
  2. 1 1
      web/apps/cast/src/pages/slideshow.tsx
  3. 1 1
      web/apps/cast/src/services/castDownloadManager.ts
  4. 29 0
      web/apps/cast/src/services/detect-type.ts
  5. 0 92
      web/apps/cast/src/services/typeDetectionService.ts
  6. 1 1
      web/apps/cast/src/types/upload.ts
  7. 5 4
      web/apps/cast/src/utils/file.ts
  8. 0 1
      web/apps/photos/package.json
  9. 1 1
      web/apps/photos/src/components/FixCreationTime.tsx
  10. 1 1
      web/apps/photos/src/components/PhotoFrame.tsx
  11. 1 1
      web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx
  12. 1 1
      web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
  13. 2 2
      web/apps/photos/src/components/PhotoViewer/index.tsx
  14. 1 1
      web/apps/photos/src/components/PlaceholderThumbnails.tsx
  15. 1 1
      web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx
  16. 1 1
      web/apps/photos/src/components/pages/gallery/PreviewCard.tsx
  17. 1 48
      web/apps/photos/src/constants/upload.ts
  18. 1 1
      web/apps/photos/src/services/clip-service.ts
  19. 1 1
      web/apps/photos/src/services/deduplicationService.ts
  20. 1 1
      web/apps/photos/src/services/download/index.ts
  21. 16 20
      web/apps/photos/src/services/exif.ts
  22. 1 1
      web/apps/photos/src/services/export/index.ts
  23. 1 1
      web/apps/photos/src/services/export/migration.ts
  24. 2 2
      web/apps/photos/src/services/fix-exif.ts
  25. 1 1
      web/apps/photos/src/services/machineLearning/mlWorkManager.ts
  26. 1 1
      web/apps/photos/src/services/machineLearning/readerService.ts
  27. 1 1
      web/apps/photos/src/services/searchService.ts
  28. 9 9
      web/apps/photos/src/services/typeDetectionService.ts
  29. 2 3
      web/apps/photos/src/services/upload/metadata.ts
  30. 1 2
      web/apps/photos/src/services/upload/thumbnail.ts
  31. 0 23
      web/apps/photos/src/services/upload/uploadCancelService.ts
  32. 40 10
      web/apps/photos/src/services/upload/uploadManager.ts
  33. 2 8
      web/apps/photos/src/services/upload/uploadService.ts
  34. 1 1
      web/apps/photos/src/types/search/index.ts
  35. 1 9
      web/apps/photos/src/types/upload/index.ts
  36. 2 3
      web/apps/photos/src/utils/file/index.ts
  37. 1 1
      web/apps/photos/src/utils/machineLearning/index.ts
  38. 1 1
      web/apps/photos/src/utils/photoFrame/index.ts
  39. 1 1
      web/apps/photos/tests/upload.test.ts
  40. 7 2
      web/docs/dependencies.md
  41. 58 0
      web/packages/media/file-type.ts
  42. 0 6
      web/packages/media/file.ts
  43. 1 1
      web/packages/media/live-photo.ts
  44. 1 0
      web/packages/media/package.json
  45. 0 2
      web/packages/shared/error/index.ts
  46. 1 1
      web/yarn.lock

+ 0 - 42
web/apps/cast/src/constants/upload.ts

@@ -1,6 +1,3 @@
-import { FILE_TYPE } from "@/media/file";
-import { FileTypeInfo } from "types/upload";
-
 export const RAW_FORMATS = [
     "heic",
     "rw2",
@@ -14,42 +11,3 @@ export const RAW_FORMATS = [
     "dng",
     "tif",
 ];
-
-// list of format that were missed by type-detection for some files.
-export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
-    { fileType: FILE_TYPE.IMAGE, exactType: "jpeg", mimeType: "image/jpeg" },
-    { fileType: FILE_TYPE.IMAGE, exactType: "jpg", mimeType: "image/jpeg" },
-    { fileType: FILE_TYPE.VIDEO, exactType: "webm", mimeType: "video/webm" },
-    { fileType: FILE_TYPE.VIDEO, exactType: "mod", mimeType: "video/mpeg" },
-    { fileType: FILE_TYPE.VIDEO, exactType: "mp4", mimeType: "video/mp4" },
-    { fileType: FILE_TYPE.IMAGE, exactType: "gif", mimeType: "image/gif" },
-    { fileType: FILE_TYPE.VIDEO, exactType: "dv", mimeType: "video/x-dv" },
-    {
-        fileType: FILE_TYPE.VIDEO,
-        exactType: "wmv",
-        mimeType: "video/x-ms-asf",
-    },
-    {
-        fileType: FILE_TYPE.VIDEO,
-        exactType: "hevc",
-        mimeType: "video/hevc",
-    },
-    {
-        fileType: FILE_TYPE.IMAGE,
-        exactType: "raf",
-        mimeType: "image/x-fuji-raf",
-    },
-    {
-        fileType: FILE_TYPE.IMAGE,
-        exactType: "orf",
-        mimeType: "image/x-olympus-orf",
-    },
-
-    {
-        fileType: FILE_TYPE.IMAGE,
-        exactType: "crw",
-        mimeType: "image/x-canon-crw",
-    },
-];
-
-export const KNOWN_NON_MEDIA_FORMATS = ["xmp", "html", "txt"];

+ 1 - 1
web/apps/cast/src/pages/slideshow.tsx

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import log from "@/next/log";
 import PairedSuccessfullyOverlay from "components/PairedSuccessfullyOverlay";
 import { PhotoAuditorium } from "components/PhotoAuditorium";

+ 1 - 1
web/apps/cast/src/services/castDownloadManager.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import ComlinkCryptoWorker from "@ente/shared/crypto";
 import { CustomError } from "@ente/shared/error";
 import HTTPService from "@ente/shared/network/HTTPService";

+ 29 - 0
web/apps/cast/src/services/detect-type.ts

@@ -0,0 +1,29 @@
+import { KnownFileTypeInfos } from "@/media/file-type";
+import { nameAndExtension } from "@/next/file";
+import FileType from "file-type";
+
+/**
+ * Try to deduce the MIME type for the given {@link file}. Return the MIME type
+ * string if successful _and_ if it is an image or a video, otherwise return
+ * `undefined`.
+ *
+ * It first peeks into the file's initial contents to detect the MIME type. If
+ * that doesn't give any results, it tries to deduce it from the file's name.
+ */
+export const detectMediaMIMEType = async (file: File): Promise<string> => {
+    const chunkSizeForTypeDetection = 4100;
+    const fileChunk = file.slice(0, chunkSizeForTypeDetection);
+    const chunk = new Uint8Array(await fileChunk.arrayBuffer());
+    const result = await FileType.fromBuffer(chunk);
+
+    const mime = result?.mime;
+    if (mime) {
+        if (mime.startsWith("image/") || mime.startsWith("video/")) return mime;
+        else throw new Error(`Detected MIME type ${mime} is not a media file`);
+    }
+
+    let [, ext] = nameAndExtension(file.name);
+    if (!ext) return undefined;
+    ext = ext.toLowerCase();
+    return KnownFileTypeInfos.find((f) => f.exactType == ext)?.mimeType;
+};

+ 0 - 92
web/apps/cast/src/services/typeDetectionService.ts

@@ -1,92 +0,0 @@
-import { FILE_TYPE } from "@/media/file";
-import { convertBytesToHumanReadable, nameAndExtension } from "@/next/file";
-import log from "@/next/log";
-import { CustomError } from "@ente/shared/error";
-import {
-    KNOWN_NON_MEDIA_FORMATS,
-    WHITELISTED_FILE_FORMATS,
-} from "constants/upload";
-import FileType from "file-type";
-import { FileTypeInfo } from "types/upload";
-
-const TYPE_VIDEO = "video";
-const TYPE_IMAGE = "image";
-const CHUNK_SIZE_FOR_TYPE_DETECTION = 4100;
-
-export async function getFileType(receivedFile: File): Promise<FileTypeInfo> {
-    try {
-        let fileType: FILE_TYPE;
-
-        const typeResult = await extractFileType(receivedFile);
-        const mimTypeParts: string[] = typeResult.mime?.split("/");
-        if (mimTypeParts?.length !== 2) {
-            throw Error(CustomError.INVALID_MIME_TYPE(typeResult.mime));
-        }
-
-        switch (mimTypeParts[0]) {
-            case TYPE_IMAGE:
-                fileType = FILE_TYPE.IMAGE;
-                break;
-            case TYPE_VIDEO:
-                fileType = FILE_TYPE.VIDEO;
-                break;
-            default:
-                throw Error(CustomError.NON_MEDIA_FILE);
-        }
-        return {
-            fileType,
-            exactType: typeResult.ext,
-            mimeType: typeResult.mime,
-        };
-    } catch (e) {
-        const ne = nameAndExtension(receivedFile.name);
-        const fileFormat = ne[1].toLowerCase();
-        const whiteListedFormat = WHITELISTED_FILE_FORMATS.find(
-            (a) => a.exactType === fileFormat,
-        );
-        if (whiteListedFormat) {
-            return whiteListedFormat;
-        }
-        if (KNOWN_NON_MEDIA_FORMATS.includes(fileFormat)) {
-            throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
-        }
-        if (e.message === CustomError.NON_MEDIA_FILE) {
-            log.error(`unsupported file format ${fileFormat}`, e);
-            throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
-        }
-        log.error(`type detection failed for format ${fileFormat}`, e);
-        throw Error(CustomError.TYPE_DETECTION_FAILED(fileFormat));
-    }
-}
-
-async function extractFileType(file: File) {
-    const fileBlobChunk = file.slice(0, CHUNK_SIZE_FOR_TYPE_DETECTION);
-    const fileDataChunk = await getUint8ArrayView(fileBlobChunk);
-    return getFileTypeFromBuffer(fileDataChunk);
-}
-
-export async function getUint8ArrayView(file: Blob): Promise<Uint8Array> {
-    try {
-        return new Uint8Array(await file.arrayBuffer());
-    } catch (e) {
-        log.error(
-            `Failed to read file blob of size ${convertBytesToHumanReadable(file.size)}`,
-            e,
-        );
-        throw e;
-    }
-}
-
-async function getFileTypeFromBuffer(buffer: Uint8Array) {
-    const result = await FileType.fromBuffer(buffer);
-    if (!result?.mime) {
-        let logableInfo = "";
-        try {
-            logableInfo = `result: ${JSON.stringify(result)}`;
-        } catch (e) {
-            logableInfo = "failed to stringify result";
-        }
-        throw Error(`mimetype missing from file type result - ${logableInfo}`);
-    }
-    return result;
-}

+ 1 - 1
web/apps/cast/src/types/upload.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 
 export interface Metadata {
     title: string;

+ 5 - 4
web/apps/cast/src/utils/file.ts

@@ -1,10 +1,10 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { decodeLivePhoto } from "@/media/live-photo";
 import log from "@/next/log";
 import ComlinkCryptoWorker from "@ente/shared/crypto";
 import { RAW_FORMATS } from "constants/upload";
 import CastDownloadManager from "services/castDownloadManager";
-import { getFileType } from "services/typeDetectionService";
+import { detectMediaMIMEType } from "services/detect-type";
 import {
     EncryptedEnteFile,
     EnteFile,
@@ -132,10 +132,11 @@ export const getPreviewableImage = async (
             );
             fileBlob = new Blob([imageData]);
         }
-        const fileType = await getFileType(
+        const mimeType = await detectMediaMIMEType(
             new File([fileBlob], file.metadata.title),
         );
-        fileBlob = new Blob([fileBlob], { type: fileType.mimeType });
+        if (!mimeType) return undefined;
+        fileBlob = new Blob([fileBlob], { type: mimeType });
         return fileBlob;
     } catch (e) {
         log.error("failed to download file", e);

+ 0 - 1
web/apps/photos/package.json

@@ -21,7 +21,6 @@
         "exifr": "^7.1.3",
         "fast-srp-hap": "^2.0.4",
         "ffmpeg-wasm": "file:./thirdparty/ffmpeg-wasm",
-        "file-type": "^16.5.4",
         "formik": "^2.1.5",
         "hdbscan": "0.0.1-alpha.5",
         "heic-convert": "^2.0.0",

+ 1 - 1
web/apps/photos/src/components/FixCreationTime.tsx

@@ -13,7 +13,7 @@ import { useFormik } from "formik";
 import { t } from "i18next";
 import { GalleryContext } from "pages/gallery";
 import React, { useContext, useEffect, useState } from "react";
-import { updateCreationTimeWithExif } from "services/updateCreationTimeWithExif";
+import { updateCreationTimeWithExif } from "services/fix-exif";
 import { EnteFile } from "types/file";
 import EnteDateTimePicker from "./EnteDateTimePicker";
 

+ 1 - 1
web/apps/photos/src/components/PhotoFrame.tsx

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import log from "@/next/log";
 import { PHOTOS_PAGES } from "@ente/shared/constants/pages";
 import { CustomError } from "@ente/shared/error";

+ 1 - 1
web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { nameAndExtension } from "@/next/file";
 import log from "@/next/log";
 import { FlexWrapper } from "@ente/shared/components/Container";

+ 1 - 1
web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx

@@ -17,7 +17,7 @@ import { t } from "i18next";
 import { AppContext } from "pages/_app";
 import { GalleryContext } from "pages/gallery";
 import { useContext, useEffect, useMemo, useState } from "react";
-import { getEXIFLocation } from "services/upload/exifService";
+import { getEXIFLocation } from "services/exif";
 import { EnteFile } from "types/file";
 import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
 import {

+ 2 - 2
web/apps/photos/src/components/PhotoViewer/index.tsx

@@ -16,7 +16,7 @@ import {
     isSupportedRawFormat,
 } from "utils/file";
 
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { FlexWrapper } from "@ente/shared/components/Container";
 import EnteSpinner from "@ente/shared/components/EnteSpinner";
 import AlbumOutlined from "@mui/icons-material/AlbumOutlined";
@@ -44,9 +44,9 @@ import isElectron from "is-electron";
 import { AppContext } from "pages/_app";
 import { GalleryContext } from "pages/gallery";
 import downloadManager, { LoadedLivePhotoSourceURL } from "services/download";
+import { getParsedExifData } from "services/exif";
 import { trashFiles } from "services/fileService";
 import { getFileType } from "services/typeDetectionService";
-import { getParsedExifData } from "services/upload/exifService";
 import { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
 import { isClipboardItemPresent } from "utils/common";
 import { pauseVideo, playVideo } from "utils/photoFrame";

+ 1 - 1
web/apps/photos/src/components/PlaceholderThumbnails.tsx

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { Overlay } from "@ente/shared/components/Container";
 import PhotoOutlined from "@mui/icons-material/PhotoOutlined";
 import PlayCircleOutlineOutlined from "@mui/icons-material/PlayCircleOutlineOutlined";

+ 1 - 1
web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import CloseIcon from "@mui/icons-material/Close";
 import { IconButton } from "@mui/material";
 import { t } from "i18next";

+ 1 - 1
web/apps/photos/src/components/pages/gallery/PreviewCard.tsx

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import log from "@/next/log";
 import { Overlay } from "@ente/shared/components/Container";
 import { CustomError } from "@ente/shared/error";

+ 1 - 48
web/apps/photos/src/constants/upload.ts

@@ -1,52 +1,5 @@
-import { FILE_TYPE } from "@/media/file";
 import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants";
-import { FileTypeInfo, Location } from "types/upload";
-
-// list of format that were missed by type-detection for some files.
-export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
-    { fileType: FILE_TYPE.IMAGE, exactType: "jpeg", mimeType: "image/jpeg" },
-    { fileType: FILE_TYPE.IMAGE, exactType: "jpg", mimeType: "image/jpeg" },
-    { fileType: FILE_TYPE.VIDEO, exactType: "webm", mimeType: "video/webm" },
-    { fileType: FILE_TYPE.VIDEO, exactType: "mod", mimeType: "video/mpeg" },
-    { fileType: FILE_TYPE.VIDEO, exactType: "mp4", mimeType: "video/mp4" },
-    { fileType: FILE_TYPE.IMAGE, exactType: "gif", mimeType: "image/gif" },
-    { fileType: FILE_TYPE.VIDEO, exactType: "dv", mimeType: "video/x-dv" },
-    {
-        fileType: FILE_TYPE.VIDEO,
-        exactType: "wmv",
-        mimeType: "video/x-ms-asf",
-    },
-    {
-        fileType: FILE_TYPE.VIDEO,
-        exactType: "hevc",
-        mimeType: "video/hevc",
-    },
-    {
-        fileType: FILE_TYPE.IMAGE,
-        exactType: "raf",
-        mimeType: "image/x-fuji-raf",
-    },
-    {
-        fileType: FILE_TYPE.IMAGE,
-        exactType: "orf",
-        mimeType: "image/x-olympus-orf",
-    },
-
-    {
-        fileType: FILE_TYPE.IMAGE,
-        exactType: "crw",
-        mimeType: "image/x-canon-crw",
-    },
-    {
-        fileType: FILE_TYPE.VIDEO,
-        exactType: "mov",
-        mimeType: "video/quicktime",
-    },
-];
-
-export const KNOWN_NON_MEDIA_FORMATS = ["xmp", "html", "txt"];
-
-export const EXIFLESS_FORMATS = ["gif", "bmp"];
+import { Location } from "types/upload";
 
 // this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
 export const MULTIPART_PART_SIZE = 20 * 1024 * 1024;

+ 1 - 1
web/apps/photos/src/services/clip-service.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { ensureElectron } from "@/next/electron";
 import log from "@/next/log";
 import ComlinkCryptoWorker from "@ente/shared/crypto";

+ 1 - 1
web/apps/photos/src/services/deduplicationService.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import log from "@/next/log";
 import HTTPService from "@ente/shared/network/HTTPService";
 import { getEndpoint } from "@ente/shared/network/api";

+ 1 - 1
web/apps/photos/src/services/download/index.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { decodeLivePhoto } from "@/media/live-photo";
 import { openCache, type BlobCache } from "@/next/blob-cache";
 import log from "@/next/log";

+ 16 - 20
web/apps/photos/src/services/upload/exifService.ts → web/apps/photos/src/services/exif.ts

@@ -1,12 +1,10 @@
+import { type FileTypeInfo } from "@/media/file-type";
 import log from "@/next/log";
-import { CustomError } from "@ente/shared/error";
 import { validateAndGetCreationUnixTimeInMicroSeconds } from "@ente/shared/time";
-import { EXIFLESS_FORMATS, NULL_LOCATION } from "constants/upload";
+import { NULL_LOCATION } from "constants/upload";
 import exifr from "exifr";
 import piexif from "piexifjs";
-import { FileTypeInfo, Location } from "types/upload";
-
-const EXIFR_UNSUPPORTED_FILE_FORMAT_MESSAGE = "Unknown file format";
+import { Location } from "types/upload";
 
 type ParsedEXIFData = Record<string, any> &
     Partial<{
@@ -38,11 +36,14 @@ type RawEXIFData = Record<string, any> &
 
 export async function getParsedExifData(
     receivedFile: File,
-    fileTypeInfo: FileTypeInfo,
+    { exactType }: FileTypeInfo,
     tags?: string[],
 ): Promise<ParsedEXIFData> {
+    const exifLessFormats = ["gif", "bmp"];
+    const exifrUnsupportedFileFormatMessage = "Unknown file format";
+
     try {
-        if (EXIFLESS_FORMATS.includes(fileTypeInfo.exactType)) {
+        if (exifLessFormats.includes(exactType)) {
             return null;
         }
         const exifData: RawEXIFData = await exifr.parse(receivedFile, {
@@ -66,16 +67,11 @@ export async function getParsedExifData(
             : exifData;
         return parseExifData(filteredExifData);
     } catch (e) {
-        if (e.message === EXIFR_UNSUPPORTED_FILE_FORMAT_MESSAGE) {
-            log.error(
-                `exif library unsupported format ${fileTypeInfo.exactType}`,
-                e,
-            );
+        if (e.message == exifrUnsupportedFileFormatMessage) {
+            log.error(`EXIFR does not support format ${exactType}`, e);
+            return undefined;
         } else {
-            log.error(
-                `get parsed exif data failed for file type ${fileTypeInfo.exactType}`,
-                e,
-            );
+            log.error(`Failed to parse EXIF data of ${exactType} file`, e);
             throw e;
         }
     }
@@ -180,7 +176,7 @@ function parseExifData(exifData: RawEXIFData): ParsedEXIFData {
 function parseEXIFDate(dateTimeString: string) {
     try {
         if (typeof dateTimeString !== "string" || dateTimeString === "") {
-            throw Error(CustomError.NOT_A_DATE);
+            throw new Error("Invalid date string");
         }
 
         // Check and parse date in the format YYYYMMDD
@@ -211,7 +207,7 @@ function parseEXIFDate(dateTimeString: string) {
             typeof day === "undefined" ||
             Number.isNaN(day)
         ) {
-            throw Error(CustomError.NOT_A_DATE);
+            throw new Error("Invalid date");
         }
         let date: Date;
         if (
@@ -227,7 +223,7 @@ function parseEXIFDate(dateTimeString: string) {
             date = new Date(year, month - 1, day, hour, minute, second);
         }
         if (Number.isNaN(+date)) {
-            throw Error(CustomError.NOT_A_DATE);
+            throw new Error("Invalid date");
         }
         return date;
     } catch (e) {
@@ -249,7 +245,7 @@ export function parseEXIFLocation(
             gpsLatitude.length !== 3 ||
             gpsLongitude.length !== 3
         ) {
-            throw Error(CustomError.NOT_A_LOCATION);
+            throw new Error("Invalid EXIF location");
         }
         const latitude = convertDMSToDD(
             gpsLatitude[0],

+ 1 - 1
web/apps/photos/src/services/export/index.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { decodeLivePhoto } from "@/media/live-photo";
 import { ensureElectron } from "@/next/electron";
 import log from "@/next/log";

+ 1 - 1
web/apps/photos/src/services/export/migration.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { decodeLivePhoto } from "@/media/live-photo";
 import { ensureElectron } from "@/next/electron";
 import log from "@/next/log";

+ 2 - 2
web/apps/photos/src/services/updateCreationTimeWithExif.ts → web/apps/photos/src/services/fix-exif.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import log from "@/next/log";
 import { validateAndGetCreationUnixTimeInMicroSeconds } from "@ente/shared/time";
 import type { FixOption } from "components/FixCreationTime";
@@ -9,7 +9,7 @@ import {
     updateExistingFilePubMetadata,
 } from "utils/file";
 import downloadManager from "./download";
-import { getParsedExifData } from "./upload/exifService";
+import { getParsedExifData } from "./exif";
 
 const EXIF_TIME_TAGS = [
     "DateTimeOriginal",

+ 1 - 1
web/apps/photos/src/services/machineLearning/mlWorkManager.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import log from "@/next/log";
 import { ComlinkWorker } from "@/next/worker/comlink-worker";
 import { eventBus, Events } from "@ente/shared/events";

+ 1 - 1
web/apps/photos/src/services/machineLearning/readerService.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import log from "@/next/log";
 import { MLSyncContext, MLSyncFileContext } from "types/machineLearning";
 import {

+ 1 - 1
web/apps/photos/src/services/searchService.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import log from "@/next/log";
 import * as chrono from "chrono-node";
 import { t } from "i18next";

+ 9 - 9
web/apps/photos/src/services/typeDetectionService.ts

@@ -1,13 +1,13 @@
-import { FILE_TYPE } from "@/media/file";
+import {
+    FILE_TYPE,
+    KnownFileTypeInfos,
+    KnownNonMediaFileExtensions,
+    type FileTypeInfo,
+} from "@/media/file-type";
 import log from "@/next/log";
 import { ElectronFile } from "@/next/types/file";
 import { CustomError } from "@ente/shared/error";
-import {
-    KNOWN_NON_MEDIA_FORMATS,
-    WHITELISTED_FILE_FORMATS,
-} from "constants/upload";
-import FileType, { FileTypeResult } from "file-type";
-import { FileTypeInfo } from "types/upload";
+import FileType, { type FileTypeResult } from "file-type";
 import { getFileExtension } from "utils/file";
 import { getUint8ArrayView } from "./readerService";
 
@@ -50,13 +50,13 @@ export async function getFileType(
         };
     } catch (e) {
         const fileFormat = getFileExtension(receivedFile.name);
-        const whiteListedFormat = WHITELISTED_FILE_FORMATS.find(
+        const whiteListedFormat = KnownFileTypeInfos.find(
             (a) => a.exactType === fileFormat,
         );
         if (whiteListedFormat) {
             return whiteListedFormat;
         }
-        if (KNOWN_NON_MEDIA_FORMATS.includes(fileFormat)) {
+        if (KnownNonMediaFileExtensions.includes(fileFormat)) {
             throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
         }
         if (e.message === CustomError.NON_MEDIA_FILE) {

+ 2 - 3
web/apps/photos/src/services/upload/metadata.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE, type FileTypeInfo } from "@/media/file-type";
 import { getFileNameSize } from "@/next/file";
 import log from "@/next/log";
 import { ElectronFile } from "@/next/types/file";
@@ -12,17 +12,16 @@ import {
 import type { DataStream } from "@ente/shared/utils/data-stream";
 import { Remote } from "comlink";
 import { FILE_READER_CHUNK_SIZE, NULL_LOCATION } from "constants/upload";
+import { getEXIFLocation, getEXIFTime, getParsedExifData } from "services/exif";
 import * as ffmpegService from "services/ffmpeg";
 import { getElectronFileStream, getFileStream } from "services/readerService";
 import { FilePublicMagicMetadataProps } from "types/file";
 import {
-    FileTypeInfo,
     Metadata,
     ParsedExtractedMetadata,
     type LivePhotoAssets2,
     type UploadAsset2,
 } from "types/upload";
-import { getEXIFLocation, getEXIFTime, getParsedExifData } from "./exifService";
 import {
     MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT,
     getClippedMetadataJSONMapKeyForFile,

+ 1 - 2
web/apps/photos/src/services/upload/thumbnail.ts

@@ -1,11 +1,10 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE, type FileTypeInfo } from "@/media/file-type";
 import log from "@/next/log";
 import { type Electron } from "@/next/types/ipc";
 import { withTimeout } from "@ente/shared/utils";
 import { BLACK_THUMBNAIL_BASE64 } from "constants/upload";
 import * as ffmpeg from "services/ffmpeg";
 import { heicToJPEG } from "services/heic-convert";
-import { FileTypeInfo } from "types/upload";
 import { isFileHEIC } from "utils/file";
 
 /** Maximum width or height of the generated thumbnail */

+ 0 - 23
web/apps/photos/src/services/upload/uploadCancelService.ts

@@ -1,23 +0,0 @@
-interface UploadCancelStatus {
-    value: boolean;
-}
-
-class UploadCancelService {
-    private shouldUploadBeCancelled: UploadCancelStatus = {
-        value: false,
-    };
-
-    reset() {
-        this.shouldUploadBeCancelled.value = false;
-    }
-
-    requestUploadCancelation() {
-        this.shouldUploadBeCancelled.value = true;
-    }
-
-    isUploadCancelationRequested(): boolean {
-        return this.shouldUploadBeCancelled.value;
-    }
-}
-
-export default new UploadCancelService();

+ 40 - 10
web/apps/photos/src/services/upload/uploadManager.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { potentialFileTypeFromExtension } from "@/media/live-photo";
 import { ensureElectron } from "@/next/electron";
 import { nameAndExtension } from "@/next/file";
@@ -48,7 +48,6 @@ import {
     tryParseTakeoutMetadataJSON,
     type ParsedMetadataJSON,
 } from "./takeout";
-import uploadCancelService from "./uploadCancelService";
 import UploadService, {
     assetName,
     getAssetName,
@@ -56,7 +55,32 @@ import UploadService, {
     uploader,
 } from "./uploadService";
 
-const MAX_CONCURRENT_UPLOADS = 4;
+/** The number of uploads to process in parallel. */
+const maxConcurrentUploads = 4;
+
+interface UploadCancelStatus {
+    value: boolean;
+}
+
+class UploadCancelService {
+    private shouldUploadBeCancelled: UploadCancelStatus = {
+        value: false,
+    };
+
+    reset() {
+        this.shouldUploadBeCancelled.value = false;
+    }
+
+    requestUploadCancelation() {
+        this.shouldUploadBeCancelled.value = true;
+    }
+
+    isUploadCancelationRequested(): boolean {
+        return this.shouldUploadBeCancelled.value;
+    }
+}
+
+const uploadCancelService = new UploadCancelService();
 
 class UIService {
     private progressUpdater: ProgressUpdater;
@@ -261,7 +285,7 @@ function segregatedFinishedUploadsToList(finishedUploads: FinishedUploads) {
 class UploadManager {
     private cryptoWorkers = new Array<
         ComlinkWorker<typeof DedicatedCryptoWorker>
-    >(MAX_CONCURRENT_UPLOADS);
+    >(maxConcurrentUploads);
     private parsedMetadataJSONMap: Map<string, ParsedMetadataJSON>;
     private filesToBeUploaded: FileWithCollection2[];
     private remainingFiles: FileWithCollection2[] = [];
@@ -411,7 +435,7 @@ class UploadManager {
             }
         } finally {
             this.uiService.setUploadStage(UPLOAD_STAGES.FINISH);
-            for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
+            for (let i = 0; i < maxConcurrentUploads; i++) {
                 this.cryptoWorkers[i]?.terminate();
             }
             this.uploadInProgress = false;
@@ -428,6 +452,12 @@ class UploadManager {
         }
     }
 
+    private abortIfCancelled = () => {
+        if (uploadCancelService.isUploadCancelationRequested()) {
+            throw Error(CustomError.UPLOAD_CANCELLED);
+        }
+    };
+
     private async parseMetadataJSONFiles(metadataFiles: FileWithCollection2[]) {
         try {
             log.info(`parseMetadataJSONFiles function executed `);
@@ -435,12 +465,9 @@ class UploadManager {
             this.uiService.reset(metadataFiles.length);
 
             for (const { file, collectionID } of metadataFiles) {
+                this.abortIfCancelled();
                 const name = getFileName(file);
                 try {
-                    if (uploadCancelService.isUploadCancelationRequested()) {
-                        throw Error(CustomError.UPLOAD_CANCELLED);
-                    }
-
                     log.info(`parsing metadata json file ${name}`);
 
                     const metadataJSON =
@@ -490,7 +517,7 @@ class UploadManager {
         const uploadProcesses = [];
         for (
             let i = 0;
-            i < MAX_CONCURRENT_UPLOADS && this.filesToBeUploaded.length > 0;
+            i < maxConcurrentUploads && this.filesToBeUploaded.length > 0;
             i++
         ) {
             this.cryptoWorkers[i] = getDedicatedCryptoWorker();
@@ -522,6 +549,9 @@ class UploadManager {
                 this.parsedMetadataJSONMap,
                 this.uploaderName,
                 this.isCFUploadProxyDisabled,
+                () => {
+                    this.abortIfCancelled();
+                },
                 (
                     fileLocalID: number,
                     percentPerPart?: number,

+ 2 - 8
web/apps/photos/src/services/upload/uploadService.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE, type FileTypeInfo } from "@/media/file-type";
 import { encodeLivePhoto } from "@/media/live-photo";
 import { ensureElectron } from "@/next/electron";
 import { basename } from "@/next/file";
@@ -28,7 +28,6 @@ import {
     BackupedFile,
     EncryptedFile,
     FileInMemory,
-    FileTypeInfo,
     FileWithMetadata,
     ProcessedFile,
     PublicUploadProps,
@@ -58,7 +57,6 @@ import {
     generateThumbnailNative,
     generateThumbnailWeb,
 } from "./thumbnail";
-import uploadCancelService from "./uploadCancelService";
 import UploadHttpClient from "./uploadHttpClient";
 
 /** Upload files to cloud storage */
@@ -168,16 +166,12 @@ export const uploader = async (
     parsedMetadataJSONMap: Map<string, ParsedMetadataJSON>,
     uploaderName: string,
     isCFUploadProxyDisabled: boolean,
+    abortIfCancelled: () => void,
     makeProgessTracker: MakeProgressTracker,
 ): Promise<UploadResponse> => {
     const name = assetName(fileWithCollection);
     log.info(`Uploading ${name}`);
 
-    const abortIfCancelled = () => {
-        if (uploadCancelService.isUploadCancelationRequested())
-            throw Error(CustomError.UPLOAD_CANCELLED);
-    };
-
     const { collection, localID, ...uploadAsset2 } = fileWithCollection;
     /* TODO(MR): ElectronFile changes */
     const uploadAsset = uploadAsset2 as UploadAsset;

+ 1 - 1
web/apps/photos/src/types/search/index.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { City } from "services/locationSearchService";
 import { LocationTagData } from "types/entity";
 import { EnteFile } from "types/file";

+ 1 - 9
web/apps/photos/src/types/upload/index.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import type { ElectronFile } from "@/next/types/file";
 import {
     B64EncryptionResult,
@@ -46,14 +46,6 @@ export interface MultipartUploadURLs {
     completeURL: string;
 }
 
-export interface FileTypeInfo {
-    fileType: FILE_TYPE;
-    exactType: string;
-    mimeType?: string;
-    imageType?: string;
-    videoType?: string;
-}
-
 export interface UploadAsset {
     isLivePhoto?: boolean;
     file?: File | ElectronFile;

+ 2 - 3
web/apps/photos/src/utils/file/index.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE, type FileTypeInfo } from "@/media/file-type";
 import { decodeLivePhoto } from "@/media/live-photo";
 import log from "@/next/log";
 import { CustomErrorMessage, type Electron } from "@/next/types/ipc";
@@ -11,6 +11,7 @@ import { t } from "i18next";
 import isElectron from "is-electron";
 import { moveToHiddenCollection } from "services/collectionService";
 import DownloadManager from "services/download";
+import { updateFileCreationDateInEXIF } from "services/exif";
 import {
     deleteFromTrash,
     trashFiles,
@@ -19,7 +20,6 @@ import {
 } from "services/fileService";
 import { heicToJPEG } from "services/heic-convert";
 import { getFileType } from "services/typeDetectionService";
-import { updateFileCreationDateInEXIF } from "services/upload/exifService";
 import {
     EncryptedEnteFile,
     EnteFile,
@@ -35,7 +35,6 @@ import {
     SetFilesDownloadProgressAttributesCreator,
 } from "types/gallery";
 import { VISIBILITY_STATE } from "types/magicMetadata";
-import { FileTypeInfo } from "types/upload";
 import { isArchivedFile, updateMagicMetadata } from "utils/magicMetadata";
 import { safeFileName } from "utils/native-fs";
 import { writeStream } from "utils/native-stream";

+ 1 - 1
web/apps/photos/src/utils/machineLearning/index.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { decodeLivePhoto } from "@/media/live-photo";
 import log from "@/next/log";
 import PQueue from "p-queue";

+ 1 - 1
web/apps/photos/src/utils/photoFrame/index.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import log from "@/next/log";
 import { LivePhotoSourceURL, SourceURLs } from "services/download";
 import { EnteFile } from "types/file";

+ 1 - 1
web/apps/photos/tests/upload.test.ts

@@ -1,4 +1,4 @@
-import { FILE_TYPE } from "@/media/file";
+import { FILE_TYPE } from "@/media/file-type";
 import { tryToParseDateTime } from "@ente/shared/time";
 import { getLocalCollections } from "services/collectionService";
 import { getLocalFiles } from "services/fileService";

+ 7 - 2
web/docs/dependencies.md

@@ -133,8 +133,13 @@ some cases.
 
 ## Media
 
--   "jszip" is used for reading zip files in JavaScript. Live photos are zip
-    files under the hood.
+-   ["jszip"](https://github.com/Stuk/jszip) is used for reading zip files in
+    JavaScript (Live photos are zip files under the hood).
+
+-   ["file-type"](https://github.com/sindresorhus/file-type) is used for MIME
+    type detection. We are at an old version 16.5.4 because v17 onwards the
+    package became ESM only - for our limited use case, the custom Webpack
+    configuration that entails is not worth the upgrade.
 
 ## Photos app specific
 

+ 58 - 0
web/packages/media/file-type.ts

@@ -0,0 +1,58 @@
+export enum FILE_TYPE {
+    IMAGE,
+    VIDEO,
+    LIVE_PHOTO,
+    OTHERS,
+}
+
+export interface FileTypeInfo {
+    fileType: FILE_TYPE;
+    exactType: string;
+    mimeType?: string;
+    imageType?: string;
+    videoType?: string;
+}
+
+// list of format that were missed by type-detection for some files.
+export const KnownFileTypeInfos: FileTypeInfo[] = [
+    { fileType: FILE_TYPE.IMAGE, exactType: "jpeg", mimeType: "image/jpeg" },
+    { fileType: FILE_TYPE.IMAGE, exactType: "jpg", mimeType: "image/jpeg" },
+    { fileType: FILE_TYPE.VIDEO, exactType: "webm", mimeType: "video/webm" },
+    { fileType: FILE_TYPE.VIDEO, exactType: "mod", mimeType: "video/mpeg" },
+    { fileType: FILE_TYPE.VIDEO, exactType: "mp4", mimeType: "video/mp4" },
+    { fileType: FILE_TYPE.IMAGE, exactType: "gif", mimeType: "image/gif" },
+    { fileType: FILE_TYPE.VIDEO, exactType: "dv", mimeType: "video/x-dv" },
+    {
+        fileType: FILE_TYPE.VIDEO,
+        exactType: "wmv",
+        mimeType: "video/x-ms-asf",
+    },
+    {
+        fileType: FILE_TYPE.VIDEO,
+        exactType: "hevc",
+        mimeType: "video/hevc",
+    },
+    {
+        fileType: FILE_TYPE.IMAGE,
+        exactType: "raf",
+        mimeType: "image/x-fuji-raf",
+    },
+    {
+        fileType: FILE_TYPE.IMAGE,
+        exactType: "orf",
+        mimeType: "image/x-olympus-orf",
+    },
+
+    {
+        fileType: FILE_TYPE.IMAGE,
+        exactType: "crw",
+        mimeType: "image/x-canon-crw",
+    },
+    {
+        fileType: FILE_TYPE.VIDEO,
+        exactType: "mov",
+        mimeType: "video/quicktime",
+    },
+];
+
+export const KnownNonMediaFileExtensions = ["xmp", "html", "txt"];

+ 0 - 6
web/packages/media/file.ts

@@ -1,6 +0,0 @@
-export enum FILE_TYPE {
-    IMAGE,
-    VIDEO,
-    LIVE_PHOTO,
-    OTHERS,
-}

+ 1 - 1
web/packages/media/live-photo.ts

@@ -1,6 +1,6 @@
 import { fileNameFromComponents, nameAndExtension } from "@/next/file";
 import JSZip from "jszip";
-import { FILE_TYPE } from "./file";
+import { FILE_TYPE } from "./file-type";
 
 const potentialImageExtensions = [
     "heic",

+ 1 - 0
web/packages/media/package.json

@@ -4,6 +4,7 @@
     "private": true,
     "dependencies": {
         "@/next": "*",
+        "file-type": "16.5.4",
         "jszip": "^3.10"
     }
 }

+ 0 - 2
web/packages/shared/error/index.ts

@@ -48,8 +48,6 @@ export const CustomError = {
     SUBSCRIPTION_NEEDED: "subscription not present",
     NOT_FOUND: "not found ",
     NO_METADATA: "no metadata",
-    NOT_A_DATE: "not a date",
-    NOT_A_LOCATION: "not a location",
     FILE_ID_NOT_FOUND: "file with id not found",
     WEAK_DEVICE: "password decryption failed on the device",
     INCORRECT_PASSWORD: "incorrect password",

+ 1 - 1
web/yarn.lock

@@ -2505,7 +2505,7 @@ file-selector@^0.4.0:
   dependencies:
     tslib "^2.0.3"
 
-file-type@^16.5.4:
+file-type@16.5.4:
   version "16.5.4"
   resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd"
   integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==