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 { FileApiDriverLocal } from 'src/file-api-driver-local.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 { Folder } from 'src/models/folder.js';
import { Note } from 'src/models/note.js';
import { Setting } from 'src/models/setting.js';
import { Synchronizer } from 'src/synchronizer.js';
import { uuid } from 'src/uuid.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(() => {
BaseModel.db_ = db;
}).then(() => {
return Setting.load();
}).then(() => {
let commands = [];
let currentFolder = null;
@ -291,6 +296,7 @@ db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => {
commands.push({
usage: 'ls [list-title]',
alias: 'll',
description: 'Lists items in [list-title].',
action: function (args, end) {
let folderTitle = args['list-title'];

View File

@ -14,6 +14,7 @@
"promise": "^7.1.1",
"react": "16.0.0-alpha.6",
"sax": "^1.2.2",
"source-map-support": "^0.4.15",
"sprintf-js": "^1.1.1",
"sqlite3": "^3.1.8",
"string-to-stream": "^1.1.0",
@ -29,7 +30,7 @@
},
"scripts": {
"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"
}
}

View File

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

View File

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

View File

@ -25,15 +25,29 @@ class FileApiDriverLocal {
return Math.round(m.toDate().getTime() / 1000);
}
metadataFromStats_(name, stats) {
metadataFromStats_(path, stats) {
return {
name: name,
path: path,
createdTime: this.statTimeToUnixTimestamp_(stats.birthtime),
updatedTime: this.statTimeToUnixTimestamp_(stats.mtime),
createdTimeOrig: stats.birthtime,
updatedTimeOrig: stats.mtime,
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) {
return new Promise((resolve, reject) => {
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) {
return this.driver_.mkdir(this.baseDir_ + '/' + path);
}

View File

@ -24,8 +24,6 @@ class Change extends BaseModel {
static deleteMultiple(ids) {
if (ids.length == 0) return Promise.resolve();
console.warn('TODO: deleteMultiple: CHECK THAT IT WORKS');
let queries = [];
for (let i = 0; i < ids.length; 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 { folderItemFilename } from 'src/string-utils.js'
import { _ } from 'src/locale.js';
import moment from 'moment';
class Folder extends BaseModel {
@ -19,6 +20,40 @@ class Folder extends BaseModel {
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() {
return true;
}

View File

@ -116,7 +116,8 @@ Setting.defaults_ = {
'sessionId': { value: '', type: 'string' },
'user.email': { 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 };

View File

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

View File

@ -114,9 +114,10 @@ function escapeFilename(s, maxLength = 32) {
}
function folderItemFilename(item) {
let output = escapeFilename(item.title).trim();
if (!output.length) output = '_';
return output + '.' + item.id.substr(0, 7);
return item.id;
// let output = escapeFilename(item.title).trim();
// if (!output.length) output = '_';
// return output + '.' + item.id.substr(0, 7);
}
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++) {
if (remoteFiles[i].name == name) return remoteFiles[i];
if (remoteFiles[i].path == path) return remoteFiles[i];
}
return null;
}
conflictDir(remoteFiles) {
let d = this.remoteFileByName('Conflicts');
let d = this.remoteFileByPath('Conflicts');
if (!d) {
return this.api().mkdir('Conflicts').then(() => {
return 'Conflicts';
@ -69,11 +69,11 @@ class Synchronizer {
if (item.isDir) return Promise.resolve();
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;
p.splice(pos, 0, moment().format('YYYYMMDDThhmmss'));
let newName = p.join('.');
return this.api().move(item.name, conflictDirPath + '/' + newName);
let newPath = p.join('.');
return this.api().move(item.path, conflictDirPath + '/' + newPath);
});
}
@ -86,6 +86,7 @@ class Synchronizer {
}).then((changes) => {
let mergedChanges = Change.mergeChanges(changes);
let chain = [];
const lastSyncTime = Setting.value('sync.lastUpdateTime');
for (let i = 0; i < mergedChanges.length; i++) {
let c = mergedChanges[i];
chain.push(() => {
@ -102,11 +103,13 @@ class Synchronizer {
p = Promise.resolve();
} else if (c.type == Change.TYPE_CREATE) {
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;
if (remoteFile) {
p = this.moveConflict(remoteFile);
@ -116,7 +119,39 @@ class Synchronizer {
return p.then(() => {
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 {
return this.api().put(path, Note.toFriendlyString(result.item));
}
@ -124,7 +159,6 @@ class Synchronizer {
});
}
// TODO: handle UPDATE
// TODO: handle DELETE
return p.then(() => {
@ -142,11 +176,17 @@ class Synchronizer {
}
return promiseChain(chain);
// }).then(() => {
// console.info(remoteFiles);
// for (let i = 0; i < remoteFiles.length; i++) {
// const remoteFile = remoteFiles[i];
// }
}).catch((error) => {
Log.warn('Synchronization was interrupted due to an error:', error);
Log.error('Synchronization was interrupted due to an error:', error);
}).then(() => {
Log.info('IDs to delete: ', processedChangeIds);
// Change.deleteMultiple(processedChangeIds);
//Log.info('IDs to delete: ', processedChangeIds);
//return Change.deleteMultiple(processedChangeIds);
}).then(() => {
this.processState('downloadChanges');
});
@ -240,63 +280,126 @@ class Synchronizer {
}
processState_downloadChanges() {
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;
// return this.api().list('', true).then((items) => {
// remoteFiles = items;
// return Change.all();
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 });
});
}
// 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;
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 });
});
});
}
// let ItemClass = null;
// if (syncOp.item_type == 'folder') {
// ItemClass = Folder;
// } else if (syncOp.item_type == 'note') {
// ItemClass = Note;
// }
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);
});
// 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);
// });
// 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) {

View File

@ -99,4 +99,4 @@ CREATE TABLE `files` (
`is_encrypted` tinyint(1) NOT NULL default '0',
`encryption_method` int(11) NOT NULL default '0',
PRIMARY KEY (`id`)
) CHARACTER SET=utf8;
) CHARACTER SET=utf8;