[web] Upload refactoring - Part x/x (#1531)
This commit is contained in:
commit
c684b5fd69
46 changed files with 202 additions and 312 deletions
|
@ -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,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,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
web/apps/cast/src/services/detect-type.ts
Normal file
29
web/apps/cast/src/services/detect-type.ts
Normal 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;
|
||||
};
|
|
@ -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,4 +1,4 @@
|
|||
import { FILE_TYPE } from "@/media/file";
|
||||
import { FILE_TYPE } from "@/media/file-type";
|
||||
|
||||
export interface Metadata {
|
||||
title: string;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,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,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";
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,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,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,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,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,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,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,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";
|
||||
|
|
|
@ -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,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,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,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,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,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,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";
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,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 */
|
||||
|
|
|
@ -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();
|
|
@ -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,
|
||||
|
|
|
@ -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,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,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;
|
||||
|
|
|
@ -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,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,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,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";
|
||||
|
|
|
@ -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
web/packages/media/file-type.ts
Normal file
58
web/packages/media/file-type.ts
Normal 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"];
|
|
@ -1,6 +0,0 @@
|
|||
export enum FILE_TYPE {
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
LIVE_PHOTO,
|
||||
OTHERS,
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@/next": "*",
|
||||
"file-type": "16.5.4",
|
||||
"jszip": "^3.10"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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==
|
||||
|
|
Loading…
Add table
Reference in a new issue