Rejig type

This commit is contained in:
Manav Rathi 2024-04-25 09:54:54 +05:30
parent 2e7b12ad29
commit 5324d805c6
No known key found for this signature in database
10 changed files with 193 additions and 153 deletions

View file

@ -42,8 +42,8 @@ import { t } from "i18next";
import mime from "mime-types";
import { AppContext } from "pages/_app";
import { getLocalCollections } from "services/collectionService";
import { detectFileTypeInfo } from "services/detect-type";
import downloadManager from "services/download";
import { detectFileTypeInfo } from "services/typeDetectionService";
import uploadManager from "services/upload/uploadManager";
import { EnteFile } from "types/file";
import { FileWithCollection } from "types/upload";

View file

@ -43,10 +43,10 @@ import { t } from "i18next";
import isElectron from "is-electron";
import { AppContext } from "pages/_app";
import { GalleryContext } from "pages/gallery";
import { detectFileTypeInfo } from "services/detect-type";
import downloadManager, { LoadedLivePhotoSourceURL } from "services/download";
import { getParsedExifData } from "services/exif";
import { trashFiles } from "services/fileService";
import { detectFileTypeInfo } from "services/typeDetectionService";
import { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
import { isClipboardItemPresent } from "utils/common";
import { pauseVideo, playVideo } from "utils/photoFrame";

View file

@ -0,0 +1,101 @@
import {
FILE_TYPE,
KnownFileTypeInfos,
KnownNonMediaFileExtensions,
type FileTypeInfo,
} from "@/media/file-type";
import { lowercaseExtension } from "@/next/file";
import { CustomError } from "@ente/shared/error";
import FileType from "file-type";
import { getUint8ArrayView } from "./readerService";
/**
* Read the file's initial contents or use the file's name to detect its type.
*
* This function first reads an initial chunk of the file and tries to detect
* the file's {@link FileTypeInfo} from it. If that doesn't work, it then falls
* back to using the file's name to detect it.
*
* If neither of these two approaches work, it throws an exception.
*
* If we were able to detect the file type, but it is explicitly not a media
* (image or video) format that we support, this function throws an error with
* the message `CustomError.UNSUPPORTED_FILE_FORMAT`.
*
* @param file A {@link File} object
*
* @returns The detected {@link FileTypeInfo}.
*/
export const detectFileTypeInfo = async (file: File): Promise<FileTypeInfo> =>
detectFileTypeInfoFromChunk(() => readInitialChunkOfFile(file), file.name);
/**
* The lower layer implementation of the type detector.
*
* Usually, when the code already has a {@link File} object at hand, it is
* easier to use the higher level {@link detectFileTypeInfo} function.
*
* However, this lower level function is also exposed for use in cases like
* during upload where we might not have a File object and would like to provide
* the initial chunk of the file's contents in a different way.
*
* @param readInitialChunk A function to call to read the initial chunk of the
* file's data. There is no strict requirement for the size of the chunk this
* function should return, generally the first few KBs should be good.
*
* @param fileNameOrPath The full path or just the file name of the file whose
* type we're trying to determine. This is used by the fallback layer that tries
* to detect the type info from the file's extension.
*/
export const detectFileTypeInfoFromChunk = async (
readInitialChunk: () => Promise<Uint8Array>,
fileNameOrPath: string,
): Promise<FileTypeInfo> => {
try {
const typeResult = await detectFileTypeFromBuffer(
await readInitialChunk(),
);
const mimeType = typeResult.mime;
let fileType: FILE_TYPE;
if (mimeType.startsWith("image/")) {
fileType = FILE_TYPE.IMAGE;
} else if (mimeType.startsWith("video/")) {
fileType = FILE_TYPE.VIDEO;
} else {
throw new Error(CustomError.UNSUPPORTED_FILE_FORMAT);
}
return {
fileType,
// See https://github.com/sindresorhus/file-type/blob/main/core.d.ts
// for the full list of ext values.
extension: typeResult.ext,
mimeType,
};
} catch (e) {
const extension = lowercaseExtension(fileNameOrPath);
const known = KnownFileTypeInfos.find((f) => f.extension == extension);
if (known) return known;
if (KnownNonMediaFileExtensions.includes(extension))
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
throw e;
}
};
const readInitialChunkOfFile = async (file: File) => {
const chunkSizeForTypeDetection = 4100;
const chunk = file.slice(0, chunkSizeForTypeDetection);
return await getUint8ArrayView(chunk);
};
const detectFileTypeFromBuffer = async (buffer: Uint8Array) => {
const result = await FileType.fromBuffer(buffer);
if (!result?.ext || !result?.mime) {
throw Error(`Could not deduce file type from buffer`);
}
return result;
};

View file

@ -51,7 +51,7 @@ const generateVideoThumbnail = async (
* for the new files that the user is adding.
*
* @param dataOrPath The input video's data or the path to the video on the
* user's local filesystem. See: [Note: The fileOrPath parameter to upload].
* user's local filesystem. See: [Note: Reading a fileOrPath].
*
* @returns JPEG data of the generated thumbnail.
*

View file

@ -2,7 +2,7 @@ import { FILE_TYPE } from "@/media/file-type";
import log from "@/next/log";
import { validateAndGetCreationUnixTimeInMicroSeconds } from "@ente/shared/time";
import type { FixOption } from "components/FixCreationTime";
import { detectFileTypeInfo } from "services/typeDetectionService";
import { detectFileTypeInfo } from "services/detect-type";
import { EnteFile } from "types/file";
import {
changeFileCreationTime,

View file

@ -1,98 +0,0 @@
import {
FILE_TYPE,
KnownFileTypeInfos,
KnownNonMediaFileExtensions,
type FileTypeInfo,
} from "@/media/file-type";
import { lowercaseExtension } from "@/next/file";
import { ElectronFile } from "@/next/types/file";
import { CustomError } from "@ente/shared/error";
import FileType, { type FileTypeResult } from "file-type";
import { getUint8ArrayView } from "./readerService";
/**
* Read the file's initial contents or use the file's name to detect its type.
*
* This function first reads an initial chunk of the file and tries to detect
* the file's {@link FileTypeInfo} from it. If that doesn't work, it then falls
* back to using the file's name to detect it.
*
* If neither of these two approaches work, it throws an exception.
*
* If we were able to detect the file type, but it is explicitly not a media
* (image or video) format that we support, this function throws an error with
* the message `CustomError.UNSUPPORTED_FILE_FORMAT`.
*
* @param fileOrPath A {@link File} object, or the path to the file on the
* user's local filesystem. It is only valid to provide a path if we're running
* in the context of our desktop app.
*
* @returns The detected {@link FileTypeInfo}.
*/
export const detectFileTypeInfo = async (
fileOrPath: File | ElectronFile,
): Promise<FileTypeInfo> => {
try {
let fileType: FILE_TYPE;
let typeResult: FileTypeResult;
if (fileOrPath instanceof File) {
typeResult = await extractFileType(fileOrPath);
} else {
typeResult = await extractElectronFileType(fileOrPath);
}
const mimTypeParts: string[] = typeResult.mime?.split("/");
if (mimTypeParts?.length !== 2) {
throw Error(CustomError.INVALID_MIME_TYPE(typeResult.mime));
}
switch (mimTypeParts[0]) {
case "image":
fileType = FILE_TYPE.IMAGE;
break;
case "video":
fileType = FILE_TYPE.VIDEO;
break;
default:
throw new Error(CustomError.UNSUPPORTED_FILE_FORMAT);
}
return {
fileType,
extension: typeResult.ext,
mimeType: typeResult.mime,
};
} catch (e) {
const extension = lowercaseExtension(fileOrPath.name);
const known = KnownFileTypeInfos.find((f) => f.extension == extension);
if (known) return known;
if (KnownNonMediaFileExtensions.includes(extension))
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
throw e;
}
};
async function extractFileType(file: File) {
const chunkSizeForTypeDetection = 4100;
const fileBlobChunk = file.slice(0, chunkSizeForTypeDetection);
const fileDataChunk = await getUint8ArrayView(fileBlobChunk);
return getFileTypeFromBuffer(fileDataChunk);
}
async function extractElectronFileType(file: ElectronFile) {
const stream = await file.stream();
const reader = stream.getReader();
const { value: fileDataChunk } = await reader.read();
await reader.cancel();
return getFileTypeFromBuffer(fileDataChunk);
}
async function getFileTypeFromBuffer(buffer: Uint8Array) {
const result = await FileType.fromBuffer(buffer);
if (!result?.ext || !result?.mime) {
throw Error(`Could not deduce file type from buffer`);
}
return result;
}

View file

@ -47,8 +47,8 @@ import {
import { readStream } from "utils/native-stream";
import { hasFileHash } from "utils/upload";
import * as convert from "xml-js";
import { detectFileTypeInfo } from "../detect-type";
import { getFileStream } from "../readerService";
import { detectFileTypeInfo } from "../typeDetectionService";
import { extractAssetMetadata } from "./metadata";
import publicUploadHttpClient from "./publicUploadHttpClient";
import type { ParsedMetadataJSON } from "./takeout";
@ -175,14 +175,12 @@ export const uploader = async (
const { collection, localID, ...uploadAsset2 } = fileWithCollection;
/* TODO(MR): ElectronFile changes */
const uploadAsset = uploadAsset2 as UploadAsset;
let fileTypeInfo: FileTypeInfo;
let fileSize: number;
try {
/*
* We read the file three times:
* 1. To determine its MIME type (only needs first few KBs).
* 2. To calculate its hash.
* 3. To compute its thumbnail and then encrypt it.
* 3. To encrypt it.
*
* When we already have a File object the multiple reads are fine. When
* we're in the context of our desktop app and have a path, it might be
@ -192,13 +190,15 @@ export const uploader = async (
* manner (tee will not work for strictly sequential reads of large
* streams).
*/
const maxFileSize = 4 * 1024 * 1024 * 1024; // 4 GB
fileSize = getAssetSize(uploadAsset);
if (fileSize >= maxFileSize) {
const { fileTypeInfo, fileSize } =
await readFileTypeInfoAndSize(uploadAsset);
const maxFileSize = 4 * 1024 * 1024 * 1024; /* 4 GB */
if (fileSize >= maxFileSize)
return { fileUploadResult: UPLOAD_RESULT.TOO_LARGE };
}
fileTypeInfo = await getAssetFileType(uploadAsset);
abortIfCancelled();
const { metadata, publicMagicMetadata } = await extractAssetMetadata(
worker,
@ -313,9 +313,6 @@ export const uploader = async (
export const getFileName = (file: File | ElectronFile | string) =>
typeof file == "string" ? basename(file) : file.name;
function getFileSize(file: File | ElectronFile) {
return file.size;
}
export const getAssetName = ({
isLivePhoto,
file,
@ -330,41 +327,10 @@ export const assetName = ({
}: UploadAsset2) =>
isLivePhoto ? getFileName(livePhotoAssets.image) : getFileName(file);
const getAssetSize = ({ isLivePhoto, file, livePhotoAssets }: UploadAsset) => {
return isLivePhoto ? getLivePhotoSize(livePhotoAssets) : getFileSize(file);
};
const getLivePhotoSize = (livePhotoAssets: LivePhotoAssets) => {
return livePhotoAssets.image.size + livePhotoAssets.video.size;
};
const getAssetFileType = ({
isLivePhoto,
file,
livePhotoAssets,
}: UploadAsset) => {
return isLivePhoto
? getLivePhotoFileType(livePhotoAssets)
: detectFileTypeInfo(file);
};
const getLivePhotoFileType = async (
livePhotoAssets: LivePhotoAssets,
): Promise<FileTypeInfo> => {
const imageFileTypeInfo = await detectFileTypeInfo(livePhotoAssets.image);
const videoFileTypeInfo = await detectFileTypeInfo(livePhotoAssets.video);
return {
fileType: FILE_TYPE.LIVE_PHOTO,
extension: `${imageFileTypeInfo.extension}+${videoFileTypeInfo.extension}`,
imageType: imageFileTypeInfo.extension,
videoType: videoFileTypeInfo.extension,
};
};
/**
* Read the given file or path into an in-memory representation.
*
* [Note: The fileOrPath parameter to upload]
* See: [Note: Reading a fileOrPath]
*
* The file can be either a web
* [File](https://developer.mozilla.org/en-US/docs/Web/API/File) or the absolute
@ -437,6 +403,75 @@ const readFileOrPath = async (
return { dataOrStream, fileSize };
};
/**
* Read the beginning of the file or use its filename to determine its MIME
* type. Use that to construct and return a {@link FileTypeInfo}.
*
* While we're at it, also return the size of the file.
*
* @param fileOrPath See: [Note: Reading a fileOrPath]
*/
const readFileTypeInfoAndSize = async (
fileOrPath: File | string,
): Promise<{ fileTypeInfo: FileTypeInfo; fileSize: number }> => {
const { dataOrStream, fileSize } = await readFileOrPath(fileOrPath);
function getFileSize(file: File | ElectronFile) {
return file.size;
}
async function extractElectronFileType(file: ElectronFile) {
const stream = await file.stream();
const reader = stream.getReader();
const { value: fileDataChunk } = await reader.read();
await reader.cancel();
return getFileTypeFromBuffer(fileDataChunk);
}
fileSize = getAssetSize(uploadAsset);
fileTypeInfo = await getAssetFileType(uploadAsset);
const getAssetSize = ({
isLivePhoto,
file,
livePhotoAssets,
}: UploadAsset) => {
return isLivePhoto
? getLivePhotoSize(livePhotoAssets)
: getFileSize(file);
};
const getLivePhotoSize = (livePhotoAssets: LivePhotoAssets) => {
return livePhotoAssets.image.size + livePhotoAssets.video.size;
};
const getAssetFileType = ({
isLivePhoto,
file,
livePhotoAssets,
}: UploadAsset) => {
return isLivePhoto
? getLivePhotoFileType(livePhotoAssets)
: detectFileTypeInfo(file);
};
const getLivePhotoFileType = async (
livePhotoAssets: LivePhotoAssets,
): Promise<FileTypeInfo> => {
const imageFileTypeInfo = await detectFileTypeInfo(
livePhotoAssets.image,
);
const videoFileTypeInfo = await detectFileTypeInfo(
livePhotoAssets.video,
);
return {
fileType: FILE_TYPE.LIVE_PHOTO,
extension: `${imageFileTypeInfo.extension}+${videoFileTypeInfo.extension}`,
imageType: imageFileTypeInfo.extension,
videoType: videoFileTypeInfo.extension,
};
};
};
const readAsset = async (
fileTypeInfo: FileTypeInfo,
{ isLivePhoto, file, livePhotoAssets }: UploadAsset2,

View file

@ -11,6 +11,7 @@ import { downloadUsingAnchor, withTimeout } from "@ente/shared/utils";
import { t } from "i18next";
import isElectron from "is-electron";
import { moveToHiddenCollection } from "services/collectionService";
import { detectFileTypeInfo } from "services/detect-type";
import DownloadManager from "services/download";
import { updateFileCreationDateInEXIF } from "services/exif";
import {
@ -20,7 +21,6 @@ import {
updateFilePublicMagicMetadata,
} from "services/fileService";
import { heicToJPEG } from "services/heic-convert";
import { detectFileTypeInfo } from "services/typeDetectionService";
import {
EncryptedEnteFile,
EnteFile,

View file

@ -11,9 +11,6 @@ export interface FileTypeInfo {
* A lowercased, standardized extension for files of the current type.
*
* TODO(MR): This in not valid for LIVE_PHOTO.
*
* See https://github.com/sindresorhus/file-type/blob/main/core.d.ts for the
* full list of values this property can have.
*/
extension: string;
mimeType?: string;

View file

@ -26,17 +26,22 @@ export const nameAndExtension = (fileName: string): FileNameComponents => {
};
/**
* If the file has an extension, return a lowercased version of it.
* If the file name or path has an extension, return a lowercased version of it.
*
* This is handy when comparing the extension to a known set without worrying
* about case sensitivity.
*
* See {@link nameAndExtension} for its more generic sibling.
*/
export const lowercaseExtension = (fileName: string): string | undefined => {
const [, ext] = nameAndExtension(fileName);
export const lowercaseExtension = (
fileNameOrPath: string,
): string | undefined => {
// We rely on the implementation of nameAndExtension using lastIndexOf to
// allow us to also work on paths.
const [, ext] = nameAndExtension(fileNameOrPath);
return ext?.toLowerCase();
};
/**
* Construct a file name from its components (name and extension).
*