1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-09-16 08:56:40 +02:00
This commit is contained in:
Laurent Cozic
2017-07-03 18:58:01 +00:00
parent dde3ea2008
commit b36905cb3c
9 changed files with 238 additions and 120 deletions

View File

@@ -1,3 +1,5 @@
"use strict"
require('source-map-support').install(); require('source-map-support').install();
require('babel-plugin-transform-runtime'); require('babel-plugin-transform-runtime');
@@ -94,12 +96,32 @@ async function clientItems(client) {
} }
} }
function randomTag(items) {
let tags = [];
for (let i = 0; i < items.length; i++) {
if (items[i].type_ != 5) continue;
tags.push(items[i]);
}
return randomElement(tags);
}
function randomNote(items) {
let notes = [];
for (let i = 0; i < items.length; i++) {
if (items[i].type_ != 1) continue;
notes.push(items[i]);
}
return randomElement(notes);
}
async function execRandomCommand(client) { async function execRandomCommand(client) {
let possibleCommands = [ let possibleCommands = [
['mkbook {word}', 40], // CREATE FOLDER ['mkbook {word}', 40], // CREATE FOLDER
['mknote {word}', 70], // CREATE NOTE ['mknote {word}', 70], // CREATE NOTE
[async () => { // DELETE RANDOM ITEM [async () => { // DELETE RANDOM ITEM
let items = clientItems(client); let items = await clientItems(client);
let item = randomElement(items); let item = randomElement(items);
if (!item) return; if (!item) return;
@@ -107,6 +129,8 @@ async function execRandomCommand(client) {
return execCommand(client, 'rm -f ' + item.title); return execCommand(client, 'rm -f ' + item.title);
} else if (item.type_ == 2) { } else if (item.type_ == 2) {
return execCommand(client, 'rm -f ' + '../' + item.title); return execCommand(client, 'rm -f ' + '../' + item.title);
} else if (item.type_ == 5) {
// tag
} else { } else {
throw new Error('Unknown type: ' + item.type_); throw new Error('Unknown type: ' + item.type_);
} }
@@ -122,12 +146,22 @@ async function execRandomCommand(client) {
return execCommand(client, 'sync --random-failures', options); return execCommand(client, 'sync --random-failures', options);
}, 30], }, 30],
[async () => { // UPDATE RANDOM ITEM [async () => { // UPDATE RANDOM ITEM
let items = clientItems(client); let items = await clientItems(client);
let item = randomElement(items); let item = randomElement(items);
if (!item) return; if (!item) return;
return execCommand(client, 'set ' + item.id + ' title "' + randomWord() + '"'); return execCommand(client, 'set ' + item.id + ' title "' + randomWord() + '"');
}, 50], }, 50],
[async () => { // ADD TAG
let items = await clientItems(client);
let note = randomNote(items);
if (!note) return;
let tag = randomTag(items);
let tagTitle = !tag || Math.random() >= 0.9 ? 'tag-' + randomWord() : tag.title;
return execCommand(client, 'tag add ' + tagTitle + ' "' + note.title + '"');
}, 50],
]; ];
let cmd = null; let cmd = null;
@@ -171,7 +205,16 @@ function compareItems(item1, item2) {
if (n == 'sync_time') continue; if (n == 'sync_time') continue;
let p1 = item1[n]; let p1 = item1[n];
let p2 = item2[n]; let p2 = item2[n];
if (p1 !== p2) output.push(n);
if (n == 'notes_') {
p1.sort();
p2.sort();
if (JSON.stringify(p1) !== JSON.stringify(p2)) {
output.push(n);
}
} else {
if (p1 !== p2) output.push(n);
}
} }
return output; return output;
} }
@@ -235,8 +278,8 @@ async function compareClientItems(clientItems) {
let diff = compareItems(item1, item2); let diff = compareItems(item1, item2);
if (diff.length) { if (diff.length) {
differences.push({ differences.push({
item1: item1, item1: JSON.stringify(item1),
item2: item2, item2: JSON.stringify(item2),
}); });
} }
} }

View File

@@ -14,6 +14,7 @@ import { Folder } from 'lib/models/folder.js';
import { Resource } from 'lib/models/resource.js'; import { Resource } from 'lib/models/resource.js';
import { BaseItem } from 'lib/models/base-item.js'; import { BaseItem } from 'lib/models/base-item.js';
import { Note } from 'lib/models/note.js'; import { Note } from 'lib/models/note.js';
import { Tag } from 'lib/models/tag.js';
import { Setting } from 'lib/models/setting.js'; import { Setting } from 'lib/models/setting.js';
import { Synchronizer } from 'lib/synchronizer.js'; import { Synchronizer } from 'lib/synchronizer.js';
import { Logger } from 'lib/logger.js'; import { Logger } from 'lib/logger.js';
@@ -59,7 +60,7 @@ commands.push({
aliases: ['mkdir'], aliases: ['mkdir'],
description: 'Creates a new notebook', description: 'Creates a new notebook',
action: function(args, end) { action: function(args, end) {
Folder.save({ title: args['notebook'] }).then((folder) => { Folder.save({ title: args['notebook'] }, { duplicateCheck: true }).then((folder) => {
switchCurrentFolder(folder); switchCurrentFolder(folder);
}).catch((error) => { }).catch((error) => {
this.log(error); this.log(error);
@@ -70,7 +71,7 @@ commands.push({
}); });
commands.push({ commands.push({
usage: 'mknote <note-title>', usage: 'mknote <note>',
aliases: ['touch'], aliases: ['touch'],
description: 'Creates a new note', description: 'Creates a new note',
action: function(args, end) { action: function(args, end) {
@@ -81,7 +82,7 @@ commands.push({
} }
let note = { let note = {
title: args['note-title'], title: args['note'],
parent_id: currentFolder.id, parent_id: currentFolder.id,
}; };
Note.save(note).catch((error) => { Note.save(note).catch((error) => {
@@ -265,6 +266,45 @@ commands.push({
autocomplete: autocompleteItems, autocomplete: autocompleteItems,
}); });
commands.push({
usage: 'tag <command> [tag] [note]',
description: '<command> can be "add", "remove" or "list" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.',
action: async function(args, end) {
try {
let tag = null;
if (args.tag) tag = await loadItem(BaseModel.MODEL_TYPE_TAG, args.tag);
let note = null;
if (args.note) note = await loadItem(BaseModel.MODEL_TYPE_NOTE, args.note);
if (args.command == 'remove' && !tag) throw new Error(_('Tag does not exist: "%s"', args.tag));
if (args.command == 'add') {
if (!note) throw new Error(_('Note does not exist: "%s"', args.note));
if (!tag) tag = await Tag.save({ title: args.tag });
await Tag.addNote(tag.id, note.id);
} else if (args.command == 'remove') {
if (!tag) throw new Error(_('Tag does not exist: "%s"', args.tag));
if (!note) throw new Error(_('Note does not exist: "%s"', args.note));
await Tag.removeNote(tag.id, note.id);
} else if (args.command == 'list') {
if (tag) {
let notes = await Tag.notes(tag.id);
notes.map((note) => { this.log(note.title); });
} else {
let tags = await Tag.all();
tags.map((tag) => { this.log(tag.title); });
}
} else {
throw new Error(_('Invalid command: "%s"', args.command));
}
} catch (error) {
this.log(error);
}
end();
}
});
commands.push({ commands.push({
usage: 'dump', usage: 'dump',
description: 'Dumps the complete database as JSON.', description: 'Dumps the complete database as JSON.',
@@ -278,6 +318,13 @@ commands.push({
items.push(folder); items.push(folder);
items = items.concat(notes); items = items.concat(notes);
} }
let tags = await Tag.all();
for (let i = 0; i < tags.length; i++) {
tags[i].notes_ = await Tag.tagNoteIds(tags[i].id);
}
items = items.concat(tags);
this.log(JSON.stringify(items)); this.log(JSON.stringify(items));
} catch (error) { } catch (error) {
@@ -285,8 +332,7 @@ commands.push({
} }
end(); end();
}, }
autocomplete: autocompleteFolders,
}); });
commands.push({ commands.push({
@@ -495,6 +541,19 @@ commands.push({
}, },
}); });
async function loadItem(type, pattern) {
let output = await loadItems(type, pattern);
return output.length ? output[0] : null;
}
async function loadItems(type, pattern) {
let ItemClass = BaseItem.itemClass(type);
let item = await ItemClass.loadByTitle(pattern);
if (item) return [item];
item = await ItemClass.load(pattern);
return [item];
}
function commandByName(name) { function commandByName(name) {
for (let i = 0; i < commands.length; i++) { for (let i = 0; i < commands.length; i++) {
let c = commands[i]; let c = commands[i];
@@ -519,58 +578,6 @@ function execCommand(name, args) {
}); });
} }
// async function execCommand(args) {
// var parseArgs = require('minimist');
// let results = parseArgs(args);
// //var results = vorpal.parse(args, { use: 'minimist' });
// if (!results['_'].length) throw new Error(_('Invalid command: %s', args));
// console.info(results);
// let commandName = results['_'].splice(0, 1);
// let cmd = commandByName(commandName);
// if (!cmd) throw new Error(_('Unknown command: %s', args));
// let usage = cmd.usage.split(' ');
// let commandArgs = [];
// usage.splice(0, 1);
// for (let i = 0; i < usage.length; i++) {
// let u = usage[i].trim();
// if (u == '') continue;
// let required = false;
// if (u.length >= 3 && u[0] == '<' && u[u.length - 1] == '>') {
// required = true;
// u = u.substr(1, u.length - 2);
// }
// if (u.length >= 3 && u[0] == '[' && u[u.length - 1] == ']') {
// u = u.substr(1, u.length - 2);
// }
// if (required && !results['_'].length) throw new Error(_('Missing argument: %s', args));
// if (!results['_'].length) break;
// console.info(u);
// commandArgs[u] = results['_'].splice(0, 1);
// }
// console.info(commandArgs);
// // usage: 'import-enex <file> [notebook]',
// // description: _('Imports en Evernote notebook file (.enex file).'),
// // options: [
// // ['--fuzzy-matching', 'For debugging purposes. Do not use.'],
// // ],
// }
async function synchronizer(syncTarget) { async function synchronizer(syncTarget) {
if (synchronizers_[syncTarget]) return synchronizers_[syncTarget]; if (synchronizers_[syncTarget]) return synchronizers_[syncTarget];

View File

@@ -31,7 +31,7 @@ function sleep(n) {
} }
async function switchClient(id) { async function switchClient(id) {
await time.msleep(200); await time.msleep(200); // Always leave a little time so that updated_time properties don't overlap
await Setting.saveAll(); await Setting.saveAll();
currentClient_ = id; currentClient_ = id;

View File

@@ -128,16 +128,35 @@ class BaseModel {
if (options.orderByDir) sql += ' ' + options.orderByDir; if (options.orderByDir) sql += ' ' + options.orderByDir;
} }
if (options.limit) sql += ' LIMIT ' + options.limit; if (options.limit) sql += ' LIMIT ' + options.limit;
//if (options.fields && options.fields.length) sql = sql.replace('SELECT *', 'SELECT ' + this.db().escapeFields(options.fields).join(','));
return { sql: sql, params: params }; return { sql: sql, params: params };
} }
static async all(options = null) { static async all(options = null) {
let q = this.applySqlOptions(options, 'SELECT * FROM `' + this.tableName() + '`'); let q = this.applySqlOptions(options, 'SELECT * FROM `' + this.tableName() + '`');
return this.modelSelectAll(q.sql); return this.modelSelectAll(q.sql);
} }
static async search(options = null) {
if (!options) options = {};
if (!options.fields) options.fields = '*';
let conditions = options.conditions ? options.conditions.slice(0) : [];
let params = options.conditionsParams ? options.conditionsParams.slice(0) : [];
if (options.titlePattern) {
let pattern = options.titlePattern.replace(/\*/g, '%');
conditions.push('title LIKE ?');
params.push(pattern);
}
let sql = 'SELECT ' + this.db().escapeFields(options.fields) + ' FROM `' + this.tableName() + '`';
if (conditions.length) sql += ' WHERE ' + conditions.join(' AND ');
let query = this.applySqlOptions(options, sql, params);
return this.modelSelectAll(query.sql, query.params);
}
static modelSelectOne(sql, params = null) { static modelSelectOne(sql, params = null) {
if (params === null) params = []; if (params === null) params = [];
return this.db().selectOne(sql, params).then((model) => { return this.db().selectOne(sql, params).then((model) => {

View File

@@ -163,10 +163,13 @@ class Database {
} }
escapeField(field) { escapeField(field) {
if (field == '*') return '*';
return '`' + field + '`'; return '`' + field + '`';
} }
escapeFields(fields) { escapeFields(fields) {
if (fields == '*') return '*';
let output = []; let output = [];
for (let i = 0; i < fields.length; i++) { for (let i = 0; i < fields.length; i++) {
output.push(this.escapeField(fields[i])); output.push(this.escapeField(fields[i]));
@@ -174,50 +177,46 @@ class Database {
return output; return output;
} }
selectOne(sql, params = null) { async tryCall(callName, sql, params) {
this.logQuery(sql, params);
return this.driver().selectOne(sql, params).catch((error) => {
throw this.sqliteErrorToJsError(error, sql, params);
});
}
selectAll(sql, params = null) {
this.logQuery(sql, params);
return this.driver().selectAll(sql, params).catch((error) => {
throw this.sqliteErrorToJsError(error, sql, params);
});
}
async exec(sql, params = null) {
if (typeof sql === 'object') { if (typeof sql === 'object') {
params = sql.params; params = sql.params;
sql = sql.sql; sql = sql.sql;
} }
let result = null;
let waitTime = 50; let waitTime = 50;
let totalWaitTime = 0; let totalWaitTime = 0;
while (true) { while (true) {
try { try {
this.logQuery(sql, params); this.logQuery(sql, params);
let result = await this.driver().exec(sql, params); let result = await this.driver()[callName](sql, params);
return result;; // No exception was thrown return result; // No exception was thrown
} catch (error) { } catch (error) {
throw error; if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) {
if (error && error.code == 'SQLITE_IOERR') { if (totalWaitTime >= 20000) throw this.sqliteErrorToJsError(error, sql, params);
if (totalWaitTime >= 20000) throw error; this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime));
this.logger().warn(sprintf('SQLITE_IOERR: will retry in %s milliseconds', waitTime));
this.logger().warn('Error was: ' + error.toString()); this.logger().warn('Error was: ' + error.toString());
await time.msleep(waitTime); await time.msleep(waitTime);
totalWaitTime += waitTime; totalWaitTime += waitTime;
waitTime *= 1.5; waitTime *= 1.5;
} else { } else {
throw this.sqliteErrorToJsError(error, sql, params); throw this.sqliteErrorToJsError(error, sql, params);
} }
} }
} }
} }
async selectOne(sql, params = null) {
return this.tryCall('selectOne', sql, params);
}
async selectAll(sql, params = null) {
return this.tryCall('selectAll', sql, params);
}
async exec(sql, params = null) {
return this.tryCall('exec', sql, params);
}
transactionExecBatch(queries) { transactionExecBatch(queries) {
if (queries.length <= 0) return Promise.resolve(); if (queries.length <= 0) return Promise.resolve();

View File

@@ -5,6 +5,13 @@ import { time } from 'lib/time-utils.js';
class FileApiDriverLocal { class FileApiDriverLocal {
fsErrorToJsError_(error) {
let msg = error.toString();
let output = new Error(msg);
if (error.code) output.code = error.code;
return output;
}
stat(path) { stat(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.stat(path, (error, s) => { fs.stat(path, (error, s) => {
@@ -12,7 +19,7 @@ class FileApiDriverLocal {
if (error.code == 'ENOENT') { if (error.code == 'ENOENT') {
resolve(null); resolve(null);
} else { } else {
reject(error); reject(this.fsErrorToJsError_(error));
} }
return; return;
} }
@@ -45,7 +52,7 @@ class FileApiDriverLocal {
let t = Math.floor(timestampMs / 1000); let t = Math.floor(timestampMs / 1000);
fs.utimes(path, t, t, (error) => { fs.utimes(path, t, t, (error) => {
if (error) { if (error) {
reject(error); reject(this.fsErrorToJsError_(error));
return; return;
} }
resolve(); resolve();
@@ -54,20 +61,24 @@ class FileApiDriverLocal {
} }
async list(path, options) { async list(path, options) {
let items = await fs.readdir(path); try {
let output = []; let items = await fs.readdir(path);
for (let i = 0; i < items.length; i++) { let output = [];
let stat = await this.stat(path + '/' + items[i]); for (let i = 0; i < items.length; i++) {
if (!stat) continue; // Has been deleted between the readdir() call and now let stat = await this.stat(path + '/' + items[i]);
stat.path = items[i]; if (!stat) continue; // Has been deleted between the readdir() call and now
output.push(stat); stat.path = items[i];
} output.push(stat);
}
return { return {
items: output, items: output,
hasMore: false, hasMore: false,
context: null, context: null,
}; };
} catch(error) {
throw this.fsErrorToJsError_(error);
}
} }
async get(path, options) { async get(path, options) {
@@ -81,7 +92,7 @@ class FileApiDriverLocal {
} }
} catch (error) { } catch (error) {
if (error.code == 'ENOENT') return null; if (error.code == 'ENOENT') return null;
throw error; throw this.fsErrorToJsError_(error);
} }
return output; return output;
@@ -99,7 +110,7 @@ class FileApiDriverLocal {
mkdirp(path, (error) => { mkdirp(path, (error) => {
if (error) { if (error) {
reject(error); reject(this.fsErrorToJsError_(error));
} else { } else {
resolve(); resolve();
} }
@@ -112,7 +123,7 @@ class FileApiDriverLocal {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.writeFile(path, content, function(error) { fs.writeFile(path, content, function(error) {
if (error) { if (error) {
reject(error); reject(this.fsErrorToJsError_(error));
} else { } else {
resolve(); resolve();
} }
@@ -128,7 +139,7 @@ class FileApiDriverLocal {
// File doesn't exist - it's fine // File doesn't exist - it's fine
resolve(); resolve();
} else { } else {
reject(error); reject(this.fsErrorToJsError_(error));
} }
} else { } else {
resolve(); resolve();
@@ -152,7 +163,7 @@ class FileApiDriverLocal {
await time.sleep(1); await time.sleep(1);
continue; continue;
} }
throw error; throw this.fsErrorToJsError_(error);
} }
} }

View File

@@ -85,7 +85,12 @@ class Folder extends BaseItem {
return Folder.save(folder, { isNew: true }); return Folder.save(folder, { isNew: true });
} }
static save(o, options = null) { static async save(o, options = null) {
if (options && options.duplicateCheck === true && o.title) {
let existingFolder = await Folder.loadByTitle(o.title);
if (existingFolder) throw new Error(_('A notebook with this title already exists: "%s"', o.title));
}
return super.save(o, options).then((folder) => { return super.save(o, options).then((folder) => {
this.dispatch({ this.dispatch({
type: 'FOLDERS_UPDATE_ONE', type: 'FOLDERS_UPDATE_ONE',

View File

@@ -56,28 +56,48 @@ class Note extends BaseItem {
if (!options) options = {}; if (!options) options = {};
if (!options.orderBy) options.orderBy = 'updated_time'; if (!options.orderBy) options.orderBy = 'updated_time';
if (!options.orderByDir) options.orderByDir = 'DESC'; if (!options.orderByDir) options.orderByDir = 'DESC';
if (!options.conditions) options.conditions = [];
if (!options.conditionsParams) options.conditionsParams = [];
if (!options.fields) options.fields = this.previewFields();
options.conditions.push('is_conflict = 0');
options.conditions.push('parent_id = ?');
options.conditionsParams.push(parentId);
let sql = 'SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE is_conflict = 0 AND parent_id = ?';
let params = [parentId];
if (options.itemTypes && options.itemTypes.length) { if (options.itemTypes && options.itemTypes.length) {
if (options.itemTypes.indexOf('note') >= 0 && options.itemTypes.indexOf('todo') >= 0) { if (options.itemTypes.indexOf('note') >= 0 && options.itemTypes.indexOf('todo') >= 0) {
// Fetch everything // Fetch everything
} else if (options.itemTypes.indexOf('note') >= 0) { } else if (options.itemTypes.indexOf('note') >= 0) {
sql += ' AND is_todo = 0'; options.conditions.push('is_todo = 0');
} else if (options.itemTypes.indexOf('todo') >= 0) { } else if (options.itemTypes.indexOf('todo') >= 0) {
sql += ' AND is_todo = 1'; options.conditions.push('is_todo = 1');
} }
} }
if (options.titlePattern) { return this.search(options);
let pattern = options.titlePattern.replace(/\*/g, '%');
sql += ' AND title LIKE ?';
params.push(pattern);
}
let query = this.applySqlOptions(options, sql, params); // let sql = 'SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE is_conflict = 0 AND parent_id = ?';
// let params = [parentId];
// if (options.itemTypes && options.itemTypes.length) {
// if (options.itemTypes.indexOf('note') >= 0 && options.itemTypes.indexOf('todo') >= 0) {
// // Fetch everything
// } else if (options.itemTypes.indexOf('note') >= 0) {
// sql += ' AND is_todo = 0';
// } else if (options.itemTypes.indexOf('todo') >= 0) {
// sql += ' AND is_todo = 1';
// }
// }
return this.modelSelectAll(query.sql, query.params); // if (options.titlePattern) {
// let pattern = options.titlePattern.replace(/\*/g, '%');
// sql += ' AND title LIKE ?';
// params.push(pattern);
// }
// let query = this.applySqlOptions(options, sql, params);
// return this.modelSelectAll(query.sql, query.params);
} }
static preview(noteId) { static preview(noteId) {

View File

@@ -1,6 +1,7 @@
import { BaseModel } from 'lib/base-model.js'; import { BaseModel } from 'lib/base-model.js';
import { Database } from 'lib/database.js'; import { Database } from 'lib/database.js';
import { BaseItem } from 'lib/models/base-item.js'; import { BaseItem } from 'lib/models/base-item.js';
import { Note } from 'lib/models/note.js';
import { time } from 'lib/time-utils.js'; import { time } from 'lib/time-utils.js';
import lodash from 'lodash'; import lodash from 'lodash';
@@ -37,6 +38,19 @@ class Tag extends BaseItem {
return output; return output;
} }
static async notes(tagId) {
let noteIds = await this.tagNoteIds(tagId);
if (!noteIds.length) return [];
let noteIdsSql = noteIds.join('","');
noteIdsSql = '"' + noteIdsSql + '"';
let options = {
conditions: ['id IN (' + noteIdsSql + ')'],
};
return Note.search(options);
}
static async addNote(tagId, noteId) { static async addNote(tagId, noteId) {
let hasIt = await this.hasNote(tagId, noteId); let hasIt = await this.hasNote(tagId, noteId);
if (hasIt) return; if (hasIt) return;