diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index 876de0c0..d6b7f846 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -7,6 +7,7 @@ import moment from 'moment' import _ from 'lodash' import ee from 'browser/main/lib/eventEmitter' import dataApi from 'browser/main/lib/dataApi' +import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import ConfigManager from 'browser/main/lib/ConfigManager' import NoteItem from 'browser/components/NoteItem' import NoteItemSimple from 'browser/components/NoteItemSimple' @@ -662,6 +663,10 @@ class NoteList extends React.Component { title: firstNote.title + ' ' + i18n.__('copy'), content: firstNote.content }) + .then((note) => { + attachmentManagement.cloneAttachments(firstNote, note) + return note + }) .then((note) => { dispatch({ type: 'UPDATE_NOTE', diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index 1373cf94..893e03d1 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -42,7 +42,7 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr const targetStorage = findStorage.findStorage(storageKey) - const inputFile = fs.createReadStream(sourceFilePath) + const inputFileStream = fs.createReadStream(sourceFilePath) let destinationName if (useRandomName) { destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}` @@ -52,8 +52,10 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) createAttachmentDestinationFolder(targetStorage.path, noteKey) const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName)) - inputFile.pipe(outputFile) - resolve(destinationName) + inputFileStream.pipe(outputFile) + inputFileStream.on('end', () => { + resolve(destinationName) + }) } catch (e) { return reject(e) } @@ -149,7 +151,7 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem base64data = reader.result.replace(/^data:image\/png;base64,/, '') base64data += base64data.replace('+', ' ') const binaryData = new Buffer(base64data, 'base64').toString('binary') - fs.writeFile(imagePath, binaryData, 'binary') + fs.writeFileSync(imagePath, binaryData, 'binary') const imageMd = generateAttachmentMarkdown(imageName, imagePath, true) codeEditor.insertAttachmentMd(imageMd) } @@ -174,7 +176,7 @@ function getAttachmentsInContent (markdownContent) { * @returns {String[]} Absolute paths of the referenced attachments */ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) { - const temp = getAttachmentsInContent(markdownContent) + const temp = getAttachmentsInContent(markdownContent) || [] const result = [] for (const relativePath of temp) { result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER))) @@ -198,8 +200,19 @@ function moveAttachments (oldPath, newPath, noteKey, newNoteKey, noteContent) { if (fse.existsSync(src)) { fse.moveSync(src, dest) } + return replaceNoteKeyWithNewNoteKey(noteContent, noteKey, newNoteKey) +} + +/** + * Modifies the given content so that in all attachment references the oldNoteKey is replaced by the new one + * @param noteContent content that should be modified + * @param oldNoteKey note key to be replaced + * @param newNoteKey note key serving as a replacement + * @returns {String} modified note content + */ +function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) { if (noteContent) { - return noteContent.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey)) + return noteContent.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey)) } return noteContent } @@ -268,6 +281,33 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey } } +/** + * Clones the attachments of a given note. + * Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination. + * @param oldNote Note that is being cloned + * @param newNote Clone of the note + */ +function cloneAttachments (oldNote, newNote) { + if (newNote.type === 'MARKDOWN_NOTE') { + const oldStorage = findStorage.findStorage(oldNote.storage) + const newStorage = findStorage.findStorage(newNote.storage) + const attachmentsPaths = getAbsolutePathsOfAttachmentsInContent(oldNote.content, oldStorage.path) || [] + + const destinationFolder = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key) + if (!sander.existsSync(destinationFolder)) { + sander.mkdirSync(destinationFolder) + } + + for (const attachment of attachmentsPaths) { + const destination = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key, path.basename(attachment)) + sander.copyFileSync(attachment).to(destination) + } + newNote.content = replaceNoteKeyWithNewNoteKey(newNote.content, oldNote.key, newNote.key) + } else { + console.debug('Cloning of the attachment was skipped since it only works for MARKDOWN_NOTEs') + } +} + module.exports = { copyAttachment, fixLocalURLS, @@ -280,6 +320,7 @@ module.exports = { deleteAttachmentFolder, deleteAttachmentsNotPresentInNote, moveAttachments, + cloneAttachments, STORAGE_FOLDER_PLACEHOLDER, DESTINATION_FOLDER } diff --git a/tests/dataApi/attachmentManagement.test.js b/tests/dataApi/attachmentManagement.test.js index feb9207c..b61d8bf9 100644 --- a/tests/dataApi/attachmentManagement.test.js +++ b/tests/dataApi/attachmentManagement.test.js @@ -8,6 +8,7 @@ jest.mock('unique-slug') const uniqueSlug = require('unique-slug') const mdurl = require('mdurl') const fse = require('fs-extra') +jest.mock('sander') const sander = require('sander') const systemUnderTest = require('browser/main/lib/dataApi/attachmentManagement') @@ -50,11 +51,13 @@ it('should test that copyAttachment works correctly assuming correct working of const noteKey = 'noteKey' const dummyUniquePath = 'dummyPath' const dummyStorage = {path: 'dummyStoragePath'} + const dummyReadStream = {} + dummyReadStream.pipe = jest.fn() + dummyReadStream.on = jest.fn((event, callback) => { callback() }) fs.existsSync = jest.fn() fs.existsSync.mockReturnValue(true) - fs.createReadStream = jest.fn() - fs.createReadStream.mockReturnValue({pipe: jest.fn()}) + fs.createReadStream = jest.fn(() => dummyReadStream) fs.createWriteStream = jest.fn() findStorage.findStorage = jest.fn() @@ -77,7 +80,11 @@ it('should test that copyAttachment creates a new folder if the attachment folde const noteKey = 'noteKey' const attachmentFolderPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER) const attachmentFolderNoteKyPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey) + const dummyReadStream = {} + dummyReadStream.pipe = jest.fn() + dummyReadStream.on = jest.fn() + fs.createReadStream = jest.fn(() => dummyReadStream) fs.existsSync = jest.fn() fs.existsSync.mockReturnValueOnce(true) fs.existsSync.mockReturnValueOnce(false) @@ -99,7 +106,11 @@ it('should test that copyAttachment creates a new folder if the attachment folde it('should test that copyAttachment don\'t uses a random file name if not intended ', function () { const dummyStorage = {path: 'dummyStoragePath'} + const dummyReadStream = {} + dummyReadStream.pipe = jest.fn() + dummyReadStream.on = jest.fn() + fs.createReadStream = jest.fn(() => dummyReadStream) fs.existsSync = jest.fn() fs.existsSync.mockReturnValueOnce(true) fs.existsSync.mockReturnValueOnce(false) @@ -383,3 +394,76 @@ it('should test that moveAttachments returns a correct modified content version' const actualContent = systemUnderTest.moveAttachments(oldPath, newPath, oldNoteKey, newNoteKey, testInput) expect(actualContent).toBe(expectedOutput) }) + +it('should test that cloneAttachments modifies the content of the new note correctly', function () { + const oldNote = {key: 'oldNoteKey', content: 'oldNoteContent', storage: 'storageKey', type: 'MARKDOWN_NOTE'} + const newNote = {key: 'newNoteKey', content: 'oldNoteContent', storage: 'storageKey', type: 'MARKDOWN_NOTE'} + const testInput = + 'Test input' + + '![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'image.jpg](imageName}) \n' + + '[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'pdf.pdf](pdf})' + newNote.content = testInput + + const expectedOutput = + 'Test input' + + '![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNote.key + path.sep + 'image.jpg](imageName}) \n' + + '[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNote.key + path.sep + 'pdf.pdf](pdf})' + systemUnderTest.cloneAttachments(oldNote, newNote) + + expect(newNote.content).toBe(expectedOutput) +}) + +it('should test that cloneAttachments finds all attachments and copies them to the new location', function () { + const storagePathOld = 'storagePathOld' + const storagePathNew = 'storagePathNew' + const dummyStorageOld = {path: storagePathOld} + const dummyStorageNew = {path: storagePathNew} + const oldNote = {key: 'oldNoteKey', content: 'oldNoteContent', storage: 'storageKeyOldNote', type: 'MARKDOWN_NOTE'} + const newNote = {key: 'newNoteKey', content: 'oldNoteContent', storage: 'storageKeyNewNote', type: 'MARKDOWN_NOTE'} + const testInput = + 'Test input' + + '![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'image.jpg](imageName}) \n' + + '[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'pdf.pdf](pdf})' + oldNote.content = testInput + newNote.content = testInput + + const copyFileSyncResp = {to: jest.fn()} + sander.copyFileSync = jest.fn() + sander.copyFileSync.mockReturnValue(copyFileSyncResp) + findStorage.findStorage = jest.fn() + findStorage.findStorage.mockReturnValueOnce(dummyStorageOld) + findStorage.findStorage.mockReturnValue(dummyStorageNew) + + const pathAttachmentOneFrom = path.join(storagePathOld, systemUnderTest.DESTINATION_FOLDER, oldNote.key, 'image.jpg') + const pathAttachmentOneTo = path.join(storagePathNew, systemUnderTest.DESTINATION_FOLDER, newNote.key, 'image.jpg') + + const pathAttachmentTwoFrom = path.join(storagePathOld, systemUnderTest.DESTINATION_FOLDER, oldNote.key, 'pdf.pdf') + const pathAttachmentTwoTo = path.join(storagePathNew, systemUnderTest.DESTINATION_FOLDER, newNote.key, 'pdf.pdf') + + systemUnderTest.cloneAttachments(oldNote, newNote) + + expect(findStorage.findStorage).toHaveBeenCalledWith(oldNote.storage) + expect(findStorage.findStorage).toHaveBeenCalledWith(newNote.storage) + expect(sander.copyFileSync).toHaveBeenCalledTimes(2) + expect(copyFileSyncResp.to).toHaveBeenCalledTimes(2) + expect(sander.copyFileSync.mock.calls[0][0]).toBe(pathAttachmentOneFrom) + expect(copyFileSyncResp.to.mock.calls[0][0]).toBe(pathAttachmentOneTo) + expect(sander.copyFileSync.mock.calls[1][0]).toBe(pathAttachmentTwoFrom) + expect(copyFileSyncResp.to.mock.calls[1][0]).toBe(pathAttachmentTwoTo) +}) + +it('should test that cloneAttachments finds all attachments and copies them to the new location', function () { + const oldNote = {key: 'oldNoteKey', content: 'oldNoteContent', storage: 'storageKeyOldNote', type: 'SOMETHING_ELSE'} + const newNote = {key: 'newNoteKey', content: 'oldNoteContent', storage: 'storageKeyNewNote', type: 'SOMETHING_ELSE'} + const testInput = 'Test input' + oldNote.content = testInput + newNote.content = testInput + + sander.copyFileSync = jest.fn() + findStorage.findStorage = jest.fn() + + systemUnderTest.cloneAttachments(oldNote, newNote) + + expect(findStorage.findStorage).not.toHaveBeenCalled() + expect(sander.copyFileSync).not.toHaveBeenCalled() +})