From 06e1f02256aed76e83ef566fd5a5ee0fb06b4be7 Mon Sep 17 00:00:00 2001 From: Gareth Evans <63365672+ggfevans@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:13:59 -0700 Subject: [PATCH] Add rename option for file collisions during archive (#3) * Add rename option for file collisions during archive - Extend SimpleArchiverPromptModal to support optional third button - Add generateUniqueArchivePath helper for timestamp-based naming - Update archiveFile to show Replace/Rename/Cancel for collisions - Generate unique filenames using YYYYMMDD-HHMMSS format - Handle edge cases: files without extensions, multiple renames per second - Preserve both existing and new files when rename is chosen - Maintain backward compatibility with two-button modal usage Fixes #18 * Use originalName in error message for consistency - Move originalName declaration outside try-catch for proper scope - Ensure both success and error messages reference original filename - Prevents confusion if file.name is mutated during error handling --- main.ts | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 9 deletions(-) diff --git a/main.ts b/main.ts index de362bd..91750a4 100644 --- a/main.ts +++ b/main.ts @@ -179,6 +179,42 @@ export default class SimpleArchiver extends Plugin { return file instanceof TFolder; } + private generateUniqueArchivePath( + file: TAbstractFile, + destinationPath: string + ): string { + const fileName = file.name; + const lastDotIndex = fileName.lastIndexOf("."); + const baseName = + lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName; + const extension = + lastDotIndex > 0 ? fileName.substring(lastDotIndex) : ""; + + // Generate timestamp-based suffix + const now = new Date(); + const timestamp = now + .toISOString() + .replace(/[-:]/g, "") + .replace(/\..+/, "") + .replace("T", "-") + .substring(0, 15); // YYYYMMDD-HHMMSS + + let counter = 0; + let uniquePath = normalizePath( + `${destinationPath}/${baseName}-${timestamp}${extension}` + ); + + // Handle edge case: multiple renames in same second + while (this.app.vault.getAbstractFileByPath(uniquePath) != null) { + counter++; + uniquePath = normalizePath( + `${destinationPath}/${baseName}-${timestamp}-${counter}${extension}` + ); + } + + return uniquePath; + } + private async archiveFile(file: TAbstractFile): Promise { if (this.isFileArchived(file)) { return { success: false, message: "Item is already archived" }; @@ -197,10 +233,10 @@ export default class SimpleArchiver extends Plugin { return new Promise((resolve) => { const prompt = new SimpleArchiverPromptModal( this.app, - isFolder ? "Merge folders?" : "Replace archived item?", + isFolder ? "Merge folders?" : "File exists in archive", isFolder ? `A folder called "${file.name}" already exists in the archive. Merge the contents?` - : `An item called "${file.name}" already exists in the destination folder in the archive. Would you like to replace it?`, + : `An item called "${file.name}" already exists in the destination folder in the archive. Replace, rename, or cancel?`, isFolder ? "Merge" : "Replace", "Cancel", async () => { @@ -228,7 +264,44 @@ export default class SimpleArchiver extends Plugin { success: false, message: "Archive operation cancelled", }); - } + }, + // Only add rename option for files, not folders + isFolder ? undefined : "Rename", + isFolder + ? undefined + : async () => { + const destinationPath = normalizePath( + `${this.settings.archiveFolder}/${file.parent?.path}` + ); + + const destinationFolder = + this.app.vault.getFolderByPath(destinationPath); + + if (destinationFolder == null) { + await this.app.vault.createFolder(destinationPath); + } + + const uniquePath = this.generateUniqueArchivePath( + file, + destinationPath + ); + + const originalName = file.name; + + try { + await this.app.fileManager.renameFile(file, uniquePath); + const newName = uniquePath.split("/").pop() || file.name; + resolve({ + success: true, + message: `${originalName} archived as ${newName}`, + }); + } catch (error) { + resolve({ + success: false, + message: `Unable to archive ${originalName}: ${error}`, + }); + } + } ); prompt.open(); }); @@ -506,7 +579,9 @@ class SimpleArchiverPromptModal extends Modal { yesButtonText: string, noButtonText: string, callback: () => Promise, - cancelCallback: () => Promise + cancelCallback: () => Promise, + middleButtonText?: string, + middleCallback?: () => Promise ) { super(app); @@ -514,7 +589,7 @@ class SimpleArchiverPromptModal extends Modal { this.setContent(message); - new Setting(this.contentEl) + const setting = new Setting(this.contentEl) .addButton((btn) => btn .setButtonText(yesButtonText) @@ -523,13 +598,24 @@ class SimpleArchiverPromptModal extends Modal { callback(); this.close(); }) - ) - .addButton((btn) => - btn.setButtonText(noButtonText).onClick(() => { - cancelCallback(); + ); + + // Add optional middle button if provided + if (middleButtonText && middleCallback) { + setting.addButton((btn) => + btn.setButtonText(middleButtonText).onClick(() => { + middleCallback(); this.close(); }) ); + } + + setting.addButton((btn) => + btn.setButtonText(noButtonText).onClick(() => { + cancelCallback(); + this.close(); + }) + ); } }