You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-10-06 22:17:10 +02:00
Added support for local sync and fixed sync bug
This commit is contained in:
@@ -6,6 +6,7 @@ require('babel-plugin-transform-runtime');
|
|||||||
import { FileApi } from 'lib/file-api.js';
|
import { FileApi } from 'lib/file-api.js';
|
||||||
import { FileApiDriverOneDrive } from 'lib/file-api-driver-onedrive.js';
|
import { FileApiDriverOneDrive } from 'lib/file-api-driver-onedrive.js';
|
||||||
import { FileApiDriverMemory } from 'lib/file-api-driver-memory.js';
|
import { FileApiDriverMemory } from 'lib/file-api-driver-memory.js';
|
||||||
|
import { FileApiDriverLocal } from 'lib/file-api-driver-local.js';
|
||||||
import { Database } from 'lib/database.js';
|
import { Database } from 'lib/database.js';
|
||||||
import { DatabaseDriverNode } from 'lib/database-driver-node.js';
|
import { DatabaseDriverNode } from 'lib/database-driver-node.js';
|
||||||
import { BaseModel } from 'lib/base-model.js';
|
import { BaseModel } from 'lib/base-model.js';
|
||||||
@@ -29,11 +30,15 @@ process.on('unhandledRejection', (reason, p) => {
|
|||||||
|
|
||||||
const packageJson = require('./package.json');
|
const packageJson = require('./package.json');
|
||||||
|
|
||||||
let profileDir = os.homedir() + '/.config/' + Setting.value('appName');
|
let initArgs = {
|
||||||
|
profileDir: null,
|
||||||
|
syncTarget: null,
|
||||||
|
}
|
||||||
|
|
||||||
let currentFolder = null;
|
let currentFolder = null;
|
||||||
let commands = [];
|
let commands = [];
|
||||||
let database_ = null;
|
let database_ = null;
|
||||||
let synchronizer_ = null;
|
let synchronizers_ = {};
|
||||||
let logger = new Logger();
|
let logger = new Logger();
|
||||||
let dbLogger = new Logger();
|
let dbLogger = new Logger();
|
||||||
let syncLogger = new Logger();
|
let syncLogger = new Logger();
|
||||||
@@ -41,11 +46,18 @@ let syncLogger = new Logger();
|
|||||||
commands.push({
|
commands.push({
|
||||||
usage: 'root',
|
usage: 'root',
|
||||||
options: [
|
options: [
|
||||||
['-p, --profile <filePath>', 'Sets the profile path directory.'],
|
['--profile <filePath>', 'Sets the profile path directory.'],
|
||||||
|
['--sync-target <target>', 'Sets the sync target.'],
|
||||||
],
|
],
|
||||||
action: function(args, end) {
|
action: function(args, end) {
|
||||||
let p = args.profile || args.p;
|
if (args.profile) {
|
||||||
if (p) profileDir = p;
|
initArgs.profileDir = args.profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args['sync-target']) {
|
||||||
|
initArgs.syncTarget = args['sync-target'];
|
||||||
|
}
|
||||||
|
|
||||||
end();
|
end();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -312,11 +324,11 @@ commands.push({
|
|||||||
usage: 'sync',
|
usage: 'sync',
|
||||||
description: 'Synchronizes with remote storage.',
|
description: 'Synchronizes with remote storage.',
|
||||||
action: function(args, end) {
|
action: function(args, end) {
|
||||||
//synchronizer('onedrive').then((s) => {
|
this.log(_('Synchronization target: %s', Setting.value('sync.target')));
|
||||||
synchronizer('memory').then((s) => {
|
synchronizer(Setting.value('sync.target')).then((s) => {
|
||||||
return s.start();
|
return s.start();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
logger.error(error);
|
this.log(error);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
end();
|
end();
|
||||||
});
|
});
|
||||||
@@ -416,12 +428,12 @@ function execCommand(name, args) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function synchronizer(remoteBackend) {
|
async function synchronizer(syncTarget) {
|
||||||
if (synchronizer_) return synchronizer_;
|
if (synchronizers_[syncTarget]) return synchronizers_[syncTarget];
|
||||||
|
|
||||||
let fileApi = null;
|
let fileApi = null;
|
||||||
|
|
||||||
if (remoteBackend == 'onedrive') {
|
if (syncTarget == 'onedrive') {
|
||||||
const CLIENT_ID = 'e09fc0de-c958-424f-83a2-e56a721d331b';
|
const CLIENT_ID = 'e09fc0de-c958-424f-83a2-e56a721d331b';
|
||||||
const CLIENT_SECRET = 'JA3cwsqSGHFtjMwd5XoF5L5';
|
const CLIENT_SECRET = 'JA3cwsqSGHFtjMwd5XoF5L5';
|
||||||
|
|
||||||
@@ -431,7 +443,7 @@ async function synchronizer(remoteBackend) {
|
|||||||
if (auth) {
|
if (auth) {
|
||||||
auth = JSON.parse(auth);
|
auth = JSON.parse(auth);
|
||||||
} else {
|
} else {
|
||||||
auth = await driver.api().oauthDance();
|
auth = await driver.api().oauthDance(vorpal);
|
||||||
Setting.setValue('sync.onedrive.auth', JSON.stringify(auth));
|
Setting.setValue('sync.onedrive.auth', JSON.stringify(auth));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,24 +456,25 @@ async function synchronizer(remoteBackend) {
|
|||||||
logger.info('App dir: ' + appDir);
|
logger.info('App dir: ' + appDir);
|
||||||
fileApi = new FileApi(appDir, driver);
|
fileApi = new FileApi(appDir, driver);
|
||||||
fileApi.setLogger(logger);
|
fileApi.setLogger(logger);
|
||||||
} else if (remoteBackend == 'memory') {
|
} else if (syncTarget == 'memory') {
|
||||||
let driver = new FileApiDriverMemory();
|
fileApi = new FileApi('joplin', new FileApiDriverMemory());
|
||||||
fileApi = new FileApi('joplin', driver);
|
fileApi.setLogger(logger);
|
||||||
|
} else if (syncTarget == 'local') {
|
||||||
|
let syncDir = Setting.value('profileDir') + '/sync';
|
||||||
|
vorpal.log(syncDir);
|
||||||
|
await fs.mkdirp(syncDir, 0o755);
|
||||||
|
fileApi = new FileApi(syncDir, new FileApiDriverLocal());
|
||||||
fileApi.setLogger(logger);
|
fileApi.setLogger(logger);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unknown backend: ' + remoteBackend);
|
throw new Error('Unknown backend: ' + syncTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronizer_ = new Synchronizer(database_, fileApi);
|
synchronizers_[syncTarget] = new Synchronizer(database_, fileApi);
|
||||||
synchronizer_.setLogger(syncLogger);
|
synchronizers_[syncTarget].setLogger(syncLogger);
|
||||||
|
|
||||||
return synchronizer_;
|
return synchronizers_[syncTarget];
|
||||||
}
|
}
|
||||||
|
|
||||||
// let s = await synchronizer('onedrive');
|
|
||||||
// await synchronizer_.start();
|
|
||||||
// return;
|
|
||||||
|
|
||||||
function switchCurrentFolder(folder) {
|
function switchCurrentFolder(folder) {
|
||||||
if (!folder) throw new Error(_('No active folder is defined.'));
|
if (!folder) throw new Error(_('No active folder is defined.'));
|
||||||
|
|
||||||
@@ -571,25 +584,6 @@ process.stdin.on('keypress', (_, key) => {
|
|||||||
const vorpal = require('vorpal')();
|
const vorpal = require('vorpal')();
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// console.info('DELETING ALL DATA');
|
|
||||||
// await db.exec('DELETE FROM notes');
|
|
||||||
// await db.exec('DELETE FROM changes');
|
|
||||||
// await db.exec('DELETE FROM folders');
|
|
||||||
// await db.exec('DELETE FROM resources');
|
|
||||||
// await db.exec('DELETE FROM deleted_items');
|
|
||||||
// await db.exec('DELETE FROM tags');
|
|
||||||
// await db.exec('DELETE FROM note_tags');
|
|
||||||
// let folder1 = await Folder.save({ title: 'test1' });
|
|
||||||
// let folder2 = await Folder.save({ title: 'test2' });
|
|
||||||
// await importEnex(folder1.id, '/mnt/c/Users/Laurent/Desktop/Laurent.enex');
|
|
||||||
// return;
|
|
||||||
|
|
||||||
|
|
||||||
// let testglob = await Note.glob('title', 'La *', {
|
|
||||||
// fields: ['title', 'updated_time'],
|
|
||||||
// });
|
|
||||||
// console.info(testglob);
|
|
||||||
|
|
||||||
for (let commandIndex = 0; commandIndex < commands.length; commandIndex++) {
|
for (let commandIndex = 0; commandIndex < commands.length; commandIndex++) {
|
||||||
let c = commands[commandIndex];
|
let c = commands[commandIndex];
|
||||||
if (c.usage == 'root') continue;
|
if (c.usage == 'root') continue;
|
||||||
@@ -618,6 +612,7 @@ async function main() {
|
|||||||
|
|
||||||
await handleStartArgs(process.argv);
|
await handleStartArgs(process.argv);
|
||||||
|
|
||||||
|
const profileDir = initArgs.profileDir ? initArgs.profileDir : os.homedir() + '/.config/' + Setting.value('appName');
|
||||||
const resourceDir = profileDir + '/resources';
|
const resourceDir = profileDir + '/resources';
|
||||||
|
|
||||||
Setting.setConstant('profileDir', profileDir);
|
Setting.setConstant('profileDir', profileDir);
|
||||||
@@ -644,6 +639,8 @@ async function main() {
|
|||||||
BaseModel.db_ = database_;
|
BaseModel.db_ = database_;
|
||||||
await Setting.load();
|
await Setting.load();
|
||||||
|
|
||||||
|
if (initArgs.syncTarget) Setting.setValue('sync.target', initArgs.syncTarget);
|
||||||
|
|
||||||
let activeFolderId = Setting.value('activeFolderId');
|
let activeFolderId = Setting.value('activeFolderId');
|
||||||
let activeFolder = null;
|
let activeFolder = null;
|
||||||
if (activeFolderId) activeFolder = await Folder.load(activeFolderId);
|
if (activeFolderId) activeFolder = await Folder.load(activeFolderId);
|
||||||
@@ -654,5 +651,6 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
console.error('Fatal error: ', error);
|
vorpal.log('Fatal error:');
|
||||||
|
vorpal.log(error);
|
||||||
});
|
});
|
@@ -2,4 +2,4 @@
|
|||||||
set -e
|
set -e
|
||||||
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
bash $CLIENT_DIR/build.sh
|
bash $CLIENT_DIR/build.sh
|
||||||
NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/Temp/TestNotes
|
NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/Temp/TestNotes --sync-target local
|
@@ -461,9 +461,10 @@ class Database {
|
|||||||
|
|
||||||
this.logger().info('Database is new - creating the schema...');
|
this.logger().info('Database is new - creating the schema...');
|
||||||
|
|
||||||
|
let now = time.unixMs();
|
||||||
let queries = this.wrapQueries(this.sqlStringToLines(structureSql));
|
let queries = this.wrapQueries(this.sqlStringToLines(structureSql));
|
||||||
queries.push(this.wrapQuery('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumId('settings', 'string') + '")'));
|
queries.push(this.wrapQuery('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumId('settings', 'string') + '")'));
|
||||||
queries.push(this.wrapQuery('INSERT INTO folders (`id`, `title`, `created_time`) VALUES ("' + uuid.create() + '", "' + _('Notebook') + '", ' + (new Date()).getTime() + ')'));
|
queries.push(this.wrapQuery('INSERT INTO folders (`id`, `title`, `created_time`, `updated_time`) VALUES ("' + uuid.create() + '", "' + _('Notebook') + '", ' + now + ', ' + now + ')'));
|
||||||
|
|
||||||
return this.transactionExecBatch(queries).then(() => {
|
return this.transactionExecBatch(queries).then(() => {
|
||||||
this.logger().info('Database schema created successfully');
|
this.logger().info('Database schema created successfully');
|
||||||
|
@@ -65,6 +65,7 @@ class FileApiDriverLocal {
|
|||||||
chain.push((output) => {
|
chain.push((output) => {
|
||||||
if (!output) output = [];
|
if (!output) output = [];
|
||||||
return this.stat(path + '/' + items[i]).then((stat) => {
|
return this.stat(path + '/' + items[i]).then((stat) => {
|
||||||
|
stat.path = items[i];
|
||||||
output.push(stat);
|
output.push(stat);
|
||||||
return output;
|
return output;
|
||||||
});
|
});
|
||||||
|
@@ -2,7 +2,7 @@ import { time } from 'lib/time-utils.js';
|
|||||||
|
|
||||||
class FileApiDriverMemory {
|
class FileApiDriverMemory {
|
||||||
|
|
||||||
constructor(baseDir) {
|
constructor() {
|
||||||
this.items_ = [];
|
this.items_ = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,6 +23,7 @@ class FileApi {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DRIVER MUST RETURN PATHS RELATIVE TO `path`
|
||||||
list(path = '', options = null) {
|
list(path = '', options = null) {
|
||||||
if (!options) options = {};
|
if (!options) options = {};
|
||||||
if (!('includeHidden' in options)) options.includeHidden = false;
|
if (!('includeHidden' in options)) options.includeHidden = false;
|
||||||
@@ -41,17 +42,17 @@ class FileApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimestamp(path, timestamp) {
|
setTimestamp(path, timestamp) {
|
||||||
this.logger().debug('setTimestamp ' + path);
|
this.logger().debug('setTimestamp ' + this.fullPath_(path));
|
||||||
return this.driver_.setTimestamp(this.fullPath_(path), timestamp);
|
return this.driver_.setTimestamp(this.fullPath_(path), timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdir(path) {
|
mkdir(path) {
|
||||||
this.logger().debug('mkdir ' + path);
|
this.logger().debug('mkdir ' + this.fullPath_(path));
|
||||||
return this.driver_.mkdir(this.fullPath_(path));
|
return this.driver_.mkdir(this.fullPath_(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
stat(path) {
|
stat(path) {
|
||||||
this.logger().debug('stat ' + path);
|
this.logger().debug('stat ' + this.fullPath_(path));
|
||||||
return this.driver_.stat(this.fullPath_(path)).then((output) => {
|
return this.driver_.stat(this.fullPath_(path)).then((output) => {
|
||||||
if (!output) return output;
|
if (!output) return output;
|
||||||
output.path = path;
|
output.path = path;
|
||||||
@@ -60,22 +61,22 @@ class FileApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(path) {
|
get(path) {
|
||||||
this.logger().debug('get ' + path);
|
this.logger().debug('get ' + this.fullPath_(path));
|
||||||
return this.driver_.get(this.fullPath_(path));
|
return this.driver_.get(this.fullPath_(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
put(path, content) {
|
put(path, content) {
|
||||||
this.logger().debug('put ' + path);
|
this.logger().debug('put ' + this.fullPath_(path));
|
||||||
return this.driver_.put(this.fullPath_(path), content);
|
return this.driver_.put(this.fullPath_(path), content);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(path) {
|
delete(path) {
|
||||||
this.logger().debug('delete ' + path);
|
this.logger().debug('delete ' + this.fullPath_(path));
|
||||||
return this.driver_.delete(this.fullPath_(path));
|
return this.driver_.delete(this.fullPath_(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
move(oldPath, newPath) {
|
move(oldPath, newPath) {
|
||||||
this.logger().debug('move ' + oldPath + ' => ' + newPath);
|
this.logger().debug('move ' + this.fullPath_(oldPath) + ' => ' + this.fullPath_(newPath));
|
||||||
return this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath));
|
return this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -72,6 +72,8 @@ class Logger {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.scheduleFileAppendQueueProcessing_();
|
this.scheduleFileAppendQueueProcessing_();
|
||||||
|
} else if (t.type == 'vorpal') {
|
||||||
|
t.vorpal.log(object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -145,10 +145,9 @@ class BaseItem extends BaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static itemsThatNeedSync(limit = 100) {
|
static itemsThatNeedSync(limit = 100) {
|
||||||
let conflictFolderId = Setting.value('sync.conflictFolderId');
|
return Folder.modelSelectAll('SELECT * FROM folders WHERE sync_time < updated_time LIMIT ' + limit).then((items) => {
|
||||||
return Folder.modelSelectAll('SELECT * FROM folders WHERE sync_time < updated_time AND id != ? LIMIT ' + limit, [conflictFolderId]).then((items) => {
|
|
||||||
if (items.length) return { hasMore: true, items: items };
|
if (items.length) return { hasMore: true, items: items };
|
||||||
return Note.modelSelectAll('SELECT * FROM notes WHERE sync_time < updated_time AND parent_id != ? LIMIT ' + limit, [conflictFolderId]).then((items) => {
|
return Note.modelSelectAll('SELECT * FROM notes WHERE sync_time < updated_time AND is_conflict = 0 LIMIT ' + limit).then((items) => {
|
||||||
return { hasMore: items.length >= limit, items: items };
|
return { hasMore: items.length >= limit, items: items };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -134,12 +134,8 @@ Setting.defaults_ = {
|
|||||||
'clientId': { value: '', type: 'string' },
|
'clientId': { value: '', type: 'string' },
|
||||||
'sessionId': { value: '', type: 'string' },
|
'sessionId': { value: '', type: 'string' },
|
||||||
'activeFolderId': { value: '', type: 'string' },
|
'activeFolderId': { value: '', type: 'string' },
|
||||||
'user.email': { value: '', type: 'string' },
|
|
||||||
'user.session': { value: '', type: 'string' },
|
|
||||||
'sync.lastRevId': { value: 0, type: 'int' }, // DEPRECATED
|
|
||||||
'sync.lastUpdateTime': { value: 0, type: 'int' },
|
|
||||||
'sync.conflictFolderId': { value: '', type: 'string' },
|
|
||||||
'sync.onedrive.auth': { value: '', type: 'string' },
|
'sync.onedrive.auth': { value: '', type: 'string' },
|
||||||
|
'sync.target': { value: 'onedrive', type: 'string' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Contains constants that are set by the application and
|
// Contains constants that are set by the application and
|
||||||
|
@@ -152,7 +152,9 @@ class OneDriveApi {
|
|||||||
this.dispatch('authRefreshed', this.auth_);
|
this.dispatch('authRefreshed', this.auth_);
|
||||||
}
|
}
|
||||||
|
|
||||||
async oauthDance() {
|
async oauthDance(targetConsole = null) {
|
||||||
|
if (targetConsole === null) targetConsole = console;
|
||||||
|
|
||||||
this.auth_ = null;
|
this.auth_ = null;
|
||||||
|
|
||||||
let ports = this.possibleOAuthDancePorts();
|
let ports = this.possibleOAuthDancePorts();
|
||||||
@@ -224,8 +226,9 @@ class OneDriveApi {
|
|||||||
|
|
||||||
enableServerDestroy(server);
|
enableServerDestroy(server);
|
||||||
|
|
||||||
console.info('Please open this URL in your browser to authentify the application:');
|
targetConsole.log('Please open this URL in your browser to authentify the application:');
|
||||||
console.info(authCodeUrl);
|
targetConsole.log('');
|
||||||
|
targetConsole.log(authCodeUrl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -225,6 +225,7 @@ class Synchronizer {
|
|||||||
|
|
||||||
let remoteIds = [];
|
let remoteIds = [];
|
||||||
let remotes = await this.api().list();
|
let remotes = await this.api().list();
|
||||||
|
|
||||||
for (let i = 0; i < remotes.length; i++) {
|
for (let i = 0; i < remotes.length; i++) {
|
||||||
let remote = remotes[i];
|
let remote = remotes[i];
|
||||||
let path = remote.path;
|
let path = remote.path;
|
||||||
@@ -249,7 +250,7 @@ class Synchronizer {
|
|||||||
|
|
||||||
if (action == 'createLocal' || action == 'updateLocal') {
|
if (action == 'createLocal' || action == 'updateLocal') {
|
||||||
let content = await this.api().get(path);
|
let content = await this.api().get(path);
|
||||||
if (!content) {
|
if (content === null) {
|
||||||
this.logger().warn('Remote has been deleted between now and the list() call? In that case it will be handled during the next sync: ' + path);
|
this.logger().warn('Remote has been deleted between now and the list() call? In that case it will be handled during the next sync: ' + path);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user