1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +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('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) {
let possibleCommands = [
['mkbook {word}', 40], // CREATE FOLDER
['mknote {word}', 70], // CREATE NOTE
[async () => { // DELETE RANDOM ITEM
let items = clientItems(client);
let items = await clientItems(client);
let item = randomElement(items);
if (!item) return;
@ -107,6 +129,8 @@ async function execRandomCommand(client) {
return execCommand(client, 'rm -f ' + item.title);
} else if (item.type_ == 2) {
return execCommand(client, 'rm -f ' + '../' + item.title);
} else if (item.type_ == 5) {
// tag
} else {
throw new Error('Unknown type: ' + item.type_);
}
@ -122,12 +146,22 @@ async function execRandomCommand(client) {
return execCommand(client, 'sync --random-failures', options);
}, 30],
[async () => { // UPDATE RANDOM ITEM
let items = clientItems(client);
let items = await clientItems(client);
let item = randomElement(items);
if (!item) return;
return execCommand(client, 'set ' + item.id + ' title "' + randomWord() + '"');
}, 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;
@ -171,8 +205,17 @@ function compareItems(item1, item2) {
if (n == 'sync_time') continue;
let p1 = item1[n];
let p2 = item2[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;
}
@ -235,8 +278,8 @@ async function compareClientItems(clientItems) {
let diff = compareItems(item1, item2);
if (diff.length) {
differences.push({
item1: item1,
item2: item2,
item1: JSON.stringify(item1),
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 { BaseItem } from 'lib/models/base-item.js';
import { Note } from 'lib/models/note.js';
import { Tag } from 'lib/models/tag.js';
import { Setting } from 'lib/models/setting.js';
import { Synchronizer } from 'lib/synchronizer.js';
import { Logger } from 'lib/logger.js';
@ -59,7 +60,7 @@ commands.push({
aliases: ['mkdir'],
description: 'Creates a new notebook',
action: function(args, end) {
Folder.save({ title: args['notebook'] }).then((folder) => {
Folder.save({ title: args['notebook'] }, { duplicateCheck: true }).then((folder) => {
switchCurrentFolder(folder);
}).catch((error) => {
this.log(error);
@ -70,7 +71,7 @@ commands.push({
});
commands.push({
usage: 'mknote <note-title>',
usage: 'mknote <note>',
aliases: ['touch'],
description: 'Creates a new note',
action: function(args, end) {
@ -81,7 +82,7 @@ commands.push({
}
let note = {
title: args['note-title'],
title: args['note'],
parent_id: currentFolder.id,
};
Note.save(note).catch((error) => {
@ -265,6 +266,45 @@ commands.push({
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({
usage: 'dump',
description: 'Dumps the complete database as JSON.',
@ -279,14 +319,20 @@ commands.push({
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));
} catch (error) {
this.log(error);
}
end();
},
autocomplete: autocompleteFolders,
}
});
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) {
for (let i = 0; i < commands.length; 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) {
if (synchronizers_[syncTarget]) return synchronizers_[syncTarget];

View File

@ -31,7 +31,7 @@ function sleep(n) {
}
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();
currentClient_ = id;

View File

@ -128,7 +128,6 @@ class BaseModel {
if (options.orderByDir) sql += ' ' + options.orderByDir;
}
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 };
}
@ -138,6 +137,26 @@ class BaseModel {
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) {
if (params === null) params = [];
return this.db().selectOne(sql, params).then((model) => {

View File

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

View File

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

View File

@ -85,7 +85,12 @@ class Folder extends BaseItem {
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) => {
this.dispatch({
type: 'FOLDERS_UPDATE_ONE',

View File

@ -56,28 +56,48 @@ class Note extends BaseItem {
if (!options) options = {};
if (!options.orderBy) options.orderBy = 'updated_time';
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.indexOf('note') >= 0 && options.itemTypes.indexOf('todo') >= 0) {
// Fetch everything
} 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) {
sql += ' AND is_todo = 1';
options.conditions.push('is_todo = 1');
}
}
if (options.titlePattern) {
let pattern = options.titlePattern.replace(/\*/g, '%');
sql += ' AND title LIKE ?';
params.push(pattern);
}
return this.search(options);
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) {

View File

@ -1,6 +1,7 @@
import { BaseModel } from 'lib/base-model.js';
import { Database } from 'lib/database.js';
import { BaseItem } from 'lib/models/base-item.js';
import { Note } from 'lib/models/note.js';
import { time } from 'lib/time-utils.js';
import lodash from 'lodash';
@ -37,6 +38,19 @@ class Tag extends BaseItem {
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) {
let hasIt = await this.hasNote(tagId, noteId);
if (hasIt) return;