mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
Merge pull request #2292 from zhoufeng1989/master
Export all markdown files in a storage
This commit is contained in:
@@ -38,6 +38,22 @@ class StorageItem extends React.Component {
|
|||||||
{
|
{
|
||||||
type: 'separator'
|
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'),
|
label: i18n.__('Unlink Storage'),
|
||||||
click: (e) => this.handleUnlinkStorageClick(e)
|
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) {
|
handleToggleButtonClick (e) {
|
||||||
const { storage, dispatch } = this.props
|
const { storage, dispatch } = this.props
|
||||||
const isOpen = !this.state.isOpen
|
const isOpen = !this.state.isOpen
|
||||||
|
|||||||
63
browser/main/lib/dataApi/exportStorage.js
Normal file
63
browser/main/lib/dataApi/exportStorage.js
Normal file
@@ -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
|
||||||
@@ -9,6 +9,7 @@ const dataApi = {
|
|||||||
deleteFolder: require('./deleteFolder'),
|
deleteFolder: require('./deleteFolder'),
|
||||||
reorderFolder: require('./reorderFolder'),
|
reorderFolder: require('./reorderFolder'),
|
||||||
exportFolder: require('./exportFolder'),
|
exportFolder: require('./exportFolder'),
|
||||||
|
exportStorage: require('./exportStorage'),
|
||||||
createNote: require('./createNote'),
|
createNote: require('./createNote'),
|
||||||
updateNote: require('./updateNote'),
|
updateNote: require('./updateNote'),
|
||||||
deleteNote: require('./deleteNote'),
|
deleteNote: require('./deleteNote'),
|
||||||
|
|||||||
@@ -216,16 +216,10 @@ function data (state = defaultDataMap(), action) {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
case 'UPDATE_FOLDER':
|
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':
|
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 'EXPORT_FOLDER':
|
||||||
|
case 'RENAME_STORAGE':
|
||||||
|
case 'EXPORT_STORAGE':
|
||||||
state = Object.assign({}, state)
|
state = Object.assign({}, state)
|
||||||
state.storageMap = new Map(state.storageMap)
|
state.storageMap = new Map(state.storageMap)
|
||||||
state.storageMap.set(action.storage.key, action.storage)
|
state.storageMap.set(action.storage.key, action.storage)
|
||||||
@@ -355,11 +349,6 @@ function data (state = defaultDataMap(), action) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return state
|
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':
|
case 'EXPAND_STORAGE':
|
||||||
state = Object.assign({}, state)
|
state = Object.assign({}, state)
|
||||||
state.storageMap = new Map(state.storageMap)
|
state.storageMap = new Map(state.storageMap)
|
||||||
|
|||||||
52
tests/dataApi/exportStorage-test.js
Normal file
52
tests/dataApi/exportStorage-test.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
const test = require('ava')
|
||||||
|
const exportStorage = require('browser/main/lib/dataApi/exportStorage')
|
||||||
|
|
||||||
|
global.document = require('jsdom').jsdom('<body></body>')
|
||||||
|
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))
|
||||||
|
t.is(fs.readFileSync(noteDir, 'utf8'), note.content)
|
||||||
|
} 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)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user