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

This commit is contained in:
Manav Rathi 2024-04-24 18:51:43 +05:30 committed by GitHub
commit c684b5fd69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 202 additions and 312 deletions

View file

@ -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"];

View file

@ -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";

View file

@ -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";

View file

@ -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;
};

View file

@ -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;
}

View file

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

View file

@ -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);

View file

@ -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",

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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 {

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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;

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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],

View file

@ -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";

View file

@ -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";

View file

@ -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",

View file

@ -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";

View file

@ -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 {

View file

@ -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";

View file

@ -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) {

View file

@ -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,

View file

@ -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 */

View file

@ -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();

View file

@ -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,

View file

@ -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;

View file

@ -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";

View file

@ -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;

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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

View file

@ -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"];

View file

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

View file

@ -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",

View file

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

View file

@ -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",

View file

@ -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==