mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +02:00
Allow exporting notes and notebooks
This commit is contained in:
parent
1213819467
commit
671e8a3fc8
@ -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, '\\)');
|
||||
}
|
||||
|
@ -28,6 +28,10 @@ class BaseCommand {
|
||||
return true;
|
||||
}
|
||||
|
||||
cancellable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async cancel() {}
|
||||
|
||||
name() {
|
||||
|
59
CliClient/app/command-export.js
Normal file
59
CliClient/app/command-export.js
Normal 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;
|
@ -160,6 +160,10 @@ class Command extends BaseCommand {
|
||||
this.syncTarget_ = null;
|
||||
}
|
||||
|
||||
cancellable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Command;
|
@ -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 ) {
|
||||
|
@ -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 ""
|
||||
|
@ -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é."
|
||||
|
||||
|
@ -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 ""
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -1 +1 @@
|
||||
1c9cbfd029fc567f391359ff05b34587
|
||||
bd9c058875b3fa6fb7091aa9f3ee0e74
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
@ -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;
|
||||
|
@ -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),
|
||||
|
@ -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) => {
|
||||
|
96
ReactNativeClient/lib/services/exporter.js
Normal file
96
ReactNativeClient/lib/services/exporter.js
Normal 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 }
|
Loading…
Reference in New Issue
Block a user