diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 872e9ad73..dfe072ef2 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -3,12 +3,10 @@ import React from 'react' import _ from 'lodash' import CodeMirror from 'codemirror' import 'codemirror-mode-elixir' -import path from 'path' -import copyImage from 'browser/main/lib/dataApi/copyImage' -import { findStorage } from 'browser/lib/findStorage' -import fs from 'fs' +import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import eventEmitter from 'browser/main/lib/eventEmitter' import iconv from 'iconv-lite' + const { ipcRenderer } = require('electron') CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' @@ -275,23 +273,13 @@ export default class CodeEditor extends React.Component { this.editor.setCursor(cursor) } - handleDropImage (e) { - e.preventDefault() - const ValidImageTypes = ['image/gif', 'image/jpeg', 'image/png'] - - const file = e.dataTransfer.files[0] - const filePath = file.path - const filename = path.basename(filePath) - const fileType = file['type'] - - copyImage(filePath, this.props.storageKey).then((imagePath) => { - var showPreview = ValidImageTypes.indexOf(fileType) > 0 - const imageMd = `${showPreview ? '!' : ''}[${filename}](${path.join('/:storage', imagePath)})` - this.insertImageMd(imageMd) - }) + handleDropImage (dropEvent) { + dropEvent.preventDefault() + const {storageKey, noteKey} = this.props + attachmentManagement.handleAttachmentDrop(this, storageKey, noteKey, dropEvent) } - insertImageMd (imageMd) { + insertAttachmentMd (imageMd) { this.editor.replaceSelection(imageMd) } @@ -317,24 +305,8 @@ export default class CodeEditor extends React.Component { return prevChar === '](' && nextChar === ')' } if (dataTransferItem.type.match('image')) { - const blob = dataTransferItem.getAsFile() - const reader = new FileReader() - let base64data - - reader.readAsDataURL(blob) - reader.onloadend = () => { - base64data = reader.result.replace(/^data:image\/png;base64,/, '') - base64data += base64data.replace('+', ' ') - const binaryData = new Buffer(base64data, 'base64').toString('binary') - const imageName = Math.random().toString(36).slice(-16) - const storagePath = findStorage(this.props.storageKey).path - const imageDir = path.join(storagePath, 'images') - if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir) - const imagePath = path.join(imageDir, `${imageName}.png`) - fs.writeFile(imagePath, binaryData, 'binary') - const imageMd = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})` - this.insertImageMd(imageMd) - } + const {storageKey, noteKey} = this.props + attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem) } else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { this.handlePasteUrl(e, editor, pastedTxt) } diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 83509184e..cefd367bb 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -5,7 +5,7 @@ import styles from './MarkdownEditor.styl' import CodeEditor from 'browser/components/CodeEditor' import MarkdownPreview from 'browser/components/MarkdownPreview' import eventEmitter from 'browser/main/lib/eventEmitter' -import {findStorage} from 'browser/lib/findStorage' +import { findStorage } from 'browser/lib/findStorage' class MarkdownEditor extends React.Component { constructor (props) { @@ -223,7 +223,7 @@ class MarkdownEditor extends React.Component { } render () { - const { className, value, config, storageKey } = this.props + const {className, value, config, storageKey, noteKey} = this.props let editorFontSize = parseInt(config.editor.fontSize, 10) if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 @@ -249,50 +249,52 @@ class MarkdownEditor extends React.Component { ? 'codeEditor' : 'codeEditor--hide' } - ref='code' - mode='GitHub Flavored Markdown' - value={value} - theme={config.editor.theme} - keyMap={config.editor.keyMap} - fontFamily={config.editor.fontFamily} - fontSize={editorFontSize} - indentType={config.editor.indentType} - indentSize={editorIndentSize} - enableRulers={config.editor.enableRulers} - rulers={config.editor.rulers} - displayLineNumbers={config.editor.displayLineNumbers} - scrollPastEnd={config.editor.scrollPastEnd} - storageKey={storageKey} - fetchUrlTitle={config.editor.fetchUrlTitle} - onChange={(e) => this.handleChange(e)} - onBlur={(e) => this.handleBlur(e)} + ref='code' + mode='GitHub Flavored Markdown' + value={value} + theme={config.editor.theme} + keyMap={config.editor.keyMap} + fontFamily={config.editor.fontFamily} + fontSize={editorFontSize} + indentType={config.editor.indentType} + indentSize={editorIndentSize} + enableRulers={config.editor.enableRulers} + rulers={config.editor.rulers} + displayLineNumbers={config.editor.displayLineNumbers} + scrollPastEnd={config.editor.scrollPastEnd} + storageKey={storageKey} + noteKey={noteKey} + fetchUrlTitle={config.editor.fetchUrlTitle} + onChange={(e) => this.handleChange(e)} + onBlur={(e) => this.handleBlur(e)} /> this.handleContextMenu(e)} - onDoubleClick={(e) => this.handleDoubleClick(e)} - tabIndex='0' - value={this.state.renderValue} - onMouseUp={(e) => this.handlePreviewMouseUp(e)} - onMouseDown={(e) => this.handlePreviewMouseDown(e)} - onCheckboxClick={(e) => this.handleCheckboxClick(e)} - showCopyNotification={config.ui.showCopyNotification} - storagePath={storage.path} + style={previewStyle} + theme={config.ui.theme} + keyMap={config.editor.keyMap} + fontSize={config.preview.fontSize} + fontFamily={config.preview.fontFamily} + codeBlockTheme={config.preview.codeBlockTheme} + codeBlockFontFamily={config.editor.fontFamily} + lineNumber={config.preview.lineNumber} + indentSize={editorIndentSize} + scrollPastEnd={config.preview.scrollPastEnd} + smartQuotes={config.preview.smartQuotes} + sanitize={config.preview.sanitize} + ref='preview' + onContextMenu={(e) => this.handleContextMenu(e)} + onDoubleClick={(e) => this.handleDoubleClick(e)} + tabIndex='0' + value={this.state.renderValue} + onMouseUp={(e) => this.handlePreviewMouseUp(e)} + onMouseDown={(e) => this.handlePreviewMouseDown(e)} + onCheckboxClick={(e) => this.handleCheckboxClick(e)} + showCopyNotification={config.ui.showCopyNotification} + storagePath={storage.path} + noteKey={noteKey} /> ) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 6e6bb9ec7..efa4ca6b5 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -13,9 +13,11 @@ import htmlTextHelper from 'browser/lib/htmlTextHelper' import copy from 'copy-to-clipboard' import mdurl from 'mdurl' import exportNote from 'browser/main/lib/dataApi/exportNote' -import {escapeHtmlCharacters} from 'browser/lib/utils' +import { escapeHtmlCharacters } from 'browser/lib/utils' const { remote } = require('electron') +const attachmentManagement = require('../main/lib/dataApi/attachmentManagement') + const { app } = remote const path = require('path') const dialog = remote.dialog @@ -209,8 +211,9 @@ export default class MarkdownPreview extends React.Component { const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams() const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber) - const body = this.markdown.render(escapeHtmlCharacters(noteContent)) + let body = this.markdown.render(escapeHtmlCharacters(noteContent)) const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] + const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath) files.forEach((file) => { file = file.replace('file://', '') @@ -219,6 +222,13 @@ export default class MarkdownPreview extends React.Component { dst: 'css' }) }) + attachmentsAbsolutePaths.forEach((attachment) => { + exportTasks.push({ + src: attachment, + dst: attachmentManagement.DESTINATION_FOLDER + }) + }) + body = attachmentManagement.removeStorageAndNoteReferences(body, this.props.noteKey) let styles = '' files.forEach((file) => { @@ -391,13 +401,11 @@ export default class MarkdownPreview extends React.Component { value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) }) } - this.refs.root.contentWindow.document.body.innerHTML = this.markdown.render(value) + let renderedHTML = this.markdown.render(value) + this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath) _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { this.fixDecodedURI(el) - el.href = this.markdown.normalizeLinkText(el.href) - if (!/\/:storage/.test(el.href)) return - el.href = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.href)))}` el.addEventListener('click', this.anchorClickHandler) }) @@ -409,12 +417,6 @@ export default class MarkdownPreview extends React.Component { el.addEventListener('click', this.linkClickHandler) }) - _.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => { - el.src = this.markdown.normalizeLinkText(el.src) - if (!/\/:storage/.test(el.src)) return - el.src = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}` - }) - codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme) ? codeBlockTheme : 'default' diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index c30f50dae..27505a5aa 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -88,7 +88,7 @@ class MarkdownSplitEditor extends React.Component { } render () { - const { config, value, storageKey } = this.props + const {config, value, storageKey, noteKey} = this.props const storage = findStorage(storageKey) let editorFontSize = parseInt(config.editor.fontSize, 10) if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 @@ -115,6 +115,7 @@ class MarkdownSplitEditor extends React.Component { scrollPastEnd={config.editor.scrollPastEnd} fetchUrlTitle={config.editor.fetchUrlTitle} storageKey={storageKey} + noteKey={noteKey} onChange={this.handleOnChange.bind(this)} onScroll={this.handleScroll.bind(this)} /> @@ -138,6 +139,7 @@ class MarkdownSplitEditor extends React.Component { onScroll={this.handleScroll.bind(this)} showCopyNotification={config.ui.showCopyNotification} storagePath={storage.path} + noteKey={noteKey} /> ) diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 6f1f2f00b..1ef488a70 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -5,7 +5,7 @@ import math from '@rokt33r/markdown-it-math' import _ from 'lodash' import ConfigManager from 'browser/main/lib/ConfigManager' import katex from 'katex' -import {lastFindInArray} from './utils' +import { lastFindInArray } from './utils' function createGutter (str, firstLineNumber) { if (Number.isNaN(firstLineNumber)) firstLineNumber = 1 @@ -234,10 +234,6 @@ class Markdown { if (!_.isString(content)) content = '' return this.md.render(content) } - - normalizeLinkText (linkText) { - return this.md.normalizeLinkText(linkText) - } } export default Markdown diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 6821bf2fa..b497a1dbd 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -288,6 +288,7 @@ class MarkdownNoteDetail extends React.Component { config={config} value={note.content} storageKey={note.storage} + noteKey={note.key} onChange={this.handleUpdateContent.bind(this)} ignorePreviewPointerEvents={ignorePreviewPointerEvents} /> @@ -297,6 +298,7 @@ class MarkdownNoteDetail extends React.Component { config={config} value={note.content} storageKey={note.storage} + noteKey={note.key} onChange={this.handleUpdateContent.bind(this)} ignorePreviewPointerEvents={ignorePreviewPointerEvents} /> diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js new file mode 100644 index 000000000..e696f4ff3 --- /dev/null +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -0,0 +1,204 @@ +const uniqueSlug = require('unique-slug') +const fs = require('fs') +const path = require('path') +const findStorage = require('browser/lib/findStorage') +const mdurl = require('mdurl') +const escapeStringRegexp = require('escape-string-regexp') + +const STORAGE_FOLDER_PLACEHOLDER = ':storage' +const DESTINATION_FOLDER = 'attachments' + +/** + * @description + * Copies a copy of an attachment to the storage folder specified by the given key and return the generated attachment name. + * Renames the file to match a unique file name. + * + * @param {String} sourceFilePath The source path of the attachment to be copied + * @param {String} storageKey Storage key of the destination storage + * @param {String} noteKey Key of the current note. Will be used as subfolder in :storage + * @param {boolean} useRandomName determines whether a random filename for the new file is used. If false the source file name is used + * @return {Promise} name (inclusive extension) of the generated file + */ +function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = true) { + return new Promise((resolve, reject) => { + if (!sourceFilePath) { + reject('sourceFilePath has to be given') + } + + if (!storageKey) { + reject('storageKey has to be given') + } + + if (!noteKey) { + reject('noteKey has to be given') + } + + try { + if (!fs.existsSync(sourceFilePath)) { + reject('source file does not exist') + } + + const targetStorage = findStorage.findStorage(storageKey) + + const inputFile = fs.createReadStream(sourceFilePath) + let destinationName + if (useRandomName) { + destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}` + } else { + destinationName = path.basename(sourceFilePath) + } + 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) + } catch (e) { + return reject(e) + } + }) +} + +function createAttachmentDestinationFolder (destinationStoragePath, noteKey) { + let destinationDir = path.join(destinationStoragePath, DESTINATION_FOLDER) + if (!fs.existsSync(destinationDir)) { + fs.mkdirSync(destinationDir) + } + destinationDir = path.join(destinationStoragePath, DESTINATION_FOLDER, noteKey) + if (!fs.existsSync(destinationDir)) { + fs.mkdirSync(destinationDir) + } +} + +/** + * @description Fixes the URLs embedded in the generated HTML so that they again refer actual local files. + * @param {String} renderedHTML HTML in that the links should be fixed + * @param {String} storagePath Path of the current storage + * @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths. + */ +function fixLocalURLS (renderedHTML, storagePath) { + return renderedHTML.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER)) +} + +/** + * @description Generates the markdown code for a given attachment + * @param {String} fileName Name of the attachment + * @param {String} path Path of the attachment + * @param {Boolean} showPreview Indicator whether the generated markdown should show a preview of the image. Note that at the moment only previews for images are supported + * @returns {String} Generated markdown code + */ +function generateAttachmentMarkdown (fileName, path, showPreview) { + return `${showPreview ? '!' : ''}[${fileName}](${path})` +} + +/** + * @description Handles the drop-event of a file. Includes the necessary markdown code and copies the file to the corresponding storage folder. + * The method calls {CodeEditor#insertAttachmentMd()} to include the generated markdown at the needed place! + * @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code + * @param {String} storageKey Key of the current storage + * @param {String} noteKey Key of the current note + * @param {Event} dropEvent DropEvent + */ +function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) { + const file = dropEvent.dataTransfer.files[0] + const filePath = file.path + const originalFileName = path.basename(filePath) + const fileType = file['type'] + + copyAttachment(filePath, storageKey, noteKey).then((fileName) => { + let showPreview = fileType.startsWith('image') + let imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview) + codeEditor.insertAttachmentMd(imageMd) + }) +} + +/** + * @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code + * @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code + * @param {String} storageKey Key of the current storage + * @param {String} noteKey Key of the current note + * @param {DataTransferItem} dataTransferItem Part of the past-event + */ +function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) { + if (!codeEditor) { + throw new Error('codeEditor has to be given') + } + if (!storageKey) { + throw new Error('storageKey has to be given') + } + + if (!noteKey) { + throw new Error('noteKey has to be given') + } + if (!dataTransferItem) { + throw new Error('dataTransferItem has to be given') + } + + const blob = dataTransferItem.getAsFile() + const reader = new FileReader() + let base64data + const targetStorage = findStorage.findStorage(storageKey) + const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + createAttachmentDestinationFolder(targetStorage.path, noteKey) + + let imageName = `${uniqueSlug()}.png` + const imagePath = path.join(destinationDir, imageName) + + reader.onloadend = function () { + base64data = reader.result.replace(/^data:image\/png;base64,/, '') + base64data += base64data.replace('+', ' ') + const binaryData = new Buffer(base64data, 'base64').toString('binary') + fs.writeFile(imagePath, binaryData, 'binary') + let imageMd = generateAttachmentMarkdown(imageName, imagePath, true) + codeEditor.insertAttachmentMd(imageMd) + } + reader.readAsDataURL(blob) +} + +/** + * @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 + */ +function getAttachmentsInContent (markdownContent) { + let preparedInput = markdownContent.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep) + let regexp = new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + '([a-zA-Z0-9]|-)+' + escapeStringRegexp(path.sep) + '[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)?', 'g') + return preparedInput.match(regexp) +} + +/** + * @description Returns an array of the absolute paths of the attachments referenced in the given markdown code + * @param {String} markdownContent content in which the attachment paths should be found + * @param {String} storagePath path of the current storage + * @returns {String[]} Absolute paths of the referenced attachments + */ +function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) { + let temp = getAttachmentsInContent(markdownContent) + let result = [] + for (let relativePath of temp) { + result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER))) + } + return result +} + +/** + * @description Deletes all :storage and noteKey references from the given input. + * @param input Input in which the references should be deleted + * @param noteKey Key of the current note + * @returns {String} Input without the references + */ +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) +} + +module.exports = { + copyAttachment, + fixLocalURLS, + generateAttachmentMarkdown, + handleAttachmentDrop, + handlePastImageEvent, + getAttachmentsInContent, + getAbsolutePathsOfAttachmentsInContent, + removeStorageAndNoteReferences, + STORAGE_FOLDER_PLACEHOLDER, + DESTINATION_FOLDER +} diff --git a/browser/main/lib/dataApi/copyImage.js b/browser/main/lib/dataApi/copyImage.js index 6a79b8b73..73f64b7c0 100644 --- a/browser/main/lib/dataApi/copyImage.js +++ b/browser/main/lib/dataApi/copyImage.js @@ -2,6 +2,8 @@ const fs = require('fs') const path = require('path') const { findStorage } = require('browser/lib/findStorage') +//TODO: ehhc: delete this + /** * @description Copy an image and return the path. * @param {String} filePath diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index 313bb85b3..e4fec5f45 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -1,13 +1,9 @@ import copyFile from 'browser/main/lib/dataApi/copyFile' -import {findStorage} from 'browser/lib/findStorage' -import filenamify from 'filenamify' +import { findStorage } from 'browser/lib/findStorage' const fs = require('fs') const path = require('path') -const LOCAL_STORED_REGEX = /!\[(.*?)]\(\s*?\/:storage\/(.*\.\S*?)\)/gi -const IMAGES_FOLDER_NAME = 'images' - /** * Export note together with images * @@ -28,21 +24,7 @@ function exportNote (storageKey, noteContent, targetPath, outputFormatter) { throw new Error('Storage path is not found') } - let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => { - dstFilename = filenamify(dstFilename, {replacement: '_'}) - if (!path.extname(dstFilename)) { - dstFilename += path.extname(srcFilename) - } - - const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename) - - exportTasks.push({ - src: path.join(IMAGES_FOLDER_NAME, srcFilename), - dst: dstRelativePath - }) - - return `![${dstFilename}](${dstRelativePath})` - }) + let exportedData = noteContent if (outputFormatter) { exportedData = outputFormatter(exportedData, exportTasks) diff --git a/browser/main/lib/dataApi/moveNote.js b/browser/main/lib/dataApi/moveNote.js index 928d331b2..4528b8354 100644 --- a/browser/main/lib/dataApi/moveNote.js +++ b/browser/main/lib/dataApi/moveNote.js @@ -75,6 +75,7 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) { while (match != null) { const [, filename] = match const oldPath = path.join(oldStorage.path, 'images', filename) + //TODO: ehhc: attachmentManagement moveTasks.push( copyImage(oldPath, noteData.storage, false) .then(() => { diff --git a/package.json b/package.json index 038d4c00c..82c908968 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "striptags": "^2.2.1", "superagent": "^1.2.0", "superagent-promise": "^1.0.3", + "unique-slug": "2.0.0", "uuid": "^3.2.1" }, "devDependencies": { diff --git a/tests/dataApi/attachmentManagement.test.js b/tests/dataApi/attachmentManagement.test.js new file mode 100644 index 000000000..45d8c548a --- /dev/null +++ b/tests/dataApi/attachmentManagement.test.js @@ -0,0 +1,263 @@ +'use strict' + +jest.mock('fs') +const fs = require('fs') +const path = require('path') +const findStorage = require('browser/lib/findStorage') +jest.mock('unique-slug') +const uniqueSlug = require('unique-slug') +const mdurl = require('mdurl') + +const systemUnderTest = require('browser/main/lib/dataApi/attachmentManagement') + +it('should test that copyAttachment should throw an error if sourcePath or storageKey or noteKey are undefined', function () { + systemUnderTest.copyAttachment(undefined, 'storageKey').then(() => {}, error => { + expect(error).toBe('sourceFilePath has to be given') + }) + systemUnderTest.copyAttachment(null, 'storageKey', 'noteKey').then(() => {}, error => { + expect(error).toBe('sourceFilePath has to be given') + }) + systemUnderTest.copyAttachment('source', undefined, 'noteKey').then(() => {}, error => { + expect(error).toBe('storageKey has to be given') + }) + systemUnderTest.copyAttachment('source', null, 'noteKey').then(() => {}, error => { + expect(error).toBe('storageKey has to be given') + }) + systemUnderTest.copyAttachment('source', 'storageKey', null).then(() => {}, error => { + expect(error).toBe('noteKey has to be given') + }) + systemUnderTest.copyAttachment('source', 'storageKey', undefined).then(() => {}, error => { + expect(error).toBe('noteKey has to be given') + }) +}) + +it('should test that copyAttachment should throw an error if sourcePath dosen\'t exists', function () { + fs.existsSync = jest.fn() + fs.existsSync.mockReturnValue(false) + + systemUnderTest.copyAttachment('path', 'storageKey', 'noteKey').then(() => {}, error => { + expect(error).toBe('source file does not exist') + expect(fs.existsSync).toHaveBeenCalledWith('path') + }) +}) + +it('should test that copyAttachment works correctly assuming correct working of fs', function () { + const dummyExtension = '.ext' + const sourcePath = 'path' + dummyExtension + const storageKey = 'storageKey' + const noteKey = 'noteKey' + const dummyUniquePath = 'dummyPath' + const dummyStorage = {path: 'dummyStoragePath'} + + fs.existsSync = jest.fn() + fs.existsSync.mockReturnValue(true) + fs.createReadStream = jest.fn() + fs.createReadStream.mockReturnValue({pipe: jest.fn()}) + fs.createWriteStream = jest.fn() + + findStorage.findStorage = jest.fn() + findStorage.findStorage.mockReturnValue(dummyStorage) + uniqueSlug.mockReturnValue(dummyUniquePath) + + systemUnderTest.copyAttachment(sourcePath, storageKey, noteKey).then( + function (newFileName) { + expect(findStorage.findStorage).toHaveBeenCalledWith(storageKey) + expect(fs.createReadStream).toHaveBeenCalledWith(sourcePath) + expect(fs.existsSync).toHaveBeenCalledWith(sourcePath) + expect(fs.createReadStream().pipe).toHaveBeenCalled() + expect(fs.createWriteStream).toHaveBeenCalledWith(path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey, dummyUniquePath + dummyExtension)) + expect(newFileName).toBe(dummyUniquePath + dummyExtension) + }) +}) + +it('should test that copyAttachment creates a new folder if the attachment folder doesn\'t exist', function () { + const dummyStorage = {path: 'dummyStoragePath'} + const noteKey = 'noteKey' + const attachmentFolderPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER) + const attachmentFolderNoteKyPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey) + + fs.existsSync = jest.fn() + fs.existsSync.mockReturnValueOnce(true) + fs.existsSync.mockReturnValueOnce(false) + fs.existsSync.mockReturnValueOnce(false) + fs.mkdirSync = jest.fn() + + findStorage.findStorage = jest.fn() + findStorage.findStorage.mockReturnValue(dummyStorage) + uniqueSlug.mockReturnValue('dummyPath') + + systemUnderTest.copyAttachment('path', 'storageKey', 'noteKey').then( + function () { + expect(fs.existsSync).toHaveBeenCalledWith(attachmentFolderPath) + expect(fs.mkdirSync).toHaveBeenCalledWith(attachmentFolderPath) + expect(fs.existsSync).toHaveBeenLastCalledWith(attachmentFolderNoteKyPath) + expect(fs.mkdirSync).toHaveBeenLastCalledWith(attachmentFolderNoteKyPath) + }) +}) + +it('should test that copyAttachment don\'t uses a random file name if not intended ', function () { + const dummyStorage = {path: 'dummyStoragePath'} + + fs.existsSync = jest.fn() + fs.existsSync.mockReturnValueOnce(true) + fs.existsSync.mockReturnValueOnce(false) + fs.mkdirSync = jest.fn() + + findStorage.findStorage = jest.fn() + findStorage.findStorage.mockReturnValue(dummyStorage) + uniqueSlug.mockReturnValue('dummyPath') + + systemUnderTest.copyAttachment('path', 'storageKey', 'noteKey', false).then( + function (newFileName) { + expect(newFileName).toBe('path') + }) +}) + +it('should replace the all ":storage" path with the actual storage path', function () { + let storageFolder = systemUnderTest.DESTINATION_FOLDER + let testInput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + let storagePath = '<>' + let expectedOutput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + let actual = systemUnderTest.fixLocalURLS(testInput, storagePath) + expect(actual).toEqual(expectedOutput) +}) + +it('should test that generateAttachmentMarkdown works correct both with previews and without', function () { + let fileName = 'fileName' + let path = 'path' + let expected = `![${fileName}](${path})` + let actual = systemUnderTest.generateAttachmentMarkdown(fileName, path, true) + expect(actual).toEqual(expected) + expected = `[${fileName}](${path})` + actual = systemUnderTest.generateAttachmentMarkdown(fileName, path, false) + expect(actual).toEqual(expected) +}) + +it('should test that getAttachmentsInContent finds all attachments', function () { + let testInput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + let actual = systemUnderTest.getAttachmentsInContent(testInput) + let expected = [':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg'] + expect(actual).toEqual(expect.arrayContaining(expected)) +}) + +it('should test that getAbsolutePathsOfAttachmentsInContent returns all absolute paths', function () { + let dummyStoragePath = 'dummyStoragePath' + let testInput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + let actual = systemUnderTest.getAbsolutePathsOfAttachmentsInContent(testInput, dummyStoragePath) + let expected = [dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp', + dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx', + dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg'] + expect(actual).toEqual(expect.arrayContaining(expected)) +}) + +it('should remove the all ":storage" and noteKey references', function () { + let storageFolder = systemUnderTest.DESTINATION_FOLDER + let noteKey = 'noteKey' + let testInput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + let storagePath = '<>' + let expectedOutput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + let actual = systemUnderTest.removeStorageAndNoteReferences(testInput, noteKey) + expect(actual).toEqual(expectedOutput) +}) diff --git a/yarn.lock b/yarn.lock index 62b6600af..a0e4d96f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8714,6 +8714,12 @@ uniqs@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" +unique-slug@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" + dependencies: + imurmurhash "^0.1.4" + unique-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"