diff --git a/CliClient/app/cmd.js b/CliClient/app/cmd.js index 4df0990f1..c6cd8e2a4 100644 --- a/CliClient/app/cmd.js +++ b/CliClient/app/cmd.js @@ -17,57 +17,6 @@ import { NoteFolderService } from 'src/services/note-folder-service.js'; let db = new Database(new DatabaseDriverNode()); -db.setDebugEnabled(false); - -// function whilePromise(callback) { -// let isDone = false; - -// function done() { -// isDone = true; -// } - -// let iterationDone = false; -// let p = callback(done).then(() => { -// iterationDone = true; -// }); - -// let iid = setInterval(() => { -// if (iterationDone) { -// if (isDone) { -// clearInterval(iid); -// return; -// } - -// iterationDone = false; -// callback(done).then(() => { -// iterationDone = true; -// }); -// } -// }, 100); -// } - -// function myPromise() { -// return new Promise((resolve, reject) => { -// setTimeout(() => { -// resolve(); -// }, 500); -// }); -// } - -// let counter = 0; -// whilePromise((done) => { -// return myPromise().then(() => { -// counter++; -// console.info(counter); -// if (counter == 5) { -// done(); -// } -// }); -// }); - - - - let fileDriver = new FileApiDriverLocal(); let fileApi = new FileApi('/home/laurent/Temp/TestImport', fileDriver); let synchronizer = new Synchronizer(db, fileApi); @@ -124,9 +73,11 @@ function createLocalItems() { }); } +db.setDebugEnabled(true); db.open({ name: '/home/laurent/Temp/test-sync.sqlite3' }).then(() => { BaseModel.db_ = db; - return clearDatabase().then(createLocalItems); + //return clearDatabase(); + //return clearDatabase().then(createLocalItems); }).then(() => { return synchronizer.start(); }).catch((error) => { diff --git a/CliClient/tests/test-utils.js b/CliClient/tests/test-utils.js index d0990050d..8ae9f259f 100644 --- a/CliClient/tests/test-utils.js +++ b/CliClient/tests/test-utils.js @@ -28,7 +28,6 @@ function setupDatabase(done) { return fs.unlink(filePath).catch(() => { // Don't care if the file doesn't exist }).then(() => { - //console.info('Opening database ' + filePath); database_ = new Database(new DatabaseDriverNode()); database_.setDebugEnabled(false); return database_.open({ name: filePath }).then(() => { diff --git a/ReactNativeClient/src/base-model.js b/ReactNativeClient/src/base-model.js index 8d9c6387d..bfd2d0b82 100644 --- a/ReactNativeClient/src/base-model.js +++ b/ReactNativeClient/src/base-model.js @@ -36,11 +36,20 @@ class BaseModel { return this.db().tableFieldNames(this.tableName()); } + static fieldType(name) { + let fields = this.fields(); + for (let i = 0; i < fields.length; i++) { + if (fields[i].name == name) return fields[i].type; + } + throw new Error('Unknown field: ' + name); + } + static fields() { return this.db().tableFields(this.tableName()); } static identifyItemType(item) { + if (!item) throw new Error('Cannot identify undefined item'); if ('body' in item || ('parent_id' in item && !!item.parent_id)) return BaseModel.ITEM_TYPE_NOTE; if ('sync_time' in item) return BaseModel.ITEM_TYPE_FOLDER; throw new Error('Cannot identify item: ' + JSON.stringify(item)); @@ -161,7 +170,7 @@ class BaseModel { queries.push(saveQuery); // TODO: DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED DISABLED - if (0&& ptions.trackChanges && this.trackChanges()) { + if (0&& options.trackChanges && this.trackChanges()) { // Cannot import this class the normal way due to cyclical dependencies between Change and BaseModel // which are not handled by React Native. const { Change } = require('src/models/change.js'); diff --git a/ReactNativeClient/src/database.js b/ReactNativeClient/src/database.js index 82537a54d..166bfaf40 100644 --- a/ReactNativeClient/src/database.js +++ b/ReactNativeClient/src/database.js @@ -11,7 +11,7 @@ CREATE TABLE folders ( created_time INT NOT NULL DEFAULT 0, updated_time INT NOT NULL DEFAULT 0, sync_time INT NOT NULL DEFAULT 0, - is_default BOOLEAN NOT NULL DEFAULT 0 + is_default INT NOT NULL DEFAULT 0 ); CREATE TABLE notes ( @@ -28,9 +28,9 @@ CREATE TABLE notes ( source TEXT NOT NULL DEFAULT "", author TEXT NOT NULL DEFAULT "", source_url TEXT NOT NULL DEFAULT "", - is_todo BOOLEAN NOT NULL DEFAULT 0, + is_todo INT NOT NULL DEFAULT 0, todo_due INT NOT NULL DEFAULT 0, - todo_completed BOOLEAN NOT NULL DEFAULT 0, + todo_completed INT NOT NULL DEFAULT 0, source_application TEXT NOT NULL DEFAULT "", application_data TEXT NOT NULL DEFAULT "", \`order\` INT NOT NULL DEFAULT 0 @@ -162,9 +162,7 @@ class Database { if (this.inTransaction_) { return new Promise((resolve, reject) => { let iid = setInterval(() => { - console.info('Waiting...'); if (!this.inTransaction_) { - console.info('OKKKKKKKKKKK'); clearInterval(iid); this.transactionExecBatch(queries).then(() => { resolve(); @@ -224,7 +222,6 @@ class Database { if (value === null || value === undefined) return null; if (type == this.TYPE_INT) return Number(value); if (type == this.TYPE_TEXT) return value; - if (type == this.TYPE_BOOLEAN) return !!Number(value); if (type == this.TYPE_NUMERIC) return Number(value); throw new Error('Unknown type: ' + type); } @@ -249,7 +246,7 @@ class Database { logQuery(sql, params = null) { if (!this.debugMode()) return; if (params !== null) { - Log.debug('DB: ' + sql, params); + Log.debug('DB: ' + sql, JSON.stringify(params)); } else { Log.debug('DB: ' + sql); } @@ -430,7 +427,6 @@ class Database { Database.TYPE_INT = 1; Database.TYPE_TEXT = 2; -Database.TYPE_BOOLEAN = 3; -Database.TYPE_NUMERIC = 4; +Database.TYPE_NUMERIC = 3; export { Database }; \ No newline at end of file diff --git a/ReactNativeClient/src/file-api.js b/ReactNativeClient/src/file-api.js index a556c8edc..eb49d7c66 100644 --- a/ReactNativeClient/src/file-api.js +++ b/ReactNativeClient/src/file-api.js @@ -13,11 +13,25 @@ class FileApi { return output; } + scopeItemToBaseDir_(item) { + let output = Object.assign({}, item); + output.path = item.path.substr(this.baseDir_.length + 1); + return output; + } + + scopeItemsToBaseDir_(items) { + let output = []; + for (let i = 0; i < items.length; i++) { + output.push(this.scopeItemToBaseDir_(items[i])); + } + return output; + } + listDirectories() { return this.driver_.list(this.fullPath_('')).then((items) => { let output = []; for (let i = 0; i < items.length; i++) { - if (items[i].isDir) output.push(items[i]); + if (items[i].isDir) output.push(this.scopeItemToBaseDir_(items[i])); } return output; }); @@ -26,6 +40,7 @@ class FileApi { list(path = '', recursive = false, context = null) { let fullPath = this.fullPath_(path); return this.driver_.list(fullPath).then((items) => { + items = this.scopeItemsToBaseDir_(items); if (recursive) { let chain = []; for (let i = 0; i < items.length; i++) { diff --git a/ReactNativeClient/src/models/base-item.js b/ReactNativeClient/src/models/base-item.js new file mode 100644 index 000000000..f7dc0fd0d --- /dev/null +++ b/ReactNativeClient/src/models/base-item.js @@ -0,0 +1,110 @@ +import { BaseModel } from 'src/base-model.js'; +import { Note } from 'src/models/note.js'; +import { Folder } from 'src/models/folder.js'; +import { folderItemFilename } from 'src/string-utils.js' +import { Database } from 'src/database.js'; +import moment from 'moment'; + +class BaseItem extends BaseModel { + + static useUuid() { + return true; + } + + static systemPath(item) { + return folderItemFilename(item) + '.md'; + } + + static pathToId(path) { + let s = path.split('.'); + return s[0]; + } + + static loadItemByPath(path) { + let id = this.pathToId(path); + return Note.load(id).then((item) => { + if (item) return item; + return Folder.load(id); + }); + } + + static toFriendlyString_format(propName, propValue) { + if (['created_time', 'updated_time'].indexOf(propName) >= 0) { + if (!propValue) return ''; + propValue = moment.unix(propValue).utc().format('YYYY-MM-DD HH:mm:ss') + 'Z'; + } else if (propValue === null || propValue === undefined) { + propValue = ''; + } + + return propValue; + } + + static fromFriendlyString_format(propName, propValue) { + if (propName == 'type') return propValue; + + if (['created_time', 'updated_time'].indexOf(propName) >= 0) { + if (!propValue) return 0; + propValue = moment(propValue, 'YYYY-MM-DD HH:mm:ssZ').unix(); + } else { + propValue = Database.formatValue(this.fieldType(propName), propValue); + } + + return propValue; + } + + static toFriendlyString(item, type = null, shownKeys = null) { + let output = []; + + output.push(item.title); + output.push(''); + output.push(type == 'note' ? item.body : ''); + output.push(''); + for (let i = 0; i < shownKeys.length; i++) { + let v = item[shownKeys[i]]; + v = this.toFriendlyString_format(shownKeys[i], v); + output.push(shownKeys[i] + ': ' + v); + } + + output.push('type: ' + type); + + return output.join("\n"); + } + + static fromFriendlyString(content) { + let lines = content.split("\n"); + let output = {}; + let state = 'readingProps'; + let body = []; + for (let i = lines.length - 1; i >= 0; i--) { + let line = lines[i]; + + if (state == 'readingProps') { + line = line.trim(); + + if (line == '') { + state = 'readingBody'; + continue; + } + + let p = line.indexOf(':'); + if (p < 0) throw new Error('Invalid property format: ' + line + ": " + content); + let key = line.substr(0, p).trim(); + let value = line.substr(p + 1).trim(); + output[key] = this.fromFriendlyString_format(key, value); + } else if (state == 'readingBody') { + body.splice(0, 0, line); + } + } + + if (body.length < 3) throw new Error('Invalid body size: ' + body.length + ': ' + content); + + let title = body.splice(0, 2); + output.title = title[0]; + if (output.type == 'note') output.body = body.join("\n"); + + return output; + } + +} + +export { BaseItem }; \ No newline at end of file diff --git a/ReactNativeClient/src/models/folder.js b/ReactNativeClient/src/models/folder.js index 8ae388c4b..f41b8e817 100644 --- a/ReactNativeClient/src/models/folder.js +++ b/ReactNativeClient/src/models/folder.js @@ -5,57 +5,16 @@ import { Note } from 'src/models/note.js'; import { folderItemFilename } from 'src/string-utils.js' import { _ } from 'src/locale.js'; import moment from 'moment'; +import { BaseItem } from 'src/models/base-item.js'; -class Folder extends BaseModel { +class Folder extends BaseItem { static tableName() { return 'folders'; } - static filename(folder) { - return folderItemFilename(folder); - } - - static systemPath(parent, 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() { - return true; + return super.toFriendlyString(folder, 'folder', ['id', 'created_time', 'updated_time']); } static itemType() { diff --git a/ReactNativeClient/src/models/note.js b/ReactNativeClient/src/models/note.js index 6385ddef0..19367f88d 100644 --- a/ReactNativeClient/src/models/note.js +++ b/ReactNativeClient/src/models/note.js @@ -3,68 +3,17 @@ import { Log } from 'src/log.js'; import { Folder } from 'src/models/folder.js'; import { Geolocation } from 'src/geolocation.js'; import { folderItemFilename } from 'src/string-utils.js' +import { BaseItem } from 'src/models/base-item.js'; import moment from 'moment'; -class Note extends BaseModel { +class Note extends BaseItem { static tableName() { return 'notes'; } - 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; - } - - static toFriendlyString(note) { - let shownKeys = ["author", "longitude", "latitude", "is_todo", "todo_due", "todo_completed", 'created_time', 'updated_time']; - let output = []; - - output.push(note.title); - output.push(""); - output.push(note.body); - output.push(''); - for (let i = 0; i < shownKeys.length; i++) { - let v = note[shownKeys[i]]; - v = this.toFriendlyString_format(shownKeys[i], v); - output.push(shownKeys[i] + ': ' + v); - } - - return output.join("\n"); - } - - // static fromFriendlyString(item) { - // let lines = []; - // // mynote - - // // abcdefg\nsecond line\n\nline after two newline - - // // author: - // // longitude: -3.4596633911132812 - // // latitude: 48.73219093634444 - // // is_todo: 0 - // // todo_due: 0 - // // todo_completed: 0 - // // created_time: 2017-06-12 05:02:38 - // // updated_time: 2017-06-12 05:02:38 - // } - - static filename(note) { - return folderItemFilename(note) + '.md'; - } - - static systemPath(parentFolder, note) { - return Folder.systemPath(null, parentFolder) + '/' + this.filename(note); - } - - static useUuid() { - return true; + static toFriendlyString(note, type = null, shownKeys = null) { + return super.toFriendlyString(note, 'note', ["author", "longitude", "latitude", "is_todo", "todo_due", "todo_completed", 'created_time', 'updated_time', 'id', 'parent_id']); } static itemType() { diff --git a/ReactNativeClient/src/services/note-folder-service.js b/ReactNativeClient/src/services/note-folder-service.js index f6d151c37..bb147e14c 100644 --- a/ReactNativeClient/src/services/note-folder-service.js +++ b/ReactNativeClient/src/services/note-folder-service.js @@ -20,6 +20,12 @@ class NoteFolderService extends BaseService { } } + // console.info(item); + // console.info(oldItem); + // console.info('DIFF', diff); + // return Promise.resolve(); + + let ItemClass = null; if (type == 'note') { ItemClass = Note; diff --git a/ReactNativeClient/src/synchronizer.js b/ReactNativeClient/src/synchronizer.js index 96937fc43..492d0e582 100644 --- a/ReactNativeClient/src/synchronizer.js +++ b/ReactNativeClient/src/synchronizer.js @@ -3,6 +3,7 @@ import { Setting } from 'src/models/setting.js'; import { Change } from 'src/models/change.js'; import { Folder } from 'src/models/folder.js'; import { Note } from 'src/models/note.js'; +import { BaseItem } from 'src/models/base-item.js'; import { BaseModel } from 'src/base-model.js'; import { promiseChain } from 'src/promise-utils.js'; import { NoteFolderService } from 'src/services/note-folder-service.js'; @@ -69,11 +70,11 @@ class Synchronizer { moveConflict(item) { // No need to handle folder conflicts - if (item.isDir) return Promise.resolve(); + if (item.type == 'folder') return Promise.resolve(); return this.conflictDir().then((conflictDirPath) => { let p = path.basename(item.path).split('.'); - let pos = item.isDir ? p.length - 1 : p.length - 2; + let pos = item.type == 'folder' ? p.length - 1 : p.length - 2; p.splice(pos, 0, moment().format('YYYYMMDDThhmmss')); let newPath = p.join('.'); return this.api().move(item.path, conflictDirPath + '/' + newPath); @@ -106,34 +107,35 @@ class Synchronizer { } dbItemToSyncItem(dbItem) { - let p = Promise.resolve(null); + if (!dbItem) return null; + let itemType = BaseModel.identifyItemType(dbItem); - let ItemClass = null; - if (itemType == BaseModel.ITEM_TYPE_NOTE) { - ItemClass = Note; - p = Folder.load(dbItem.parent_id); - } else { - ItemClass = Folder; - } + return { + type: itemType == BaseModel.ITEM_TYPE_FOLDER ? 'folder' : 'note', + path: Folder.systemPath(dbItem), + syncTime: dbItem.sync_time, + updatedTime: dbItem.updated_time, + dbItem: dbItem, + }; + } - return p.then((dbParent) => { - let path = ItemClass.systemPath(dbParent, dbItem); - return { - isDir: itemType == BaseModel.ITEM_TYPE_FOLDER, - path: path, - syncTime: dbItem.sync_time, - updatedTime: dbItem.updated_time, - dbParent: dbParent, - dbItem: dbItem, - }; - }); + remoteItemToSyncItem(remoteItem) { + if (!remoteItem) return null; + + return { + type: remoteItem.content.type, + path: remoteItem.path, + syncTime: 0, + updatedTime: remoteItem.updatedTime, + remoteItem: remoteItem, + }; } syncAction(localItem, remoteItem, deletedLocalPaths) { let output = this.syncActions(localItem ? [localItem] : [], remoteItem ? [remoteItem] : [], deletedLocalPaths); - if (output.length !== 1) throw new Error('Invalid number of actions returned'); - return output[0]; + if (output.length > 1) throw new Error('Invalid number of actions returned'); + return output.length ? output[0] : null; } // Assumption: it's not possible to, for example, have a directory one the dest @@ -141,13 +143,16 @@ class Synchronizer { // file and directory names are UUID so should be unique. // Each item must have these properties: // - path - // - isDir + // - type // - syncTime // - updatedTime syncActions(localItems, remoteItems, deletedLocalPaths) { let output = []; let donePaths = []; + // console.info('=================================================='); + // console.info(localItems, remoteItems); + for (let i = 0; i < localItems.length; i++) { let local = localItems[i]; let remote = this.itemByPath(remoteItems, local.path); @@ -177,7 +182,7 @@ class Synchronizer { action.dest = 'remote'; } else { action.type = 'conflict'; - if (local.isDir) { + if (local.type == 'folder') { // For folders, currently we don't completely handle conflicts, we just // we just update the local dir (.folder metadata file) with the remote // version. It means the local version is lost but shouldn't be a big deal @@ -229,334 +234,12 @@ class Synchronizer { output.push(action); } + // console.info('-----------------------------------------'); + // console.info(output); + return output; } - processState_uploadChanges() { - let remoteFiles = []; - let processedChangeIds = []; - return this.api().list('', true).then((items) => { - remoteFiles = items; - return Change.all(); - }).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(() => { - let p = null; - - let ItemClass = null; - if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) { - ItemClass = Folder; - } else if (c.item_type == BaseModel.ITEM_TYPE_NOTE) { - ItemClass = Note; - } - - if (c.type == Change.TYPE_NOOP) { - p = Promise.resolve(); - } else if (c.type == Change.TYPE_CREATE) { - p = this.loadParentAndItem(c).then((result) => { - 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(parent, item); - - let remoteFile = this.remoteFileByPath(remoteFiles, path); - let p = null; - if (remoteFile) { - p = this.moveConflict(remoteFile); - } else { - p = Promise.resolve(); - } - - return p.then(() => { - if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) { - return this.api().mkdir(path).then(() => { - return this.api().put(Folder.systemMetadataPath(parent, item), Folder.toFriendlyString(item)); - }).then(() => { - return this.api().setTimestamp(Folder.systemMetadataPath(parent, item), item.updated_time); - }); - } else { - return this.api().put(path, Note.toFriendlyString(item)).then(() => { - return this.api().setTimestamp(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)); - } - }); - }); - } - - // TODO: handle DELETE - - return p.then(() => { - processedChangeIds = processedChangeIds.concat(c.ids); - }).catch((error) => { - Log.warn('Failed applying changes', c.ids, error); - // This is fine - trying to apply changes to an object that has been deleted - // if (error.type == 'NotFoundException') { - // processedChangeIds = processedChangeIds.concat(c.ids); - // } else { - // throw error; - // } - }); - }); - } - - return promiseChain(chain); - // }).then(() => { - // console.info(remoteFiles); - // for (let i = 0; i < remoteFiles.length; i++) { - // const remoteFile = remoteFiles[i]; - - // } - }).catch((error) => { - Log.error('Synchronization was interrupted due to an error:', error); - }).then(() => { - //Log.info('IDs to delete: ', processedChangeIds); - //return Change.deleteMultiple(processedChangeIds); - }).then(() => { - this.processState('downloadChanges'); - }); - - - // }).then(() => { - // return Change.all(); - // }).then((changes) => { - // let mergedChanges = Change.mergeChanges(changes); - // let chain = []; - // let processedChangeIds = []; - // for (let i = 0; i < mergedChanges.length; i++) { - // let c = mergedChanges[i]; - // chain.push(() => { - // let p = null; - - // let ItemClass = null; - // let path = null; - // if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) { - // ItemClass = Folder; - // path = 'folders'; - // } else if (c.item_type == BaseModel.ITEM_TYPE_NOTE) { - // ItemClass = Note; - // path = 'notes'; - // } - - // if (c.type == Change.TYPE_NOOP) { - // p = Promise.resolve(); - // } else if (c.type == Change.TYPE_CREATE) { - // p = this.loadParentAndItem(c).then((result) => { - // // let options = { - // // contents: Note.toFriendlyString(result.item), - // // path: Note.systemPath(result.parent, result.item), - // // mode: 'overwrite', - // // // client_modified: - // // }; - - // // return this.api().filesUpload(options).then((result) => { - // // console.info('DROPBOX', result); - // // }); - // }); - // // p = ItemClass.load(c.item_id).then((item) => { - - // // console.info(item); - // // let options = { - // // contents: Note.toFriendlyString(item), - // // path: Note.systemPath(item), - // // mode: 'overwrite', - // // // client_modified: - // // }; - - // // // console.info(options); - - // // //let content = Note.toFriendlyString(item); - // // //console.info(content); - - // // //console.info('SYNC', item); - // // //return this.api().put(path + '/' + item.id, null, item); - // // }); - // } else if (c.type == Change.TYPE_UPDATE) { - // p = ItemClass.load(c.item_id).then((item) => { - // //return this.api().patch(path + '/' + item.id, null, item); - // }); - // } else if (c.type == Change.TYPE_DELETE) { - // p = this.api().delete(path + '/' + c.item_id); - // } - - // return p.then(() => { - // processedChangeIds = processedChangeIds.concat(c.ids); - // }).catch((error) => { - // // Log.warn('Failed applying changes', c.ids, error.message, error.type); - // // This is fine - trying to apply changes to an object that has been deleted - // if (error.type == 'NotFoundException') { - // processedChangeIds = processedChangeIds.concat(c.ids); - // } else { - // throw error; - // } - // }); - // }); - // } - - // return promiseChain(chain).catch((error) => { - // Log.warn('Synchronization was interrupted due to an error:', error); - // }).then(() => { - // // Log.info('IDs to delete: ', processedChangeIds); - // // Change.deleteMultiple(processedChangeIds); - // }).then(() => { - // this.processState('downloadChanges'); - // }); - // }); - } - - processState_downloadChanges() { - // return this.api().list('', true).then((items) => { - // remoteFiles = items; - // return Change.all(); - - - // 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); - // }); - - // 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) { Log.info('Sync: processing: ' + state); this.state_ = state; @@ -564,37 +247,76 @@ class Synchronizer { if (state == 'uploadChanges') { return this.processState_uploadChanges(); } else if (state == 'downloadChanges') { - return this.processState('idle'); - //this.processState_downloadChanges(); + //return this.processState('idle'); + return this.processState_downloadChanges(); } else if (state == 'idle') { // Nothing + return Promise.resolve(); } else { throw new Error('Invalid state: ' . state); } } processSyncAction(action) { - // console.info(action); + console.info('Sync action: ', action); + //console.info('Sync action: ' + JSON.stringify(action)); + + if (!action) return Promise.resolve(); if (action.type == 'conflict') { } else { - let item = action[action.dest == 'local' ? 'remote' : 'local']; - let ItemClass = null; - if (item.isDir) { - ItemClass = Folder; - } else { - ItemClass = Note; - } - let path = ItemClass.systemPath(item.dbParent, item.dbItem); + let syncItem = action[action.dest == 'local' ? 'remote' : 'local']; + let path = syncItem.path; if (action.type == 'create') { if (action.dest == 'remote') { - if (item.isDir) { - return this.api().mkdir(path); + let content = null; + + if (syncItem.type == 'folder') { + content = Folder.toFriendlyString(syncItem.dbItem); } else { - return this.api().put(path, Note.toFriendlyString(item.dbItem)); + content = Note.toFriendlyString(syncItem.dbItem); } + + return this.api().put(path, content).then(() => { + return this.api().setTimestamp(path, syncItem.updatedTime); + }); + } else { + let dbItem = syncItem.remoteItem.content; + dbItem.sync_time = time.unix(); + if (syncItem.type == 'folder') { + return Folder.save(dbItem, { isNew: true }); + } else { + return Note.save(dbItem, { isNew: true }); + } + } + } + + if (action.type == 'update') { + if (action.dest == 'remote') { + // let content = null; + + // if (syncItem.type == 'folder') { + // content = Folder.toFriendlyString(syncItem.dbItem); + // } else { + // content = Note.toFriendlyString(syncItem.dbItem); + // } + + // return this.api().put(path, content).then(() => { + // return this.api().setTimestamp(path, syncItem.updatedTime); + // }); + } else { + let dbItem = syncItem.remoteItem.content; + dbItem.sync_time = time.unix(); + return NoteFolderService.save(syncItem.type, dbItem, action.local.dbItem); + // let dbItem = syncItem.remoteItem.content; + // dbItem.sync_time = time.unix(); + // if (syncItem.type == 'folder') { + // return Folder.save(dbItem, { isNew: true }); + // } else { + // return Note.save(dbItem, { isNew: true }); + // } } } } @@ -603,18 +325,14 @@ class Synchronizer { } processLocalItem(dbItem) { - //console.info(dbItem); - let localItem = null; - return this.dbItemToSyncItem(dbItem).then((r) => { - localItem = r; - return this.api().stat(localItem.path); - }).then((remoteItem) => { + let localItem = this.dbItemToSyncItem(dbItem); + + return this.api().stat(localItem.path).then((remoteItem) => { let action = this.syncAction(localItem, remoteItem, []); - //console.info(action); return this.processSyncAction(action); }).then(() => { dbItem.sync_time = time.unix(); - if (localItem.isDir) { + if (localItem.type == 'folder') { return Folder.save(dbItem); } else { return Note.save(dbItem); @@ -622,16 +340,21 @@ class Synchronizer { }); } - start() { - Log.info('Sync: start'); + processRemoteItem(remoteItem) { + let remoteSyncItem = null; + return this.api().get(remoteItem.path).then((content) => { + remoteItem.content = Note.fromFriendlyString(content); + remoteSyncItem = this.remoteItemToSyncItem(remoteItem); + return BaseItem.loadItemByPath(remoteItem.path); + }).then((dbItem) => { + let localSyncItem = this.dbItemToSyncItem(dbItem); + let action = this.syncAction(localSyncItem, remoteSyncItem, []); + return this.processSyncAction(action); + }); + } - if (this.state() != 'idle') { - return Promise.reject('Cannot start synchronizer because synchronization already in progress. State: ' + this.state()); - } - - this.state_ = 'started'; - - return this.api().listDirectories().then((items) => { + processState_uploadChanges() { + return new Promise((resolve, reject) => { var context = null; let limit = 2; let finishedReading = false; @@ -645,21 +368,17 @@ class Synchronizer { let chain = []; for (let i = 0; i < result.items.length; i++) { let item = result.items[i]; - console.info(JSON.stringify(item)); chain.push(() => { - //return Promise.resolve(); return this.processLocalItem(item); }); } return promiseChain(chain).then(() => { - console.info('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'); if (!context.hasMore) finishedReading = true; isReading = false; }); }).catch((error) => { - console.error(error); - throw error; + rejec(error); }); } @@ -667,25 +386,44 @@ class Synchronizer { if (isReading) return; if (finishedReading) { clearInterval(iid); + resolve(); return; } readItems(); }, 100); - }).then(() => { - this.state_ = 'idle'; + //console.info('DOWNLOAD DISABLED'); + return this.processState('downloadChanges'); }); + } - //return NoteFolderService.itemsThatNeedSync + processState_downloadChanges() { + return this.api().list().then((items) => { + let chain = []; + for (let i = 0; i < items.length; i++) { + chain.push(() => { + return this.processRemoteItem(items[i]); + }); + } + return promiseChain(chain); + }); + } - + start() { + Log.info('Sync: start'); + + if (this.state() != 'idle') { + return Promise.reject('Cannot start synchronizer because synchronization already in progress. State: ' + this.state()); + } + + this.state_ = 'started'; // if (!this.api().session()) { // Log.info("Sync: cannot start synchronizer because user is not logged in."); // return; // } - //return this.processState('uploadChanges'); + return this.processState('uploadChanges'); } diff --git a/ReactNativeClient/src/synchronizer_old.js b/ReactNativeClient/src/synchronizer_old.js index ef827f080..507d5be6b 100644 --- a/ReactNativeClient/src/synchronizer_old.js +++ b/ReactNativeClient/src/synchronizer_old.js @@ -30,7 +30,6 @@ class Synchronizer { if (change.item_type == BaseModel.ITEM_TYPE_NOTE) { return Note.load(change.item_id).then((note) => { return Folder.load(note.parent_id).then((folder) => { - console.info('xxxxxxxxx',note); return Promise.resolve({ parent: folder, item: note }); }); });