mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Improved handling of multiple sync targets
This commit is contained in:
parent
cd6d8ce284
commit
24f61177d1
@ -239,6 +239,8 @@ class Application {
|
|||||||
|
|
||||||
let fileApi = null;
|
let fileApi = null;
|
||||||
|
|
||||||
|
// TODO: create file api based on syncTarget
|
||||||
|
|
||||||
if (syncTarget == 'onedrive') {
|
if (syncTarget == 'onedrive') {
|
||||||
const oneDriveApi = reg.oneDriveApi();
|
const oneDriveApi = reg.oneDriveApi();
|
||||||
let driver = new FileApiDriverOneDrive(oneDriveApi);
|
let driver = new FileApiDriverOneDrive(oneDriveApi);
|
||||||
@ -258,7 +260,7 @@ class Application {
|
|||||||
} else if (syncTarget == 'memory') {
|
} else if (syncTarget == 'memory') {
|
||||||
fileApi = new FileApi('joplin', new FileApiDriverMemory());
|
fileApi = new FileApi('joplin', new FileApiDriverMemory());
|
||||||
fileApi.setLogger(this.logger_);
|
fileApi.setLogger(this.logger_);
|
||||||
} else if (syncTarget == 'local') {
|
} else if (syncTarget == 'file') {
|
||||||
let syncDir = Setting.value('sync.local.path');
|
let syncDir = Setting.value('sync.local.path');
|
||||||
if (!syncDir) syncDir = Setting.value('profileDir') + '/sync';
|
if (!syncDir) syncDir = Setting.value('profileDir') + '/sync';
|
||||||
this.vorpal().log(_('Synchronizing with directory "%s"', syncDir));
|
this.vorpal().log(_('Synchronizing with directory "%s"', syncDir));
|
||||||
|
@ -25,7 +25,7 @@ class Command extends BaseCommand {
|
|||||||
['-r, --reverse', 'Reverses the sorting order.'],
|
['-r, --reverse', 'Reverses the sorting order.'],
|
||||||
['-t, --type <type>', 'Displays only the items of the specific type(s). Can be `n` for notes, `t` for todos, or `nt` for notes and todos (eg. `-tt` would display only the todos, while `-ttd` would display notes and todos.'],
|
['-t, --type <type>', 'Displays only the items of the specific type(s). Can be `n` for notes, `t` for todos, or `nt` for notes and todos (eg. `-tt` would display only the todos, while `-ttd` would display notes and todos.'],
|
||||||
['-f, --format <format>', 'Either "text" or "json"'],
|
['-f, --format <format>', 'Either "text" or "json"'],
|
||||||
['-l, --long', 'Use long list format. Format is NOTE_COUNT (for notebook), DATE, NEED_SYNC, TODO_CHECKED (for todos), TITLE'],
|
['-l, --long', 'Use long list format. Format is NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for todos), TITLE'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +90,6 @@ class Command extends BaseCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
row.push(time.unixMsToLocalDateTime(item.updated_time));
|
row.push(time.unixMsToLocalDateTime(item.updated_time));
|
||||||
row.push(item.updated_time > item.sync_time ? '*' : ' ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = item.title + suffix;
|
let title = item.title + suffix;
|
||||||
|
@ -208,7 +208,6 @@ function compareItems(item1, item2) {
|
|||||||
let output = [];
|
let output = [];
|
||||||
for (let n in item1) {
|
for (let n in item1) {
|
||||||
if (!item1.hasOwnProperty(n)) continue;
|
if (!item1.hasOwnProperty(n)) continue;
|
||||||
if (n == 'sync_time') continue;
|
|
||||||
let p1 = item1[n];
|
let p1 = item1[n];
|
||||||
let p2 = item2[n];
|
let p2 = item2[n];
|
||||||
|
|
||||||
|
@ -177,7 +177,6 @@ describe('Synchronizer', function() {
|
|||||||
let noteUpdatedFromRemote = await Note.load(note1.id);
|
let noteUpdatedFromRemote = await Note.load(note1.id);
|
||||||
for (let n in noteUpdatedFromRemote) {
|
for (let n in noteUpdatedFromRemote) {
|
||||||
if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue;
|
if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue;
|
||||||
if (n == 'sync_time') continue;
|
|
||||||
expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n);
|
expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ function setupDatabase(id = null) {
|
|||||||
// Don't care if the file doesn't exist
|
// Don't care if the file doesn't exist
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
|
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
|
||||||
databases_[id].setLogger(logger);
|
//databases_[id].setLogger(logger);
|
||||||
return databases_[id].open({ name: filePath }).then(() => {
|
return databases_[id].open({ name: filePath }).then(() => {
|
||||||
BaseModel.db_ = databases_[id];
|
BaseModel.db_ = databases_[id];
|
||||||
return setupDatabase(id);
|
return setupDatabase(id);
|
||||||
|
@ -49,8 +49,14 @@ class BaseModel {
|
|||||||
return fields.indexOf(name) >= 0;
|
return fields.indexOf(name) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fieldNames() {
|
static fieldNames(withPrefix = false) {
|
||||||
return this.db().tableFieldNames(this.tableName());
|
let output = this.db().tableFieldNames(this.tableName());
|
||||||
|
if (!withPrefix) return output;
|
||||||
|
let temp = [];
|
||||||
|
for (let i = 0; i < output.length; i++) {
|
||||||
|
temp.push(this.tableName() + '.' + output[i]);
|
||||||
|
}
|
||||||
|
return temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fieldType(name) {
|
static fieldType(name) {
|
||||||
@ -228,6 +234,10 @@ class BaseModel {
|
|||||||
|
|
||||||
queries.push(saveQuery);
|
queries.push(saveQuery);
|
||||||
|
|
||||||
|
if (options.nextQueries && options.nextQueries.length) {
|
||||||
|
queries = queries.concat(options.nextQueries);
|
||||||
|
}
|
||||||
|
|
||||||
return this.db().transactionExecBatch(queries).then(() => {
|
return this.db().transactionExecBatch(queries).then(() => {
|
||||||
o = Object.assign({}, o);
|
o = Object.assign({}, o);
|
||||||
o.id = modelId;
|
o.id = modelId;
|
||||||
|
@ -40,7 +40,11 @@ class Database {
|
|||||||
|
|
||||||
escapeField(field) {
|
escapeField(field) {
|
||||||
if (field == '*') return '*';
|
if (field == '*') return '*';
|
||||||
return '`' + field + '`';
|
let p = field.split('.');
|
||||||
|
if (p.length == 1) return '`' + field + '`';
|
||||||
|
if (p.length == 2) return p[0] + '.`' + p[1] + '`';
|
||||||
|
|
||||||
|
throw new Error('Invalid field format: ' + field);
|
||||||
}
|
}
|
||||||
|
|
||||||
escapeFields(fields) {
|
escapeFields(fields) {
|
||||||
@ -145,9 +149,23 @@ class Database {
|
|||||||
if (s == 'INTEGER') s = 'INT';
|
if (s == 'INTEGER') s = 'INT';
|
||||||
return this['TYPE_' + s];
|
return this['TYPE_' + s];
|
||||||
}
|
}
|
||||||
|
if (type == 'syncTarget') {
|
||||||
|
if (s == 'memory') return 1;
|
||||||
|
if (s == 'file') return 2;
|
||||||
|
if (s == 'onedrive') return 3;
|
||||||
|
}
|
||||||
throw new Error('Unknown enum type or value: ' + type + ', ' + s);
|
throw new Error('Unknown enum type or value: ' + type + ', ' + s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static enumName(type, id) {
|
||||||
|
if (type == 'syncTarget') {
|
||||||
|
if (id === 1) return 'memory';
|
||||||
|
if (id === 2) return 'file';
|
||||||
|
if (id === 3) return 'onedrive';
|
||||||
|
}
|
||||||
|
throw new Error('Unknown enum type or id: ' + type + ', ' + id);
|
||||||
|
}
|
||||||
|
|
||||||
static formatValue(type, value) {
|
static formatValue(type, value) {
|
||||||
if (value === null || value === undefined) return null;
|
if (value === null || value === undefined) return null;
|
||||||
if (type == this.TYPE_INT) return Number(value);
|
if (type == this.TYPE_INT) return Number(value);
|
||||||
|
@ -5,6 +5,14 @@ import { time } from 'lib/time-utils.js';
|
|||||||
|
|
||||||
class FileApiDriverLocal {
|
class FileApiDriverLocal {
|
||||||
|
|
||||||
|
syncTargetId() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncTargetName() {
|
||||||
|
return 'file';
|
||||||
|
}
|
||||||
|
|
||||||
fsErrorToJsError_(error) {
|
fsErrorToJsError_(error) {
|
||||||
let msg = error.toString();
|
let msg = error.toString();
|
||||||
let output = new Error(msg);
|
let output = new Error(msg);
|
||||||
|
@ -2,6 +2,14 @@ import { time } from 'lib/time-utils.js';
|
|||||||
|
|
||||||
class FileApiDriverMemory {
|
class FileApiDriverMemory {
|
||||||
|
|
||||||
|
syncTargetId() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncTargetName() {
|
||||||
|
return 'memory';
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.items_ = [];
|
this.items_ = [];
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,14 @@ class FileApiDriverOneDrive {
|
|||||||
this.api_ = api;
|
this.api_ = api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncTargetId() {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncTargetName() {
|
||||||
|
return 'onedrive';
|
||||||
|
}
|
||||||
|
|
||||||
api() {
|
api() {
|
||||||
return this.api_;
|
return this.api_;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,10 @@ class FileApi {
|
|||||||
this.logger_ = new Logger();
|
this.logger_ = new Logger();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
driver() {
|
||||||
|
return this.driver_;
|
||||||
|
}
|
||||||
|
|
||||||
setLogger(l) {
|
setLogger(l) {
|
||||||
this.logger_ = l;
|
this.logger_ = l;
|
||||||
}
|
}
|
||||||
|
@ -6,16 +6,13 @@ import { Database } from 'lib/database.js'
|
|||||||
const structureSql = `
|
const structureSql = `
|
||||||
CREATE TABLE folders (
|
CREATE TABLE folders (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
parent_id TEXT NOT NULL DEFAULT "",
|
|
||||||
title TEXT NOT NULL DEFAULT "",
|
title TEXT NOT NULL DEFAULT "",
|
||||||
created_time INT NOT NULL,
|
created_time INT NOT NULL,
|
||||||
updated_time INT NOT NULL,
|
updated_time INT NOT NULL
|
||||||
sync_time INT NOT NULL DEFAULT 0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX folders_title ON folders (title);
|
CREATE INDEX folders_title ON folders (title);
|
||||||
CREATE INDEX folders_updated_time ON folders (updated_time);
|
CREATE INDEX folders_updated_time ON folders (updated_time);
|
||||||
CREATE INDEX folders_sync_time ON folders (sync_time);
|
|
||||||
|
|
||||||
CREATE TABLE notes (
|
CREATE TABLE notes (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@ -24,7 +21,6 @@ CREATE TABLE notes (
|
|||||||
body TEXT NOT NULL DEFAULT "",
|
body TEXT NOT NULL DEFAULT "",
|
||||||
created_time INT NOT NULL,
|
created_time INT NOT NULL,
|
||||||
updated_time INT NOT NULL,
|
updated_time INT NOT NULL,
|
||||||
sync_time INT NOT NULL DEFAULT 0,
|
|
||||||
is_conflict INT NOT NULL DEFAULT 0,
|
is_conflict INT NOT NULL DEFAULT 0,
|
||||||
latitude NUMERIC NOT NULL DEFAULT 0,
|
latitude NUMERIC NOT NULL DEFAULT 0,
|
||||||
longitude NUMERIC NOT NULL DEFAULT 0,
|
longitude NUMERIC NOT NULL DEFAULT 0,
|
||||||
@ -42,7 +38,6 @@ CREATE TABLE notes (
|
|||||||
|
|
||||||
CREATE INDEX notes_title ON notes (title);
|
CREATE INDEX notes_title ON notes (title);
|
||||||
CREATE INDEX notes_updated_time ON notes (updated_time);
|
CREATE INDEX notes_updated_time ON notes (updated_time);
|
||||||
CREATE INDEX notes_sync_time ON notes (sync_time);
|
|
||||||
CREATE INDEX notes_is_conflict ON notes (is_conflict);
|
CREATE INDEX notes_is_conflict ON notes (is_conflict);
|
||||||
CREATE INDEX notes_is_todo ON notes (is_todo);
|
CREATE INDEX notes_is_todo ON notes (is_todo);
|
||||||
CREATE INDEX notes_order ON notes (\`order\`);
|
CREATE INDEX notes_order ON notes (\`order\`);
|
||||||
@ -58,27 +53,23 @@ CREATE TABLE tags (
|
|||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
title TEXT NOT NULL DEFAULT "",
|
title TEXT NOT NULL DEFAULT "",
|
||||||
created_time INT NOT NULL,
|
created_time INT NOT NULL,
|
||||||
updated_time INT NOT NULL,
|
updated_time INT NOT NULL
|
||||||
sync_time INT NOT NULL DEFAULT 0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX tags_title ON tags (title);
|
CREATE INDEX tags_title ON tags (title);
|
||||||
CREATE INDEX tags_updated_time ON tags (updated_time);
|
CREATE INDEX tags_updated_time ON tags (updated_time);
|
||||||
CREATE INDEX tags_sync_time ON tags (sync_time);
|
|
||||||
|
|
||||||
CREATE TABLE note_tags (
|
CREATE TABLE note_tags (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
note_id TEXT NOT NULL,
|
note_id TEXT NOT NULL,
|
||||||
tag_id TEXT NOT NULL,
|
tag_id TEXT NOT NULL,
|
||||||
created_time INT NOT NULL,
|
created_time INT NOT NULL,
|
||||||
updated_time INT NOT NULL,
|
updated_time INT NOT NULL
|
||||||
sync_time INT NOT NULL DEFAULT 0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX note_tags_note_id ON note_tags (note_id);
|
CREATE INDEX note_tags_note_id ON note_tags (note_id);
|
||||||
CREATE INDEX note_tags_tag_id ON note_tags (tag_id);
|
CREATE INDEX note_tags_tag_id ON note_tags (tag_id);
|
||||||
CREATE INDEX note_tags_updated_time ON note_tags (updated_time);
|
CREATE INDEX note_tags_updated_time ON note_tags (updated_time);
|
||||||
CREATE INDEX note_tags_sync_time ON note_tags (sync_time);
|
|
||||||
|
|
||||||
CREATE TABLE resources (
|
CREATE TABLE resources (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@ -86,13 +77,11 @@ CREATE TABLE resources (
|
|||||||
mime TEXT NOT NULL,
|
mime TEXT NOT NULL,
|
||||||
filename TEXT NOT NULL DEFAULT "",
|
filename TEXT NOT NULL DEFAULT "",
|
||||||
created_time INT NOT NULL,
|
created_time INT NOT NULL,
|
||||||
updated_time INT NOT NULL,
|
updated_time INT NOT NULL
|
||||||
sync_time INT NOT NULL DEFAULT 0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX resources_title ON resources (title);
|
CREATE INDEX resources_title ON resources (title);
|
||||||
CREATE INDEX resources_updated_time ON resources (updated_time);
|
CREATE INDEX resources_updated_time ON resources (updated_time);
|
||||||
CREATE INDEX resources_sync_time ON resources (sync_time);
|
|
||||||
|
|
||||||
CREATE TABLE settings (
|
CREATE TABLE settings (
|
||||||
\`key\` TEXT PRIMARY KEY,
|
\`key\` TEXT PRIMARY KEY,
|
||||||
@ -112,6 +101,19 @@ CREATE TABLE version (
|
|||||||
version INT NOT NULL
|
version INT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE sync_items (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
sync_target INT NOT NULL,
|
||||||
|
sync_time INT NOT NULL DEFAULT 0,
|
||||||
|
item_type INT NOT NULL,
|
||||||
|
item_id TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX sync_items_sync_time ON sync_items (sync_time);
|
||||||
|
CREATE INDEX sync_items_sync_target ON sync_items (sync_target);
|
||||||
|
CREATE INDEX sync_items_item_type ON sync_items (item_type);
|
||||||
|
CREATE INDEX sync_items_item_id ON sync_items (item_id);
|
||||||
|
|
||||||
INSERT INTO version (version) VALUES (1);
|
INSERT INTO version (version) VALUES (1);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -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 { time } from 'lib/time-utils.js';
|
import { time } from 'lib/time-utils.js';
|
||||||
|
import { sprintf } from 'sprintf-js';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
class BaseItem extends BaseModel {
|
class BaseItem extends BaseModel {
|
||||||
@ -31,44 +32,14 @@ class BaseItem extends BaseModel {
|
|||||||
throw new Error('Invalid class name: ' + name);
|
throw new Error('Invalid class name: ' + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async stats() {
|
|
||||||
let output = {
|
|
||||||
items: {},
|
|
||||||
total: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
let itemCount = 0;
|
|
||||||
let syncedCount = 0;
|
|
||||||
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
|
||||||
let d = BaseItem.syncItemDefinitions_[i];
|
|
||||||
let ItemClass = this.getClass(d.className);
|
|
||||||
let o = {
|
|
||||||
total: await ItemClass.count(),
|
|
||||||
synced: await ItemClass.syncedCount(),
|
|
||||||
};
|
|
||||||
output.items[d.className] = o;
|
|
||||||
itemCount += o.total;
|
|
||||||
syncedCount += o.synced;
|
|
||||||
}
|
|
||||||
|
|
||||||
output.total = {
|
|
||||||
total: itemCount,
|
|
||||||
synced: syncedCount,
|
|
||||||
};
|
|
||||||
|
|
||||||
output.toDelete = {
|
|
||||||
total: await this.deletedItemCount(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async syncedCount() {
|
static async syncedCount() {
|
||||||
const ItemClass = this.itemClass(this.modelType());
|
// TODO
|
||||||
let sql = 'SELECT count(*) as total FROM `' + ItemClass.tableName() + '` WHERE updated_time <= sync_time';
|
return 0;
|
||||||
if (this.modelType() == BaseModel.TYPE_NOTE) sql += ' AND is_conflict = 0';
|
// const ItemClass = this.itemClass(this.modelType());
|
||||||
const r = await this.db().selectOne(sql);
|
// let sql = 'SELECT count(*) as total FROM `' + ItemClass.tableName() + '` WHERE updated_time <= sync_time';
|
||||||
return r.total;
|
// if (this.modelType() == BaseModel.TYPE_NOTE) sql += ' AND is_conflict = 0';
|
||||||
|
// const r = await this.db().selectOne(sql);
|
||||||
|
// return r.total;
|
||||||
}
|
}
|
||||||
|
|
||||||
static systemPath(itemOrId) {
|
static systemPath(itemOrId) {
|
||||||
@ -92,13 +63,9 @@ class BaseItem extends BaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns the IDs of the items that have been synced at least once
|
// Returns the IDs of the items that have been synced at least once
|
||||||
static async syncedItems() {
|
static async syncedItems(syncTarget) {
|
||||||
let folders = await this.getClass('Folder').modelSelectAll('SELECT id FROM folders WHERE sync_time > 0');
|
if (!syncTarget) throw new Error('No syncTarget specified');
|
||||||
let notes = await this.getClass('Note').modelSelectAll('SELECT id FROM notes WHERE is_conflict = 0 AND sync_time > 0');
|
return await this.db().selectAll('SELECT item_id, item_type FROM sync_items WHERE sync_time > 0 AND sync_target = ?', [syncTarget]);
|
||||||
let resources = await this.getClass('Resource').modelSelectAll('SELECT id FROM resources WHERE sync_time > 0');
|
|
||||||
let tags = await this.getClass('Tag').modelSelectAll('SELECT id FROM tags WHERE sync_time > 0');
|
|
||||||
let noteTags = await this.getClass('NoteTag').modelSelectAll('SELECT id FROM note_tags WHERE sync_time > 0');
|
|
||||||
return folders.concat(notes).concat(resources).concat(tags).concat(noteTags);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static pathToId(path) {
|
static pathToId(path) {
|
||||||
@ -136,14 +103,6 @@ class BaseItem extends BaseModel {
|
|||||||
|
|
||||||
static async delete(id, options = null) {
|
static async delete(id, options = null) {
|
||||||
return this.batchDelete([id], options);
|
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) {
|
static async batchDelete(ids, options = null) {
|
||||||
@ -289,21 +248,54 @@ class BaseItem extends BaseModel {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async itemsThatNeedSync(limit = 100) {
|
static async itemsThatNeedSync(syncTarget, limit = 100) {
|
||||||
let items = await this.getClass('Folder').modelSelectAll('SELECT * FROM folders WHERE sync_time < updated_time LIMIT ' + limit);
|
const classNames = this.syncItemClassNames();
|
||||||
if (items.length) return { hasMore: true, items: items };
|
|
||||||
|
|
||||||
items = await this.getClass('Resource').modelSelectAll('SELECT * FROM resources WHERE sync_time < updated_time LIMIT ' + limit);
|
for (let i = 0; i < classNames.length; i++) {
|
||||||
if (items.length) return { hasMore: true, items: items };
|
const className = classNames[i];
|
||||||
|
const ItemClass = this.getClass(className);
|
||||||
|
const fieldNames = ItemClass.fieldNames(true);
|
||||||
|
fieldNames.push('sync_time');
|
||||||
|
|
||||||
items = await this.getClass('Note').modelSelectAll('SELECT * FROM notes WHERE sync_time < updated_time AND is_conflict = 0 LIMIT ' + limit);
|
let sql = sprintf(`
|
||||||
if (items.length) return { hasMore: true, items: items };
|
SELECT %s FROM %s
|
||||||
|
LEFT JOIN sync_items t ON t.item_id = %s.id
|
||||||
|
WHERE t.id IS NULL OR t.sync_time < %s.updated_time
|
||||||
|
LIMIT %d
|
||||||
|
`,
|
||||||
|
this.db().escapeFields(fieldNames),
|
||||||
|
this.db().escapeField(ItemClass.tableName()),
|
||||||
|
this.db().escapeField(ItemClass.tableName()),
|
||||||
|
this.db().escapeField(ItemClass.tableName()),
|
||||||
|
limit);
|
||||||
|
|
||||||
items = await this.getClass('Tag').modelSelectAll('SELECT * FROM tags WHERE sync_time < updated_time LIMIT ' + limit);
|
const items = await ItemClass.modelSelectAll(sql);
|
||||||
if (items.length) return { hasMore: true, items: items };
|
|
||||||
|
|
||||||
items = await this.getClass('NoteTag').modelSelectAll('SELECT * FROM note_tags WHERE sync_time < updated_time LIMIT ' + limit);
|
if (i >= classNames.length - 1) {
|
||||||
return { hasMore: items.length >= limit, items: items };
|
return { hasMore: items.length >= limit, items: items };
|
||||||
|
} else {
|
||||||
|
if (items.length) return { hasMore: true, items: items };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unreachable');
|
||||||
|
|
||||||
|
//return this.modelSelectAll('SELECT * FROM folders WHERE sync_time < updated_time LIMIT ' + limit);
|
||||||
|
|
||||||
|
// let items = await this.getClass('Folder').modelSelectAll('SELECT * FROM folders WHERE sync_time < updated_time LIMIT ' + limit);
|
||||||
|
// if (items.length) return { hasMore: true, items: items };
|
||||||
|
|
||||||
|
// items = await this.getClass('Resource').modelSelectAll('SELECT * FROM resources WHERE sync_time < updated_time LIMIT ' + limit);
|
||||||
|
// if (items.length) return { hasMore: true, items: items };
|
||||||
|
|
||||||
|
// items = await this.getClass('Note').modelSelectAll('SELECT * FROM notes WHERE sync_time < updated_time AND is_conflict = 0 LIMIT ' + limit);
|
||||||
|
// if (items.length) return { hasMore: true, items: items };
|
||||||
|
|
||||||
|
// items = await this.getClass('Tag').modelSelectAll('SELECT * FROM tags WHERE sync_time < updated_time LIMIT ' + limit);
|
||||||
|
// if (items.length) return { hasMore: true, items: items };
|
||||||
|
|
||||||
|
// items = await this.getClass('NoteTag').modelSelectAll('SELECT * FROM note_tags WHERE sync_time < updated_time LIMIT ' + limit);
|
||||||
|
// return { hasMore: items.length >= limit, items: items };
|
||||||
}
|
}
|
||||||
|
|
||||||
static syncItemClassNames() {
|
static syncItemClassNames() {
|
||||||
@ -319,6 +311,42 @@ class BaseItem extends BaseModel {
|
|||||||
throw new Error('Invalid type: ' + type);
|
throw new Error('Invalid type: ' + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static updateSyncTimeQueries(syncTarget, item, syncTime) {
|
||||||
|
const itemType = item.type_;
|
||||||
|
const itemId = item.id;
|
||||||
|
if (!itemType || !itemId || syncTime === undefined) throw new Error('Invalid parameters in updateSyncTimeQueries()');
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
sql: 'DELETE FROM sync_items WHERE sync_target = ? AND item_type = ? AND item_id = ?',
|
||||||
|
params: [syncTarget, itemType, itemId],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sql: 'INSERT INTO sync_items (sync_target, item_type, item_id, sync_time) VALUES (?, ?, ?, ?)',
|
||||||
|
params: [syncTarget, itemType, itemId, syncTime],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveSyncTime(syncTarget, item, syncTime) {
|
||||||
|
const queries = this.updateSyncTimeQueries(syncTarget, item, syncTime);
|
||||||
|
return this.db().transactionExecBatch(queries);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteOrphanSyncItems() {
|
||||||
|
const classNames = this.syncItemClassNames();
|
||||||
|
|
||||||
|
let queries = [];
|
||||||
|
for (let i = 0; i < classNames.length; i++) {
|
||||||
|
const className = classNames[i];
|
||||||
|
const ItemClass = this.getClass(className);
|
||||||
|
|
||||||
|
queries.push('DELETE FROM sync_items WHERE item_type = ' + ItemClass.modelType() + ' AND item_id NOT IN (SELECT id FROM ' + ItemClass.tableName() + ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.db().transactionExecBatch(queries);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also update:
|
// Also update:
|
||||||
|
@ -19,7 +19,7 @@ class Folder extends BaseItem {
|
|||||||
static async serialize(folder) {
|
static async serialize(folder) {
|
||||||
let fieldNames = this.fieldNames();
|
let fieldNames = this.fieldNames();
|
||||||
fieldNames.push('type_');
|
fieldNames.push('type_');
|
||||||
lodash.pull(fieldNames, 'parent_id', 'sync_time');
|
lodash.pull(fieldNames, 'parent_id');
|
||||||
return super.serialize(folder, 'folder', fieldNames);
|
return super.serialize(folder, 'folder', fieldNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ class NoteTag extends BaseItem {
|
|||||||
static async serialize(item, type = null, shownKeys = null) {
|
static async serialize(item, type = null, shownKeys = null) {
|
||||||
let fieldNames = this.fieldNames();
|
let fieldNames = this.fieldNames();
|
||||||
fieldNames.push('type_');
|
fieldNames.push('type_');
|
||||||
lodash.pull(fieldNames, 'sync_time');
|
|
||||||
return super.serialize(item, 'note_tag', fieldNames);
|
return super.serialize(item, 'note_tag', fieldNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,8 +18,6 @@ class Note extends BaseItem {
|
|||||||
static async serialize(note, type = null, shownKeys = null) {
|
static async serialize(note, type = null, shownKeys = null) {
|
||||||
let fieldNames = this.fieldNames();
|
let fieldNames = this.fieldNames();
|
||||||
fieldNames.push('type_');
|
fieldNames.push('type_');
|
||||||
//lodash.pull(fieldNames, 'is_conflict', 'sync_time');
|
|
||||||
lodash.pull(fieldNames, 'sync_time');
|
|
||||||
return super.serialize(note, 'note', fieldNames);
|
return super.serialize(note, 'note', fieldNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +66,7 @@ class Note extends BaseItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static previewFields() {
|
static previewFields() {
|
||||||
return ['id', 'title', 'body', 'is_todo', 'todo_completed', 'parent_id', 'updated_time', 'sync_time'];
|
return ['id', 'title', 'body', 'is_todo', 'todo_completed', 'parent_id', 'updated_time'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static previewFieldsSql() {
|
static previewFieldsSql() {
|
||||||
@ -212,7 +210,6 @@ class Note extends BaseItem {
|
|||||||
|
|
||||||
let newNote = Object.assign({}, originalNote);
|
let newNote = Object.assign({}, originalNote);
|
||||||
delete newNote.id;
|
delete newNote.id;
|
||||||
newNote.sync_time = 0;
|
|
||||||
|
|
||||||
for (let n in changes) {
|
for (let n in changes) {
|
||||||
if (!changes.hasOwnProperty(n)) continue;
|
if (!changes.hasOwnProperty(n)) continue;
|
||||||
|
@ -23,7 +23,6 @@ class Resource extends BaseItem {
|
|||||||
static async serialize(item, type = null, shownKeys = null) {
|
static async serialize(item, type = null, shownKeys = null) {
|
||||||
let fieldNames = this.fieldNames();
|
let fieldNames = this.fieldNames();
|
||||||
fieldNames.push('type_');
|
fieldNames.push('type_');
|
||||||
lodash.pull(fieldNames, 'sync_time');
|
|
||||||
return super.serialize(item, 'resource', fieldNames);
|
return super.serialize(item, 'resource', fieldNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ class Tag extends BaseItem {
|
|||||||
static async serialize(item, type = null, shownKeys = null) {
|
static async serialize(item, type = null, shownKeys = null) {
|
||||||
let fieldNames = this.fieldNames();
|
let fieldNames = this.fieldNames();
|
||||||
fieldNames.push('type_');
|
fieldNames.push('type_');
|
||||||
lodash.pull(fieldNames, 'sync_time');
|
|
||||||
return super.serialize(item, 'tag', fieldNames);
|
return super.serialize(item, 'tag', fieldNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,8 @@ class ReportService {
|
|||||||
let ItemClass = BaseItem.getClass(d.className);
|
let ItemClass = BaseItem.getClass(d.className);
|
||||||
let o = {
|
let o = {
|
||||||
total: await ItemClass.count(),
|
total: await ItemClass.count(),
|
||||||
synced: await ItemClass.syncedCount(),
|
// synced: await ItemClass.syncedCount(), // TODO
|
||||||
|
synced: 0,
|
||||||
};
|
};
|
||||||
output.items[d.className] = o;
|
output.items[d.className] = o;
|
||||||
itemCount += o.total;
|
itemCount += o.total;
|
||||||
|
@ -54,6 +54,7 @@ class Synchronizer {
|
|||||||
if (report.deleteLocal) lines.push(_('Deleted local items: %d.', report.deleteLocal));
|
if (report.deleteLocal) lines.push(_('Deleted local items: %d.', report.deleteLocal));
|
||||||
if (report.deleteRemote) lines.push(_('Deleted remote items: %d.', report.deleteRemote));
|
if (report.deleteRemote) lines.push(_('Deleted remote items: %d.', report.deleteRemote));
|
||||||
if (report.state) lines.push(_('State: %s.', report.state.replace(/_/g, ' ')));
|
if (report.state) lines.push(_('State: %s.', report.state.replace(/_/g, ' ')));
|
||||||
|
if (report.errors && report.errors.length) lines.push(_('Last error: %s (stacktrace in log).', report.errors[report.errors.length-1].message));
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,6 +140,8 @@ class Synchronizer {
|
|||||||
this.onProgress_ = options.onProgress ? options.onProgress : function(o) {};
|
this.onProgress_ = options.onProgress ? options.onProgress : function(o) {};
|
||||||
this.progressReport_ = { errors: [] };
|
this.progressReport_ = { errors: [] };
|
||||||
|
|
||||||
|
const syncTargetId = this.api().driver().syncTargetId();
|
||||||
|
|
||||||
if (this.state() != 'idle') {
|
if (this.state() != 'idle') {
|
||||||
this.logger().warn('Synchronization is already in progress. State: ' + this.state());
|
this.logger().warn('Synchronization is already in progress. State: ' + this.state());
|
||||||
return;
|
return;
|
||||||
@ -156,7 +159,7 @@ class Synchronizer {
|
|||||||
|
|
||||||
this.state_ = 'in_progress';
|
this.state_ = 'in_progress';
|
||||||
|
|
||||||
this.logSyncOperation('starting', null, null, 'Starting synchronization... [' + synchronizationId + ']');
|
this.logSyncOperation('starting', null, null, 'Starting synchronization to ' + this.api().driver().syncTargetName() + ' (' + syncTargetId + ')... [' + synchronizationId + ']');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api().mkdir(this.syncDirName_);
|
await this.api().mkdir(this.syncDirName_);
|
||||||
@ -166,7 +169,7 @@ class Synchronizer {
|
|||||||
while (true) {
|
while (true) {
|
||||||
if (this.cancelling()) break;
|
if (this.cancelling()) break;
|
||||||
|
|
||||||
let result = await BaseItem.itemsThatNeedSync();
|
let result = await BaseItem.itemsThatNeedSync(syncTargetId);
|
||||||
let locals = result.items;
|
let locals = result.items;
|
||||||
|
|
||||||
for (let i = 0; i < locals.length; i++) {
|
for (let i = 0; i < locals.length; i++) {
|
||||||
@ -209,7 +212,6 @@ class Synchronizer {
|
|||||||
|
|
||||||
this.logSyncOperation(action, local, remote, reason);
|
this.logSyncOperation(action, local, remote, reason);
|
||||||
|
|
||||||
|
|
||||||
if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || (action == 'itemConflict' && remote))) {
|
if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || (action == 'itemConflict' && remote))) {
|
||||||
let remoteContentPath = this.resourceDirName_ + '/' + local.id;
|
let remoteContentPath = this.resourceDirName_ + '/' + local.id;
|
||||||
let resourceContent = await Resource.content(local);
|
let resourceContent = await Resource.content(local);
|
||||||
@ -236,8 +238,8 @@ class Synchronizer {
|
|||||||
await this.api().setTimestamp(path, local.updated_time);
|
await this.api().setTimestamp(path, local.updated_time);
|
||||||
|
|
||||||
if (this.randomFailure(options, 1)) return;
|
if (this.randomFailure(options, 1)) return;
|
||||||
|
|
||||||
await ItemClass.save({ id: local.id, sync_time: time.unixMs(), type_: local.type_ }, { autoTimestamp: false });
|
await ItemClass.saveSyncTime(syncTargetId, local, time.unixMs());
|
||||||
|
|
||||||
} else if (action == 'itemConflict') {
|
} else if (action == 'itemConflict') {
|
||||||
|
|
||||||
@ -245,8 +247,8 @@ class Synchronizer {
|
|||||||
let remoteContent = await this.api().get(path);
|
let remoteContent = await this.api().get(path);
|
||||||
local = await BaseItem.unserialize(remoteContent);
|
local = await BaseItem.unserialize(remoteContent);
|
||||||
|
|
||||||
local.sync_time = time.unixMs();
|
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
|
||||||
await ItemClass.save(local, { autoTimestamp: false });
|
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries });
|
||||||
} else {
|
} else {
|
||||||
await ItemClass.delete(local.id);
|
await ItemClass.delete(local.id);
|
||||||
}
|
}
|
||||||
@ -266,8 +268,8 @@ class Synchronizer {
|
|||||||
let remoteContent = await this.api().get(path);
|
let remoteContent = await this.api().get(path);
|
||||||
local = await BaseItem.unserialize(remoteContent);
|
local = await BaseItem.unserialize(remoteContent);
|
||||||
|
|
||||||
local.sync_time = time.unixMs();
|
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
|
||||||
await ItemClass.save(local, { autoTimestamp: false });
|
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries });
|
||||||
} else {
|
} else {
|
||||||
await ItemClass.delete(local.id);
|
await ItemClass.delete(local.id);
|
||||||
}
|
}
|
||||||
@ -346,10 +348,10 @@ class Synchronizer {
|
|||||||
let ItemClass = BaseItem.itemClass(content);
|
let ItemClass = BaseItem.itemClass(content);
|
||||||
|
|
||||||
let newContent = Object.assign({}, content);
|
let newContent = Object.assign({}, content);
|
||||||
newContent.sync_time = time.unixMs();
|
|
||||||
let options = {
|
let options = {
|
||||||
autoTimestamp: false,
|
autoTimestamp: false,
|
||||||
applyMetadataChanges: true,
|
applyMetadataChanges: true,
|
||||||
|
nextQueries: BaseItem.updateSyncTimeQueries(syncTargetId, newContent, time.unixMs()),
|
||||||
};
|
};
|
||||||
if (action == 'createLocal') options.isNew = true;
|
if (action == 'createLocal') options.isNew = true;
|
||||||
|
|
||||||
@ -381,37 +383,39 @@ class Synchronizer {
|
|||||||
let localFoldersToDelete = [];
|
let localFoldersToDelete = [];
|
||||||
|
|
||||||
if (!this.cancelling()) {
|
if (!this.cancelling()) {
|
||||||
let items = await BaseItem.syncedItems();
|
let syncItems = await BaseItem.syncedItems(syncTargetId);
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < syncItems.length; i++) {
|
||||||
if (this.cancelling()) break;
|
if (this.cancelling()) break;
|
||||||
|
|
||||||
let item = items[i];
|
let syncItem = syncItems[i];
|
||||||
if (remoteIds.indexOf(item.id) < 0) {
|
if (remoteIds.indexOf(syncItem.item_id) < 0) {
|
||||||
if (item.type_ == Folder.modelType()) {
|
if (syncItem.item_type == Folder.modelType()) {
|
||||||
localFoldersToDelete.push(item);
|
localFoldersToDelete.push(syncItem);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logSyncOperation('deleteLocal', { id: item.id }, null, 'remote has been deleted');
|
this.logSyncOperation('deleteLocal', { id: syncItem.item_id }, null, 'remote has been deleted');
|
||||||
|
|
||||||
let ItemClass = BaseItem.itemClass(item);
|
let ItemClass = BaseItem.itemClass(syncItem.item_type);
|
||||||
await ItemClass.delete(item.id, { trackDeleted: false });
|
await ItemClass.delete(syncItem.item_id, { trackDeleted: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.cancelling()) {
|
if (!this.cancelling()) {
|
||||||
for (let i = 0; i < localFoldersToDelete.length; i++) {
|
for (let i = 0; i < localFoldersToDelete.length; i++) {
|
||||||
const folder = localFoldersToDelete[i];
|
const syncItem = localFoldersToDelete[i];
|
||||||
const noteIds = await Folder.noteIds(folder.id);
|
const noteIds = await Folder.noteIds(syncItem.item_id);
|
||||||
if (noteIds.length) { // CONFLICT
|
if (noteIds.length) { // CONFLICT
|
||||||
await Folder.markNotesAsConflict(folder.id);
|
await Folder.markNotesAsConflict(syncItem.item_id);
|
||||||
await Folder.delete(folder.id, { deleteChildren: false });
|
|
||||||
} else {
|
|
||||||
await Folder.delete(folder.id);
|
|
||||||
}
|
}
|
||||||
|
await Folder.delete(syncItem.item_id, { deleteChildren: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.cancelling()) {
|
||||||
|
await BaseItem.deleteOrphanSyncItems();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger().error(error);
|
this.logger().error(error);
|
||||||
this.progressReport_.errors.push(error);
|
this.progressReport_.errors.push(error);
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"ReactNativeClient/android/local.properties",
|
"ReactNativeClient/android/local.properties",
|
||||||
"ReactNativeClient/ios",
|
"ReactNativeClient/ios",
|
||||||
"_vieux",
|
"_vieux",
|
||||||
|
"tests/logs"
|
||||||
],
|
],
|
||||||
"file_exclude_patterns": [
|
"file_exclude_patterns": [
|
||||||
"*.map",
|
"*.map",
|
||||||
|
Loading…
Reference in New Issue
Block a user