diff --git a/desktop/src/main/fs.ts b/desktop/src/main/fs.ts index 0da89fb00..8d9282034 100644 --- a/desktop/src/main/fs.ts +++ b/desktop/src/main/fs.ts @@ -8,6 +8,16 @@ import { Readable } from "node:stream"; export const fsExists = (path: string) => existsSync(path); +export const fsRename = (oldPath: string, newPath: string) => + fs.rename(oldPath, newPath); + +export const fsMkdirIfNeeded = (dirPath: string) => + fs.mkdir(dirPath, { recursive: true }); + +export const fsRmdir = (path: string) => fs.rmdir(path); + +export const fsRm = (path: string) => fs.rm(path); + /** * Write a (web) ReadableStream to a file at the given {@link filePath}. * @@ -73,9 +83,6 @@ const writeNodeStream = async ( /* TODO: Audit below this */ -export const checkExistsAndCreateDir = (dirPath: string) => - fs.mkdir(dirPath, { recursive: true }); - export const saveStreamToDisk = writeStream; export const saveFileToDisk = (path: string, contents: string) => @@ -85,9 +92,6 @@ export const readTextFile = async (filePath: string) => fs.readFile(filePath, "utf-8"); export const moveFile = async (sourcePath: string, destinationPath: string) => { - if (!existsSync(sourcePath)) { - throw new Error("File does not exist"); - } if (existsSync(destinationPath)) { throw new Error("Destination file already exists"); } @@ -102,32 +106,3 @@ export const isFolder = async (dirPath: string) => { const stats = await fs.stat(dirPath); return stats.isDirectory(); }; - -export const deleteFolder = async (folderPath: string) => { - // Ensure it is folder - if (!isFolder(folderPath)) return; - - // Ensure folder is empty - const files = await fs.readdir(folderPath); - if (files.length > 0) throw new Error("Folder is not empty"); - - // rm -rf it - await fs.rmdir(folderPath); -}; - -export const rename = async (oldPath: string, newPath: string) => { - if (!existsSync(oldPath)) throw new Error("Path does not exist"); - await fs.rename(oldPath, newPath); -}; - -export const deleteFile = async (filePath: string) => { - // Ensure it exists - if (!existsSync(filePath)) return; - - // And is a file - const stat = await fs.stat(filePath); - if (!stat.isFile()) throw new Error("Path is not a file"); - - // rm it - return fs.rm(filePath); -}; diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 180e68cdc..12bcc72bf 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -18,14 +18,14 @@ import { showUploadZipDialog, } from "./dialogs"; import { - checkExistsAndCreateDir, - deleteFile, - deleteFolder, + fsRm, + fsRmdir, fsExists, + fsMkdirIfNeeded, + fsRename, isFolder, moveFile, readTextFile, - rename, saveFileToDisk, saveStreamToDisk, } from "./fs"; @@ -169,12 +169,18 @@ export const attachIPCHandlers = () => { ipcMain.handle("fsExists", (_, path) => fsExists(path)); - // - FS Legacy - - ipcMain.handle("checkExistsAndCreateDir", (_, dirPath) => - checkExistsAndCreateDir(dirPath), + ipcMain.handle("fsRename", (_, oldPath: string, newPath: string) => + fsRename(oldPath, newPath), ); + ipcMain.handle("fsMkdirIfNeeded", (_, dirPath) => fsMkdirIfNeeded(dirPath)); + + ipcMain.handle("fsRmdir", (_, path: string) => fsRmdir(path)); + + ipcMain.handle("fsRm", (_, path: string) => fsRm(path)); + + // - FS Legacy + ipcMain.handle( "saveStreamToDisk", (_, path: string, fileStream: ReadableStream) => @@ -193,14 +199,6 @@ export const attachIPCHandlers = () => { moveFile(oldPath, newPath), ); - ipcMain.handle("deleteFolder", (_, path: string) => deleteFolder(path)); - - ipcMain.handle("deleteFile", (_, path: string) => deleteFile(path)); - - ipcMain.handle("rename", (_, oldPath: string, newPath: string) => - rename(oldPath, newPath), - ); - // - Upload ipcMain.handle("getPendingUploads", () => getPendingUploads()); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 2db39e229..d33e2b675 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -99,6 +99,12 @@ const skipAppUpdate = (version: string) => { const fsExists = (path: string): Promise => ipcRenderer.invoke("fsExists", path); +const fsMkdirIfNeeded = (dirPath: string): Promise => + ipcRenderer.invoke("fsMkdirIfNeeded", dirPath); + +const fsRename = (oldPath: string, newPath: string): Promise => + ipcRenderer.invoke("fsRename", oldPath, newPath); + // - AUDIT below this // - Conversion @@ -218,9 +224,6 @@ const updateWatchMappingIgnoredFiles = ( // - FS Legacy -const checkExistsAndCreateDir = (dirPath: string): Promise => - ipcRenderer.invoke("checkExistsAndCreateDir", dirPath); - const saveStreamToDisk = ( path: string, fileStream: ReadableStream, @@ -238,14 +241,10 @@ const isFolder = (dirPath: string): Promise => const moveFile = (oldPath: string, newPath: string): Promise => ipcRenderer.invoke("moveFile", oldPath, newPath); -const deleteFolder = (path: string): Promise => - ipcRenderer.invoke("deleteFolder", path); +const fsRmdir = (path: string): Promise => + ipcRenderer.invoke("fsRmdir", path); -const deleteFile = (path: string): Promise => - ipcRenderer.invoke("deleteFile", path); - -const rename = (oldPath: string, newPath: string): Promise => - ipcRenderer.invoke("rename", oldPath, newPath); +const fsRm = (path: string): Promise => ipcRenderer.invoke("fsRm", path); // - Upload @@ -348,19 +347,19 @@ contextBridge.exposeInMainWorld("electron", { // - FS fs: { exists: fsExists, + rename: fsRename, + mkdirIfNeeded: fsMkdirIfNeeded, + rmdir: fsRmdir, + rm: fsRm, }, // - FS legacy // TODO: Move these into fs + document + rename if needed - checkExistsAndCreateDir, saveStreamToDisk, saveFileToDisk, readTextFile, isFolder, moveFile, - deleteFolder, - deleteFile, - rename, // - Upload diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index 5e956b7e2..a09cd1eae 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -166,13 +166,14 @@ class ExportService { } async changeExportDirectory() { + const electron = ensureElectron(); try { - const newRootDir = await ensureElectron().selectDirectory(); + const newRootDir = await electron.selectDirectory(); if (!newRootDir) { throw Error(CustomError.SELECT_FOLDER_ABORTED); } const newExportDir = `${newRootDir}/${exportDirectoryName}`; - await ensureElectron().checkExistsAndCreateDir(newExportDir); + await electron.fs.mkdirIfNeeded(newExportDir); return newExportDir; } catch (e) { if (e.message !== CustomError.SELECT_FOLDER_ABORTED) { @@ -485,6 +486,7 @@ class ExportService { renamedCollections: Collection[], isCanceled: CancellationStatus, ) { + const fs = ensureElectron().fs; try { for (const collection of renamedCollections) { try { @@ -498,6 +500,7 @@ class ExportService { const newCollectionExportName = await safeDirectoryName( exportFolder, getCollectionUserFacingName(collection), + fs.exists, ); log.info( `renaming collection with id ${collection.id} from ${oldCollectionExportName} to ${newCollectionExportName}`, @@ -513,7 +516,7 @@ class ExportService { newCollectionExportName, ); try { - await ensureElectron().rename( + await fs.rename( oldCollectionExportPath, newCollectionExportPath, ); @@ -561,6 +564,7 @@ class ExportService { exportFolder: string, isCanceled: CancellationStatus, ) { + const fs = ensureElectron().fs; try { const exportRecord = await this.getExportRecord(exportFolder); const collectionIDPathMap = @@ -595,13 +599,11 @@ class ExportService { ); try { // delete the collection metadata folder - await ensureElectron().deleteFolder( + await fs.rmdir( getMetadataFolderExportPath(collectionExportPath), ); // delete the collection folder - await ensureElectron().deleteFolder( - collectionExportPath, - ); + await fs.rmdir(collectionExportPath); } catch (e) { await this.addCollectionExportedRecord( exportFolder, @@ -646,6 +648,7 @@ class ExportService { incrementFailed: () => void, isCanceled: CancellationStatus, ): Promise { + const fs = ensureElectron().fs; try { for (const file of files) { log.info( @@ -681,10 +684,8 @@ class ExportService { ); } const collectionExportPath = `${exportDir}/${collectionExportName}`; - await ensureElectron().checkExistsAndCreateDir( - collectionExportPath, - ); - await ensureElectron().checkExistsAndCreateDir( + await fs.mkdirIfNeeded(collectionExportPath); + await fs.mkdirIfNeeded( getMetadataFolderExportPath(collectionExportPath), ); await this.downloadAndSave( @@ -731,6 +732,8 @@ class ExportService { removedFileUIDs: string[], isCanceled: CancellationStatus, ): Promise { + const electron = ensureElectron(); + const fs = electron.fs; try { const exportRecord = await this.getExportRecord(exportDir); const fileIDExportNameMap = convertFileIDExportNameObjectToMap( @@ -760,8 +763,8 @@ class ExportService { log.info( `moving image file ${imageExportPath} to trash folder`, ); - if (await this.exists(imageExportPath)) { - await ensureElectron().moveFile( + if (await fs.exists(imageExportPath)) { + await electron.moveFile( imageExportPath, await getTrashedFileExportPath( exportDir, @@ -773,10 +776,8 @@ class ExportService { const imageMetadataFileExportPath = getMetadataFileExportPath(imageExportPath); - if ( - await this.exists(imageMetadataFileExportPath) - ) { - await ensureElectron().moveFile( + if (await fs.exists(imageMetadataFileExportPath)) { + await electron.moveFile( imageMetadataFileExportPath, await getTrashedFileExportPath( exportDir, @@ -786,31 +787,7 @@ class ExportService { } const videoExportPath = `${collectionExportPath}/${videoExportName}`; - log.info( - `moving video file ${videoExportPath} to trash folder`, - ); - if (await this.exists(videoExportPath)) { - await ensureElectron().moveFile( - videoExportPath, - await getTrashedFileExportPath( - exportDir, - videoExportPath, - ), - ); - } - const videoMetadataFileExportPath = - getMetadataFileExportPath(videoExportPath); - if ( - await this.exists(videoMetadataFileExportPath) - ) { - await ensureElectron().moveFile( - videoMetadataFileExportPath, - await getTrashedFileExportPath( - exportDir, - videoMetadataFileExportPath, - ), - ); - } + await moveToTrash(exportDir, videoExportPath); } else { const fileExportPath = `${collectionExportPath}/${fileExportName}`; const trashedFilePath = @@ -821,16 +798,16 @@ class ExportService { log.info( `moving file ${fileExportPath} to ${trashedFilePath} trash folder`, ); - if (await this.exists(fileExportPath)) { - await ensureElectron().moveFile( + if (await fs.exists(fileExportPath)) { + await electron.moveFile( fileExportPath, trashedFilePath, ); } const metadataFileExportPath = getMetadataFileExportPath(fileExportPath); - if (await this.exists(metadataFileExportPath)) { - await ensureElectron().moveFile( + if (await fs.exists(metadataFileExportPath)) { + await electron.moveFile( metadataFileExportPath, await getTrashedFileExportPath( exportDir, @@ -984,14 +961,16 @@ class ExportService { } async getExportRecord(folder: string, retry = true): Promise { + const electron = ensureElectron(); + const fs = electron.fs; try { await this.verifyExportFolderExists(folder); const exportRecordJSONPath = `${folder}/${exportRecordFileName}`; - if (!(await this.exists(exportRecordJSONPath))) { + if (!(await fs.exists(exportRecordJSONPath))) { return this.createEmptyExportRecord(exportRecordJSONPath); } const recordFile = - await ensureElectron().readTextFile(exportRecordJSONPath); + await electron.readTextFile(exportRecordJSONPath); try { return JSON.parse(recordFile); } catch (e) { @@ -1017,15 +996,17 @@ class ExportService { collectionID: number, collectionIDNameMap: Map, ) { + const fs = ensureElectron().fs; await this.verifyExportFolderExists(exportFolder); const collectionName = collectionIDNameMap.get(collectionID); const collectionExportName = await safeDirectoryName( exportFolder, collectionName, + fs.exists, ); const collectionExportPath = `${exportFolder}/${collectionExportName}`; - await ensureElectron().checkExistsAndCreateDir(collectionExportPath); - await ensureElectron().checkExistsAndCreateDir( + await fs.mkdirIfNeeded(collectionExportPath); + await fs.mkdirIfNeeded( getMetadataFolderExportPath(collectionExportPath), ); @@ -1037,6 +1018,7 @@ class ExportService { collectionExportPath: string, file: EnteFile, ): Promise { + const electron = ensureElectron(); try { const fileUID = getExportRecordFileUID(file); const originalFileStream = await downloadManager.getFile(file); @@ -1060,6 +1042,7 @@ class ExportService { const fileExportName = await safeFileName( collectionExportPath, file.metadata.title, + electron.fs.exists, ); await this.addFileExportedRecord( exportDir, @@ -1072,7 +1055,7 @@ class ExportService { fileExportName, file, ); - await ensureElectron().saveStreamToDisk( + await electron.saveStreamToDisk( `${collectionExportPath}/${fileExportName}`, updatedFileStream, ); @@ -1094,15 +1077,18 @@ class ExportService { fileStream: ReadableStream, file: EnteFile, ) { + const electron = ensureElectron(); const fileBlob = await new Response(fileStream).blob(); const livePhoto = await decodeLivePhoto(file, fileBlob); const imageExportName = await safeFileName( collectionExportPath, livePhoto.imageNameTitle, + electron.fs.exists, ); const videoExportName = await safeFileName( collectionExportPath, livePhoto.videoNameTitle, + electron.fs.exists, ); const livePhotoExportName = getLivePhotoExportName( imageExportName, @@ -1120,7 +1106,7 @@ class ExportService { imageExportName, file, ); - await ensureElectron().saveStreamToDisk( + await electron.saveStreamToDisk( `${collectionExportPath}/${imageExportName}`, imageStream, ); @@ -1132,12 +1118,12 @@ class ExportService { file, ); try { - await ensureElectron().saveStreamToDisk( + await electron.saveStreamToDisk( `${collectionExportPath}/${videoExportName}`, videoStream, ); } catch (e) { - await ensureElectron().deleteFile( + await electron.fs.rm( `${collectionExportPath}/${imageExportName}`, ); throw e; @@ -1163,20 +1149,8 @@ class ExportService { return this.exportInProgress; }; - exists = (path: string) => { - return ensureElectron().fs.exists(path); - }; - - rename = (oldPath: string, newPath: string) => { - return ensureElectron().rename(oldPath, newPath); - }; - - checkExistsAndCreateDir = (path: string) => { - return ensureElectron().checkExistsAndCreateDir(path); - }; - exportFolderExists = async (exportFolder: string) => { - return exportFolder && (await this.exists(exportFolder)); + return exportFolder && (await ensureElectron().fs.exists(exportFolder)); }; private verifyExportFolderExists = async (exportFolder: string) => { @@ -1412,7 +1386,7 @@ const getTrashedFileExportPath = async (exportDir: string, path: string) => { const fileRelativePath = path.replace(`${exportDir}/`, ""); let trashedFilePath = `${exportDir}/${exportTrashDirectoryName}/${fileRelativePath}`; let count = 1; - while (await exportService.exists(trashedFilePath)) { + while (await ensureElectron().fs.exists(trashedFilePath)) { const trashedFilePathParts = splitFilenameAndExtension(trashedFilePath); if (trashedFilePathParts[1]) { trashedFilePath = `${trashedFilePathParts[0]}(${count}).${trashedFilePathParts[1]}`; @@ -1460,3 +1434,25 @@ const parseLivePhotoExportName = ( const isExportInProgress = (exportStage: ExportStage) => exportStage > ExportStage.INIT && exportStage < ExportStage.FINISHED; + +const moveToTrash = async (exportDir: string, videoExportPath: string) => { + const fs = ensureElectron().fs; + log.info(`moving video file ${videoExportPath} to trash folder`); + if (await fs.exists(videoExportPath)) { + await electron.moveFile( + videoExportPath, + await getTrashedFileExportPath(exportDir, videoExportPath), + ); + } + const videoMetadataFileExportPath = + getMetadataFileExportPath(videoExportPath); + if (await fs.exists(videoMetadataFileExportPath)) { + await electron.moveFile( + videoMetadataFileExportPath, + await getTrashedFileExportPath( + exportDir, + videoMetadataFileExportPath, + ), + ); + } +}; diff --git a/web/apps/photos/src/services/export/migration.ts b/web/apps/photos/src/services/export/migration.ts index 71c4937d1..b90c12e1c 100644 --- a/web/apps/photos/src/services/export/migration.ts +++ b/web/apps/photos/src/services/export/migration.ts @@ -1,3 +1,4 @@ +import { ensureElectron } from "@/next/electron"; import log from "@/next/log"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { User } from "@ente/shared/user/types"; @@ -188,40 +189,29 @@ async function migrationV4ToV5(exportDir: string, exportRecord: ExportRecord) { await removeCollectionExportMissingMetadataFolder(exportDir, exportRecord); } -/* - This updates the folder name of already exported folders from the earlier format of - `collectionID_collectionName` to newer `collectionName(numbered)` format -*/ -async function migrateCollectionFolders( +/** + * Update the folder name of already exported folders from the earlier format of + * `collectionID_collectionName` to newer `collectionName(numbered)` format. + */ +const migrateCollectionFolders = async ( collections: Collection[], exportDir: string, collectionIDPathMap: Map, -) { +) => { + const fs = ensureElectron().fs; for (const collection of collections) { - const oldCollectionExportPath = getOldCollectionFolderPath( - exportDir, - collection.id, - collection.name, - ); - const newCollectionExportPath = await safeDirectoryName( + const oldPath = `${exportDir}/${collection.id}_${oldSanitizeName(collection.name)}`; + const newPath = await safeDirectoryName( exportDir, collection.name, + fs.exists, ); - collectionIDPathMap.set(collection.id, newCollectionExportPath); - if (!(await exportService.exists(oldCollectionExportPath))) { - continue; - } - await exportService.rename( - oldCollectionExportPath, - newCollectionExportPath, - ); - await addCollectionExportedRecordV1( - exportDir, - collection.id, - newCollectionExportPath, - ); + collectionIDPathMap.set(collection.id, newPath); + if (!(await fs.exists(oldPath))) continue; + await fs.rename(oldPath, newPath); + await addCollectionExportedRecordV1(exportDir, collection.id, newPath); } -} +}; /* This updates the file name of already exported files from the earlier format of @@ -231,6 +221,7 @@ async function migrateFiles( files: EnteFile[], collectionIDPathMap: Map, ) { + const fs = ensureElectron().fs; for (const file of files) { const collectionPath = collectionIDPathMap.get(file.collectionID); const metadataPath = `${collectionPath}/${exportMetadataDirectoryName}`; @@ -242,14 +233,15 @@ async function migrateFiles( const newFileName = await safeFileName( collectionPath, file.metadata.title, + fs.exists, ); const newFilePath = `${collectionPath}/${newFileName}`; const newFileMetadataPath = `${metadataPath}/${newFileName}.json`; - if (!(await exportService.exists(oldFilePath))) continue; + if (!(await fs.exists(oldFilePath))) continue; - await exportService.rename(oldFilePath, newFilePath); - await exportService.rename(oldFileMetadataPath, newFileMetadataPath); + await fs.rename(oldFilePath, newFilePath); + await fs.rename(oldFileMetadataPath, newFileMetadataPath); } } @@ -409,6 +401,7 @@ async function removeCollectionExportMissingMetadataFolder( exportDir: string, exportRecord: ExportRecord, ) { + const fs = ensureElectron().fs; if (!exportRecord?.collectionExportNames) { return; } @@ -422,7 +415,7 @@ async function removeCollectionExportMissingMetadataFolder( collectionExportName, ] of properlyExportedCollectionsAll) { if ( - await exportService.exists( + await fs.exists( getMetadataFolderExportPath( `${exportDir}/${collectionExportName}`, ), @@ -493,12 +486,6 @@ const oldSanitizeName = (name: string) => const getFileSavePath = (collectionFolderPath: string, fileSaveName: string) => `${collectionFolderPath}/${fileSaveName}`; -const getOldCollectionFolderPath = ( - dir: string, - collectionID: number, - collectionName: string, -) => `${dir}/${collectionID}_${oldSanitizeName(collectionName)}`; - const getUniqueFileExportNameForMigration = ( collectionPath: string, filename: string, diff --git a/web/apps/photos/src/utils/collection/index.ts b/web/apps/photos/src/utils/collection/index.ts index b57f9799f..9b92d794a 100644 --- a/web/apps/photos/src/utils/collection/index.ts +++ b/web/apps/photos/src/utils/collection/index.ts @@ -1,3 +1,4 @@ +import { ensureElectron } from "@/next/electron"; import log from "@/next/log"; import { CustomError } from "@ente/shared/error"; import { getAlbumsURL } from "@ente/shared/network/api"; @@ -30,7 +31,6 @@ import { updatePublicCollectionMagicMetadata, updateSharedCollectionMagicMetadata, } from "services/collectionService"; -import exportService from "services/export"; import { getAllLocalFiles, getLocalFiles } from "services/fileService"; import { COLLECTION_ROLE, @@ -169,12 +169,14 @@ async function createCollectionDownloadFolder( downloadDirPath: string, collectionName: string, ) { + const fs = ensureElectron().fs; const collectionDownloadName = await safeDirectoryName( downloadDirPath, collectionName, + fs.exists, ); const collectionDownloadPath = `${downloadDirPath}/${collectionDownloadName}`; - await exportService.checkExistsAndCreateDir(collectionDownloadPath); + await fs.mkdirIfNeeded(collectionDownloadPath); return collectionDownloadPath; } diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 42b859772..8f72cb450 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -815,6 +815,7 @@ async function downloadFileDesktop( const imageExportName = await safeFileName( downloadPath, livePhoto.imageNameTitle, + electron.fs.exists, ); const imageStream = generateStreamFromArrayBuffer(livePhoto.image); await electron.saveStreamToDisk( @@ -825,6 +826,7 @@ async function downloadFileDesktop( const videoExportName = await safeFileName( downloadPath, livePhoto.videoNameTitle, + electron.fs.exists, ); const videoStream = generateStreamFromArrayBuffer(livePhoto.video); await electron.saveStreamToDisk( @@ -832,13 +834,14 @@ async function downloadFileDesktop( videoStream, ); } catch (e) { - await electron.deleteFile(`${downloadPath}/${imageExportName}`); + await electron.fs.rm(`${downloadPath}/${imageExportName}`); throw e; } } else { const fileExportName = await safeFileName( downloadPath, file.metadata.title, + electron.fs.exists, ); await electron.saveStreamToDisk( `${downloadPath}/${fileExportName}`, diff --git a/web/apps/photos/src/utils/native-fs.ts b/web/apps/photos/src/utils/native-fs.ts index c35c0cd8f..2ef896302 100644 --- a/web/apps/photos/src/utils/native-fs.ts +++ b/web/apps/photos/src/utils/native-fs.ts @@ -1,12 +1,10 @@ /** - * @file Native filesystem access using custom Node.js functionality provided by - * our desktop app. + * @file Utilities for native filesystem access. * - * Precondition: Unless mentioned otherwise, the functions in these file only - * work when we are running in our desktop app. + * While they don't have any direct dependencies to our desktop app, they were + * written for use by the code that runs in our desktop app. */ -import { ensureElectron } from "@/next/electron"; import { nameAndExtension } from "@/next/file"; import sanitize from "sanitize-filename"; import { @@ -31,15 +29,15 @@ export const sanitizeFilename = (s: string) => * We also ensure we don't return names which might collide with our own special * directories. * - * This function only works when we are running inside an electron app (since it - * requires permissionless access to the native filesystem to find a new - * filename that doesn't conflict with any existing items). + * @param exists A function to check if an item already exists at the given + * path. Usually, you'd pass `fs.exists` from {@link Electron}. * - * See also: {@link safeDirectoryName} + * See also: {@link safeFileame} */ export const safeDirectoryName = async ( directoryPath: string, name: string, + exists: (path: string) => Promise, ): Promise => { const specialDirectoryNames = [ exportTrashDirectoryName, @@ -62,10 +60,13 @@ export const safeDirectoryName = async ( * Return a new sanitized and unique file name based on {@link name} that is not * the same as any existing item in the given {@link directoryPath}. * - * This function only works when we are running inside an electron app. - * @see {@link safeDirectoryName}. + * This is a sibling of {@link safeDirectoryName} for use with file names. */ -export const safeFileName = async (directoryPath: string, name: string) => { +export const safeFileName = async ( + directoryPath: string, + name: string, + exists: (path: string) => Promise, +) => { let result = sanitizeFilename(name); let count = 1; while (await exists(`${directoryPath}/${result}`)) { @@ -76,9 +77,3 @@ export const safeFileName = async (directoryPath: string, name: string) => { } return result; }; - -/** - * Return true if an item exists an the given {@link path} on the user's local - * filesystem. - */ -export const exists = (path: string) => ensureElectron().fs.exists(path); diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 82e4c2eb1..301a95191 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -155,15 +155,39 @@ export interface Electron { * or watching some folders for changes and syncing them automatically. * * Towards this end, this fs object provides some generic file system access - * functions that are needed for such features. In addition, there are other - * feature specific methods too in the top level electron object. + * functions that are needed for such features (in some cases, there are + * other feature specific methods too in the top level electron object). */ fs: { - /** - * Return true if there is a file or directory at the given - * {@link path}. - */ + /** Return true if there is an item at the given {@link path}. */ exists: (path: string) => Promise; + + /** + * Equivalent of `mkdir -p`. + * + * Create a directory at the given path if it does not already exist. + * Any parent directories in the path that don't already exist will also + * be created recursively, i.e. this command is analogous to an running + * `mkdir -p`. + */ + mkdirIfNeeded: (dirPath: string) => Promise; + + /** Rename {@link oldPath} to {@link newPath} */ + rename: (oldPath: string, newPath: string) => Promise; + + /** + * Equivalent of `rmdir`. + * + * Delete the directory at the {@link path} if it is empty. + */ + rmdir: (path: string) => Promise; + + /** + * Equivalent of `rm`. + * + * Delete the file at {@link path}. + */ + rm: (path: string) => Promise; }; /* @@ -276,7 +300,6 @@ export interface Electron { ) => Promise; // - FS legacy - checkExistsAndCreateDir: (dirPath: string) => Promise; saveStreamToDisk: ( path: string, fileStream: ReadableStream, @@ -285,9 +308,6 @@ export interface Electron { readTextFile: (path: string) => Promise; isFolder: (dirPath: string) => Promise; moveFile: (oldPath: string, newPath: string) => Promise; - deleteFolder: (path: string) => Promise; - deleteFile: (path: string) => Promise; - rename: (oldPath: string, newPath: string) => Promise; // - Upload