diff --git a/web/apps/cast/src/services/detect-type.ts b/web/apps/cast/src/services/detect-type.ts index be4a51e15..4ff8801a4 100644 --- a/web/apps/cast/src/services/detect-type.ts +++ b/web/apps/cast/src/services/detect-type.ts @@ -1,5 +1,5 @@ import { KnownFileTypeInfos } from "@/media/file-type"; -import { nameAndExtension } from "@/next/file"; +import { lowercaseExtension } from "@/next/file"; import FileType from "file-type"; /** @@ -22,8 +22,7 @@ export const detectMediaMIMEType = async (file: File): Promise => { else throw new Error(`Detected MIME type ${mime} is not a media file`); } - let [, ext] = nameAndExtension(file.name); + const ext = lowercaseExtension(file.name); if (!ext) return undefined; - ext = ext.toLowerCase(); return KnownFileTypeInfos.find((f) => f.exactType == ext)?.mimeType; }; diff --git a/web/apps/photos/src/components/PhotoViewer/index.tsx b/web/apps/photos/src/components/PhotoViewer/index.tsx index 465c83367..8eabecf0d 100644 --- a/web/apps/photos/src/components/PhotoViewer/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/index.tsx @@ -10,13 +10,13 @@ import { EnteFile } from "types/file"; import { copyFileToClipboard, downloadSingleFile, - getFileExtension, getFileFromURL, isRawFile, isSupportedRawFormat, } from "utils/file"; import { FILE_TYPE } from "@/media/file-type"; +import { lowercaseExtension } from "@/next/file"; import { FlexWrapper } from "@ente/shared/components/Container"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import AlbumOutlined from "@mui/icons-material/AlbumOutlined"; @@ -348,7 +348,7 @@ function PhotoViewer(props: Iprops) { } function updateShowEditButton(file: EnteFile) { - const extension = getFileExtension(file.metadata.title); + const extension = lowercaseExtension(file.metadata.title); const isSupported = !isRawFile(extension) || isSupportedRawFormat(extension); setShowEditButton( @@ -611,9 +611,8 @@ function PhotoViewer(props: Iprops) { } } catch (e) { setExif({ key: file.src, value: null }); - const fileExtension = getFileExtension(file.metadata.title); log.error( - `checkExifAvailable failed for extension ${fileExtension}`, + `checkExifAvailable failed for file ${file.metadata.title}`, e, ); } diff --git a/web/apps/photos/src/services/export/migration.ts b/web/apps/photos/src/services/export/migration.ts index a6d0af29f..9404ddde5 100644 --- a/web/apps/photos/src/services/export/migration.ts +++ b/web/apps/photos/src/services/export/migration.ts @@ -1,6 +1,7 @@ import { FILE_TYPE } from "@/media/file-type"; import { decodeLivePhoto } from "@/media/live-photo"; import { ensureElectron } from "@/next/electron"; +import { nameAndExtension } from "@/next/file"; import log from "@/next/log"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { User } from "@ente/shared/user/types"; @@ -25,7 +26,6 @@ import { getIDBasedSortedFiles, getPersonalFiles, mergeMetadata, - splitFilenameAndExtension, } from "utils/file"; import { safeDirectoryName, @@ -501,9 +501,7 @@ const getUniqueFileExportNameForMigration = ( .get(collectionPath) ?.has(getFileSavePath(collectionPath, fileExportName)) ) { - const filenameParts = splitFilenameAndExtension( - sanitizeFilename(filename), - ); + const filenameParts = nameAndExtension(sanitizeFilename(filename)); if (filenameParts[1]) { fileExportName = `${filenameParts[0]}(${count}).${filenameParts[1]}`; } else { diff --git a/web/apps/photos/src/services/typeDetectionService.ts b/web/apps/photos/src/services/typeDetectionService.ts index f570c1c6a..9644e98cf 100644 --- a/web/apps/photos/src/services/typeDetectionService.ts +++ b/web/apps/photos/src/services/typeDetectionService.ts @@ -4,7 +4,7 @@ import { KnownNonMediaFileExtensions, type FileTypeInfo, } from "@/media/file-type"; -import { nameAndExtension } from "@/next/file"; +import { lowercaseExtension } from "@/next/file"; import { ElectronFile } from "@/next/types/file"; import { CustomError } from "@ente/shared/error"; import FileType, { type FileTypeResult } from "file-type"; @@ -63,7 +63,7 @@ export const detectFileTypeInfo = async ( mimeType: typeResult.mime, }; } catch (e) { - const [, extension] = nameAndExtension(fileOrPath.name); + const extension = lowercaseExtension(fileOrPath.name); const known = KnownFileTypeInfos.find((f) => f.exactType == extension); if (known) return known; diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 1bc15c6cc..5026162da 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -1,5 +1,6 @@ import { FILE_TYPE, type FileTypeInfo } from "@/media/file-type"; import { decodeLivePhoto } from "@/media/live-photo"; +import { lowercaseExtension } from "@/next/file"; import log from "@/next/log"; import { CustomErrorMessage, type Electron } from "@/next/types/ipc"; import { workerBridge } from "@/next/worker/worker-bridge"; @@ -41,8 +42,6 @@ import { writeStream } from "utils/native-stream"; const TYPE_HEIC = "heic"; const TYPE_HEIF = "heif"; -const TYPE_JPEG = "jpeg"; -const TYPE_JPG = "jpg"; const RAW_FORMATS = [ "heic", @@ -102,11 +101,11 @@ export async function getUpdatedEXIFFileForDownload( file: EnteFile, fileStream: ReadableStream, ): Promise> { - const extension = getFileExtension(file.metadata.title); + const extension = lowercaseExtension(file.metadata.title); if ( file.metadata.fileType === FILE_TYPE.IMAGE && file.pubMagicMetadata?.data.editedTime && - (extension === TYPE_JPEG || extension === TYPE_JPG) + (extension == "jpeg" || extension == "jpg") ) { const fileBlob = await new Response(fileStream).blob(); const updatedFileBlob = await updateFileCreationDateInEXIF( @@ -278,20 +277,6 @@ export async function decryptFile( } } -export function splitFilenameAndExtension(filename: string): [string, string] { - const lastDotPosition = filename.lastIndexOf("."); - if (lastDotPosition === -1) return [filename, null]; - else - return [ - filename.slice(0, lastDotPosition), - filename.slice(lastDotPosition + 1), - ]; -} - -export function getFileExtension(filename: string) { - return splitFilenameAndExtension(filename)[1]?.toLocaleLowerCase(); -} - export function generateStreamFromArrayBuffer(data: Uint8Array) { return new ReadableStream({ async start(controller: ReadableStreamDefaultController) { diff --git a/web/packages/media/live-photo.ts b/web/packages/media/live-photo.ts index 0e48d7120..5cf0291fa 100644 --- a/web/packages/media/live-photo.ts +++ b/web/packages/media/live-photo.ts @@ -1,4 +1,8 @@ -import { fileNameFromComponents, nameAndExtension } from "@/next/file"; +import { + fileNameFromComponents, + lowercaseExtension, + nameAndExtension, +} from "@/next/file"; import JSZip from "jszip"; import { FILE_TYPE } from "./file-type"; @@ -38,11 +42,9 @@ const potentialVideoExtensions = [ export const potentialFileTypeFromExtension = ( fileName: string, ): FILE_TYPE | undefined => { - let [, ext] = nameAndExtension(fileName); + const ext = lowercaseExtension(fileName); if (!ext) return undefined; - ext = ext.toLowerCase(); - if (potentialImageExtensions.includes(ext)) return FILE_TYPE.IMAGE; else if (potentialVideoExtensions.includes(ext)) return FILE_TYPE.VIDEO; else return undefined; diff --git a/web/packages/next/file.ts b/web/packages/next/file.ts index 02f936a18..aec755558 100644 --- a/web/packages/next/file.ts +++ b/web/packages/next/file.ts @@ -25,6 +25,18 @@ export const nameAndExtension = (fileName: string): FileNameComponents => { return [fileName.slice(0, i), fileName.slice(i + 1)]; }; +/** + * If the file 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); + return ext?.toLowerCase(); +}; /** * Construct a file name from its components (name and extension). *