diff --git a/main.ts b/main.ts index dcc4ea0..d872020 100644 --- a/main.ts +++ b/main.ts @@ -1,4 +1,4 @@ -import { Plugin, PluginSettingTab, Setting, TFile, TFolder, Notice, MarkdownView, Modal, App } from 'obsidian'; +import { Plugin, PluginSettingTab, Setting, TFile, TFolder, TAbstractFile, Notice, MarkdownView, Modal, App } from 'obsidian'; import { EditorView, ViewUpdate, ViewPlugin } from '@codemirror/view'; import { StateField } from '@codemirror/state'; import { EditorState } from '@codemirror/state'; @@ -305,6 +305,20 @@ export default class LockdownPlugin extends Plugin { }) ); + // Handle file/folder rename (move) to update lock registry + this.registerEvent( + this.app.vault.on('rename', (file, oldPath) => { + this.handleFileRename(file, oldPath); + }) + ); + + // Handle file deletion to clean up lock registry + this.registerEvent( + this.app.vault.on('delete', (file) => { + this.handleFileDelete(file); + }) + ); + // Intercept file opening to show overlay for locked files this.registerEvent( this.app.workspace.on('file-open', (file) => { @@ -1488,6 +1502,198 @@ export default class LockdownPlugin extends Plugin { await this.unlockFolder(lockedFolderPath); } + /** + * Handle file or folder rename/move events to update lock registry. + * This ensures locked files maintain their lock status when moved. + */ + private handleFileRename(file: TAbstractFile, oldPath: string): void { + const newPath = file.path; + + if (file instanceof TFile) { + // Handle file rename/move + if (this.lockedFiles.has(oldPath)) { + // Update locked files set + this.lockedFiles.delete(oldPath); + this.lockedFiles.add(newPath); + + // Update password hash + const passwordHash = this.passwordHashes.get(oldPath); + if (passwordHash) { + this.passwordHashes.delete(oldPath); + this.passwordHashes.set(newPath, passwordHash); + } + + // Update cached password + const cachedPassword = this.filePasswords.get(oldPath); + if (cachedPassword) { + this.filePasswords.delete(oldPath); + this.filePasswords.set(newPath, cachedPassword); + } + + // Update lock registry + this.lockRegistry.updateFilePath(oldPath, newPath); + + // Save the updated state + void this.saveLockedFiles(); + + // Update UI + this.updateFileExplorerIndicators(); + + // If there's an overlay for the old path, update it + if (this.lockOverlayManager.has(oldPath)) { + this.removeLockOverlay(oldPath); + // Show overlay for new path after a short delay to ensure DOM is updated + setTimeout(() => { + if (this.isFileLocked(newPath)) { + this.showLockOverlay(newPath); + } + }, 100); + } + } + } else if (file instanceof TFolder) { + // Handle folder rename/move + const oldPrefix = oldPath + '/'; + const newPrefix = newPath + '/'; + + // Update the locked folder itself if it was locked + if (this.lockedFolders.has(oldPath)) { + this.lockedFolders.delete(oldPath); + this.lockedFolders.add(newPath); + this.lockRegistry.updateFolderPath(oldPath, newPath); + } + + // Update all locked files that were inside the renamed folder + const filesToUpdate: Array<{ oldFilePath: string; newFilePath: string }> = []; + for (const filePath of this.lockedFiles) { + if (filePath.startsWith(oldPrefix)) { + const relativePath = filePath.substring(oldPrefix.length); + const newFilePath = newPrefix + relativePath; + filesToUpdate.push({ oldFilePath: filePath, newFilePath }); + } + } + + for (const { oldFilePath, newFilePath } of filesToUpdate) { + // Update locked files set + this.lockedFiles.delete(oldFilePath); + this.lockedFiles.add(newFilePath); + + // Update password hash + const passwordHash = this.passwordHashes.get(oldFilePath); + if (passwordHash) { + this.passwordHashes.delete(oldFilePath); + this.passwordHashes.set(newFilePath, passwordHash); + } + + // Update cached password + const cachedPassword = this.filePasswords.get(oldFilePath); + if (cachedPassword) { + this.filePasswords.delete(oldFilePath); + this.filePasswords.set(newFilePath, cachedPassword); + } + + // Update lock registry + this.lockRegistry.updateFilePath(oldFilePath, newFilePath); + + // Update overlays if needed + if (this.lockOverlayManager.has(oldFilePath)) { + this.removeLockOverlay(oldFilePath); + } + } + + // Update all locked subfolders + const foldersToUpdate: Array<{ oldFolderPath: string; newFolderPath: string }> = []; + for (const folderPath of this.lockedFolders) { + if (folderPath.startsWith(oldPrefix)) { + const relativePath = folderPath.substring(oldPrefix.length); + const newFolderPath = newPrefix + relativePath; + foldersToUpdate.push({ oldFolderPath: folderPath, newFolderPath }); + } + } + + for (const { oldFolderPath, newFolderPath } of foldersToUpdate) { + this.lockedFolders.delete(oldFolderPath); + this.lockedFolders.add(newFolderPath); + this.lockRegistry.updateFolderPath(oldFolderPath, newFolderPath); + } + + // Also update child paths in lock registry + this.lockRegistry.updateChildFilePaths(oldPath, newPath); + this.lockRegistry.updateChildFolderPaths(oldPath, newPath); + + // Save the updated state + void this.saveLockedFiles(); + + // Update UI + this.updateFileExplorerIndicators(); + } + } + + /** + * Handle file or folder deletion to clean up lock registry. + * This prevents stale entries from accumulating. + */ + private handleFileDelete(file: TAbstractFile): void { + const path = file.path; + + if (file instanceof TFile) { + // Clean up file from lock registry + if (this.lockedFiles.has(path)) { + this.lockedFiles.delete(path); + this.passwordHashes.delete(path); + this.filePasswords.delete(path); + + // Remove from lock overlay manager + this.removeLockOverlay(path); + + // Save the updated state + void this.saveLockedFiles(); + + // Update UI + this.updateFileExplorerIndicators(); + } + } else if (file instanceof TFolder) { + // Clean up folder from lock registry + if (this.lockedFolders.has(path)) { + this.lockedFolders.delete(path); + } + + // Clean up any files that were in the deleted folder + const prefix = path + '/'; + const filesToRemove: string[] = []; + + for (const filePath of this.lockedFiles) { + if (filePath.startsWith(prefix)) { + filesToRemove.push(filePath); + } + } + + for (const filePath of filesToRemove) { + this.lockedFiles.delete(filePath); + this.passwordHashes.delete(filePath); + this.filePasswords.delete(filePath); + this.removeLockOverlay(filePath); + } + + // Clean up any subfolders that were locked + const foldersToRemove: string[] = []; + for (const folderPath of this.lockedFolders) { + if (folderPath.startsWith(prefix)) { + foldersToRemove.push(folderPath); + } + } + + for (const folderPath of foldersToRemove) { + this.lockedFolders.delete(folderPath); + } + + // Save the updated state + void this.saveLockedFiles(); + + // Update UI + this.updateFileExplorerIndicators(); + } + } + createLockdownExtension() { // eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for CodeMirror extension closure const plugin = this; diff --git a/package-lock.json b/package-lock.json index e1520b0..6d40058 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lockdown", - "version": "0.15.13", + "version": "0.16.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lockdown", - "version": "0.15.13", + "version": "0.16.2", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/src/infrastructure/storage/LockRegistry.ts b/src/infrastructure/storage/LockRegistry.ts index 536b9f8..0ea9c8a 100644 --- a/src/infrastructure/storage/LockRegistry.ts +++ b/src/infrastructure/storage/LockRegistry.ts @@ -58,6 +58,118 @@ export class LockRegistry { return Array.from(this.lockedFolders); } + /** + * Update the path of a locked file when it is moved or renamed. + * This preserves the lock status and password hash across path changes. + * @param oldPath - The previous file path + * @param newPath - The new file path + * @returns true if the file was tracked and updated, false otherwise + */ + updateFilePath(oldPath: string, newPath: string): boolean { + if (!this.lockedFiles.has(oldPath)) { + return false; + } + + // Move the file entry from old path to new path + this.lockedFiles.delete(oldPath); + this.lockedFiles.add(newPath); + + // Move the password hash if it exists + const passwordHash = this.passwordHashes.get(oldPath); + if (passwordHash) { + this.passwordHashes.delete(oldPath); + this.passwordHashes.set(newPath, passwordHash); + } + + return true; + } + + /** + * Update the path of a locked folder when it is moved or renamed. + * This preserves the lock status across path changes. + * @param oldPath - The previous folder path + * @param newPath - The new folder path + * @returns true if the folder was tracked and updated, false otherwise + */ + updateFolderPath(oldPath: string, newPath: string): boolean { + if (!this.lockedFolders.has(oldPath)) { + return false; + } + + // Move the folder entry from old path to new path + this.lockedFolders.delete(oldPath); + this.lockedFolders.add(newPath); + + return true; + } + + /** + * Update all locked file paths that are children of a renamed folder. + * @param oldFolderPath - The previous folder path + * @param newFolderPath - The new folder path + * @returns Number of files updated + */ + updateChildFilePaths(oldFolderPath: string, newFolderPath: string): number { + let updatedCount = 0; + const oldPrefix = oldFolderPath + '/'; + const filesToUpdate: Array<{ oldPath: string; newPath: string }> = []; + + // Find all locked files that are children of the old folder path + for (const filePath of this.lockedFiles) { + if (filePath.startsWith(oldPrefix)) { + const relativePath = filePath.substring(oldPrefix.length); + const newFilePath = newFolderPath + '/' + relativePath; + filesToUpdate.push({ oldPath: filePath, newPath: newFilePath }); + } + } + + // Update each file path + for (const { oldPath, newPath } of filesToUpdate) { + this.lockedFiles.delete(oldPath); + this.lockedFiles.add(newPath); + + const passwordHash = this.passwordHashes.get(oldPath); + if (passwordHash) { + this.passwordHashes.delete(oldPath); + this.passwordHashes.set(newPath, passwordHash); + } + + updatedCount++; + } + + return updatedCount; + } + + /** + * Update all locked folder paths that are children of a renamed folder. + * @param oldFolderPath - The previous folder path + * @param newFolderPath - The new folder path + * @returns Number of folders updated + */ + updateChildFolderPaths(oldFolderPath: string, newFolderPath: string): number { + let updatedCount = 0; + const oldPrefix = oldFolderPath + '/'; + const foldersToUpdate: Array<{ oldPath: string; newPath: string }> = []; + + // Find all locked folders that are children of the old folder path + for (const folderPath of this.lockedFolders) { + if (folderPath.startsWith(oldPrefix)) { + const relativePath = folderPath.substring(oldPrefix.length); + const newChildFolderPath = newFolderPath + '/' + relativePath; + foldersToUpdate.push({ oldPath: folderPath, newPath: newChildFolderPath }); + } + } + + // Update each folder path + for (const { oldPath, newPath } of foldersToUpdate) { + this.lockedFolders.delete(oldPath); + this.lockedFolders.add(newPath); + updatedCount++; + } + + return updatedCount; + } + clear(): void { this.lockedFiles.clear(); this.lockedFolders.clear();