1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-09-16 08:56:40 +02:00

Added cp command

This commit is contained in:
Laurent Cozic
2017-07-11 18:17:23 +00:00
parent 4fa65de31d
commit 8c5f0622a2
17 changed files with 221 additions and 150 deletions

View File

@@ -42,7 +42,7 @@ class Application {
updatePrompt() { updatePrompt() {
if (!this.showPromptString_) return ''; if (!this.showPromptString_) return '';
let path = '~'; let path = '';
if (this.currentFolder()) { if (this.currentFolder()) {
path += '/' + this.currentFolder().title; path += '/' + this.currentFolder().title;
} }
@@ -57,45 +57,38 @@ class Application {
this.updatePrompt(); this.updatePrompt();
} }
async parseNotePattern(pattern) {
if (pattern.indexOf('..') === 0) {
let pieces = pattern.split('/');
if (pieces.length != 3) throw new Error(_('Invalid pattern: %s', pattern));
let parent = await this.loadItem(BaseModel.TYPE_FOLDER, pieces[1]);
if (!parent) throw new Error(_('Notebook not found: %s', pieces[1]));
return {
parent: parent,
title: pieces[2],
};
} else {
return {
parent: null,
title: pattern,
};
}
}
async loadItem(type, pattern) { async loadItem(type, pattern) {
let output = await this.loadItems(type, pattern); let output = await this.loadItems(type, pattern);
return output.length ? output[0] : null; return output.length ? output[0] : null;
} }
async loadItems(type, pattern) { async loadItems(type, pattern, options = null) {
let ItemClass = BaseItem.itemClass(type); if (!options) options = {};
let item = null;
if (type == BaseModel.TYPE_NOTE) { const parent = options.parent ? options.parent : app().currentFolder();
if (!app().currentFolder()) throw new Error(_('No notebook has been created.')); const ItemClass = BaseItem.itemClass(type);
item = await ItemClass.loadFolderNoteByField(app().currentFolder().id, 'title', pattern);
} else { if (type == BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) { // Handle it as pattern
item = await ItemClass.loadByTitle(pattern); if (!parent) throw new Error(_('No notebook selected.'));
return await Note.previews(parent.id, { titlePattern: pattern });
} else { // Single item
let item = null;
if (type == BaseModel.TYPE_NOTE) {
if (!parent) throw new Error(_('No notebook has been specified.'));
item = await ItemClass.loadFolderNoteByField(parent.id, 'title', pattern);
} else {
item = await ItemClass.loadByTitle(pattern);
}
if (item) return [item];
item = await ItemClass.load(pattern); // Load by id
if (item) return [item];
if (pattern.length >= 4) {
item = await ItemClass.loadByPartialId(pattern);
if (item) return [item];
}
} }
if (item) return [item];
item = await ItemClass.load(pattern); // Load by id
if (item) return [item];
item = await ItemClass.loadByPartialId(pattern);
if (item) return [item];
return []; return [];
} }

View File

@@ -23,22 +23,10 @@ class Command extends BaseCommand {
async action(args) { async action(args) {
let title = args['title']; let title = args['title'];
let item = null; let item = await app().loadItem(BaseModel.TYPE_NOTE, title);
if (!app().currentFolder()) { if (!item) throw new Error(_('No item "%s" found.', title));
item = await Folder.loadByField('title', title);
} else {
item = await app().loadItem(BaseModel.TYPE_NOTE, title);
}
if (!item) throw new Error(_('No item with title "%s" found.', title));
let content = null;
if (!app().currentFolder()) {
content = await Folder.serialize(item);
} else {
content = await Note.serialize(item);
}
const content = await Note.serialize(item);
this.log(content); this.log(content);
} }

View File

@@ -0,0 +1,48 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js';
import { autocompleteItems } from './autocomplete.js';
class Command extends BaseCommand {
usage() {
return 'cp <pattern> [notebook]';
}
description() {
return 'Duplicates the notes matching <pattern> to [notebook]. If no notebook is specified the note is duplicated in the current notebook.';
}
autocomplete() {
return { data: autocompleteItems };
}
async action(args) {
let folder = null;
if (args['notebook']) {
folder = await app().loadItem(BaseModel.TYPE_FOLDER, args['notebook']);
} else {
folder = app().currentFolder();
}
if (!folder) throw new Error(_('No notebook "%s"', args['notebook']));
const notes = await app().loadItems(BaseModel.TYPE_NOTE, args['pattern']);
if (!notes.length) throw new Error(_('No note matches this pattern: "%s"', args['pattern']));
for (let i = 0; i < notes.length; i++) {
const newNote = await Note.duplicate(notes[i].id, {
changes: {
parent_id: folder.id
},
});
Note.updateGeolocation(newNote.id);
}
}
}
module.exports = Command;

View File

@@ -13,7 +13,7 @@ class Command extends BaseCommand {
} }
description() { description() {
return 'Displays the notes in [notebook]. Use `ls ..` to display the list of notebooks.'; return 'Displays the notes in [notebook]. Use `ls /` to display the list of notebooks.';
} }
options() { options() {
@@ -51,7 +51,7 @@ class Command extends BaseCommand {
} }
if (pattern) queryOptions.titlePattern = pattern; if (pattern) queryOptions.titlePattern = pattern;
if (pattern == '..' || !app().currentFolder()) { if (pattern == '/' || !app().currentFolder()) {
items = await Folder.all(queryOptions); items = await Folder.all(queryOptions);
suffix = '/'; suffix = '/';
} else { } else {

View File

@@ -20,11 +20,9 @@ class Command extends BaseCommand {
async action(args) { async action(args) {
if (!app().currentFolder()) throw new Error(_('Notes can only be created within a notebook.')); if (!app().currentFolder()) throw new Error(_('Notes can only be created within a notebook.'));
let path = await app().parseNotePattern(args['note']);
let note = { let note = {
title: path.title, title: args.note,
parent_id: path.parent ? path.parent.id : app().currentFolder().id, parent_id: app().currentFolder().id,
}; };
note = await Note.save(note); note = await Note.save(note);

View File

@@ -1,6 +1,7 @@
import { BaseCommand } from './base-command.js'; import { BaseCommand } from './base-command.js';
import { app } from './app.js'; import { app } from './app.js';
import { _ } from 'lib/locale.js'; import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js'; import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js'; import { Note } from 'lib/models/note.js';
import { autocompleteItems } from './autocomplete.js'; import { autocompleteItems } from './autocomplete.js';
@@ -20,13 +21,12 @@ class Command extends BaseCommand {
} }
async action(args) { async action(args) {
if (!app().currentFolder()) throw new Error(_('Please select a notebook first.')); const pattern = args['pattern'];
let pattern = args['pattern']; const folder = await Folder.loadByField('title', args['notebook']);
if (!folder) throw new Error(_('No notebook "%s"', args['notebook']));
let folder = await Folder.loadByField('title', args['notebook']); const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
if (!folder) throw new Error(_('No folder with title "%s"', args['notebook']));
let notes = await Note.previews(app().currentFolder().id, { titlePattern: pattern });
if (!notes.length) throw new Error(_('No note matches this pattern: "%s"', pattern)); if (!notes.length) throw new Error(_('No note matches this pattern: "%s"', pattern));
for (let i = 0; i < notes.length; i++) { for (let i = 0; i < notes.length; i++) {

View File

@@ -15,7 +15,7 @@ class Command extends BaseCommand {
} }
description() { description() {
return 'Deletes the given item. For a notebook, all the notes within that notebook will be deleted. Use `rm ../<notebook>` to delete a notebook.'; return 'Deletes the items matching <pattern>.';
} }
autocomplete() { autocomplete() {
@@ -25,44 +25,28 @@ class Command extends BaseCommand {
options() { options() {
return [ return [
['-f, --force', 'Deletes the items without asking for confirmation.'], ['-f, --force', 'Deletes the items without asking for confirmation.'],
['-r, --recursive', 'Deletes a notebook.'],
]; ];
} }
async action(args) { async action(args) {
let pattern = args['pattern'].toString(); const pattern = args['pattern'].toString();
let itemType = null; const recursive = args.options && args.options.recursive === true;
let force = args.options && args.options.force === true; const force = args.options && args.options.force === true;
if (pattern.indexOf('*') < 0) { // Handle it as a simple title if (recursive) {
if (pattern.substr(0, 3) == '../') { const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
itemType = BaseModel.TYPE_FOLDER; if (!folder) throw new Error(_('No notebook matchin pattern "%s"', pattern));
pattern = pattern.substr(3); const ok = force ? true : await vorpalUtils.cmdPromptConfirm(this, _('Delete notebook "%s"?', folder.title));
} else { if (!ok) return;
itemType = BaseModel.TYPE_NOTE; await Folder.delete(folder.id);
} } else {
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
let item = item = await app().loadItem(itemType, pattern); // await BaseItem.loadItemByField(itemType, 'title', pattern); if (!notes.length) throw new Error(_('No note matchin pattern "%s"', pattern));
if (!item) throw new Error(_('No item "%s" found.', pattern)); const ok = force ? true : await vorpalUtils.cmdPromptConfirm(this, _('%d notes match this pattern. Delete them?', notes.length));
if (!ok) return;
let ok = force ? true : await vorpalUtils.cmdPromptConfirm(this, _('Delete "%s"?', item.title)); let ids = notes.map((n) => n.id);
if (ok) { await Note.batchDelete(ids);
await BaseItem.deleteItem(itemType, item.id);
if (app().currentFolder() && app().currentFolder().id == item.id) {
let f = await Folder.defaultFolder();
app().switchCurrentFolder(f);
}
}
} else { // Handle it as a glob pattern
if (app().currentFolder()) {
let notes = await Note.previews(app().currentFolder().id, { titlePattern: pattern });
if (!notes.length) throw new Error(_('No note matches this pattern: "%s"', pattern));
let ok = force ? true : await vorpalUtils.cmdPromptConfirm(this, _('%d notes match this pattern. Delete them?', notes.length));
if (ok) {
for (let i = 0; i < notes.length; i++) {
await Note.delete(notes[i].id);
}
}
}
} }
} }

View File

@@ -1,6 +1,7 @@
import { BaseCommand } from './base-command.js'; import { BaseCommand } from './base-command.js';
import { app } from './app.js'; import { app } from './app.js';
import { _ } from 'lib/locale.js'; import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js'; import { Folder } from 'lib/models/folder.js';
import { Note } from 'lib/models/note.js'; import { Note } from 'lib/models/note.js';
import { BaseItem } from 'lib/models/base-item.js'; import { BaseItem } from 'lib/models/base-item.js';
@@ -9,11 +10,11 @@ import { autocompleteItems } from './autocomplete.js';
class Command extends BaseCommand { class Command extends BaseCommand {
usage() { usage() {
return 'set <item> <name> [value]'; return 'set <note> <name> [value]';
} }
description() { description() {
return 'Sets the property <name> of the given <item> to the given [value].'; return 'Sets the property <name> of the given <note> to the given [value].';
} }
autocomplete() { autocomplete() {
@@ -21,31 +22,22 @@ class Command extends BaseCommand {
} }
async action(args) { async action(args) {
let title = args['item']; let title = args['note'];
let propName = args['name']; let propName = args['name'];
let propValue = args['value']; let propValue = args['value'];
if (!propValue) propValue = ''; if (!propValue) propValue = '';
let item = null; let notes = await app().loadItems(BaseModel.TYPE_NOTE, title);
if (!app().currentFolder()) { if (!notes.length) throw new Error(_('No note "%s" found.', title));
item = await Folder.loadByField('title', title);
} else { for (let i = 0; i < notes.length; i++) {
item = await Note.loadFolderNoteByField(app().currentFolder().id, 'title', title); let newNote = {
id: notes[i].id,
type_: notes[i].type_,
};
newNote[propName] = propValue;
await Note.save(newNote);
} }
if (!item) {
item = await BaseItem.loadItemById(title);
}
if (!item) throw new Error(_('No item with title "%s" found.', title));
let newItem = {
id: item.id,
type_: item.type_,
};
newItem[propName] = propValue;
let ItemClass = BaseItem.itemClass(newItem);
await ItemClass.save(newItem);
} }
} }

View File

@@ -56,6 +56,7 @@ class Command extends BaseCommand {
this.log(_('Starting synchronization...')); this.log(_('Starting synchronization...'));
await sync.start(options); await sync.start(options);
vorpalUtils.redrawDone();
this.log(_('Done.')); this.log(_('Done.'));
} }
} }

View File

@@ -17,19 +17,25 @@ class Command extends BaseCommand {
async action(args) { async action(args) {
let tag = null; let tag = null;
if (args.tag) tag = await app().loadItem(BaseModel.TYPE_TAG, args.tag); if (args.tag) tag = await app().loadItem(BaseModel.TYPE_TAG, args.tag);
let note = null; let notes = [];
if (args.note) note = await app().loadItem(BaseModel.TYPE_NOTE, args.note); if (args.note) {
notes = await app().loadItems(BaseModel.TYPE_NOTE, args.note);
}
if (args.command == 'remove' && !tag) throw new Error(_('Tag does not exist: "%s"', args.tag)); if (args.command == 'remove' && !tag) throw new Error(_('Tag does not exist: "%s"', args.tag));
if (args.command == 'add') { if (args.command == 'add') {
if (!note) throw new Error(_('Note does not exist: "%s"', args.note)); if (!notes.length) throw new Error(_('Note does not exist: "%s"', args.note));
if (!tag) tag = await Tag.save({ title: args.tag }); if (!tag) tag = await Tag.save({ title: args.tag });
await Tag.addNote(tag.id, note.id); for (let i = 0; i < notes.length; i++) {
await Tag.addNote(tag.id, notes[i].id);
}
} else if (args.command == 'remove') { } else if (args.command == 'remove') {
if (!tag) throw new Error(_('Tag does not exist: "%s"', args.tag)); if (!tag) throw new Error(_('Tag does not exist: "%s"', args.tag));
if (!note) throw new Error(_('Note does not exist: "%s"', args.note)); if (!notes.length) throw new Error(_('Note does not exist: "%s"', args.note));
await Tag.removeNote(tag.id, note.id); for (let i = 0; i < notes.length; i++) {
await Tag.removeNote(tag.id, notes[i].id);
}
} else if (args.command == 'list') { } else if (args.command == 'list') {
if (tag) { if (tag) {
let notes = await Tag.notes(tag.id); let notes = await Tag.notes(tag.id);

View File

@@ -1,6 +1,7 @@
import { BaseCommand } from './base-command.js'; import { BaseCommand } from './base-command.js';
import { app } from './app.js'; import { app } from './app.js';
import { _ } from 'lib/locale.js'; import { _ } from 'lib/locale.js';
import { BaseModel } from 'lib/base-model.js';
import { Folder } from 'lib/models/folder.js'; import { Folder } from 'lib/models/folder.js';
import { autocompleteFolders } from './autocomplete.js'; import { autocompleteFolders } from './autocomplete.js';
@@ -23,10 +24,8 @@ class Command extends BaseCommand {
} }
async action(args) { async action(args) {
let title = args['notebook']; let folder = await app().loadItem(BaseModel.TYPE_FOLDER, args['notebook']);
if (!folder) throw new Error(_('No folder "%s"', title));
let folder = await Folder.loadByField('title', title);
if (!folder) throw new Error(_('Invalid folder title: %s', title));
app().switchCurrentFolder(folder); app().switchCurrentFolder(folder);
} }

View File

@@ -134,7 +134,7 @@ async function execRandomCommand(client) {
if (item.type_ == 1) { if (item.type_ == 1) {
return execCommand(client, 'rm -f ' + item.id); return execCommand(client, 'rm -f ' + item.id);
} else if (item.type_ == 2) { } else if (item.type_ == 2) {
return execCommand(client, 'rm -f ' + '../' + item.id); return execCommand(client, 'rm -r -f ' + item.id);
} else if (item.type_ == 5) { } else if (item.type_ == 5) {
// tag // tag
} else { } else {
@@ -153,7 +153,7 @@ async function execRandomCommand(client) {
}, 30], }, 30],
[async () => { // UPDATE RANDOM ITEM [async () => { // UPDATE RANDOM ITEM
let items = await clientItems(client); let items = await clientItems(client);
let item = randomElement(items); let item = randomNote(items);
if (!item) return; if (!item) return;
return execCommand(client, 'set ' + item.id + ' title "' + randomWord() + '"'); return execCommand(client, 'set ' + item.id + ' title "' + randomWord() + '"');

View File

@@ -14,9 +14,9 @@ function initialize(vorpal) {
} }
function redrawEnabled() { function redrawEnabled() {
// Always disabled for now - doesn't play well with command.cancel() // // Always disabled for now - doesn't play well with command.cancel()
// function (it makes the whole app quit instead of just the // // function (it makes the whole app quit instead of just the
// current command). // // current command).
return false; return false;
return redrawEnabled_; return redrawEnabled_;
@@ -33,7 +33,7 @@ function setStackTraceEnabled(v) {
function redraw(s) { function redraw(s) {
if (!redrawEnabled()) { if (!redrawEnabled()) {
const now = time.unixMs(); const now = time.unixMs();
if (now - redrawLastUpdateTime_ > 1000) { if (now - redrawLastUpdateTime_ > 4000) {
if (vorpal_.activeCommand) { if (vorpal_.activeCommand) {
vorpal_.activeCommand.log(s); vorpal_.activeCommand.log(s);
} else { } else {

View File

@@ -7,7 +7,7 @@
"url": "https://github.com/laurent22/joplin" "url": "https://github.com/laurent22/joplin"
}, },
"url": "git://github.com/laurent22/joplin.git", "url": "git://github.com/laurent22/joplin.git",
"version": "0.8.35", "version": "0.8.36",
"bin": { "bin": {
"joplin": "./main_launcher.js" "joplin": "./main_launcher.js"
}, },

View File

@@ -277,6 +277,12 @@ class BaseModel {
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id = ?', [id]); return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id = ?', [id]);
} }
static batchDelete(ids, options = null) {
options = this.modOptions(options);
if (!ids.length) throw new Error('Cannot delete object without an ID');
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id IN ("' + ids.join('","') + '")');
}
static db() { static db() {
if (!this.db_) throw new Error('Accessing database before it has been initialised'); if (!this.db_) throw new Error('Accessing database before it has been initialised');
return this.db_; return this.db_;

View File

@@ -133,13 +133,33 @@ class BaseItem extends BaseModel {
} }
static async delete(id, options = null) { static async delete(id, options = null) {
return this.batchDelete([id], options);
// let trackDeleted = true;
// if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted;
// await super.delete(id, options);
// if (trackDeleted) {
// await this.db().exec('INSERT INTO deleted_items (item_type, item_id, deleted_time) VALUES (?, ?, ?)', [this.modelType(), id, time.unixMs()]);
// }
}
static async batchDelete(ids, options = null) {
let trackDeleted = true; let trackDeleted = true;
if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted; if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted;
await super.delete(id, options); await super.batchDelete(ids, options);
if (trackDeleted) { if (trackDeleted) {
await this.db().exec('INSERT INTO deleted_items (item_type, item_id, deleted_time) VALUES (?, ?, ?)', [this.modelType(), id, time.unixMs()]); let queries = [];
let now = time.unixMs();
for (let i = 0; i < ids.length; i++) {
queries.push({
sql: 'INSERT INTO deleted_items (item_type, item_id, deleted_time) VALUES (?, ?, ?)',
params: [this.modelType(), ids[i], now],
});
}
await this.db().transactionExecBatch(queries);
} }
} }

View File

@@ -4,6 +4,7 @@ import { Folder } from 'lib/models/folder.js';
import { BaseItem } from 'lib/models/base-item.js'; import { BaseItem } from 'lib/models/base-item.js';
import { Setting } from 'lib/models/setting.js'; import { Setting } from 'lib/models/setting.js';
import { shim } from 'lib/shim.js'; import { shim } from 'lib/shim.js';
import { time } from 'lib/time-utils.js';
import moment from 'moment'; import moment from 'moment';
import lodash from 'lodash'; import lodash from 'lodash';
@@ -95,25 +96,41 @@ class Note extends BaseItem {
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0'); return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0');
} }
static updateGeolocation(noteId) { static async updateGeolocation(noteId) {
if (!Note.updateGeolocationEnabled_) return; if (!Note.updateGeolocationEnabled_) return;
let startWait = time.unixMs();
while (true) {
if (!this.geolocationUpdating_) break;
this.logger().info('Waiting for geolocation update...');
await time.sleep(1);
if (startWait + 1000 * 20 < time.unixMs()) {
this.logger().warn('Failed to update geolocation for: timeout: ' + noteId);
return;
}
}
let geoData = null;
if (this.geolocationCache_ && this.geolocationCache_.timestamp + 1000 * 60 * 10 > time.unixMs()) {
geoData = Object.assign({}, this.geolocationCache_);
} else {
this.geolocationUpdating_ = true;
this.logger().info('Fetching geolocation...');
geoData = await shim.Geolocation.currentPosition();
this.logger().info('Got lat/long');
this.geolocationCache_ = geoData;
this.geolocationUpdating_ = false;
}
this.logger().info('Updating lat/long of note ' + noteId); this.logger().info('Updating lat/long of note ' + noteId);
let geoData = null; let note = Note.load(noteId);
return shim.Geolocation.currentPosition().then((data) => { if (!note) return; // Race condition - note has been deleted in the meantime
this.logger().info('Got lat/long');
geoData = data; note.longitude = geoData.coords.longitude;
return Note.load(noteId); note.latitude = geoData.coords.latitude;
}).then((note) => { note.altitude = geoData.coords.altitude;
if (!note) return; // Race condition - note has been deleted in the meantime return Note.save(note);
note.longitude = geoData.coords.longitude;
note.latitude = geoData.coords.latitude;
note.altitude = geoData.coords.altitude;
return Note.save(note);
}).catch((error) => {
this.logger().warn('Cannot get location:', error);
});
} }
static filter(note) { static filter(note) {
@@ -126,6 +143,24 @@ class Note extends BaseItem {
return output; return output;
} }
static async duplicate(noteId, options = null) {
const changes = options && options.changes;
const originalNote = await Note.load(noteId);
if (!originalNote) throw new Error('Unknown note: ' + noteId);
let newNote = Object.assign({}, originalNote);
delete newNote.id;
newNote.sync_time = 0;
for (let n in changes) {
if (!changes.hasOwnProperty(n)) continue;
newNote[n] = changes[n];
}
return this.save(newNote);
}
static save(o, options = null) { static save(o, options = null) {
let isNew = this.isNew(o, options); let isNew = this.isNew(o, options);
if (isNew && !o.source) o.source = Setting.value('appName'); if (isNew && !o.source) o.source = Setting.value('appName');
@@ -147,5 +182,6 @@ class Note extends BaseItem {
} }
Note.updateGeolocationEnabled_ = true; Note.updateGeolocationEnabled_ = true;
Note.geolocationUpdating_ = false;
export { Note }; export { Note };