From c7804318c69c5e3b67fb2724dfcb7b98d2ec901a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 1 Feb 2026 19:02:35 +0000 Subject: [PATCH] fix: preserve lock status when files are moved or renamed This fix addresses issue #7 where locking a note and moving it in the folder structure would cause the lock marker to be dismissed, leaving the encrypted content visible without unlock options. Changes: - Added rename event handler in main.ts to update lock registry when files/folders are moved or renamed - Added delete event handler to clean up stale lock entries - Extended LockRegistry with methods to update file/folder paths: - updateFilePath(): Updates a single locked file path - updateFolderPath(): Updates a single locked folder path - updateChildFilePaths(): Updates all locked files within a folder - updateChildFolderPaths(): Updates all locked subfolders within a folder - Properly handles password hashes and cached passwords during path updates - Updates UI (overlays and file explorer indicators) after path changes This prevents users from seeing encrypted content without unlock options and protects against accidental crypto corruption from editing encrypted content directly. Fixes #7 Co-authored-by: hlavezzo --- main.ts | 208 ++++++++++++++++++++- package-lock.json | 4 +- src/infrastructure/storage/LockRegistry.ts | 112 +++++++++++ 3 files changed, 321 insertions(+), 3 deletions(-) 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();