Rejig type
This commit is contained in:
parent
2e7b12ad29
commit
5324d805c6
10 changed files with 193 additions and 153 deletions
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
101
web/apps/photos/src/services/detect-type.ts
Normal file
101
web/apps/photos/src/services/detect-type.ts
Normal 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;
|
||||
};
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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).
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue