mirror of
https://github.com/mfarr/obsidian-archive.git
synced 2026-07-22 05:41:33 +00:00
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
This commit is contained in:
parent
e97ec7458a
commit
06e1f02256
1 changed files with 95 additions and 9 deletions
104
main.ts
104
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<ArchiveResult> {
|
||||
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<ArchiveResult>((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<void>,
|
||||
cancelCallback: () => Promise<void>
|
||||
cancelCallback: () => Promise<void>,
|
||||
middleButtonText?: string,
|
||||
middleCallback?: () => Promise<void>
|
||||
) {
|
||||
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();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue