mirror of
https://github.com/mfarr/obsidian-archive.git
synced 2026-07-22 12:10:27 +00:00
Merge main into feature/issue-18-rename-option
- Integrate folder merge functionality from main branch - Combine rename option (files) with merge option (folders) - Folders show Merge/Cancel prompt (2-button) - Files show Replace/Rename/Cancel prompt (3-button) - Fix success message to capture original filename before rename - Both features now work together seamlessly
This commit is contained in:
commit
e0aba65173
1 changed files with 191 additions and 40 deletions
231
main.ts
231
main.ts
|
|
@ -9,6 +9,8 @@ import {
|
|||
PluginSettingTab,
|
||||
Setting,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
TFolder,
|
||||
} from "obsidian";
|
||||
|
||||
interface SimpleArchiverSettings {
|
||||
|
|
@ -20,6 +22,14 @@ interface ArchiveResult {
|
|||
message: string;
|
||||
}
|
||||
|
||||
interface MergeResult {
|
||||
filesAdded: number;
|
||||
filesReplaced: number;
|
||||
filesSkipped: number;
|
||||
foldersCreated: number;
|
||||
failedFiles: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: SimpleArchiverSettings = {
|
||||
archiveFolder: "Archive",
|
||||
};
|
||||
|
|
@ -98,7 +108,7 @@ export default class SimpleArchiver extends Plugin {
|
|||
if (result.success) {
|
||||
new Notice(result.message);
|
||||
} else {
|
||||
new Error(result.message);
|
||||
new Notice(result.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -137,7 +147,7 @@ export default class SimpleArchiver extends Plugin {
|
|||
if (result.success) {
|
||||
new Notice(result.message);
|
||||
} else {
|
||||
new Error(result.message);
|
||||
new Notice(result.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -165,6 +175,10 @@ export default class SimpleArchiver extends Plugin {
|
|||
return file.path.startsWith(this.settings.archiveFolder);
|
||||
}
|
||||
|
||||
private isFolder(file: TAbstractFile): file is TFolder {
|
||||
return file instanceof TFolder;
|
||||
}
|
||||
|
||||
private generateUniqueArchivePath(
|
||||
file: TAbstractFile,
|
||||
destinationPath: string
|
||||
|
|
@ -214,19 +228,36 @@ export default class SimpleArchiver extends Plugin {
|
|||
this.app.vault.getAbstractFileByPath(destinationFilePath);
|
||||
|
||||
if (existingItem != null) {
|
||||
// Same item exists in archive, prompt to replace or rename
|
||||
const isFolder = this.isFolder(file);
|
||||
|
||||
return new Promise<ArchiveResult>((resolve) => {
|
||||
const prompt = new SimpleArchiverPromptModal(
|
||||
this.app,
|
||||
"File exists in archive",
|
||||
`An item called "${file.name}" already exists in the destination folder in the archive. Replace, rename, or cancel?`,
|
||||
"Replace",
|
||||
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. Replace, rename, or cancel?`,
|
||||
isFolder ? "Merge" : "Replace",
|
||||
"Cancel",
|
||||
async () => {
|
||||
await this.app.fileManager.trashFile(existingItem);
|
||||
const response = await this.moveFileToArchive(file);
|
||||
|
||||
resolve(response);
|
||||
try {
|
||||
if (isFolder) {
|
||||
const response = await this.mergeFolderIntoArchive(
|
||||
file as TFolder,
|
||||
destinationFilePath
|
||||
);
|
||||
resolve(response);
|
||||
} else {
|
||||
await this.app.fileManager.trashFile(existingItem);
|
||||
const response = await this.moveFileToArchive(file);
|
||||
resolve(response);
|
||||
}
|
||||
} catch (error) {
|
||||
resolve({
|
||||
success: false,
|
||||
message: `Archive operation failed: ${error}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
resolve({
|
||||
|
|
@ -234,38 +265,42 @@ export default class SimpleArchiver extends Plugin {
|
|||
message: "Archive operation cancelled",
|
||||
});
|
||||
},
|
||||
"Rename",
|
||||
async () => {
|
||||
const destinationPath = normalizePath(
|
||||
`${this.settings.archiveFolder}/${file.parent?.path}`
|
||||
);
|
||||
// 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);
|
||||
const destinationFolder =
|
||||
this.app.vault.getFolderByPath(destinationPath);
|
||||
|
||||
if (destinationFolder == null) {
|
||||
await this.app.vault.createFolder(destinationPath);
|
||||
}
|
||||
if (destinationFolder == null) {
|
||||
await this.app.vault.createFolder(destinationPath);
|
||||
}
|
||||
|
||||
const uniquePath = this.generateUniqueArchivePath(
|
||||
file,
|
||||
destinationPath
|
||||
);
|
||||
const uniquePath = this.generateUniqueArchivePath(
|
||||
file,
|
||||
destinationPath
|
||||
);
|
||||
|
||||
try {
|
||||
await this.app.fileManager.renameFile(file, uniquePath);
|
||||
const newName = uniquePath.split("/").pop() || file.name;
|
||||
resolve({
|
||||
success: true,
|
||||
message: `${file.name} archived as ${newName}`,
|
||||
});
|
||||
} catch (error) {
|
||||
resolve({
|
||||
success: false,
|
||||
message: `Unable to archive ${file.name}: ${error}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
const originalName = file.name;
|
||||
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 ${file.name}: ${error}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
prompt.open();
|
||||
});
|
||||
|
|
@ -320,6 +355,122 @@ export default class SimpleArchiver extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
private async mergeFolderIntoArchive(
|
||||
sourceFolder: TFolder,
|
||||
destinationFolderPath: string
|
||||
): Promise<ArchiveResult> {
|
||||
const stats: MergeResult = {
|
||||
filesAdded: 0,
|
||||
filesReplaced: 0,
|
||||
filesSkipped: 0,
|
||||
foldersCreated: 0,
|
||||
failedFiles: [],
|
||||
};
|
||||
|
||||
try {
|
||||
await this.recursiveMerge(sourceFolder, destinationFolderPath, stats);
|
||||
|
||||
// Only delete the source folder if it's empty
|
||||
if (sourceFolder.children.length === 0) {
|
||||
await this.app.vault.delete(sourceFolder);
|
||||
}
|
||||
|
||||
const totalFiles = stats.filesAdded + stats.filesReplaced;
|
||||
let message = `Merged ${sourceFolder.name}: ${totalFiles} file${totalFiles !== 1 ? 's' : ''}`;
|
||||
|
||||
const details: string[] = [];
|
||||
if (stats.filesReplaced > 0) {
|
||||
details.push(`${stats.filesReplaced} replaced`);
|
||||
}
|
||||
if (stats.filesSkipped > 0) {
|
||||
details.push(`${stats.filesSkipped} skipped`);
|
||||
}
|
||||
if (stats.failedFiles.length > 0) {
|
||||
details.push(`${stats.failedFiles.length} failed`);
|
||||
}
|
||||
|
||||
if (details.length > 0) {
|
||||
message += ` (${details.join(', ')})`;
|
||||
}
|
||||
|
||||
if (stats.failedFiles.length > 0) {
|
||||
message += `. Failed: ${stats.failedFiles.join(', ')}`;
|
||||
}
|
||||
|
||||
if (sourceFolder.children.length > 0) {
|
||||
message += `. Source folder not deleted (contains remaining files)`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: stats.failedFiles.length === 0,
|
||||
message: message,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Unable to merge ${sourceFolder.name}: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async recursiveMerge(
|
||||
sourceFolder: TFolder,
|
||||
destinationBasePath: string,
|
||||
stats: MergeResult
|
||||
): Promise<void> {
|
||||
// Create a copy of children array to avoid modification during iteration
|
||||
const children = [...sourceFolder.children];
|
||||
|
||||
for (const child of children) {
|
||||
if (this.isFolder(child)) {
|
||||
const childDestPath = normalizePath(
|
||||
`${destinationBasePath}/${child.name}`
|
||||
);
|
||||
|
||||
const existingFolder = this.app.vault.getFolderByPath(childDestPath);
|
||||
if (existingFolder == null) {
|
||||
await this.app.vault.createFolder(childDestPath);
|
||||
stats.foldersCreated++;
|
||||
}
|
||||
|
||||
await this.recursiveMerge(child, childDestPath, stats);
|
||||
} else {
|
||||
const childDestPath = normalizePath(
|
||||
`${destinationBasePath}/${child.name}`
|
||||
);
|
||||
|
||||
const existingFile = this.app.vault.getAbstractFileByPath(childDestPath);
|
||||
|
||||
try {
|
||||
if (existingFile != null) {
|
||||
// Compare file contents before replacing
|
||||
const sourceContent = await this.app.vault.read(child as TFile);
|
||||
const existingContent = await this.app.vault.read(existingFile as TFile);
|
||||
|
||||
if (sourceContent === existingContent) {
|
||||
// Files are identical - just delete source, keep existing
|
||||
await this.app.vault.delete(child);
|
||||
stats.filesSkipped++;
|
||||
} else {
|
||||
// Files differ - replace existing with source
|
||||
await this.app.fileManager.trashFile(existingFile);
|
||||
await this.app.fileManager.renameFile(child, childDestPath);
|
||||
stats.filesReplaced++;
|
||||
}
|
||||
} else {
|
||||
// No existing file - just move the source
|
||||
await this.app.fileManager.renameFile(child, childDestPath);
|
||||
stats.filesAdded++;
|
||||
}
|
||||
} catch (error) {
|
||||
// Track failed files but continue processing others
|
||||
stats.failedFiles.push(child.name);
|
||||
console.error(`Failed to process ${child.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async moveFileOutOfArchive(
|
||||
file: TAbstractFile
|
||||
): Promise<ArchiveResult> {
|
||||
|
|
@ -336,12 +487,12 @@ export default class SimpleArchiver extends Plugin {
|
|||
this.app.vault.getFolderByPath(originalParentPath);
|
||||
|
||||
if (originalFolder == null) {
|
||||
await this.app.vault.createFolder(originalParentPath);
|
||||
await this.app.vault.createFolder(normalizePath(originalParentPath));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.app.fileManager.renameFile(file, originalPath);
|
||||
await this.app.fileManager.renameFile(file, normalizePath(originalPath));
|
||||
return {
|
||||
success: true,
|
||||
message: `${file.name} unarchived successfully`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue