diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index dfe072ef..8acf6362 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -50,6 +50,9 @@ export default class CodeEditor extends React.Component { el = el.parentNode } this.props.onBlur != null && this.props.onBlur(e) + + const {storageKey, noteKey} = this.props + attachmentManagement.deleteAttachmentsNotPresentInNote(this.editor.getValue(), storageKey, noteKey) } this.pasteHandler = (editor, e) => this.handlePaste(editor, e) this.loadStyleHandler = (e) => { diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index c2e7f6d6..efacd47c 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -157,7 +157,7 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem /** * @description Returns all attachment paths of the given markdown * @param {String} markdownContent content in which the attachment paths should be found - * @returns {String[]} Array of the relativ paths (starting with :storage) of the attachments of the given markdown + * @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown */ function getAttachmentsInContent (markdownContent) { const preparedInput = markdownContent.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep) @@ -190,6 +190,49 @@ function removeStorageAndNoteReferences (input, noteKey) { return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey, 'g'), DESTINATION_FOLDER) } +/** + * @description Deletes all attachments stored in the attachment folder of the give not that are not referenced in the markdownContent + * @param markdownContent Content of the note. All unreferenced notes will be deleted + * @param storageKey StorageKey of the current note. Is used to determine the belonging attachment folder. + * @param noteKey NoteKey of the current note. Is used to determine the belonging attachment folder. + */ +function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey) { + const targetStorage = findStorage.findStorage(storageKey) + const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + const attachmentsInNote = getAttachmentsInContent(markdownContent) + const attachmentsInNoteOnlyFileNames = [] + if (attachmentsInNote) { + for (let i = 0; i < attachmentsInNote.length; i++) { + attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), '')) + } + } + + if (fs.existsSync(attachmentFolder)) { + fs.readdir(attachmentFolder, (err, files) => { + if (err) { + console.error("Error reading directory '" + attachmentFolder + "'. Error:") + console.error(err) + return + } + files.forEach(file => { + if (!attachmentsInNoteOnlyFileNames.includes(file)) { + const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file) + fs.unlink(absolutePathOfFile, (err) => { + if (err) { + console.error("Could not delete '%s'", absolutePathOfFile) + console.error(err) + return + } + console.info("File '" + absolutePathOfFile + "' deleted because it was not included in the content of the note") + }) + } + }) + }) + } else { + console.info("Attachment folder ('" + attachmentFolder + "') did not exist..") + } +} + module.exports = { copyAttachment, fixLocalURLS, @@ -199,6 +242,7 @@ module.exports = { getAttachmentsInContent, getAbsolutePathsOfAttachmentsInContent, removeStorageAndNoteReferences, + deleteAttachmentsNotPresentInNote, STORAGE_FOLDER_PLACEHOLDER, DESTINATION_FOLDER } diff --git a/tests/dataApi/attachmentManagement.test.js b/tests/dataApi/attachmentManagement.test.js index d58a8eb8..7efe4f21 100644 --- a/tests/dataApi/attachmentManagement.test.js +++ b/tests/dataApi/attachmentManagement.test.js @@ -260,3 +260,55 @@ it('should remove the all ":storage" and noteKey references', function () { const actual = systemUnderTest.removeStorageAndNoteReferences(testInput, noteKey) expect(actual).toEqual(expectedOutput) }) + +it('should test that deleteAttachmentsNotPresentInNote deletes all unreferenced attachments ', function () { + const dummyStorage = {path: 'dummyStoragePath'} + const noteKey = 'noteKey' + const storageKey = 'storageKey' + const markdownContent = '' + const dummyFilesInFolder = ['file1.txt', 'file2.pdf', 'file3.jpg'] + const attachmentFolderPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey) + + findStorage.findStorage = jest.fn(() => dummyStorage) + fs.existsSync = jest.fn(() => true) + fs.readdir = jest.fn((paht, callback) => callback(undefined, dummyFilesInFolder)) + fs.unlink = jest.fn() + + systemUnderTest.deleteAttachmentsNotPresentInNote(markdownContent, storageKey, noteKey) + expect(fs.existsSync).toHaveBeenLastCalledWith(attachmentFolderPath) + expect(fs.readdir).toHaveBeenCalledTimes(1) + expect(fs.readdir.mock.calls[0][0]).toBe(attachmentFolderPath) + + expect(fs.unlink).toHaveBeenCalledTimes(dummyFilesInFolder.length) + const fsUnlinkCallArguments = [] + for (let i = 0; i < dummyFilesInFolder.length; i++) { + fsUnlinkCallArguments.push(fs.unlink.mock.calls[i][0]) + } + + dummyFilesInFolder.forEach(function (file) { + expect(fsUnlinkCallArguments.includes(path.join(attachmentFolderPath, file))).toBe(true) + }) +}) + +it('should test that deleteAttachmentsNotPresentInNote does not delete referenced attachments', function () { + const dummyStorage = {path: 'dummyStoragePath'} + const noteKey = 'noteKey' + const storageKey = 'storageKey' + const dummyFilesInFolder = ['file1.txt', 'file2.pdf', 'file3.jpg'] + const markdownContent = systemUnderTest.generateAttachmentMarkdown('fileLabel', path.join(systemUnderTest.STORAGE_FOLDER_PLACEHOLDER, noteKey, dummyFilesInFolder[0]), false) + const attachmentFolderPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey) + + findStorage.findStorage = jest.fn(() => dummyStorage) + fs.existsSync = jest.fn(() => true) + fs.readdir = jest.fn((paht, callback) => callback(undefined, dummyFilesInFolder)) + fs.unlink = jest.fn() + + systemUnderTest.deleteAttachmentsNotPresentInNote(markdownContent, storageKey, noteKey) + + expect(fs.unlink).toHaveBeenCalledTimes(dummyFilesInFolder.length - 1) + const fsUnlinkCallArguments = [] + for (let i = 0; i < dummyFilesInFolder.length - 1; i++) { + fsUnlinkCallArguments.push(fs.unlink.mock.calls[i][0]) + } + expect(fsUnlinkCallArguments.includes(path.join(attachmentFolderPath, dummyFilesInFolder[0]))).toBe(false) +})