From b93d7a204fc1b3a0bb9e9d46c33961a706f549b3 Mon Sep 17 00:00:00 2001 From: zhoufeng1989 Date: Tue, 14 Aug 2018 12:38:31 +1200 Subject: [PATCH 1/2] Fix 2207 and 2273, add export for storage. --- browser/main/SideNav/StorageItem.js | 40 ++++++++++++++ browser/main/lib/dataApi/exportStorage.js | 63 +++++++++++++++++++++++ browser/main/lib/dataApi/index.js | 1 + browser/main/store.js | 15 +----- tests/dataApi/exportStorage-test.js | 51 ++++++++++++++++++ 5 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 browser/main/lib/dataApi/exportStorage.js create mode 100644 tests/dataApi/exportStorage-test.js diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index d72f0a8f..d17314b3 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -38,6 +38,22 @@ class StorageItem extends React.Component { { type: 'separator' }, + { + label: i18n.__('Export Storage'), + submenu: [ + { + label: i18n.__('Export as txt'), + click: (e) => this.handleExportStorageClick(e, 'txt') + }, + { + label: i18n.__('Export as md'), + click: (e) => this.handleExportStorageClick(e, 'md') + } + ] + }, + { + type: 'separator' + }, { label: i18n.__('Unlink Storage'), click: (e) => this.handleUnlinkStorageClick(e) @@ -68,6 +84,30 @@ class StorageItem extends React.Component { } } + handleExportStorageClick (e, fileType) { + const options = { + properties: ['openDirectory', 'createDirectory'], + buttonLabel: i18n.__('Select directory'), + title: i18n.__('Select a folder to export the files to'), + multiSelections: false + } + dialog.showOpenDialog(remote.getCurrentWindow(), options, + (paths) => { + if (paths && paths.length === 1) { + const { storage, dispatch } = this.props + dataApi + .exportStorage(storage.key, fileType, paths[0]) + .then(data => { + dispatch({ + type: 'EXPORT_STORAGE', + storage: data.storage, + fileType: data.fileType + }) + }) + } + }) + } + handleToggleButtonClick (e) { const { storage, dispatch } = this.props const isOpen = !this.state.isOpen diff --git a/browser/main/lib/dataApi/exportStorage.js b/browser/main/lib/dataApi/exportStorage.js new file mode 100644 index 00000000..ce2c4573 --- /dev/null +++ b/browser/main/lib/dataApi/exportStorage.js @@ -0,0 +1,63 @@ +import { findStorage } from 'browser/lib/findStorage' +import resolveStorageData from './resolveStorageData' +import resolveStorageNotes from './resolveStorageNotes' +import filenamify from 'filenamify' +import * as path from 'path' +import * as fs from 'fs' + +/** + * @param {String} storageKey + * @param {String} fileType + * @param {String} exportDir + * + * @return {Object} + * ``` + * { + * storage: Object, + * fileType: String, + * exportDir: String + * } + * ``` + */ + +function exportStorage (storageKey, fileType, exportDir) { + let targetStorage + try { + targetStorage = findStorage(storageKey) + } catch (e) { + return Promise.reject(e) + } + + return resolveStorageData(targetStorage) + .then(storage => ( + resolveStorageNotes(storage).then(notes => ({storage, notes})) + )) + .then(function exportNotes (data) { + const { storage, notes } = data + const folderNamesMapping = {} + storage.folders.forEach(folder => { + const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'})) + folderNamesMapping[folder.key] = folderExportedDir + // make sure directory exists + try { + fs.mkdirSync(folderExportedDir) + } catch (e) {} + }) + notes + .filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE') + .forEach(markdownNote => { + const folderExportedDir = folderNamesMapping[markdownNote.folder] + const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}` + const notePath = path.join(folderExportedDir, snippetName) + fs.writeFileSync(notePath, markdownNote.content) + }) + + return { + storage, + fileType, + exportDir + } + }) +} + +module.exports = exportStorage diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js index 4e2f0061..92be6b93 100644 --- a/browser/main/lib/dataApi/index.js +++ b/browser/main/lib/dataApi/index.js @@ -9,6 +9,7 @@ const dataApi = { deleteFolder: require('./deleteFolder'), reorderFolder: require('./reorderFolder'), exportFolder: require('./exportFolder'), + exportStorage: require('./exportStorage'), createNote: require('./createNote'), updateNote: require('./updateNote'), deleteNote: require('./deleteNote'), diff --git a/browser/main/store.js b/browser/main/store.js index a1b6b791..b8f13cc8 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -216,16 +216,10 @@ function data (state = defaultDataMap(), action) { return state } case 'UPDATE_FOLDER': - state = Object.assign({}, state) - state.storageMap = new Map(state.storageMap) - state.storageMap.set(action.storage.key, action.storage) - return state case 'REORDER_FOLDER': - state = Object.assign({}, state) - state.storageMap = new Map(state.storageMap) - state.storageMap.set(action.storage.key, action.storage) - return state case 'EXPORT_FOLDER': + case 'RENAME_STORAGE': + case 'EXPORT_STORAGE': state = Object.assign({}, state) state.storageMap = new Map(state.storageMap) state.storageMap.set(action.storage.key, action.storage) @@ -355,11 +349,6 @@ function data (state = defaultDataMap(), action) { }) } return state - case 'RENAME_STORAGE': - state = Object.assign({}, state) - state.storageMap = new Map(state.storageMap) - state.storageMap.set(action.storage.key, action.storage) - return state case 'EXPAND_STORAGE': state = Object.assign({}, state) state.storageMap = new Map(state.storageMap) diff --git a/tests/dataApi/exportStorage-test.js b/tests/dataApi/exportStorage-test.js new file mode 100644 index 00000000..1ee98328 --- /dev/null +++ b/tests/dataApi/exportStorage-test.js @@ -0,0 +1,51 @@ +const test = require('ava') +const exportStorage = require('browser/main/lib/dataApi/exportStorage') + +global.document = require('jsdom').jsdom('') +global.window = document.defaultView +global.navigator = window.navigator + +const Storage = require('dom-storage') +const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true }) +const path = require('path') +const TestDummy = require('../fixtures/TestDummy') +const os = require('os') +const fs = require('fs') +const sander = require('sander') + +test.beforeEach(t => { + t.context.storageDir = path.join(os.tmpdir(), 'test/export-storage') + t.context.storage = TestDummy.dummyStorage(t.context.storageDir) + t.context.exportDir = path.join(os.tmpdir(), 'test/export-storage-output') + try { fs.mkdirSync(t.context.exportDir) } catch (e) {} + localStorage.setItem('storages', JSON.stringify([t.context.storage.cache])) +}) + +test.serial('Export a storage', t => { + const storageKey = t.context.storage.cache.key + const folders = t.context.storage.json.folders + const notes = t.context.storage.notes + const exportDir = t.context.exportDir + const folderKeyToName = folders.reduce( + (acc, folder) => { + acc[folder.key] = folder.name + return acc + }, {}) + return exportStorage(storageKey, 'md', exportDir) + .then(() => { + notes.forEach(note => { + const noteDir = path.join(exportDir, folderKeyToName[note.folder], `${note.title}.md`) + if (note.type === 'MARKDOWN_NOTE') { + t.true(fs.existsSync(noteDir)) + } else if (note.type === 'SNIPPET_NOTE') { + t.false(fs.existsSync(noteDir)) + } + }) + }) +}) + +test.afterEach.always(t => { + localStorage.clear() + sander.rimrafSync(t.context.storageDir) + sander.rimrafSync(t.context.exportDir) +}) From aa0566b8ca2aae8bc64f30e6a844cc8683477f5a Mon Sep 17 00:00:00 2001 From: zhoufeng1989 Date: Sun, 26 Aug 2018 21:15:20 +1200 Subject: [PATCH 2/2] Update test cases for export storage, check content of exported notes. --- tests/dataApi/exportStorage-test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/dataApi/exportStorage-test.js b/tests/dataApi/exportStorage-test.js index 1ee98328..e5594329 100644 --- a/tests/dataApi/exportStorage-test.js +++ b/tests/dataApi/exportStorage-test.js @@ -37,6 +37,7 @@ test.serial('Export a storage', t => { const noteDir = path.join(exportDir, folderKeyToName[note.folder], `${note.title}.md`) if (note.type === 'MARKDOWN_NOTE') { t.true(fs.existsSync(noteDir)) + t.is(fs.readFileSync(noteDir, 'utf8'), note.content) } else if (note.type === 'SNIPPET_NOTE') { t.false(fs.existsSync(noteDir)) }