diff --git a/CliClient/app/cmd.js b/CliClient/app/cmd.js index 63e6e1040..7565a4134 100644 --- a/CliClient/app/cmd.js +++ b/CliClient/app/cmd.js @@ -2,6 +2,7 @@ require('source-map-support').install(); import { FileApi } from 'src/file-api.js'; import { FileApiDriverLocal } from 'src/file-api-driver-local.js'; +import { FileApiDriverMemory } from 'src/file-api-driver-memory.js'; import { Database } from 'src/database.js'; import { DatabaseDriverNode } from 'src/database-driver-node.js'; import { BaseModel } from 'src/base-model.js'; @@ -14,356 +15,359 @@ import { sprintf } from 'sprintf-js'; import { _ } from 'src/locale.js'; import { NoteFolderService } from 'src/services/note-folder-service.js'; - -// name: 'f42b0e23f06948ee9dda3fcf1b1c4205/.folder.md', -// createdTime: 1497216952, -// updatedTime: 1497216952, -// createdTimeOrig: 2017-06-11T21:35:52.362Z, -// updatedTimeOrig: 2017-06-11T21:35:52.362Z, -// isDir: false - -// Sun, 11 Jun 2017 21:35:52 GMT - - -// import moment from 'moment'; - -// let m = moment('2017-06-11T21:35:52.362Z'); -// console.info(Math.round(m.toDate().getTime() / 1000)); - -// //let m = moment(time, 'YYYY-MM-DDTHH:mm:ss.SSSZ'); - -// // if (!m.isValid()) { -// // throw new Error('Invalid date: ' + time); -// // } -// // return Math.round(m.toDate().getTime() / 1000); - - - const vorpal = require('vorpal')(); let db = new Database(new DatabaseDriverNode()); db.setDebugEnabled(false); -let fileDriver = new FileApiDriverLocal(); -let fileApi = new FileApi('/home/laurent/Temp/TestImport', fileDriver); +// let fileDriver = new FileApiDriverLocal(); +// let fileApi = new FileApi('/home/laurent/Temp/TestImport', fileDriver); +// let synchronizer = new Synchronizer(db, fileApi); + + +let fileDriver = new FileApiDriverMemory(); +let fileApi = new FileApi('/root', fileDriver); let synchronizer = new Synchronizer(db, fileApi); - -// import moment from 'moment-timezone'; - -// console.info(moment.tz.guess()); - -db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => { - BaseModel.db_ = db; +fileApi.mkdir('test').then(() => { + return fileApi.mkdir('test2'); }).then(() => { - return Setting.load(); + return fileApi.put('test/un', 'abcd1111').then(fileApi.put('test/deux', 'abcd2222')); }).then(() => { - let commands = []; - let currentFolder = null; + return fileApi.list(); +}).then((items) => { + //console.info(items); +}).then(() => { + return fileApi.delete('test/un'); +}).then(() => { + return fileApi.get('test/deux').then((content) => { console.info(content); }); +}).then(() => { + return fileApi.list('test', true); +}).then((items) => { + console.info(items); +}).catch((error) => { + console.error(error); +}).then(() => { + process.exit(); +}); - function switchCurrentFolder(folder) { - currentFolder = folder; - updatePrompt(); - } - function promptString() { - let path = '~'; - if (currentFolder) { - path += '/' + currentFolder.title; - } - return 'joplin:' + path + '$ '; - } - function updatePrompt() { - vorpal.delimiter(promptString()); - } - // For now, to go around this issue: https://github.com/dthree/vorpal/issues/114 - function quotePromptArg(s) { - if (s.indexOf(' ') >= 0) { - return '"' + s + '"'; - } - return s; - } - function autocompleteFolders() { - return Folder.all().then((folders) => { - let output = []; - for (let i = 0; i < folders.length; i++) { - output.push(quotePromptArg(folders[i].title)); - } - output.push('..'); - output.push('.'); - return output; - }); - } - function autocompleteItems() { - let promise = null; - if (!currentFolder) { - promise = Folder.all(); - } else { - promise = Note.previews(currentFolder.id); - } - return promise.then((items) => { - let output = []; - for (let i = 0; i < items.length; i++) { - output.push(quotePromptArg(items[i].title)); - } - return output; - }); - } +// db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => { +// BaseModel.db_ = db; +// }).then(() => { +// return Setting.load(); +// }).then(() => { +// let commands = []; +// let currentFolder = null; - process.stdin.on('keypress', (_, key) => { - if (key && key.name === 'return') { - updatePrompt(); - } +// function switchCurrentFolder(folder) { +// currentFolder = folder; +// updatePrompt(); +// } - if (key.name === 'tab') { - vorpal.ui.imprint(); - vorpal.log(vorpal.ui.input()); - } - }); +// function promptString() { +// let path = '~'; +// if (currentFolder) { +// path += '/' + currentFolder.title; +// } +// return 'joplin:' + path + '$ '; +// } - commands.push({ - usage: 'cd ', - description: 'Moved to [list-title] - all further operations will happen within this list. Use `cd ..` to go back one level.', - action: function (args, end) { - let folderTitle = args['list-title']; +// function updatePrompt() { +// vorpal.delimiter(promptString()); +// } - if (folderTitle == '..') { - switchCurrentFolder(null); - end(); - return; - } +// // For now, to go around this issue: https://github.com/dthree/vorpal/issues/114 +// function quotePromptArg(s) { +// if (s.indexOf(' ') >= 0) { +// return '"' + s + '"'; +// } +// return s; +// } - if (folderTitle == '.') { - end(); - return; - } +// function autocompleteFolders() { +// return Folder.all().then((folders) => { +// let output = []; +// for (let i = 0; i < folders.length; i++) { +// output.push(quotePromptArg(folders[i].title)); +// } +// output.push('..'); +// output.push('.'); +// return output; +// }); +// } - Folder.loadByField('title', folderTitle).then((folder) => { - switchCurrentFolder(folder); - end(); - }); - }, - autocomplete: autocompleteFolders, - }); +// function autocompleteItems() { +// let promise = null; +// if (!currentFolder) { +// promise = Folder.all(); +// } else { +// promise = Note.previews(currentFolder.id); +// } - commands.push({ - usage: 'mklist ', - alias: 'mkdir', - description: 'Creates a new list', - action: function (args, end) { - NoteFolderService.save('folder', { title: args['list-title'] }).catch((error) => { - this.log(error); - }).then((folder) => { - switchCurrentFolder(folder); - end(); - }); - }, - }); +// return promise.then((items) => { +// let output = []; +// for (let i = 0; i < items.length; i++) { +// output.push(quotePromptArg(items[i].title)); +// } +// return output; +// }); +// } - commands.push({ - usage: 'mknote ', - alias: 'touch', - description: 'Creates a new note', - action: function (args, end) { - if (!currentFolder) { - this.log('Notes can only be created within a list.'); - end(); - return; - } +// process.stdin.on('keypress', (_, key) => { +// if (key && key.name === 'return') { +// updatePrompt(); +// } - let note = { - title: args['note-title'], - parent_id: currentFolder.id, - }; - NoteFolderService.save('note', note).catch((error) => { - this.log(error); - }).then((note) => { - end(); - }); - }, - }); +// if (key.name === 'tab') { +// vorpal.ui.imprint(); +// vorpal.log(vorpal.ui.input()); +// } +// }); - commands.push({ - usage: 'set [prop-value]', - description: 'Sets the given of the given item.', - action: function (args, end) { - let promise = null; - let title = args['item-title']; - let propName = args['prop-name']; - let propValue = args['prop-value']; - if (!propValue) propValue = ''; +// commands.push({ +// usage: 'cd ', +// description: 'Moved to [list-title] - all further operations will happen within this list. Use `cd ..` to go back one level.', +// action: function (args, end) { +// let folderTitle = args['list-title']; - if (!currentFolder) { - promise = Folder.loadByField('title', title); - } else { - promise = Folder.loadNoteByField(currentFolder.id, 'title', title); - } +// if (folderTitle == '..') { +// switchCurrentFolder(null); +// end(); +// return; +// } - promise.then((item) => { - if (!item) { - this.log(_('No item with title "%s" found.', title)); - end(); - return; - } +// if (folderTitle == '.') { +// end(); +// return; +// } - let newItem = Object.assign({}, item); - newItem[propName] = propValue; - let itemType = currentFolder ? 'note' : 'folder'; - return NoteFolderService.save(itemType, newItem, item); - }).catch((error) => { - this.log(error); - }).then(() => { - end(); - }); - }, - autocomplete: autocompleteItems, - }); +// Folder.loadByField('title', folderTitle).then((folder) => { +// switchCurrentFolder(folder); +// end(); +// }); +// }, +// autocomplete: autocompleteFolders, +// }); - commands.push({ - usage: 'cat ', - description: 'Displays the given item data.', - action: function (args, end) { - let title = args['item-title']; +// commands.push({ +// usage: 'mklist ', +// alias: 'mkdir', +// description: 'Creates a new list', +// action: function (args, end) { +// NoteFolderService.save('folder', { title: args['list-title'] }).catch((error) => { +// this.log(error); +// }).then((folder) => { +// switchCurrentFolder(folder); +// end(); +// }); +// }, +// }); - let promise = null; - if (!currentFolder) { - promise = Folder.loadByField('title', title); - } else { - promise = Folder.loadNoteByField(currentFolder.id, 'title', title); - } +// commands.push({ +// usage: 'mknote ', +// alias: 'touch', +// description: 'Creates a new note', +// action: function (args, end) { +// if (!currentFolder) { +// this.log('Notes can only be created within a list.'); +// end(); +// return; +// } - promise.then((item) => { - if (!item) { - this.log(_('No item with title "%s" found.', title)); - end(); - return; - } +// let note = { +// title: args['note-title'], +// parent_id: currentFolder.id, +// }; +// NoteFolderService.save('note', note).catch((error) => { +// this.log(error); +// }).then((note) => { +// end(); +// }); +// }, +// }); - if (!currentFolder) { - this.log(Folder.toFriendlyString(item)); - } else { - this.log(Note.toFriendlyString(item)); - } - }).catch((error) => { - this.log(error); - }).then(() => { - end(); - }); - }, - autocomplete: autocompleteItems, - }); +// commands.push({ +// usage: 'set [prop-value]', +// description: 'Sets the given of the given item.', +// action: function (args, end) { +// let promise = null; +// let title = args['item-title']; +// let propName = args['prop-name']; +// let propValue = args['prop-value']; +// if (!propValue) propValue = ''; - commands.push({ - usage: 'rm ', - description: 'Deletes the given item. For a list, all the notes within that list will be deleted.', - action: function (args, end) { - let title = args['item-title']; +// if (!currentFolder) { +// promise = Folder.loadByField('title', title); +// } else { +// promise = Folder.loadNoteByField(currentFolder.id, 'title', title); +// } - let promise = null; - let itemType = currentFolder ? 'note' : 'folder'; - if (itemType == 'folder') { - promise = Folder.loadByField('title', title); - } else { - promise = Folder.loadNoteByField(currentFolder.id, 'title', title); - } +// promise.then((item) => { +// if (!item) { +// this.log(_('No item with title "%s" found.', title)); +// end(); +// return; +// } - promise.then((item) => { - if (!item) { - this.log(_('No item with title "%s" found.', title)); - end(); - return; - } +// let newItem = Object.assign({}, item); +// newItem[propName] = propValue; +// let itemType = currentFolder ? 'note' : 'folder'; +// return NoteFolderService.save(itemType, newItem, item); +// }).catch((error) => { +// this.log(error); +// }).then(() => { +// end(); +// }); +// }, +// autocomplete: autocompleteItems, +// }); - if (itemType == 'folder') { - return Folder.delete(item.id); - } else { - return Note.delete(item.id); - } - }).catch((error) => { - this.log(error); - }).then(() => { - end(); - }); - }, - autocomplete: autocompleteItems, - }); +// commands.push({ +// usage: 'cat ', +// description: 'Displays the given item data.', +// action: function (args, end) { +// let title = args['item-title']; - commands.push({ - usage: 'ls [list-title]', - alias: 'll', - description: 'Lists items in [list-title].', - action: function (args, end) { - let folderTitle = args['list-title']; +// let promise = null; +// if (!currentFolder) { +// promise = Folder.loadByField('title', title); +// } else { +// promise = Folder.loadNoteByField(currentFolder.id, 'title', title); +// } - let promise = null; +// promise.then((item) => { +// if (!item) { +// this.log(_('No item with title "%s" found.', title)); +// end(); +// return; +// } - if (folderTitle == '..') { - promise = Promise.resolve('root'); - } else if (folderTitle && folderTitle != '.') { - promise = Folder.loadByField('title', folderTitle); - } else if (currentFolder) { - promise = Promise.resolve(currentFolder); - } else { - promise = Promise.resolve('root'); - } +// if (!currentFolder) { +// this.log(Folder.toFriendlyString(item)); +// } else { +// this.log(Note.toFriendlyString(item)); +// } +// }).catch((error) => { +// this.log(error); +// }).then(() => { +// end(); +// }); +// }, +// autocomplete: autocompleteItems, +// }); - promise.then((folder) => { - let p = null - let postfix = ''; - if (folder === 'root') { - p = Folder.all(); - postfix = '/'; - } else if (!folder) { - throw new Error(_('Unknown list: "%s"', folderTitle)); - } else { - p = Note.previews(folder.id); - } +// commands.push({ +// usage: 'rm ', +// description: 'Deletes the given item. For a list, all the notes within that list will be deleted.', +// action: function (args, end) { +// let title = args['item-title']; - return p.then((previews) => { - for (let i = 0; i < previews.length; i++) { - this.log(previews[i].title + postfix); - } - }); - }).catch((error) => { - this.log(error); - }).then(() => { - end(); - }); - }, - autocomplete: autocompleteFolders, - }); +// let promise = null; +// let itemType = currentFolder ? 'note' : 'folder'; +// if (itemType == 'folder') { +// promise = Folder.loadByField('title', title); +// } else { +// promise = Folder.loadNoteByField(currentFolder.id, 'title', title); +// } - commands.push({ - usage: 'sync', - description: 'Synchronizes with remote storage.', - action: function (args, end) { - synchronizer.start().catch((error) => { - console.error(error); - }).then(() => { - end(); - }); - }, - }); +// promise.then((item) => { +// if (!item) { +// this.log(_('No item with title "%s" found.', title)); +// end(); +// return; +// } - for (let i = 0; i < commands.length; i++) { - let c = commands[i]; - let o = vorpal.command(c.usage, c.description); - if (c.alias) { - o.alias(c.alias); - } - if (c.autocomplete) { - o.autocomplete({ - data: c.autocomplete, - }); - } - o.action(c.action); - } +// if (itemType == 'folder') { +// return Folder.delete(item.id); +// } else { +// return Note.delete(item.id); +// } +// }).catch((error) => { +// this.log(error); +// }).then(() => { +// end(); +// }); +// }, +// autocomplete: autocompleteItems, +// }); - vorpal.delimiter(promptString()).show(); -}); \ No newline at end of file +// commands.push({ +// usage: 'ls [list-title]', +// alias: 'll', +// description: 'Lists items in [list-title].', +// action: function (args, end) { +// let folderTitle = args['list-title']; + +// let promise = null; + +// if (folderTitle == '..') { +// promise = Promise.resolve('root'); +// } else if (folderTitle && folderTitle != '.') { +// promise = Folder.loadByField('title', folderTitle); +// } else if (currentFolder) { +// promise = Promise.resolve(currentFolder); +// } else { +// promise = Promise.resolve('root'); +// } + +// promise.then((folder) => { +// let p = null +// let postfix = ''; +// if (folder === 'root') { +// p = Folder.all(); +// postfix = '/'; +// } else if (!folder) { +// throw new Error(_('Unknown list: "%s"', folderTitle)); +// } else { +// p = Note.previews(folder.id); +// } + +// return p.then((previews) => { +// for (let i = 0; i < previews.length; i++) { +// this.log(previews[i].title + postfix); +// } +// }); +// }).catch((error) => { +// this.log(error); +// }).then(() => { +// end(); +// }); +// }, +// autocomplete: autocompleteFolders, +// }); + +// commands.push({ +// usage: 'sync', +// description: 'Synchronizes with remote storage.', +// action: function (args, end) { +// synchronizer.start().catch((error) => { +// console.error(error); +// }).then(() => { +// end(); +// }); +// }, +// }); + +// for (let i = 0; i < commands.length; i++) { +// let c = commands[i]; +// let o = vorpal.command(c.usage, c.description); +// if (c.alias) { +// o.alias(c.alias); +// } +// if (c.autocomplete) { +// o.autocomplete({ +// data: c.autocomplete, +// }); +// } +// o.action(c.action); +// } + +// vorpal.delimiter(promptString()).show(); +// }); \ No newline at end of file diff --git a/ReactNativeClient/src/file-api-driver-local.js b/ReactNativeClient/src/file-api-driver-local.js index 80cac7d97..f8dc75869 100644 --- a/ReactNativeClient/src/file-api-driver-local.js +++ b/ReactNativeClient/src/file-api-driver-local.js @@ -36,7 +36,7 @@ class FileApiDriverLocal { }; } - setFileTimestamp(path, timestamp) { + setTimestamp(path, timestamp) { return new Promise((resolve, reject) => { fs.utimes(path, timestamp, timestamp, (error) => { if (error) { diff --git a/ReactNativeClient/src/file-api-driver-memory.js b/ReactNativeClient/src/file-api-driver-memory.js new file mode 100644 index 000000000..404a3bedd --- /dev/null +++ b/ReactNativeClient/src/file-api-driver-memory.js @@ -0,0 +1,109 @@ +class FileApiDriverMemory { + + constructor(baseDir) { + this.items_ = []; + } + + currentTimestamp() { + return Math.round((new Date()).getTime() / 1000); + } + + itemIndexByPath(path) { + for (let i = 0; i < this.items_.length; i++) { + if (this.items_[i].path == path) return i; + } + return -1; + } + + itemByPath(path) { + let index = this.itemIndexByPath(path); + return index < 0 ? null : this.items_[index]; + } + + newItem(path, isDir = false) { + return { + path: path, + isDir: isDir, + updatedTime: this.currentTimestamp(), + createdTime: this.currentTimestamp(), + content: '', + }; + } + + stat(path) { + let item = this.itemIndexByPath(path); + if (!item) return Promise.reject(new Error('File not found: ' + path)); + return Promise.resolve(item); + } + + setTimestamp(path, timestamp) { + let item = this.itemIndexByPath(path); + if (!item) return Promise.reject(new Error('File not found: ' + path)); + item.updatedTime = timestamp; + return Promise.resolve(); + } + + list(path) { + let output = []; + + for (let i = 0; i < this.items_.length; i++) { + let item = this.items_[i]; + if (item.path == path) continue; + if (item.path.indexOf(path + '/') === 0) { + let s = item.path.substr(path.length + 1); + if (s.split('/').length === 1) { + let it = Object.assign({}, item); + it.path = it.path.substr(path.length + 1); + output.push(it); + } + } + } + + return Promise.resolve(output); + } + + get(path) { + let item = this.itemByPath(path); + if (!item) return Promise.reject(new Error('File not found: ' + path)); + if (item.isDir) return Promise.reject(new Error(path + ' is a directory, not a file')); + return Promise.resolve(item.content); + } + + mkdir(path) { + let index = this.itemIndexByPath(path); + if (index >= 0) return Promise.resolve(); + this.items_.push(this.newItem(path, true)); + return Promise.resolve(); + } + + put(path, content) { + let index = this.itemIndexByPath(path); + if (index < 0) { + let item = this.newItem(path, false); + item.content = content; + this.items_.push(item); + } else { + this.items_[index].content = content; + } + return Promise.resolve(); + } + + delete(path) { + let index = this.itemIndexByPath(path); + if (index >= 0) { + this.items_.splice(index, 1); + } + return Promise.resolve(); + } + + move(oldPath, newPath) { + let sourceItem = this.itemByPath(oldPath); + if (!sourceItem) return Promise.reject(new Error('Path not found: ' + oldPath)); + this.delete(newPath); // Overwrite if newPath already exists + sourceItem.path = newPath; + return Promise.resolve(); + } + +} + +export { FileApiDriverMemory }; \ No newline at end of file diff --git a/ReactNativeClient/src/file-api.js b/ReactNativeClient/src/file-api.js index 8c2ed589e..5cc8d247f 100644 --- a/ReactNativeClient/src/file-api.js +++ b/ReactNativeClient/src/file-api.js @@ -7,8 +7,15 @@ class FileApi { this.driver_ = driver; } - list(path, recursive = false) { - return this.driver_.list(this.baseDir_ + '/' + path, recursive).then((items) => { + fullPath_(path) { + let output = this.baseDir_; + if (path != '') output += '/' + path; + return output; + } + + list(path = '', recursive = false) { + let fullPath = this.fullPath_(path); + return this.driver_.list(fullPath, recursive).then((items) => { if (recursive) { let chain = []; for (let i = 0; i < items.length; i++) { @@ -16,10 +23,10 @@ class FileApi { if (!item.isDir) continue; chain.push(() => { - return this.list(path + '/' + item.name, true).then((children) => { + return this.list(item.path, true).then((children) => { for (let j = 0; j < children.length; j++) { let md = children[j]; - md.name = item.name + '/' + md.name; + md.path = item.path + '/' + md.path; items.push(md); } }); @@ -35,28 +42,28 @@ class FileApi { }); } - setFileTimestamp(path, timestamp) { - return this.driver_.setFileTimestamp(this.baseDir_ + '/' + path, timestamp); + setTimestamp(path, timestamp) { + return this.driver_.setTimestamp(this.fullPath_(path), timestamp); } mkdir(path) { - return this.driver_.mkdir(this.baseDir_ + '/' + path); + return this.driver_.mkdir(this.fullPath_(path)); } get(path) { - return this.driver_.get(this.baseDir_ + '/' + path); + return this.driver_.get(this.fullPath_(path)); } put(path, content) { - return this.driver_.put(this.baseDir_ + '/' + path, content); + return this.driver_.put(this.fullPath_(path), content); } delete(path) { - return this.driver_.delete(this.baseDir_ + '/' + path); + return this.driver_.delete(this.fullPath_(path)); } move(oldPath, newPath) { - return this.driver_.move(this.baseDir_ + '/' + oldPath, this.baseDir_ + '/' + newPath); + return this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath)); } } diff --git a/ReactNativeClient/src/synchronizer.js b/ReactNativeClient/src/synchronizer.js index 62868377e..5e5c10d64 100644 --- a/ReactNativeClient/src/synchronizer.js +++ b/ReactNativeClient/src/synchronizer.js @@ -122,11 +122,11 @@ class Synchronizer { 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); + return this.api().setTimestamp(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); + return this.api().setTimestamp(path, item.updated_time); }); } });