From 4e6e3e7abf1276129fca251264f7cbdde51ab55c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 27 Feb 2025 09:18:14 +0530 Subject: [PATCH] [desktop] Improve export_status.json writes --- desktop/src/main/ipc.ts | 7 +++++++ desktop/src/main/services/fs.ts | 6 ++++++ desktop/src/preload.ts | 4 ++++ web/apps/photos/src/services/export/index.ts | 2 +- web/packages/base/types/ipc.ts | 12 ++++++++++++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index ff98e2ec88..b0a2eccf0c 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -41,6 +41,7 @@ import { fsRm, fsRmdir, fsWriteFile, + fsWriteFileViaBackup, } from "./services/fs"; import { convertToJPEG, generateImageThumbnail } from "./services/image"; import { logout } from "./services/logout"; @@ -154,6 +155,12 @@ export const attachIPCHandlers = () => { fsWriteFile(path, contents), ); + ipcMain.handle( + "fsWriteFileViaBackup", + (_, path: string, contents: string) => + fsWriteFileViaBackup(path, contents), + ); + ipcMain.handle("fsIsDir", (_, dirPath: string) => fsIsDir(dirPath)); ipcMain.handle("fsFindFiles", (_, folderPath: string) => diff --git a/desktop/src/main/services/fs.ts b/desktop/src/main/services/fs.ts index cdbded0beb..6cfe101ebf 100644 --- a/desktop/src/main/services/fs.ts +++ b/desktop/src/main/services/fs.ts @@ -24,6 +24,12 @@ export const fsReadTextFile = async (filePath: string) => export const fsWriteFile = (path: string, contents: string) => fs.writeFile(path, contents, { flush: true }); +export const fsWriteFileViaBackup = async (path: string, contents: string) => { + const backupPath = path + ".backup"; + await fs.writeFile(backupPath, contents, { flush: true }); + return fs.rename(backupPath, path); +}; + export const fsIsDir = async (dirPath: string) => { if (!existsSync(dirPath)) return false; const stat = await fs.stat(dirPath); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index cb9d594511..3fab447722 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -178,6 +178,9 @@ const fsReadTextFile = (path: string) => const fsWriteFile = (path: string, contents: string) => ipcRenderer.invoke("fsWriteFile", path, contents); +const fsWriteFileViaBackup = (path: string, contents: string) => + ipcRenderer.invoke("fsWriteFileViaBackup", path, contents); + const fsIsDir = (dirPath: string) => ipcRenderer.invoke("fsIsDir", dirPath); // - Conversion @@ -373,6 +376,7 @@ contextBridge.exposeInMainWorld("electron", { rm: fsRm, readTextFile: fsReadTextFile, writeFile: fsWriteFile, + writeFileViaBackup: fsWriteFileViaBackup, isDir: fsIsDir, findFiles: fsFindFiles, }, diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index bd5394c792..4ffc035aaa 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -888,7 +888,7 @@ class ExportService { try { const exportRecord = await this.getExportRecord(folder); const newRecord: ExportRecord = { ...exportRecord, ...newData }; - await ensureElectron().fs.writeFile( + await ensureElectron().fs.writeFileViaBackup( joinPath(folder, exportRecordFileName), JSON.stringify(newRecord, null, 2), ); diff --git a/web/packages/base/types/ipc.ts b/web/packages/base/types/ipc.ts index 5a1e39bddb..07f6b79db7 100644 --- a/web/packages/base/types/ipc.ts +++ b/web/packages/base/types/ipc.ts @@ -259,6 +259,18 @@ export interface Electron { */ writeFile: (path: string, contents: string) => Promise; + /** + * A variant of {@link writeFile} that first writes the file to a backup + * file, and then renames the backup file to the actual path. This both + * makes the write atomic (as far as the node's fs.rename guarantees + * atomicity), and also keeps the backup file around for recovery if + * something goes wrong during the rename. + * + * This behaviour of this function is tailored around for writes to the + * "export_status.json" during exports. + */ + writeFileViaBackup: (path: string, contents: string) => Promise; + /** * Return true if there is an item at {@link dirPath}, and it is as * directory.