1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

various changes

This commit is contained in:
Laurent Cozic 2017-06-12 22:56:27 +01:00
parent 32e9b1ada7
commit 8578642307
13 changed files with 255 additions and 84 deletions

View File

@ -1,3 +1,5 @@
require('source-map-support').install();
import { FileApi } from 'src/file-api.js'; import { FileApi } from 'src/file-api.js';
import { FileApiDriverLocal } from 'src/file-api-driver-local.js'; import { FileApiDriverLocal } from 'src/file-api-driver-local.js';
import { Database } from 'src/database.js'; import { Database } from 'src/database.js';
@ -5,6 +7,7 @@ import { DatabaseDriverNode } from 'src/database-driver-node.js';
import { BaseModel } from 'src/base-model.js'; import { BaseModel } from 'src/base-model.js';
import { Folder } from 'src/models/folder.js'; import { Folder } from 'src/models/folder.js';
import { Note } from 'src/models/note.js'; import { Note } from 'src/models/note.js';
import { Setting } from 'src/models/setting.js';
import { Synchronizer } from 'src/synchronizer.js'; import { Synchronizer } from 'src/synchronizer.js';
import { uuid } from 'src/uuid.js'; import { uuid } from 'src/uuid.js';
import { sprintf } from 'sprintf-js'; import { sprintf } from 'sprintf-js';
@ -53,7 +56,9 @@ let synchronizer = new Synchronizer(db, fileApi);
db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => { db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => {
BaseModel.db_ = db; BaseModel.db_ = db;
}).then(() => {
return Setting.load();
}).then(() => {
let commands = []; let commands = [];
let currentFolder = null; let currentFolder = null;
@ -291,6 +296,7 @@ db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => {
commands.push({ commands.push({
usage: 'ls [list-title]', usage: 'ls [list-title]',
alias: 'll',
description: 'Lists items in [list-title].', description: 'Lists items in [list-title].',
action: function (args, end) { action: function (args, end) {
let folderTitle = args['list-title']; let folderTitle = args['list-title'];

View File

@ -14,6 +14,7 @@
"promise": "^7.1.1", "promise": "^7.1.1",
"react": "16.0.0-alpha.6", "react": "16.0.0-alpha.6",
"sax": "^1.2.2", "sax": "^1.2.2",
"source-map-support": "^0.4.15",
"sprintf-js": "^1.1.1", "sprintf-js": "^1.1.1",
"sqlite3": "^3.1.8", "sqlite3": "^3.1.8",
"string-to-stream": "^1.1.0", "string-to-stream": "^1.1.0",
@ -29,7 +30,7 @@
}, },
"scripts": { "scripts": {
"babelbuild": "babel app -d build", "babelbuild": "babel app -d build",
"build": "babel-changed app -d build && babel-changed app/src/models -d build/src/models && babel-changed app/src/services -d build/src/services", "build": "babel-changed app -d build --source-maps && babel-changed app/src/models -d build/src/models --source-maps && babel-changed app/src/services -d build/src/services --source-maps",
"clean": "babel-changed --reset" "clean": "babel-changed --reset"
} }
} }

View File

@ -73,7 +73,6 @@ class BaseModel {
static load(id) { static load(id) {
return this.loadByField('id', id); return this.loadByField('id', id);
//return this.db().selectOne('SELECT * FROM ' + this.tableName() + ' WHERE id = ?', [id]);
} }
static loadByField(fieldName, fieldValue) { static loadByField(fieldName, fieldValue) {
@ -139,7 +138,7 @@ class BaseModel {
query.id = itemId; query.id = itemId;
Log.info('Saving', o); Log.info('Saving', JSON.stringify(o));
return query; return query;
} }

View File

@ -283,6 +283,7 @@ class Database {
} }
refreshTableFields() { refreshTableFields() {
Log.info('Initializing tables...');
let queries = []; let queries = [];
queries.push(this.wrapQuery('DELETE FROM table_fields')); queries.push(this.wrapQuery('DELETE FROM table_fields'));

View File

@ -25,15 +25,29 @@ class FileApiDriverLocal {
return Math.round(m.toDate().getTime() / 1000); return Math.round(m.toDate().getTime() / 1000);
} }
metadataFromStats_(name, stats) { metadataFromStats_(path, stats) {
return { return {
name: name, path: path,
createdTime: this.statTimeToUnixTimestamp_(stats.birthtime), createdTime: this.statTimeToUnixTimestamp_(stats.birthtime),
updatedTime: this.statTimeToUnixTimestamp_(stats.mtime), updatedTime: this.statTimeToUnixTimestamp_(stats.mtime),
createdTimeOrig: stats.birthtime,
updatedTimeOrig: stats.mtime,
isDir: stats.isDirectory(), isDir: stats.isDirectory(),
}; };
} }
setFileTimestamp(path, timestamp) {
return new Promise((resolve, reject) => {
fs.utimes(path, timestamp, timestamp, (error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
list(path) { list(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.readdir(path, (error, items) => { fs.readdir(path, (error, items) => {

View File

@ -35,6 +35,10 @@ class FileApi {
}); });
} }
setFileTimestamp(path, timestamp) {
return this.driver_.setFileTimestamp(this.baseDir_ + '/' + path, timestamp);
}
mkdir(path) { mkdir(path) {
return this.driver_.mkdir(this.baseDir_ + '/' + path); return this.driver_.mkdir(this.baseDir_ + '/' + path);
} }

View File

@ -24,8 +24,6 @@ class Change extends BaseModel {
static deleteMultiple(ids) { static deleteMultiple(ids) {
if (ids.length == 0) return Promise.resolve(); if (ids.length == 0) return Promise.resolve();
console.warn('TODO: deleteMultiple: CHECK THAT IT WORKS');
let queries = []; let queries = [];
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
queries.push(['DELETE FROM changes WHERE id = ?', [ids[i]]]); queries.push(['DELETE FROM changes WHERE id = ?', [ids[i]]]);

View File

@ -4,6 +4,7 @@ import { promiseChain } from 'src/promise-chain.js';
import { Note } from 'src/models/note.js'; import { Note } from 'src/models/note.js';
import { folderItemFilename } from 'src/string-utils.js' import { folderItemFilename } from 'src/string-utils.js'
import { _ } from 'src/locale.js'; import { _ } from 'src/locale.js';
import moment from 'moment';
class Folder extends BaseModel { class Folder extends BaseModel {
@ -19,6 +20,40 @@ class Folder extends BaseModel {
return this.filename(folder); return this.filename(folder);
} }
static systemMetadataPath(parent, folder) {
return this.systemPath(parent, folder) + '/.folder.md';
}
// TODO: share with Note class
static toFriendlyString_format(propName, propValue) {
if (['created_time', 'updated_time'].indexOf(propName) >= 0) {
if (!propValue) return '';
propValue = moment.unix(propValue).format('YYYY-MM-DD hh:mm:ss');
} else if (propValue === null || propValue === undefined) {
propValue = '';
}
return propValue;
}
// TODO: share with Note class
static toFriendlyString(folder) {
let shownKeys = ['created_time', 'updated_time'];
let output = [];
output.push(folder.title);
output.push('');
output.push(''); // For consistency with the notes, leave an empty line where the body should be
output.push('');
for (let i = 0; i < shownKeys.length; i++) {
let v = folder[shownKeys[i]];
v = this.toFriendlyString_format(shownKeys[i], v);
output.push(shownKeys[i] + ': ' + v);
}
return output.join("\n");
}
static useUuid() { static useUuid() {
return true; return true;
} }

View File

@ -116,7 +116,8 @@ Setting.defaults_ = {
'sessionId': { value: '', type: 'string' }, 'sessionId': { value: '', type: 'string' },
'user.email': { value: '', type: 'string' }, 'user.email': { value: '', type: 'string' },
'user.session': { value: '', type: 'string' }, 'user.session': { value: '', type: 'string' },
'sync.lastRevId': { value: 0, type: 'int' }, 'sync.lastRevId': { value: 0, type: 'int' }, // DEPRECATED
'sync.lastUpdateTime': { value: 0, type: 'int' },
}; };
export { Setting }; export { Setting };

View File

@ -10,8 +10,9 @@ import { Registry } from 'src/registry.js';
class NoteFolderService extends BaseService { class NoteFolderService extends BaseService {
static save(type, item, oldItem) { static save(type, item, oldItem) {
let diff = null;
if (oldItem) { if (oldItem) {
let diff = BaseModel.diffObjects(oldItem, item); diff = BaseModel.diffObjects(oldItem, item);
if (!Object.getOwnPropertyNames(diff).length) { if (!Object.getOwnPropertyNames(diff).length) {
Log.info('Item not changed - not saved'); Log.info('Item not changed - not saved');
return Promise.resolve(item); return Promise.resolve(item);
@ -27,9 +28,16 @@ class NoteFolderService extends BaseService {
let isNew = !item.id; let isNew = !item.id;
let output = null; let output = null;
return ItemClass.save(item).then((item) => {
output = item; let toSave = item;
if (isNew && type == 'note') return Note.updateGeolocation(item.id); if (diff !== null) {
toSave = diff;
toSave.id = item.id;
}
return ItemClass.save(toSave).then((savedItem) => {
output = Object.assign(item, savedItem);
if (isNew && type == 'note') return Note.updateGeolocation(output.id);
}).then(() => { }).then(() => {
// Registry.synchronizer().start(); // Registry.synchronizer().start();
return output; return output;

View File

@ -114,9 +114,10 @@ function escapeFilename(s, maxLength = 32) {
} }
function folderItemFilename(item) { function folderItemFilename(item) {
let output = escapeFilename(item.title).trim(); return item.id;
if (!output.length) output = '_'; // let output = escapeFilename(item.title).trim();
return output + '.' + item.id.substr(0, 7); // if (!output.length) output = '_';
// return output + '.' + item.id.substr(0, 7);
} }
export { removeDiacritics, escapeFilename, folderItemFilename }; export { removeDiacritics, escapeFilename, folderItemFilename };

View File

@ -46,15 +46,15 @@ class Synchronizer {
} }
} }
remoteFileByName(remoteFiles, name) { remoteFileByPath(remoteFiles, path) {
for (let i = 0; i < remoteFiles.length; i++) { for (let i = 0; i < remoteFiles.length; i++) {
if (remoteFiles[i].name == name) return remoteFiles[i]; if (remoteFiles[i].path == path) return remoteFiles[i];
} }
return null; return null;
} }
conflictDir(remoteFiles) { conflictDir(remoteFiles) {
let d = this.remoteFileByName('Conflicts'); let d = this.remoteFileByPath('Conflicts');
if (!d) { if (!d) {
return this.api().mkdir('Conflicts').then(() => { return this.api().mkdir('Conflicts').then(() => {
return 'Conflicts'; return 'Conflicts';
@ -69,11 +69,11 @@ class Synchronizer {
if (item.isDir) return Promise.resolve(); if (item.isDir) return Promise.resolve();
return this.conflictDir().then((conflictDirPath) => { return this.conflictDir().then((conflictDirPath) => {
let p = path.basename(item.name).split('.'); let p = path.basename(item.path).split('.');
let pos = item.isDir ? p.length - 1 : p.length - 2; let pos = item.isDir ? p.length - 1 : p.length - 2;
p.splice(pos, 0, moment().format('YYYYMMDDThhmmss')); p.splice(pos, 0, moment().format('YYYYMMDDThhmmss'));
let newName = p.join('.'); let newPath = p.join('.');
return this.api().move(item.name, conflictDirPath + '/' + newName); return this.api().move(item.path, conflictDirPath + '/' + newPath);
}); });
} }
@ -86,6 +86,7 @@ class Synchronizer {
}).then((changes) => { }).then((changes) => {
let mergedChanges = Change.mergeChanges(changes); let mergedChanges = Change.mergeChanges(changes);
let chain = []; let chain = [];
const lastSyncTime = Setting.value('sync.lastUpdateTime');
for (let i = 0; i < mergedChanges.length; i++) { for (let i = 0; i < mergedChanges.length; i++) {
let c = mergedChanges[i]; let c = mergedChanges[i];
chain.push(() => { chain.push(() => {
@ -102,11 +103,13 @@ class Synchronizer {
p = Promise.resolve(); p = Promise.resolve();
} else if (c.type == Change.TYPE_CREATE) { } else if (c.type == Change.TYPE_CREATE) {
p = this.loadParentAndItem(c).then((result) => { p = this.loadParentAndItem(c).then((result) => {
if (!result.item) return; // Change refers to an object that doesn't exist (has probably been deleted directly in the database) let item = result.item;
let parent = result.parent;
if (!item) return; // Change refers to an object that doesn't exist (has probably been deleted directly in the database)
let path = ItemClass.systemPath(result.parent, result.item); let path = ItemClass.systemPath(parent, item);
let remoteFile = this.remoteFileByName(remoteFiles, path); let remoteFile = this.remoteFileByPath(remoteFiles, path);
let p = null; let p = null;
if (remoteFile) { if (remoteFile) {
p = this.moveConflict(remoteFile); p = this.moveConflict(remoteFile);
@ -116,7 +119,39 @@ class Synchronizer {
return p.then(() => { return p.then(() => {
if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) { if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) {
return this.api().mkdir(path); return this.api().mkdir(path).then(() => {
return this.api().put(Folder.systemMetadataPath(parent, item), Folder.toFriendlyString(item));
}).then(() => {
return this.api().setFileTimestamp(Folder.systemMetadataPath(parent, item), item.updated_time);
});
} else {
return this.api().put(path, Note.toFriendlyString(item)).then(() => {
return this.api().setFileTimestamp(path, item.updated_time);
});
}
});
});
} else if (c.type == Change.TYPE_UPDATE) {
p = this.loadParentAndItem(c).then((result) => {
if (!result.item) return; // Change refers to an object that doesn't exist (has probably been deleted directly in the database)
let path = ItemClass.systemPath(result.parent, result.item);
let remoteFile = this.remoteFileByPath(remoteFiles, path);
let p = null;
if (remoteFile && remoteFile.updatedTime > lastSyncTime) {
console.info('CONFLICT:', lastSyncTime, remoteFile);
//console.info(moment.unix(remoteFile.updatedTime), moment.unix(result.item.updated_time));
p = this.moveConflict(remoteFile);
} else {
p = Promise.resolve();
}
console.info('Uploading change:', JSON.stringify(result.item));
return p.then(() => {
if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) {
return this.api().put(Folder.systemMetadataPath(result.parent, result.item), Folder.toFriendlyString(result.item));
} else { } else {
return this.api().put(path, Note.toFriendlyString(result.item)); return this.api().put(path, Note.toFriendlyString(result.item));
} }
@ -124,7 +159,6 @@ class Synchronizer {
}); });
} }
// TODO: handle UPDATE
// TODO: handle DELETE // TODO: handle DELETE
return p.then(() => { return p.then(() => {
@ -142,11 +176,17 @@ class Synchronizer {
} }
return promiseChain(chain); return promiseChain(chain);
// }).then(() => {
// console.info(remoteFiles);
// for (let i = 0; i < remoteFiles.length; i++) {
// const remoteFile = remoteFiles[i];
// }
}).catch((error) => { }).catch((error) => {
Log.warn('Synchronization was interrupted due to an error:', error); Log.error('Synchronization was interrupted due to an error:', error);
}).then(() => { }).then(() => {
Log.info('IDs to delete: ', processedChangeIds); //Log.info('IDs to delete: ', processedChangeIds);
// Change.deleteMultiple(processedChangeIds); //return Change.deleteMultiple(processedChangeIds);
}).then(() => { }).then(() => {
this.processState('downloadChanges'); this.processState('downloadChanges');
}); });
@ -240,63 +280,126 @@ class Synchronizer {
} }
processState_downloadChanges() { processState_downloadChanges() {
let maxRevId = null; // return this.api().list('', true).then((items) => {
let hasMore = false; // remoteFiles = items;
this.api().get('synchronizer', { rev_id: Setting.value('sync.lastRevId') }).then((syncOperations) => { // return Change.all();
hasMore = syncOperations.has_more;
let chain = [];
for (let i = 0; i < syncOperations.items.length; i++) {
let syncOp = syncOperations.items[i];
if (syncOp.id > maxRevId) maxRevId = syncOp.id;
let ItemClass = null;
if (syncOp.item_type == 'folder') {
ItemClass = Folder;
} else if (syncOp.item_type == 'note') {
ItemClass = Note;
}
if (syncOp.type == 'create') { // let maxRevId = null;
chain.push(() => { // let hasMore = false;
let item = ItemClass.fromApiResult(syncOp.item); // this.api().get('synchronizer', { rev_id: Setting.value('sync.lastRevId') }).then((syncOperations) => {
// TODO: automatically handle NULL fields by checking type and default value of field // hasMore = syncOperations.has_more;
if ('parent_id' in item && !item.parent_id) item.parent_id = ''; // let chain = [];
return ItemClass.save(item, { isNew: true, trackChanges: false }); // for (let i = 0; i < syncOperations.items.length; i++) {
}); // let syncOp = syncOperations.items[i];
} // if (syncOp.id > maxRevId) maxRevId = syncOp.id;
if (syncOp.type == 'update') { // let ItemClass = null;
chain.push(() => { // if (syncOp.item_type == 'folder') {
return ItemClass.load(syncOp.item_id).then((item) => { // ItemClass = Folder;
if (!item) return; // } else if (syncOp.item_type == 'note') {
item = ItemClass.applyPatch(item, syncOp.item); // ItemClass = Note;
return ItemClass.save(item, { trackChanges: false }); // }
});
});
}
if (syncOp.type == 'delete') { // if (syncOp.type == 'create') {
chain.push(() => { // chain.push(() => {
return ItemClass.delete(syncOp.item_id, { trackChanges: false }); // let item = ItemClass.fromApiResult(syncOp.item);
}); // // TODO: automatically handle NULL fields by checking type and default value of field
} // if ('parent_id' in item && !item.parent_id) item.parent_id = '';
} // return ItemClass.save(item, { isNew: true, trackChanges: false });
return promiseChain(chain); // });
}).then(() => { // }
Log.info('All items synced. has_more = ', hasMore);
if (maxRevId) { // if (syncOp.type == 'update') {
Setting.setValue('sync.lastRevId', maxRevId); // chain.push(() => {
return Setting.saveAll(); // return ItemClass.load(syncOp.item_id).then((item) => {
} // if (!item) return;
}).then(() => { // item = ItemClass.applyPatch(item, syncOp.item);
if (hasMore) { // return ItemClass.save(item, { trackChanges: false });
this.processState('downloadChanges'); // });
} else { // });
this.processState('idle'); // }
}
}).catch((error) => { // if (syncOp.type == 'delete') {
Log.warn('Sync error', error); // chain.push(() => {
}); // return ItemClass.delete(syncOp.item_id, { trackChanges: false });
// });
// }
// }
// return promiseChain(chain);
// }).then(() => {
// Log.info('All items synced. has_more = ', hasMore);
// if (maxRevId) {
// Setting.setValue('sync.lastRevId', maxRevId);
// return Setting.saveAll();
// }
// }).then(() => {
// if (hasMore) {
// this.processState('downloadChanges');
// } else {
// this.processState('idle');
// }
// }).catch((error) => {
// Log.warn('Sync error', error);
// });
// let maxRevId = null;
// let hasMore = false;
// this.api().get('synchronizer', { rev_id: Setting.value('sync.lastRevId') }).then((syncOperations) => {
// hasMore = syncOperations.has_more;
// let chain = [];
// for (let i = 0; i < syncOperations.items.length; i++) {
// let syncOp = syncOperations.items[i];
// if (syncOp.id > maxRevId) maxRevId = syncOp.id;
// let ItemClass = null;
// if (syncOp.item_type == 'folder') {
// ItemClass = Folder;
// } else if (syncOp.item_type == 'note') {
// ItemClass = Note;
// }
// if (syncOp.type == 'create') {
// chain.push(() => {
// let item = ItemClass.fromApiResult(syncOp.item);
// // TODO: automatically handle NULL fields by checking type and default value of field
// if ('parent_id' in item && !item.parent_id) item.parent_id = '';
// return ItemClass.save(item, { isNew: true, trackChanges: false });
// });
// }
// if (syncOp.type == 'update') {
// chain.push(() => {
// return ItemClass.load(syncOp.item_id).then((item) => {
// if (!item) return;
// item = ItemClass.applyPatch(item, syncOp.item);
// return ItemClass.save(item, { trackChanges: false });
// });
// });
// }
// if (syncOp.type == 'delete') {
// chain.push(() => {
// return ItemClass.delete(syncOp.item_id, { trackChanges: false });
// });
// }
// }
// return promiseChain(chain);
// }).then(() => {
// Log.info('All items synced. has_more = ', hasMore);
// if (maxRevId) {
// Setting.setValue('sync.lastRevId', maxRevId);
// return Setting.saveAll();
// }
// }).then(() => {
// if (hasMore) {
// this.processState('downloadChanges');
// } else {
// this.processState('idle');
// }
// }).catch((error) => {
// Log.warn('Sync error', error);
// });
} }
processState(state) { processState(state) {