1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Allow exporting notes and notebooks

This commit is contained in:
Laurent Cozic 2017-08-20 16:29:18 +02:00
parent 1213819467
commit 671e8a3fc8
17 changed files with 273 additions and 23 deletions

View File

@ -325,8 +325,8 @@ class Application {
await this.activeCommand_.action(cmdArgs);
}
async cancelCurrentCommand() {
await this.activeCommand_.cancel();
currentCommand() {
return this.activeCommand_;
}
async start() {
@ -336,6 +336,12 @@ class Application {
let initArgs = startFlags.matched;
if (argv.length) this.showPromptString_ = false;
if (process.argv[1].indexOf('joplindev') >= 0) {
if (!initArgs.profileDir) initArgs.profileDir = '/mnt/d/Temp/TestNotes2';
initArgs.logLevel = Logger.LEVEL_DEBUG;
initArgs.env = 'dev';
}
Setting.setConstant('appName', initArgs.env == 'dev' ? 'joplindev' : 'joplin');
const profileDir = initArgs.profileDir ? initArgs.profileDir : os.homedir() + '/.config/' + Setting.value('appName');
@ -408,6 +414,8 @@ class Application {
if (!items.length) return;
for (let i = 0; i < items.length; i++) {
items[i] = items[i].replace(/ /g, '\\ ');
items[i] = items[i].replace(/'/g, "\\'");
items[i] = items[i].replace(/:/g, "\\:");
items[i] = items[i].replace(/\(/g, '\\(');
items[i] = items[i].replace(/\)/g, '\\)');
}

View File

@ -28,6 +28,10 @@ class BaseCommand {
return true;
}
cancellable() {
return false;
}
async cancel() {}
name() {

View File

@ -0,0 +1,59 @@
import { BaseCommand } from './base-command.js';
import { Exporter } from 'lib/services/exporter.js';
import { BaseModel } from 'lib/base-model.js';
import { Note } from 'lib/models/note.js';
import { reg } from 'lib/registry.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import fs from 'fs-extra';
class Command extends BaseCommand {
usage() {
return 'export <destination>';
}
description() {
return _('Exports Joplin data to the given target.');
}
options() {
return [
['--note <note>', _('Exports only the given note.')],
['--notebook <notebook>', _('Exports only the given notebook.')],
];
}
async action(args) {
let exportOptions = {};
exportOptions.destDir = args.destination;
exportOptions.writeFile = (filePath, data) => {
return fs.writeFile(filePath, data);
};
exportOptions.copyFile = (source, dest) => {
return fs.copy(source, dest, { overwrite: true });
};
if (args.options.note) {
const notes = await app().loadItems(BaseModel.TYPE_NOTE, args.options.note, { parent: app().currentFolder() });
if (!notes.length) throw new Error(_('Cannot find "%s".', args.options.note));
exportOptions.sourceNoteIds = notes.map((n) => n.id);
} else if (args.options.notebook) {
const folders = await app().loadItems(BaseModel.TYPE_FOLDER, args.options.notebook);
if (!folders.length) throw new Error(_('Cannot find "%s".', args.options.notebook));
exportOptions.sourceFolderIds = folders.map((n) => n.id);
}
const exporter = new Exporter();
const result = await exporter.export(exportOptions);
reg.logger().info('Export result: ', result);
}
}
module.exports = Command;

View File

@ -160,6 +160,10 @@ class Command extends BaseCommand {
this.syncTarget_ = null;
}
cancellable() {
return true;
}
}
module.exports = Command;

View File

@ -53,9 +53,17 @@ if (process.platform === "win32") {
});
}
let commandCancelCalled_ = false;
process.on("SIGINT", async function() {
console.info(_('Received %s', 'SIGINT'));
await application.cancelCurrentCommand();
const cmd = application.currentCommand();
if (!cmd.cancellable() || commandCancelCalled_) {
process.exit(0);
} else {
commandCancelCalled_ = true;
await cmd.cancel();
}
});
process.stdout.on('error', function( err ) {

View File

@ -122,6 +122,15 @@ msgstr ""
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
msgid "Exports Joplin data to the given target."
msgstr ""
msgid "Exports only the given note."
msgstr ""
msgid "Exports only the given notebook."
msgstr ""
msgid "Displays a geolocation URL for the note."
msgstr ""
@ -294,10 +303,6 @@ msgstr ""
msgid "%s %s (%s)"
msgstr ""
#, javascript-format
msgid "Received %s"
msgstr ""
msgid "Fatal error:"
msgstr ""
@ -432,6 +437,9 @@ msgstr ""
msgid "Synchronisation interval"
msgstr ""
msgid "Disabled"
msgstr ""
#, javascript-format
msgid "%d minutes"
msgstr ""

View File

@ -134,6 +134,17 @@ msgstr ""
"Edition de la note en cours. Fermez l'éditeur de texte pour retourner à "
"l'invite de commande."
msgid "Exports Joplin data to the given target."
msgstr ""
#, fuzzy
msgid "Exports only the given note."
msgstr "Affiche la note."
#, fuzzy
msgid "Exports only the given notebook."
msgstr "Affiche la note."
msgid "Displays a geolocation URL for the note."
msgstr "Afficher l'URL de l'emplacement de la note."
@ -335,10 +346,6 @@ msgstr "Affiche les informations de version"
msgid "%s %s (%s)"
msgstr "%s %s (%s)"
#, javascript-format
msgid "Received %s"
msgstr ""
msgid "Fatal error:"
msgstr "Erreur fatale :"
@ -477,6 +484,9 @@ msgstr "Enregistrer l'emplacement avec les notes"
msgid "Synchronisation interval"
msgstr "Interval de synchronisation"
msgid "Disabled"
msgstr ""
#, javascript-format
msgid "%d minutes"
msgstr "%d minutes"
@ -620,6 +630,10 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenue"
#, fuzzy
#~ msgid "Cancelling command..."
#~ msgstr "Annulation..."
#~ msgid "Done."
#~ msgstr "Terminé."

View File

@ -122,6 +122,15 @@ msgstr ""
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
msgid "Exports Joplin data to the given target."
msgstr ""
msgid "Exports only the given note."
msgstr ""
msgid "Exports only the given notebook."
msgstr ""
msgid "Displays a geolocation URL for the note."
msgstr ""
@ -294,10 +303,6 @@ msgstr ""
msgid "%s %s (%s)"
msgstr ""
#, javascript-format
msgid "Received %s"
msgstr ""
msgid "Fatal error:"
msgstr ""
@ -432,6 +437,9 @@ msgstr ""
msgid "Synchronisation interval"
msgstr ""
msgid "Disabled"
msgstr ""
#, javascript-format
msgid "%d minutes"
msgstr ""

View File

@ -7,7 +7,7 @@
"url": "https://github.com/laurent22/joplin"
},
"url": "git://github.com/laurent22/joplin.git",
"version": "0.9.6",
"version": "0.9.7",
"bin": {
"joplin": "./main.js"
},

View File

@ -1 +1 @@
1c9cbfd029fc567f391359ff05b34587
bd9c058875b3fa6fb7091aa9f3ee0e74

View File

@ -133,6 +133,12 @@ class BaseModel {
return { sql: sql, params: params };
}
static async allIds(options = null) {
let q = this.applySqlOptions(options, 'SELECT id FROM `' + this.tableName() + '`');
const rows = await this.db().selectAll(q.sql, q.params);
return rows.map((r) => r.id);
}
static async all(options = null) {
let q = this.applySqlOptions(options, 'SELECT * FROM `' + this.tableName() + '`');
return this.modelSelectAll(q.sql);

View File

@ -33,6 +33,16 @@ class BaseItem extends BaseModel {
throw new Error('Invalid class name: ' + name);
}
static getClassByItemType(itemType) {
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
if (BaseItem.syncItemDefinitions_[i].type == itemType) {
return BaseItem.syncItemDefinitions_[i].classRef;
}
}
throw new Error('Invalid item type: ' + itemType);
}
static async syncedCount(syncTarget) {
const ItemClass = this.itemClass(this.modelType());
const itemType = ItemClass.modelType();
@ -359,6 +369,12 @@ class BaseItem extends BaseModel {
});
}
static syncItemTypes() {
return BaseItem.syncItemDefinitions_.map((def) => {
return def.type;
});
}
static modelTypeToClassName(type) {
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
if (BaseItem.syncItemDefinitions_[i].type == type) return BaseItem.syncItemDefinitions_[i].className;

View File

@ -18,6 +18,11 @@ class NoteTag extends BaseItem {
return super.serialize(item, 'note_tag', fieldNames);
}
static async byNoteIds(noteIds) {
if (!noteIds.length) return [];
return this.modelSelectAll('SELECT * FROM note_tags WHERE note_id IN ("' + noteIds.join('","') + '")');
}
}
export { NoteTag };

View File

@ -51,6 +51,14 @@ class Note extends BaseItem {
return BaseModel.TYPE_NOTE;
}
static linkedResourceIds(body) {
// For example: ![](:/fcca2938a96a22570e8eae2565bc6b0b)
if (!body || body.length <= 32) return [];
const matches = body.match(/\(:\/.{32}\)/g);
if (!matches) return [];
return matches.map((m) => m.substr(3, 32));
}
static new(parentId = '') {
let output = super.new();
output.parent_id = parentId;

View File

@ -328,6 +328,7 @@ Setting.metadata_ = {
'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save location with notes') },
'sync.interval': { value: 300, type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => {
return {
0: _('Disabled'),
300: _('%d minutes', 5),
600: _('%d minutes', 10),
1800: _('%d minutes', 30),

View File

@ -10,6 +10,7 @@ import { shim } from 'lib/shim.js';
import { time } from 'lib/time-utils.js';
import { FileApiDriverMemory } from 'lib/file-api-driver-memory.js';
import { PoorManIntervals } from 'lib/poor-man-intervals.js';
import { _ } from 'lib/locale.js';
const reg = {};
@ -202,12 +203,16 @@ reg.setupRecurrentSync = () => {
reg.recurrentSyncId_ = null;
}
reg.logger().debug('Setting up recurrent sync with interval ' + Setting.value('sync.interval'));
if (!Setting.value('sync.interval')) {
reg.logger().debug('Recurrent sync is disabled');
} else {
reg.logger().debug('Setting up recurrent sync with interval ' + Setting.value('sync.interval'));
reg.recurrentSyncId_ = PoorManIntervals.setInterval(() => {
reg.logger().info('Running background sync on timer...');
reg.scheduleSync(0);
}, 1000 * Setting.value('sync.interval'));
reg.recurrentSyncId_ = PoorManIntervals.setInterval(() => {
reg.logger().info('Running background sync on timer...');
reg.scheduleSync(0);
}, 1000 * Setting.value('sync.interval'));
}
}
reg.setDb = (v) => {

View File

@ -0,0 +1,96 @@
import { BaseItem } from 'lib/models/base-item.js';
import { BaseModel } from 'lib/base-model.js';
import { Resource } from 'lib/models/resource.js';
import { Folder } from 'lib/models/folder.js';
import { NoteTag } from 'lib/models/note-tag.js';
import { Note } from 'lib/models/note.js';
import { Tag } from 'lib/models/tag.js';
import { basename } from 'lib/path-utils.js';
import fs from 'fs-extra';
class Exporter {
async export(options) {
const destDir = options.destDir ? options.destDir : null;
const resourceDir = destDir ? destDir + '/resources' : null;
const writeFile = options.writeFile ? options.writeFile : null;
const copyFile = options.copyFile ? options.copyFile : null;
const sourceFolderIds = options.sourceFolderIds ? options.sourceFolderIds : [];
const sourceNoteIds = options.sourceNoteIds ? options.sourceNoteIds : [];
let result = {
warnings: [],
};
await fs.mkdirp(destDir);
await fs.mkdirp(resourceDir);
const exportItem = async (itemType, itemOrId) => {
const ItemClass = BaseItem.getClassByItemType(itemType);
const item = typeof itemOrId === 'object' ? itemOrId : await ItemClass.load(itemOrId);
if (!item) {
result.warnings.push('Cannot find item with type ' + itemType + ' and ID ' + JSON.stringify(itemOrId));
return;
}
const serialized = await ItemClass.serialize(item);
const filePath = destDir + '/' + ItemClass.systemPath(item);
await writeFile(filePath, serialized);
if (itemType == BaseModel.TYPE_RESOURCE) {
const sourceResourcePath = Resource.fullPath(item);
const destResourcePath = resourceDir + '/' + basename(sourceResourcePath);
await copyFile(sourceResourcePath, destResourcePath);
}
}
let exportedNoteIds = [];
let resourceIds = [];
const folderIds = await Folder.allIds();
for (let folderIndex = 0; folderIndex < folderIds.length; folderIndex++) {
const folderId = folderIds[folderIndex];
if (sourceFolderIds.length && sourceFolderIds.indexOf(folderId) < 0) continue;
if (!sourceNoteIds.length) await exportItem(BaseModel.TYPE_FOLDER, folderId);
const noteIds = await Folder.noteIds(folderId);
for (let noteIndex = 0; noteIndex < noteIds.length; noteIndex++) {
const noteId = noteIds[noteIndex];
if (sourceNoteIds.length && sourceNoteIds.indexOf(noteId) < 0) continue;
const note = await Note.load(noteId);
await exportItem(BaseModel.TYPE_NOTE, note);
exportedNoteIds.push(noteId);
const rids = Note.linkedResourceIds(note.body);
resourceIds = resourceIds.concat(rids);
}
}
for (let i = 0; i < resourceIds.length; i++) {
await exportItem(BaseModel.TYPE_RESOURCE, resourceIds[i]);
}
const noteTags = await NoteTag.all();
let exportedTagIds = [];
for (let i = 0; i < noteTags.length; i++) {
const noteTag = noteTags[i];
if (exportedNoteIds.indexOf(noteTag.note_id) < 0) continue;
await exportItem(BaseModel.TYPE_NOTE_TAG, noteTag.id);
exportedTagIds.push(noteTag.tag_id);
}
for (let i = 0; i < exportedTagIds.length; i++) {
await exportItem(BaseModel.TYPE_TAG, exportedTagIds[i]);
}
return result;
}
}
export { Exporter }