mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Various changes
This commit is contained in:
parent
5b9d50947c
commit
9cec2652e7
1
.gitignore
vendored
1
.gitignore
vendored
@ -30,3 +30,4 @@ sparse_test.php
|
||||
INFO.md
|
||||
/web/env.php
|
||||
sync_staging.sh
|
||||
*.swp
|
211
CliClient/app/cmd.js
Normal file
211
CliClient/app/cmd.js
Normal file
@ -0,0 +1,211 @@
|
||||
import { FileApi } from 'src/file-api.js';
|
||||
import { FileApiDriverLocal } from 'src/file-api-driver-local.js';
|
||||
import { Database } from 'src/database.js';
|
||||
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 { Synchronizer } from 'src/synchronizer.js';
|
||||
import { uuid } from 'src/uuid.js';
|
||||
import { sprintf } from 'sprintf-js';
|
||||
import { _ } from 'src/locale.js';
|
||||
import { NoteFolderService } from 'src/services/note-folder-service.js';
|
||||
|
||||
const vorpal = require('vorpal')();
|
||||
|
||||
let db = new Database(new DatabaseDriverNode());
|
||||
db.setDebugEnabled(false);
|
||||
db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => {
|
||||
BaseModel.db_ = db;
|
||||
|
||||
let commands = [];
|
||||
let currentFolder = null;
|
||||
|
||||
function switchCurrentFolder(folder) {
|
||||
currentFolder = folder;
|
||||
updatePrompt();
|
||||
}
|
||||
|
||||
function promptString() {
|
||||
let path = '~';
|
||||
if (currentFolder) {
|
||||
path += '/' + currentFolder.title;
|
||||
}
|
||||
return 'joplin:' + path + '$ ';
|
||||
}
|
||||
|
||||
function updatePrompt() {
|
||||
vorpal.delimiter(promptString());
|
||||
}
|
||||
|
||||
process.stdin.on('keypress', (_, key) => {
|
||||
if (key && key.name === 'return') {
|
||||
updatePrompt();
|
||||
}
|
||||
});
|
||||
|
||||
commands.push({
|
||||
usage: 'cd <list-title>',
|
||||
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 (folderTitle == '..') {
|
||||
switchCurrentFolder(null);
|
||||
end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (folderTitle == '.') {
|
||||
end();
|
||||
return;
|
||||
}
|
||||
|
||||
Folder.loadByField('title', folderTitle).then((folder) => {
|
||||
switchCurrentFolder(folder);
|
||||
end();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
commands.push({
|
||||
usage: 'mklist <list-title>',
|
||||
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();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
commands.push({
|
||||
usage: 'mknote <note-title>',
|
||||
description: 'Creates a new note',
|
||||
action: function (args, end) {
|
||||
if (!currentFolder) {
|
||||
this.log('Notes can only be created within a list.');
|
||||
end();
|
||||
return;
|
||||
}
|
||||
|
||||
let note = {
|
||||
title: args['note-title'],
|
||||
parent_id: currentFolder.id,
|
||||
};
|
||||
NoteFolderService.save('note', note).catch((error) => {
|
||||
this.log(error);
|
||||
}).then((note) => {
|
||||
end();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
commands.push({
|
||||
usage: 'edit <item-title> <prop-name> [prop-value]',
|
||||
description: 'Sets the given <prop-name> 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 (!currentFolder) {
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
commands.push({
|
||||
usage: 'ls [list-title]',
|
||||
description: 'Lists items in [list-title].',
|
||||
action: function (args, end) {
|
||||
let folderTitle = args['list-title'];
|
||||
|
||||
let promise = null;
|
||||
|
||||
if (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();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// commands.push({
|
||||
// usage: 'sync',
|
||||
// description: 'Synchronizes with remote storage.',
|
||||
// action: function (args, end) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
let c = commands[i];
|
||||
let o = vorpal.command(c.usage, c.description);
|
||||
o.action(c.action);
|
||||
}
|
||||
|
||||
|
||||
let driver = new FileApiDriverLocal();
|
||||
let api = new FileApi('/home/laurent/Temp/TestImport', driver);
|
||||
//let api = new FileApi('/home/laurent/Temp/backup_test_dest', driver);
|
||||
|
||||
// api.list('', true).then((files) => {
|
||||
// console.info(files);
|
||||
// }).catch((error) => {
|
||||
// console.error(error);
|
||||
// });
|
||||
let synchronizer = new Synchronizer(db, api);
|
||||
synchronizer.start().catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
//vorpal.delimiter(promptString()).show();
|
||||
});
|
93
CliClient/app/file-api-test.js
Normal file
93
CliClient/app/file-api-test.js
Normal file
@ -0,0 +1,93 @@
|
||||
import { FileApi } from 'src/file-api.js';
|
||||
import { FileApiDriverLocal } from 'src/file-api-driver-local.js';
|
||||
import { Database } from 'src/database.js';
|
||||
import { DatabaseDriverNode } from 'src/database-driver-node.js';
|
||||
import { Log } from 'src/log.js';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
// let driver = new FileApiDriverLocal();
|
||||
// let api = new FileApi('/home/laurent/Temp/TestImport', driver);
|
||||
|
||||
// api.list('/').then((items) => {
|
||||
// console.info(items);
|
||||
// }).then(() => {
|
||||
// return api.get('un.txt');
|
||||
// }).then((content) => {
|
||||
// console.info(content);
|
||||
// }).then(() => {
|
||||
// return api.mkdir('TESTING');
|
||||
// }).then(() => {
|
||||
// return api.put('un.txt', 'testing change');
|
||||
// }).then(() => {
|
||||
// return api.delete('deux.txt');
|
||||
// }).catch((error) => {
|
||||
// console.error('ERROR', error);
|
||||
// });
|
||||
|
||||
Log.setLevel(Log.LEVEL_DEBUG);
|
||||
|
||||
let db = new Database(new DatabaseDriverNode());
|
||||
db.setDebugEnabled(true);
|
||||
db.open({ name: '/home/laurent/Temp/test.sqlite3' }).then(() => {
|
||||
return db.selectAll('SELECT * FROM table_fields');
|
||||
}).then((rows) => {
|
||||
|
||||
});
|
||||
|
||||
//'/home/laurent/Temp/TestImport'
|
||||
|
||||
|
||||
// var sqlite3 = require('sqlite3').verbose();
|
||||
// var db = new sqlite3.Database(':memory:');
|
||||
|
||||
// db.run("CREATE TABLE lorem (info TEXT)", () => {
|
||||
// db.exec('INSERT INTO lorem VALUES "un"', () => {
|
||||
// db.exec('INSERT INTO lorem VALUES "deux"', () => {
|
||||
// let st = db.prepare("SELECT rowid AS id, info FROM lorem", () => {
|
||||
// st.get((error, row) => {
|
||||
// console.info(row);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// var stmt = db.prepare("INSERT INTO lorem VALUES (?)");
|
||||
// for (var i = 0; i < 10; i++) {
|
||||
// stmt.run("Ipsum " + i);
|
||||
// }
|
||||
// stmt.finalize();
|
||||
|
||||
// let st = db.prepare("SELECT rowid AS id, info FROM lorem");
|
||||
// st.get({}, (row) => {
|
||||
// console.info('xx',row);
|
||||
// });
|
||||
|
||||
|
||||
// st.finalize();
|
||||
|
||||
|
||||
//db.serialize(function() {
|
||||
// db.run("CREATE TABLE lorem (info TEXT)");
|
||||
|
||||
// var stmt = db.prepare("INSERT INTO lorem VALUES (?)");
|
||||
// for (var i = 0; i < 10; i++) {
|
||||
// stmt.run("Ipsum " + i);
|
||||
// }
|
||||
// stmt.finalize();
|
||||
|
||||
// let st = db.prepare("SELECT rowid AS id, info FROM lorem");
|
||||
// st.get({}, (row) => {
|
||||
// console.info('xx',row);
|
||||
// });
|
||||
|
||||
|
||||
// st.finalize();
|
||||
|
||||
// db.each("SELECT rowid AS id, info FROM lorem", function(err, row) {
|
||||
// console.log(row.id + ": " + row.info);
|
||||
// });
|
||||
//});
|
||||
|
||||
//db.close();
|
@ -4,6 +4,7 @@ import { uuid } from 'src/uuid.js';
|
||||
import moment from 'moment';
|
||||
import { promiseChain } from 'src/promise-chain.js';
|
||||
import { WebApi } from 'src/web-api.js'
|
||||
import { folderItemFilename } from 'src/string-utils.js'
|
||||
import jsSHA from "jssha";
|
||||
|
||||
let webApi = new WebApi('http://joplin.local');
|
||||
@ -519,13 +520,124 @@ function saveNoteToWebApi(note) {
|
||||
delete data.tags;
|
||||
|
||||
webApi.post('notes', null, data).then((r) => {
|
||||
console.info(r);
|
||||
//console.info(r);
|
||||
}).catch((error) => {
|
||||
console.error("Error for note: " + note.title);
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function noteToFriendlyString_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;
|
||||
}
|
||||
|
||||
function noteToFriendlyString(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 = noteToFriendlyString_format(shownKeys[i], v);
|
||||
output.push(shownKeys[i] + ': ' + v);
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
|
||||
// function folderItemFilename(item) {
|
||||
// let output = escapeFilename(item.title).trim();
|
||||
// if (!output.length) output = '_';
|
||||
// return output + '.' + item.id.substr(0, 7);
|
||||
// }
|
||||
|
||||
function noteFilename(note) {
|
||||
return folderItemFilename(note) + '.md';
|
||||
}
|
||||
|
||||
function folderFilename(folder) {
|
||||
return folderItemFilename(folder);
|
||||
}
|
||||
|
||||
function filePutContents(filePath, content) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(filePath, content, function(error) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setModifiedTime(filePath, time) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.utimes(filePath, time, time, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function createDirectory(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.exists(path, (exists) => {
|
||||
if (exists) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const mkdirp = require('mkdirp');
|
||||
|
||||
mkdirp(path, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const baseNoteDir = '/home/laurent/Temp/TestImport';
|
||||
|
||||
// createDirectory('/home/laurent/Temp/TestImport').then(() => {
|
||||
// console.info('OK');
|
||||
// }).catch((error) => {
|
||||
// console.error(error);
|
||||
// });
|
||||
|
||||
function saveNoteToDisk(folder, note) {
|
||||
const noteContent = noteToFriendlyString(note);
|
||||
const notePath = baseNoteDir + '/' + folderFilename(folder) + '/' + noteFilename(note);
|
||||
|
||||
// console.info('===================================================');
|
||||
// console.info(note);//noteContent);
|
||||
return filePutContents(notePath, noteContent).then(() => {
|
||||
return setModifiedTime(notePath, note.updated_time ? note.updated_time : note.created_time);
|
||||
});
|
||||
}
|
||||
|
||||
function saveFolderToDisk(folder) {
|
||||
let path = baseNoteDir + '/' + folderFilename(folder);
|
||||
return createDirectory(path);
|
||||
}
|
||||
|
||||
function createNoteId(note) {
|
||||
let shaObj = new jsSHA("SHA-256", "TEXT");
|
||||
shaObj.update(note.title + '_' + note.body + "_" + note.created_time + "_" + note.updated_time + "_");
|
||||
@ -533,7 +645,7 @@ function createNoteId(note) {
|
||||
return hash.substr(0, 32);
|
||||
}
|
||||
|
||||
function importEnex(parentId, stream) {
|
||||
function importEnex(parentFolder, stream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let options = {};
|
||||
let strict = true;
|
||||
@ -576,10 +688,16 @@ function importEnex(parentId, stream) {
|
||||
firstAttachment = false;
|
||||
}
|
||||
|
||||
note.parent_id = parentId;
|
||||
note.parent_id = parentFolder.id;
|
||||
note.body = processMdArrayNewLines(result.lines);
|
||||
note.id = uuid.create();
|
||||
|
||||
saveNoteToWebApi(note);
|
||||
saveNoteToDisk(parentFolder, note);
|
||||
|
||||
// console.info(noteToFriendlyString(note));
|
||||
// console.info('=========================================================================================================================');
|
||||
|
||||
//saveNoteToWebApi(note);
|
||||
|
||||
// console.info('======== NOTE ============================================================================');
|
||||
// let c = note.content;
|
||||
@ -669,7 +787,7 @@ function importEnex(parentId, stream) {
|
||||
if (notes.length >= 10) {
|
||||
stream.pause();
|
||||
processNotes().then(() => {
|
||||
//stream.resume();
|
||||
stream.resume();
|
||||
}).catch((error) => {
|
||||
console.info('Error processing note', error);
|
||||
});
|
||||
@ -722,8 +840,8 @@ function importEnex(parentId, stream) {
|
||||
// TODO: make it persistent and random
|
||||
const clientId = 'AB78AB78AB78AB78AB78AB78AB78AB78';
|
||||
|
||||
//const folderTitle = 'Laurent';
|
||||
const folderTitle = 'Voiture';
|
||||
const folderTitle = 'Laurent';
|
||||
//const folderTitle = 'Voiture';
|
||||
|
||||
webApi.post('sessions', null, {
|
||||
email: 'laurent@cozic.net',
|
||||
@ -745,11 +863,15 @@ webApi.post('sessions', null, {
|
||||
}
|
||||
|
||||
return folder ? Promise.resolve(folder) : webApi.post('folders', null, { title: folderTitle });
|
||||
}).then((folder) => {
|
||||
return saveFolderToDisk(folder).then(() => {
|
||||
return folder;
|
||||
});
|
||||
}).then((folder) => {
|
||||
let fileStream = fs.createReadStream('/mnt/c/Users/Laurent/Desktop/' + folderTitle + '.enex');
|
||||
//let fileStream = fs.createReadStream('/mnt/c/Users/Laurent/Desktop/afaire.enex');
|
||||
//let fileStream = fs.createReadStream('/mnt/c/Users/Laurent/Desktop/testtags.enex');
|
||||
importEnex(folder.id, fileStream).then(() => {
|
||||
importEnex(folder, fileStream).then(() => {
|
||||
//console.info('DONE IMPORTING');
|
||||
}).catch((error) => {
|
||||
console.error('Cannot import', error);
|
||||
|
@ -5,14 +5,19 @@
|
||||
"dependencies": {
|
||||
"app-module-path": "^2.2.0",
|
||||
"form-data": "^2.1.4",
|
||||
"fs-extra": "^3.0.1",
|
||||
"jssha": "^2.3.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"moment": "^2.18.1",
|
||||
"node-fetch": "^1.7.1",
|
||||
"promise": "^7.1.1",
|
||||
"react": "16.0.0-alpha.6",
|
||||
"sax": "^1.2.2",
|
||||
"sprintf-js": "^1.1.1",
|
||||
"sqlite3": "^3.1.8",
|
||||
"string-to-stream": "^1.1.0",
|
||||
"uuid": "^3.0.1"
|
||||
"uuid": "^3.0.1",
|
||||
"vorpal": "^1.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-changed": "^7.0.0",
|
||||
@ -22,7 +27,8 @@
|
||||
"query-string": "4.3.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel-changed app -d build",
|
||||
"babelbuild": "babel app -d build",
|
||||
"build": "babel-changed app -d build && babel-changed app/src/models -d build/src/models && babel-changed app/src/services -d build/src/services",
|
||||
"clean": "babel-changed --reset"
|
||||
}
|
||||
}
|
||||
|
@ -4,4 +4,6 @@ CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
rm -f "$CLIENT_DIR/app/src"
|
||||
ln -s "$CLIENT_DIR/../ReactNativeClient/src" "$CLIENT_DIR/app"
|
||||
|
||||
npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/import-enex.js
|
||||
#npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/import-enex.js
|
||||
#npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/file-api-test.js
|
||||
npm run build && NODE_PATH="$CLIENT_DIR/build/" node build/cmd.js
|
@ -126,6 +126,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':react-native-fs')
|
||||
compile fileTree(dir: "libs", include: ["*.jar"])
|
||||
compile "com.android.support:appcompat-v7:23.0.1"
|
||||
compile "com.facebook.react:react-native:+" // From node_modules
|
||||
|
@ -3,6 +3,7 @@ package com.awesomeproject;
|
||||
import android.app.Application;
|
||||
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.rnfs.RNFSPackage;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
@ -24,7 +25,8 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
protected List<ReactPackage> getPackages() {
|
||||
return Arrays.<ReactPackage>asList(
|
||||
new SQLitePluginPackage(),
|
||||
new MainReactPackage()
|
||||
new MainReactPackage(),
|
||||
new RNFSPackage()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,6 @@
|
||||
rootProject.name = 'AwesomeProject'
|
||||
include ':react-native-fs'
|
||||
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
|
||||
|
||||
include ':app'
|
||||
|
||||
|
@ -5,7 +5,6 @@
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
|
||||
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
|
||||
@ -36,6 +35,7 @@
|
||||
2DCD954D1E0B4F2C00145EB5 /* AwesomeProjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* AwesomeProjectTests.m */; };
|
||||
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; };
|
||||
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
|
||||
EA51DDC9EBFC469F8214B3AD /* libRNFS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8E646D13B9444DE81EC441D /* libRNFS.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -255,6 +255,8 @@
|
||||
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = "<group>"; };
|
||||
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
|
||||
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; };
|
||||
8C2AA97067234408AD5BFD90 /* RNFS.xcodeproj */ = {isa = PBXFileReference; name = "RNFS.xcodeproj"; path = "../node_modules/react-native-fs/RNFS.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
|
||||
A8E646D13B9444DE81EC441D /* libRNFS.a */ = {isa = PBXFileReference; name = "libRNFS.a"; path = "libRNFS.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -281,6 +283,7 @@
|
||||
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */,
|
||||
00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */,
|
||||
139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */,
|
||||
EA51DDC9EBFC469F8214B3AD /* libRNFS.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -447,6 +450,7 @@
|
||||
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */,
|
||||
00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */,
|
||||
139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */,
|
||||
8C2AA97067234408AD5BFD90 /* RNFS.xcodeproj */,
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@ -564,7 +568,7 @@
|
||||
83CBB9F71A601CBA00E9B192 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0610;
|
||||
LastUpgradeCheck = 610;
|
||||
ORGANIZATIONNAME = Facebook;
|
||||
TargetAttributes = {
|
||||
00E356ED1AD99517003FC87E = {
|
||||
@ -972,6 +976,14 @@
|
||||
);
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject.app/AwesomeProject";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
);
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/../node_modules/react-native-fs/**",
|
||||
);
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@ -989,6 +1001,14 @@
|
||||
);
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject.app/AwesomeProject";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
);
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/../node_modules/react-native-fs/**",
|
||||
);
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@ -1007,6 +1027,10 @@
|
||||
);
|
||||
PRODUCT_NAME = AwesomeProject;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/../node_modules/react-native-fs/**",
|
||||
);
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@ -1024,6 +1048,10 @@
|
||||
);
|
||||
PRODUCT_NAME = AwesomeProject;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/../node_modules/react-native-fs/**",
|
||||
);
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@ -1050,6 +1078,14 @@
|
||||
SDKROOT = appletvos;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TVOS_DEPLOYMENT_TARGET = 9.2;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
);
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/../node_modules/react-native-fs/**",
|
||||
);
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@ -1076,6 +1112,14 @@
|
||||
SDKROOT = appletvos;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TVOS_DEPLOYMENT_TARGET = 9.2;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
);
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/../node_modules/react-native-fs/**",
|
||||
);
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@ -1097,6 +1141,10 @@
|
||||
SDKROOT = appletvos;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject-tvOS.app/AwesomeProject-tvOS";
|
||||
TVOS_DEPLOYMENT_TARGET = 10.1;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
);
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@ -1118,6 +1166,10 @@
|
||||
SDKROOT = appletvos;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject-tvOS.app/AwesomeProject-tvOS";
|
||||
TVOS_DEPLOYMENT_TARGET = 10.1;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
);
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
@ -7,12 +7,15 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"dropbox": "^2.5.4",
|
||||
"form-data": "^2.1.4",
|
||||
"moment": "^2.18.1",
|
||||
"node-fetch": "^1.7.1",
|
||||
"react": "16.0.0-alpha.6",
|
||||
"react-native": "0.44.0",
|
||||
"react-native-action-button": "^2.6.9",
|
||||
"react-native-checkbox": "^1.1.0",
|
||||
"react-native-fs": "^2.3.3",
|
||||
"react-native-popup-menu": "^0.7.4",
|
||||
"react-native-side-menu": "^0.20.1",
|
||||
"react-native-vector-icons": "^2.0.3",
|
||||
|
@ -72,7 +72,12 @@ class BaseModel {
|
||||
}
|
||||
|
||||
static load(id) {
|
||||
return this.db().selectOne('SELECT * FROM ' + this.tableName() + ' WHERE id = ?', [id]);
|
||||
return this.loadByField('id', id);
|
||||
//return this.db().selectOne('SELECT * FROM ' + this.tableName() + ' WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
static loadByField(fieldName, fieldValue) {
|
||||
return this.db().selectOne('SELECT * FROM ' + this.tableName() + ' WHERE `' + fieldName + '` = ?', [fieldValue]);
|
||||
}
|
||||
|
||||
static applyPatch(model, patch) {
|
||||
@ -143,10 +148,12 @@ class BaseModel {
|
||||
options = this.modOptions(options);
|
||||
|
||||
let isNew = options.isNew == 'auto' ? !o.id : options.isNew;
|
||||
let query = this.saveQuery(o, isNew);
|
||||
|
||||
return this.db().transaction((tx) => {
|
||||
tx.executeSql(query.sql, query.params);
|
||||
let queries = [];
|
||||
let saveQuery = this.saveQuery(o, isNew);
|
||||
let itemId = saveQuery.id;
|
||||
|
||||
queries.push(saveQuery);
|
||||
|
||||
if (options.trackChanges && this.trackChanges()) {
|
||||
// Cannot import this class the normal way due to cyclical dependencies between Change and BaseModel
|
||||
@ -156,11 +163,10 @@ class BaseModel {
|
||||
if (isNew) {
|
||||
let change = Change.newChange();
|
||||
change.type = Change.TYPE_CREATE;
|
||||
change.item_id = query.id;
|
||||
change.item_id = itemId;
|
||||
change.item_type = this.itemType();
|
||||
|
||||
let changeQuery = Change.saveQuery(change);
|
||||
tx.executeSql(changeQuery.sql, changeQuery.params);
|
||||
queries.push(Change.saveQuery(change));
|
||||
} else {
|
||||
for (let n in o) {
|
||||
if (!o.hasOwnProperty(n)) continue;
|
||||
@ -168,18 +174,18 @@ class BaseModel {
|
||||
|
||||
let change = Change.newChange();
|
||||
change.type = Change.TYPE_UPDATE;
|
||||
change.item_id = query.id;
|
||||
change.item_id = itemId;
|
||||
change.item_type = this.itemType();
|
||||
change.item_field = n;
|
||||
|
||||
let changeQuery = Change.saveQuery(change);
|
||||
tx.executeSql(changeQuery.sql, changeQuery.params);
|
||||
queries.push(Change.saveQuery(change));
|
||||
}
|
||||
}
|
||||
}
|
||||
}).then((r) => {
|
||||
|
||||
return this.db().transactionExecBatch(queries).then(() => {
|
||||
o = Object.assign({}, o);
|
||||
o.id = query.id;
|
||||
o.id = itemId;
|
||||
return o;
|
||||
}).catch((error) => {
|
||||
Log.error('Cannot save model', error);
|
||||
@ -220,5 +226,6 @@ BaseModel.ITEM_TYPE_FOLDER = 2;
|
||||
BaseModel.tableInfo_ = null;
|
||||
BaseModel.tableKeys_ = null;
|
||||
BaseModel.db_ = null;
|
||||
BaseModel.dispatch = function(o) {};
|
||||
|
||||
export { BaseModel };
|
@ -17,10 +17,6 @@ const styles = StyleSheet.create({
|
||||
|
||||
class ScreenHeaderComponent extends Component {
|
||||
|
||||
static defaultProps = {
|
||||
menuOptions: [],
|
||||
};
|
||||
|
||||
showBackButton() {
|
||||
// Note: this is hardcoded for now because navigation.state doesn't tell whether
|
||||
// it's possible to go back or not. Maybe it's possible to get this information
|
||||
@ -111,6 +107,10 @@ class ScreenHeaderComponent extends Component {
|
||||
|
||||
}
|
||||
|
||||
ScreenHeaderComponent.defaultProps = {
|
||||
menuOptions: [],
|
||||
};
|
||||
|
||||
const ScreenHeader = connect(
|
||||
(state) => {
|
||||
return { user: state.user };
|
||||
|
@ -61,7 +61,7 @@ class LoginScreenComponent extends React.Component {
|
||||
|
||||
Registry.api().setSession(session.id);
|
||||
|
||||
Registry.synchronizer().start();
|
||||
//Registry.synchronizer().start();
|
||||
}).catch((error) => {
|
||||
this.setState({ errorMessage: _('Could not login: %s)', error.message) });
|
||||
});
|
||||
|
63
ReactNativeClient/src/database-driver-node.js
Normal file
63
ReactNativeClient/src/database-driver-node.js
Normal file
@ -0,0 +1,63 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const Promise = require('promise');
|
||||
|
||||
class DatabaseDriverNode {
|
||||
|
||||
open(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_ = new sqlite3.Database(options.name, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setDebugEnabled(v) {
|
||||
// ??
|
||||
}
|
||||
|
||||
selectOne(sql, params = null) {
|
||||
if (!params) params = {};
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.get(sql, params, (error, row) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(row);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
selectAll(sql, params = null) {
|
||||
if (!params) params = {};
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.all(sql, params, (error, row) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(row);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exec(sql, params = null) {
|
||||
if (!params) params = {};
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.run(sql, params, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { DatabaseDriverNode };
|
52
ReactNativeClient/src/database-driver-react-native.js
Normal file
52
ReactNativeClient/src/database-driver-react-native.js
Normal file
@ -0,0 +1,52 @@
|
||||
import SQLite from 'react-native-sqlite-storage';
|
||||
|
||||
class DatabaseDriverReactNative {
|
||||
|
||||
open(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
SQLite.openDatabase({ name: options.name }, (db) => {
|
||||
this.db_ = db;
|
||||
resolve();
|
||||
}, (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setDebugEnabled(v) {
|
||||
SQLite.DEBUG(v);
|
||||
}
|
||||
|
||||
selectOne(sql, params = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.executeSql(sql, params, (r) => {
|
||||
resolve(r.rows.length ? r.rows.item(0) : null);
|
||||
}, (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
selectAll(sql, params = null) {
|
||||
return this.exec(sql, params).then((r) => {
|
||||
let output = []
|
||||
for (let i = 0; i < r.rows.length; i++) {
|
||||
output.push(r.rows.item(i));
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
exec(sql, params = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.executeSql(sql, params, (r) => {
|
||||
resolve(r);
|
||||
}, (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { DatabaseDriverReactNative }
|
@ -1,4 +1,3 @@
|
||||
import SQLite from 'react-native-sqlite-storage';
|
||||
import { Log } from 'src/log.js';
|
||||
import { uuid } from 'src/uuid.js';
|
||||
import { promiseChain } from 'src/promise-chain.js';
|
||||
@ -94,14 +93,15 @@ INSERT INTO version (version) VALUES (1);
|
||||
|
||||
class Database {
|
||||
|
||||
constructor() {
|
||||
constructor(driver) {
|
||||
this.debugMode_ = false;
|
||||
this.initialized_ = false;
|
||||
this.tableFields_ = null;
|
||||
this.driver_ = driver;
|
||||
}
|
||||
|
||||
setDebugEnabled(v) {
|
||||
SQLite.DEBUG(v);
|
||||
this.driver_.setDebugEnabled(v);
|
||||
this.debugMode_ = v;
|
||||
}
|
||||
|
||||
@ -113,14 +113,43 @@ class Database {
|
||||
return this.initialized_;
|
||||
}
|
||||
|
||||
open() {
|
||||
this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-32.sqlite' }, (db) => {
|
||||
driver() {
|
||||
return this.driver_;
|
||||
}
|
||||
|
||||
open(options) {
|
||||
return this.driver().open(options).then((db) => {
|
||||
Log.info('Database was open successfully');
|
||||
}, (error) => {
|
||||
return this.initialize();
|
||||
}).catch((error) => {
|
||||
Log.error('Cannot open database: ', error);
|
||||
});
|
||||
}
|
||||
|
||||
return this.initialize();
|
||||
selectOne(sql, params = null) {
|
||||
this.logQuery(sql, params);
|
||||
return this.driver().selectOne(sql, params);
|
||||
}
|
||||
|
||||
selectAll(sql, params = null) {
|
||||
this.logQuery(sql, params);
|
||||
return this.driver().selectAll(sql, params);
|
||||
}
|
||||
|
||||
exec(sql, params = null) {
|
||||
this.logQuery(sql, params);
|
||||
return this.driver().exec(sql, params);
|
||||
}
|
||||
|
||||
transactionExecBatch(queries) {
|
||||
let chain = [];
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
let query = this.wrapQuery(queries[i]);
|
||||
chain.push(() => {
|
||||
return this.exec(query.sql, query.params);
|
||||
});
|
||||
}
|
||||
return promiseChain(chain);
|
||||
}
|
||||
|
||||
static enumId(type, s) {
|
||||
@ -177,41 +206,7 @@ class Database {
|
||||
|
||||
logQuery(sql, params = null) {
|
||||
if (!this.debugMode()) return;
|
||||
//Log.debug('DB: ' + sql, params);
|
||||
}
|
||||
|
||||
selectOne(sql, params = null) {
|
||||
this.logQuery(sql, params);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.executeSql(sql, params, (r) => {
|
||||
resolve(r.rows.length ? r.rows.item(0) : null);
|
||||
}, (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
selectAll(sql, params = null) {
|
||||
this.logQuery(sql, params);
|
||||
|
||||
return this.exec(sql, params);
|
||||
}
|
||||
|
||||
exec(sql, params = null) {
|
||||
this.logQuery(sql, params);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.executeSql(sql, params, (r) => {
|
||||
resolve(r);
|
||||
}, (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
executeSql(sql, params = null) {
|
||||
return this.exec(sql, params);
|
||||
Log.debug('DB: ' + sql, params);
|
||||
}
|
||||
|
||||
static insertQuery(tableName, data) {
|
||||
@ -254,29 +249,53 @@ class Database {
|
||||
}
|
||||
|
||||
transaction(readyCallack) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.transaction(
|
||||
readyCallack,
|
||||
(error) => { reject(error); },
|
||||
() => { resolve(); }
|
||||
);
|
||||
});
|
||||
throw new Error('transaction() DEPRECATED');
|
||||
// return new Promise((resolve, reject) => {
|
||||
// this.db_.transaction(
|
||||
// readyCallack,
|
||||
// (error) => { reject(error); },
|
||||
// () => { resolve(); }
|
||||
// );
|
||||
// });
|
||||
}
|
||||
|
||||
wrapQueries(queries) {
|
||||
let output = [];
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
output.push(this.wrapQuery(queries[i]));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
wrapQuery(sql, params = null) {
|
||||
if (!sql) throw new Error('Cannot wrap empty string: ' + sql);
|
||||
|
||||
if (sql.constructor === Array) {
|
||||
let output = {};
|
||||
output.sql = sql[0];
|
||||
output.params = sql.length >= 2 ? sql[1] : null;
|
||||
return output;
|
||||
} else if (typeof sql === 'string') {
|
||||
return { sql: sql, params: params };
|
||||
} else {
|
||||
return sql; // Already wrapped
|
||||
}
|
||||
}
|
||||
|
||||
refreshTableFields() {
|
||||
return this.exec('SELECT name FROM sqlite_master WHERE type="table"').then((tableResults) => {
|
||||
let queries = [];
|
||||
queries.push(this.wrapQuery('DELETE FROM table_fields'));
|
||||
|
||||
return this.selectAll('SELECT name FROM sqlite_master WHERE type="table"').then((tableRows) => {
|
||||
let chain = [];
|
||||
for (let i = 0; i < tableResults.rows.length; i++) {
|
||||
let row = tableResults.rows.item(i);
|
||||
let tableName = row.name;
|
||||
for (let i = 0; i < tableRows.length; i++) {
|
||||
let tableName = tableRows[i].name;
|
||||
if (tableName == 'android_metadata') continue;
|
||||
if (tableName == 'table_fields') continue;
|
||||
|
||||
chain.push((queries) => {
|
||||
if (!queries) queries = [];
|
||||
return this.exec('PRAGMA table_info("' + tableName + '")').then((pragmaResult) => {
|
||||
for (let i = 0; i < pragmaResult.rows.length; i++) {
|
||||
let item = pragmaResult.rows.item(i);
|
||||
chain.push(() => {
|
||||
return this.selectAll('PRAGMA table_info("' + tableName + '")').then((pragmas) => {
|
||||
for (let i = 0; i < pragmas.length; i++) {
|
||||
let item = pragmas[i];
|
||||
// In SQLite, if the default value is a string it has double quotes around it, so remove them here
|
||||
let defaultValue = item.dflt_value;
|
||||
if (typeof defaultValue == 'string' && defaultValue.length >= 2 && defaultValue[0] == '"' && defaultValue[defaultValue.length - 1] == '"') {
|
||||
@ -290,19 +309,13 @@ class Database {
|
||||
});
|
||||
queries.push(q);
|
||||
}
|
||||
return queries;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return promiseChain(chain);
|
||||
}).then((queries) => {
|
||||
return this.transaction((tx) => {
|
||||
tx.executeSql('DELETE FROM table_fields');
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
tx.executeSql(queries[i].sql, queries[i].params);
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
return this.transactionExecBatch(queries);
|
||||
});
|
||||
}
|
||||
|
||||
@ -314,12 +327,13 @@ class Database {
|
||||
// TODO: version update logic
|
||||
|
||||
// TODO: only do this if db has been updated:
|
||||
return this.refreshTableFields();
|
||||
// return this.refreshTableFields();
|
||||
}).then(() => {
|
||||
return this.exec('SELECT * FROM table_fields').then((r) => {
|
||||
this.tableFields_ = {};
|
||||
for (let i = 0; i < r.rows.length; i++) {
|
||||
let row = r.rows.item(i);
|
||||
|
||||
return this.selectAll('SELECT * FROM table_fields').then((rows) => {
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let row = rows[i];
|
||||
if (!this.tableFields_[row.table_name]) this.tableFields_[row.table_name] = [];
|
||||
this.tableFields_[row.table_name].push({
|
||||
name: row.field_name,
|
||||
@ -330,7 +344,6 @@ class Database {
|
||||
});
|
||||
|
||||
|
||||
|
||||
// }).then(() => {
|
||||
// let p = this.exec('DELETE FROM notes').then(() => {
|
||||
// return this.exec('DELETE FROM folders');
|
||||
@ -348,11 +361,9 @@ class Database {
|
||||
|
||||
// return p;
|
||||
|
||||
|
||||
|
||||
|
||||
}).catch((error) => {
|
||||
if (error && error.code != 0) {
|
||||
//console.info(error.code);
|
||||
if (error && error.code != 0 && error.code != 'SQLITE_ERROR') {
|
||||
Log.error(error);
|
||||
return;
|
||||
}
|
||||
@ -364,19 +375,18 @@ class Database {
|
||||
|
||||
Log.info('Database is new - creating the schema...');
|
||||
|
||||
let statements = this.sqlStringToLines(structureSql)
|
||||
return this.transaction((tx) => {
|
||||
for (let i = 0; i < statements.length; i++) {
|
||||
tx.executeSql(statements[i]);
|
||||
}
|
||||
tx.executeSql('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumId('settings', 'string') + '")');
|
||||
tx.executeSql('INSERT INTO folders (`id`, `title`, `is_default`, `created_time`) VALUES ("' + uuid.create() + '", "' + _('Default list') + '", 1, ' + Math.round((new Date()).getTime() / 1000) + ')');
|
||||
}).then(() => {
|
||||
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 folders (`id`, `title`, `is_default`, `created_time`) VALUES ("' + uuid.create() + '", "' + _('Default list') + '", 1, ' + Math.round((new Date()).getTime() / 1000) + ')'));
|
||||
|
||||
return this.transactionExecBatch(queries).then(() => {
|
||||
Log.info('Database schema created successfully');
|
||||
// Calling initialize() now that the db has been created will make it go through
|
||||
// the normal db update process (applying any additional patch).
|
||||
return this.refreshTableFields();
|
||||
}).then(() => {
|
||||
return this.initialize();
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
143
ReactNativeClient/src/file-api-driver-local.js
Normal file
143
ReactNativeClient/src/file-api-driver-local.js
Normal file
@ -0,0 +1,143 @@
|
||||
import fs from 'fs';
|
||||
import fse from 'fs-extra';
|
||||
import { promiseChain } from 'src/promise-chain.js';
|
||||
import moment from 'moment';
|
||||
|
||||
class FileApiDriverLocal {
|
||||
|
||||
stat(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.stat(path, (error, s) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(s);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
statTimeToUnixTimestamp_(time) {
|
||||
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);
|
||||
}
|
||||
|
||||
metadataFromStats_(name, stats) {
|
||||
return {
|
||||
name: name,
|
||||
createdTime: this.statTimeToUnixTimestamp_(stats.birthtime),
|
||||
updatedTime: this.statTimeToUnixTimestamp_(stats.mtime),
|
||||
isDir: stats.isDirectory(),
|
||||
};
|
||||
}
|
||||
|
||||
list(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(path, (error, items) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
let chain = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
chain.push((output) => {
|
||||
if (!output) output = [];
|
||||
return this.stat(path + '/' + items[i]).then((stat) => {
|
||||
let md = this.metadataFromStats_(items[i], stat);
|
||||
output.push(md);
|
||||
return output;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return promiseChain(chain).then((results) => {
|
||||
if (!results) results = [];
|
||||
resolve(results);
|
||||
}).catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(path, 'utf8', (error, content) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
return resolve(content);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
mkdir(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.exists(path, (exists) => {
|
||||
if (exists) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const mkdirp = require('mkdirp');
|
||||
|
||||
mkdirp(path, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
put(path, content) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(path, content, function(error) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
delete(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.unlink(path, function(error) {
|
||||
if (error) {
|
||||
if (error && error.code == 'ENOENT') {
|
||||
// File doesn't exist - it's fine
|
||||
resolve();
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
move(oldPath, newPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fse.move(oldPath, newPath, function(error) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { FileApiDriverLocal };
|
60
ReactNativeClient/src/file-api.js
Normal file
60
ReactNativeClient/src/file-api.js
Normal file
@ -0,0 +1,60 @@
|
||||
import { promiseChain } from 'src/promise-chain.js';
|
||||
|
||||
class FileApi {
|
||||
|
||||
constructor(baseDir, driver) {
|
||||
this.baseDir_ = baseDir;
|
||||
this.driver_ = driver;
|
||||
}
|
||||
|
||||
list(path, recursive = false) {
|
||||
return this.driver_.list(this.baseDir_ + '/' + path, recursive).then((items) => {
|
||||
if (recursive) {
|
||||
let chain = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let item = items[i];
|
||||
if (!item.isDir) continue;
|
||||
|
||||
chain.push(() => {
|
||||
return this.list(path + '/' + item.name, true).then((children) => {
|
||||
for (let j = 0; j < children.length; j++) {
|
||||
let md = children[j];
|
||||
md.name = item.name + '/' + md.name;
|
||||
items.push(md);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return promiseChain(chain).then(() => {
|
||||
return items;
|
||||
});
|
||||
} else {
|
||||
return items;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mkdir(path) {
|
||||
return this.driver_.mkdir(this.baseDir_ + '/' + path);
|
||||
}
|
||||
|
||||
get(path) {
|
||||
return this.driver_.get(this.baseDir_ + '/' + path);
|
||||
}
|
||||
|
||||
put(path, content) {
|
||||
return this.driver_.put(this.baseDir_ + '/' + path, content);
|
||||
}
|
||||
|
||||
delete(path) {
|
||||
return this.driver_.delete(this.baseDir_ + '/' + path);
|
||||
}
|
||||
|
||||
move(oldPath, newPath) {
|
||||
return this.driver_.move(this.baseDir_ + '/' + oldPath, this.baseDir_ + '/' + newPath);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { FileApi };
|
@ -16,6 +16,11 @@ class Geolocation {
|
||||
}
|
||||
|
||||
static currentPosition(options = null) {
|
||||
if (typeof navigator === 'undefined') {
|
||||
// TODO
|
||||
return Promise.resolve(this.currentPosition_testResponse());
|
||||
}
|
||||
|
||||
if (!options) options = {};
|
||||
if (!('enableHighAccuracy' in options)) options.enableHighAccuracy = true;
|
||||
if (!('timeout' in options)) options.timeout = 10000;
|
||||
|
@ -7,12 +7,12 @@ class Log {
|
||||
}
|
||||
|
||||
static level() {
|
||||
return this.level_ === undefined ? Log.LEVEL_ERROR : this.level_;
|
||||
return this.level_ === undefined ? Log.LEVEL_DEBUG : this.level_;
|
||||
}
|
||||
|
||||
static debug(...o) {
|
||||
if (Log.level() > Log.LEVEL_DEBUG) return;
|
||||
console.debug(...o);
|
||||
console.info(...o);
|
||||
}
|
||||
|
||||
static info(...o) {
|
||||
|
@ -18,24 +18,20 @@ class Change extends BaseModel {
|
||||
}
|
||||
|
||||
static all() {
|
||||
return this.db().selectAll('SELECT * FROM changes').then((r) => {
|
||||
let output = [];
|
||||
for (let i = 0; i < r.rows.length; i++) {
|
||||
output.push(r.rows.item(i));
|
||||
}
|
||||
return output;
|
||||
});
|
||||
return this.db().selectAll('SELECT * FROM changes');
|
||||
}
|
||||
|
||||
static deleteMultiple(ids) {
|
||||
if (ids.length == 0) return Promise.resolve();
|
||||
|
||||
return this.db().transaction((tx) => {
|
||||
let sql = '';
|
||||
console.warn('TODO: deleteMultiple: CHECK THAT IT WORKS');
|
||||
|
||||
let queries = [];
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
tx.executeSql('DELETE FROM changes WHERE id = ?', [ids[i]]);
|
||||
queries.push(['DELETE FROM changes WHERE id = ?', [ids[i]]]);
|
||||
}
|
||||
});
|
||||
|
||||
return this.db().transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
static mergeChanges(changes) {
|
||||
@ -45,6 +41,7 @@ class Change extends BaseModel {
|
||||
|
||||
for (let i = 0; i < changes.length; i++) {
|
||||
let change = changes[i];
|
||||
let mergedChange = null;
|
||||
|
||||
if (itemChanges[change.item_id]) {
|
||||
mergedChange = itemChanges[change.item_id];
|
||||
|
@ -2,6 +2,7 @@ import { BaseModel } from 'src/base-model.js';
|
||||
import { Log } from 'src/log.js';
|
||||
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';
|
||||
|
||||
class Folder extends BaseModel {
|
||||
@ -10,6 +11,14 @@ class Folder extends BaseModel {
|
||||
return 'folders';
|
||||
}
|
||||
|
||||
static filename(folder) {
|
||||
return folderItemFilename(folder);
|
||||
}
|
||||
|
||||
static systemPath(parent, folder) {
|
||||
return this.filename(folder);
|
||||
}
|
||||
|
||||
static useUuid() {
|
||||
return true;
|
||||
}
|
||||
@ -30,10 +39,10 @@ class Folder extends BaseModel {
|
||||
}
|
||||
|
||||
static noteIds(id) {
|
||||
return this.db().exec('SELECT id FROM notes WHERE parent_id = ?', [id]).then((r) => {
|
||||
return this.db().selectAll('SELECT id FROM notes WHERE parent_id = ?', [id]).then((rows) => {
|
||||
let output = [];
|
||||
for (let i = 0; i < r.rows.length; i++) {
|
||||
let row = r.rows.item(i);
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let row = rows[i];
|
||||
output.push(row.id);
|
||||
}
|
||||
return output;
|
||||
@ -66,17 +75,18 @@ class Folder extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
static all() {
|
||||
return this.db().selectAll('SELECT * FROM folders').then((r) => {
|
||||
let output = [];
|
||||
for (let i = 0; i < r.rows.length; i++) {
|
||||
output.push(r.rows.item(i));
|
||||
static loadNoteByField(folderId, field, value) {
|
||||
return this.db().selectOne('SELECT * FROM notes WHERE `parent_id` = ? AND `' + field + '` = ?', [folderId, value]);
|
||||
}
|
||||
return output;
|
||||
});
|
||||
|
||||
static all() {
|
||||
return this.db().selectAll('SELECT * FROM folders');
|
||||
}
|
||||
|
||||
static save(o, options = null) {
|
||||
return Folder.loadByField('title', o.title).then((existingFolder) => {
|
||||
if (existingFolder && existingFolder.id != o.id) throw new Error(_('A folder with title "%s" already exists', o.title));
|
||||
|
||||
return super.save(o, options).then((folder) => {
|
||||
this.dispatch({
|
||||
type: 'FOLDERS_UPDATE_ONE',
|
||||
@ -84,6 +94,7 @@ class Folder extends BaseModel {
|
||||
});
|
||||
return folder;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { BaseModel } from 'src/base-model.js';
|
||||
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 moment from 'moment';
|
||||
|
||||
class Note extends BaseModel {
|
||||
|
||||
@ -8,6 +11,42 @@ class Note extends BaseModel {
|
||||
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 filename(note) {
|
||||
return folderItemFilename(note) + '.md';
|
||||
}
|
||||
|
||||
static systemPath(parentFolder, note) {
|
||||
return Folder.systemPath(null, parentFolder) + '/' + this.filename(note);
|
||||
}
|
||||
|
||||
static useUuid() {
|
||||
return true;
|
||||
}
|
||||
@ -37,13 +76,7 @@ class Note extends BaseModel {
|
||||
}
|
||||
|
||||
static previews(parentId) {
|
||||
return this.db().selectAll('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE parent_id = ?', [parentId]).then((r) => {
|
||||
let output = [];
|
||||
for (let i = 0; i < r.rows.length; i++) {
|
||||
output.push(r.rows.item(i));
|
||||
}
|
||||
return output;
|
||||
});
|
||||
return this.db().selectAll('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE parent_id = ?', [parentId]);
|
||||
}
|
||||
|
||||
static preview(noteId) {
|
||||
|
@ -27,10 +27,8 @@ class Setting extends BaseModel {
|
||||
|
||||
static load() {
|
||||
this.cache_ = [];
|
||||
return this.db().selectAll('SELECT * FROM settings').then((r) => {
|
||||
for (let i = 0; i < r.rows.length; i++) {
|
||||
this.cache_.push(r.rows.item(i));
|
||||
}
|
||||
return this.db().selectAll('SELECT * FROM settings').then((rows) => {
|
||||
this.cache_ = rows;
|
||||
});
|
||||
}
|
||||
|
||||
@ -89,13 +87,13 @@ class Setting extends BaseModel {
|
||||
clearTimeout(this.updateTimeoutId_);
|
||||
this.updateTimeoutId_ = null;
|
||||
|
||||
return BaseModel.db().transaction((tx) => {
|
||||
tx.executeSql('DELETE FROM settings');
|
||||
let queries = [];
|
||||
queries.push('DELETE FROM settings');
|
||||
for (let i = 0; i < this.cache_.length; i++) {
|
||||
let q = Database.insertQuery(this.tableName(), this.cache_[i]);
|
||||
tx.executeSql(q.sql, q.params);
|
||||
queries.push(Database.insertQuery(this.tableName(), this.cache_[i]));
|
||||
}
|
||||
}).then(() => {
|
||||
|
||||
return BaseModel.db().transactionExecBatch(queries).then(() => {
|
||||
Log.info('Settings have been saved.');
|
||||
}).catch((error) => {
|
||||
Log.warn('Could not save settings', error);
|
||||
|
@ -25,6 +25,7 @@ import { MenuContext } from 'react-native-popup-menu';
|
||||
import { SideMenu } from 'src/components/side-menu.js';
|
||||
import { SideMenuContent } from 'src/components/side-menu-content.js';
|
||||
import { NoteFolderService } from 'src/services/note-folder-service.js';
|
||||
import { DatabaseDriverReactNative } from 'src/database-driver-react-native';
|
||||
|
||||
let defaultState = {
|
||||
notes: [],
|
||||
@ -89,8 +90,6 @@ const reducer = (state = defaultState, action) => {
|
||||
// update it within the note array if it already exists.
|
||||
case 'NOTES_UPDATE_ONE':
|
||||
|
||||
Log.info('NOITTEOJTNEONTOE', action.note);
|
||||
|
||||
let newNotes = state.notes.splice(0);
|
||||
var found = false;
|
||||
for (let i = 0; i < newNotes.length; i++) {
|
||||
@ -191,7 +190,7 @@ const AppNavigator = StackNavigator({
|
||||
class AppComponent extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
let db = new Database();
|
||||
let db = new Database(new DatabaseDriverReactNative());
|
||||
//db.setDebugEnabled(Registry.debugMode());
|
||||
db.setDebugEnabled(false);
|
||||
|
||||
@ -199,7 +198,7 @@ class AppComponent extends React.Component {
|
||||
BaseModel.db_ = db;
|
||||
NoteFolderService.dispatch = this.props.dispatch;
|
||||
|
||||
db.open().then(() => {
|
||||
db.open({ name: '/storage/emulated/0/Download/joplin-42.sqlite' }).then(() => {
|
||||
Log.info('Database is ready.');
|
||||
Registry.setDb(db);
|
||||
}).then(() => {
|
||||
@ -207,6 +206,21 @@ class AppComponent extends React.Component {
|
||||
return Setting.load();
|
||||
}).then(() => {
|
||||
let user = Setting.object('user');
|
||||
|
||||
if (!user || !user.session) {
|
||||
user = {
|
||||
email: 'laurent@cozic.net',
|
||||
session: "02d0e9ca42cbbc2d35efb1bc790b9eec",
|
||||
}
|
||||
Setting.setObject('user', user);
|
||||
this.props.dispatch({
|
||||
type: 'USER_SET',
|
||||
user: user,
|
||||
});
|
||||
}
|
||||
|
||||
Setting.setValue('sync.lastRevId', '123456');
|
||||
|
||||
Log.info('Client ID', Setting.value('clientId'));
|
||||
Log.info('User', user);
|
||||
|
||||
@ -241,9 +255,25 @@ class AppComponent extends React.Component {
|
||||
// folderId: folder.id,
|
||||
// });
|
||||
}).then(() => {
|
||||
let synchronizer = new Synchronizer(db, Registry.api());
|
||||
Registry.setSynchronizer(synchronizer);
|
||||
synchronizer.start();
|
||||
|
||||
var Dropbox = require('dropbox');
|
||||
var dropboxApi = new Dropbox({ accessToken: '' });
|
||||
// dbx.filesListFolder({path: '/Joplin/Laurent.4e847cc'})
|
||||
// .then(function(response) {
|
||||
// //console.log('DROPBOX RESPONSE', response);
|
||||
// console.log('DROPBOX RESPONSE', response.entries.length, response.has_more);
|
||||
// })
|
||||
// .catch(function(error) {
|
||||
// console.log('DROPBOX ERROR', error);
|
||||
// });
|
||||
|
||||
// return this.api_;
|
||||
|
||||
|
||||
// let synchronizer = new Synchronizer(db, Registry.api());
|
||||
// let synchronizer = new Synchronizer(db, dropboxApi);
|
||||
// Registry.setSynchronizer(synchronizer);
|
||||
// synchronizer.start();
|
||||
}).catch((error) => {
|
||||
Log.error('Initialization error:', error);
|
||||
});
|
||||
|
@ -31,21 +31,21 @@ class NoteFolderService extends BaseService {
|
||||
output = item;
|
||||
if (isNew && type == 'note') return Note.updateGeolocation(item.id);
|
||||
}).then(() => {
|
||||
Registry.synchronizer().start();
|
||||
// Registry.synchronizer().start();
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
static setField(type, itemId, fieldName, fieldValue, oldValue = undefined) {
|
||||
// TODO: not really consistent as the promise will return 'null' while
|
||||
// this.save will return the note or folder. Currently not used, and maybe not needed.
|
||||
if (oldValue !== undefined && fieldValue === oldValue) return Promise.resolve();
|
||||
// static setField(type, itemId, fieldName, fieldValue, oldValue = undefined) {
|
||||
// // TODO: not really consistent as the promise will return 'null' while
|
||||
// // this.save will return the note or folder. Currently not used, and maybe not needed.
|
||||
// if (oldValue !== undefined && fieldValue === oldValue) return Promise.resolve();
|
||||
|
||||
let item = { id: itemId };
|
||||
item[fieldName] = fieldValue;
|
||||
let oldItem = { id: itemId };
|
||||
return this.save(type, item, oldItem);
|
||||
}
|
||||
// let item = { id: itemId };
|
||||
// item[fieldName] = fieldValue;
|
||||
// let oldItem = { id: itemId };
|
||||
// return this.save(type, item, oldItem);
|
||||
// }
|
||||
|
||||
static openNoteList(folderId) {
|
||||
return Note.previews(folderId).then((notes) => {
|
||||
|
122
ReactNativeClient/src/string-utils.js
Normal file
122
ReactNativeClient/src/string-utils.js
Normal file
@ -0,0 +1,122 @@
|
||||
function removeDiacritics(str) {
|
||||
|
||||
var defaultDiacriticsRemovalMap = [
|
||||
{'base':'A', 'letters':/[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g},
|
||||
{'base':'AA','letters':/[\uA732]/g},
|
||||
{'base':'AE','letters':/[\u00C6\u01FC\u01E2]/g},
|
||||
{'base':'AO','letters':/[\uA734]/g},
|
||||
{'base':'AU','letters':/[\uA736]/g},
|
||||
{'base':'AV','letters':/[\uA738\uA73A]/g},
|
||||
{'base':'AY','letters':/[\uA73C]/g},
|
||||
{'base':'B', 'letters':/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g},
|
||||
{'base':'C', 'letters':/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g},
|
||||
{'base':'D', 'letters':/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g},
|
||||
{'base':'DZ','letters':/[\u01F1\u01C4]/g},
|
||||
{'base':'Dz','letters':/[\u01F2\u01C5]/g},
|
||||
{'base':'E', 'letters':/[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g},
|
||||
{'base':'F', 'letters':/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g},
|
||||
{'base':'G', 'letters':/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g},
|
||||
{'base':'H', 'letters':/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g},
|
||||
{'base':'I', 'letters':/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g},
|
||||
{'base':'J', 'letters':/[\u004A\u24BF\uFF2A\u0134\u0248]/g},
|
||||
{'base':'K', 'letters':/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g},
|
||||
{'base':'L', 'letters':/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g},
|
||||
{'base':'LJ','letters':/[\u01C7]/g},
|
||||
{'base':'Lj','letters':/[\u01C8]/g},
|
||||
{'base':'M', 'letters':/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g},
|
||||
{'base':'N', 'letters':/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g},
|
||||
{'base':'NJ','letters':/[\u01CA]/g},
|
||||
{'base':'Nj','letters':/[\u01CB]/g},
|
||||
{'base':'O', 'letters':/[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g},
|
||||
{'base':'OI','letters':/[\u01A2]/g},
|
||||
{'base':'OO','letters':/[\uA74E]/g},
|
||||
{'base':'OU','letters':/[\u0222]/g},
|
||||
{'base':'P', 'letters':/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g},
|
||||
{'base':'Q', 'letters':/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g},
|
||||
{'base':'R', 'letters':/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g},
|
||||
{'base':'S', 'letters':/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g},
|
||||
{'base':'T', 'letters':/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g},
|
||||
{'base':'TZ','letters':/[\uA728]/g},
|
||||
{'base':'U', 'letters':/[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g},
|
||||
{'base':'V', 'letters':/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g},
|
||||
{'base':'VY','letters':/[\uA760]/g},
|
||||
{'base':'W', 'letters':/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g},
|
||||
{'base':'X', 'letters':/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g},
|
||||
{'base':'Y', 'letters':/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g},
|
||||
{'base':'Z', 'letters':/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g},
|
||||
{'base':'a', 'letters':/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g},
|
||||
{'base':'aa','letters':/[\uA733]/g},
|
||||
{'base':'ae','letters':/[\u00E6\u01FD\u01E3]/g},
|
||||
{'base':'ao','letters':/[\uA735]/g},
|
||||
{'base':'au','letters':/[\uA737]/g},
|
||||
{'base':'av','letters':/[\uA739\uA73B]/g},
|
||||
{'base':'ay','letters':/[\uA73D]/g},
|
||||
{'base':'b', 'letters':/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g},
|
||||
{'base':'c', 'letters':/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g},
|
||||
{'base':'d', 'letters':/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g},
|
||||
{'base':'dz','letters':/[\u01F3\u01C6]/g},
|
||||
{'base':'e', 'letters':/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g},
|
||||
{'base':'f', 'letters':/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g},
|
||||
{'base':'g', 'letters':/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g},
|
||||
{'base':'h', 'letters':/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g},
|
||||
{'base':'hv','letters':/[\u0195]/g},
|
||||
{'base':'i', 'letters':/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g},
|
||||
{'base':'j', 'letters':/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g},
|
||||
{'base':'k', 'letters':/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g},
|
||||
{'base':'l', 'letters':/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g},
|
||||
{'base':'lj','letters':/[\u01C9]/g},
|
||||
{'base':'m', 'letters':/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g},
|
||||
{'base':'n', 'letters':/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g},
|
||||
{'base':'nj','letters':/[\u01CC]/g},
|
||||
{'base':'o', 'letters':/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g},
|
||||
{'base':'oi','letters':/[\u01A3]/g},
|
||||
{'base':'ou','letters':/[\u0223]/g},
|
||||
{'base':'oo','letters':/[\uA74F]/g},
|
||||
{'base':'p','letters':/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g},
|
||||
{'base':'q','letters':/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g},
|
||||
{'base':'r','letters':/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g},
|
||||
{'base':'s','letters':/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g},
|
||||
{'base':'t','letters':/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g},
|
||||
{'base':'tz','letters':/[\uA729]/g},
|
||||
{'base':'u','letters':/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g},
|
||||
{'base':'v','letters':/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g},
|
||||
{'base':'vy','letters':/[\uA761]/g},
|
||||
{'base':'w','letters':/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g},
|
||||
{'base':'x','letters':/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g},
|
||||
{'base':'y','letters':/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g},
|
||||
{'base':'z','letters':/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g}
|
||||
];
|
||||
|
||||
for(var i=0; i<defaultDiacriticsRemovalMap.length; i++) {
|
||||
str = str.replace(defaultDiacriticsRemovalMap[i].letters, defaultDiacriticsRemovalMap[i].base);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function escapeFilename(s, maxLength = 32) {
|
||||
let output = removeDiacritics(s);
|
||||
output = output.replace("\n\r", " ");
|
||||
output = output.replace("\r\n", " ");
|
||||
output = output.replace("\r", " ");
|
||||
output = output.replace("\n", " ");
|
||||
output = output.replace("\t", " ");
|
||||
output = output.replace("\0", "");
|
||||
|
||||
const unsafe = "/\\:*\"'?<>|"; // In Windows
|
||||
for (let i = 0; i < unsafe.length; i++) {
|
||||
output = output.replace(unsafe[i], '_');
|
||||
}
|
||||
|
||||
if (output.toLowerCase() == 'nul') output = 'n_l'; // For Windows...
|
||||
|
||||
return output.substr(0, maxLength);
|
||||
}
|
||||
|
||||
function folderItemFilename(item) {
|
||||
let output = escapeFilename(item.title).trim();
|
||||
if (!output.length) output = '_';
|
||||
return output + '.' + item.id.substr(0, 7);
|
||||
}
|
||||
|
||||
export { removeDiacritics, escapeFilename, folderItemFilename };
|
@ -5,6 +5,10 @@ import { Folder } from 'src/models/folder.js';
|
||||
import { Note } from 'src/models/note.js';
|
||||
import { BaseModel } from 'src/base-model.js';
|
||||
import { promiseChain } from 'src/promise-chain.js';
|
||||
import moment from 'moment';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class Synchronizer {
|
||||
|
||||
@ -26,63 +30,213 @@ class Synchronizer {
|
||||
return this.api_;
|
||||
}
|
||||
|
||||
loadParentAndItem(change) {
|
||||
if (change.item_type == BaseModel.ITEM_TYPE_NOTE) {
|
||||
return Note.load(change.item_id).then((note) => {
|
||||
if (!note) return { parent:null, item: null };
|
||||
|
||||
return Folder.load(note.parent_id).then((folder) => {
|
||||
return Promise.resolve({ parent: folder, item: note });
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return Folder.load(change.item_id).then((folder) => {
|
||||
return Promise.resolve({ parent: null, item: folder });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
remoteFileByName(remoteFiles, name) {
|
||||
for (let i = 0; i < remoteFiles.length; i++) {
|
||||
if (remoteFiles[i].name == name) return remoteFiles[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
conflictDir(remoteFiles) {
|
||||
let d = this.remoteFileByName('Conflicts');
|
||||
if (!d) {
|
||||
return this.api().mkdir('Conflicts').then(() => {
|
||||
return 'Conflicts';
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve('Conflicts');
|
||||
}
|
||||
}
|
||||
|
||||
moveConflict(item) {
|
||||
// No need to handle folder conflicts
|
||||
if (item.isDir) return Promise.resolve();
|
||||
|
||||
return this.conflictDir().then((conflictDirPath) => {
|
||||
let p = path.basename(item.name).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);
|
||||
});
|
||||
}
|
||||
|
||||
processState_uploadChanges() {
|
||||
Change.all().then((changes) => {
|
||||
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 = [];
|
||||
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 = ItemClass.load(c.item_id).then((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);
|
||||
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.remoteFileByName(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);
|
||||
} else {
|
||||
return this.api().put(path, Note.toFriendlyString(result.item));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: handle UPDATE
|
||||
// TODO: handle DELETE
|
||||
|
||||
return p.then(() => {
|
||||
processedChangeIds = processedChangeIds.concat(c.ids);
|
||||
}).catch((error) => {
|
||||
Log.warn('Failed applying changes', c.ids, error.message, error.type);
|
||||
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;
|
||||
}
|
||||
// if (error.type == 'NotFoundException') {
|
||||
// processedChangeIds = processedChangeIds.concat(c.ids);
|
||||
// } else {
|
||||
// throw error;
|
||||
// }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return promiseChain(chain).catch((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);
|
||||
});
|
||||
// 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() {
|
||||
@ -150,9 +304,10 @@ class Synchronizer {
|
||||
this.state_ = state;
|
||||
|
||||
if (state == 'uploadChanges') {
|
||||
processState_uploadChanges();
|
||||
return this.processState_uploadChanges();
|
||||
} else if (state == 'downloadChanges') {
|
||||
processState_downloadChanges();
|
||||
return this.processState('idle');
|
||||
//this.processState_downloadChanges();
|
||||
} else if (state == 'idle') {
|
||||
// Nothing
|
||||
} else {
|
||||
@ -168,12 +323,12 @@ class Synchronizer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.api().session()) {
|
||||
Log.info("Sync: cannot start synchronizer because user is not logged in.");
|
||||
return;
|
||||
}
|
||||
// if (!this.api().session()) {
|
||||
// Log.info("Sync: cannot start synchronizer because user is not logged in.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.processState('uploadChanges');
|
||||
return this.processState('uploadChanges');
|
||||
}
|
||||
|
||||
}
|
||||
|
224
ReactNativeClient/src/synchronizer_old.js
Normal file
224
ReactNativeClient/src/synchronizer_old.js
Normal file
@ -0,0 +1,224 @@
|
||||
import { Log } from 'src/log.js';
|
||||
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 { BaseModel } from 'src/base-model.js';
|
||||
import { promiseChain } from 'src/promise-chain.js';
|
||||
|
||||
class Synchronizer {
|
||||
|
||||
constructor(db, api) {
|
||||
this.state_ = 'idle';
|
||||
this.db_ = db;
|
||||
this.api_ = api;
|
||||
}
|
||||
|
||||
state() {
|
||||
return this.state_;
|
||||
}
|
||||
|
||||
db() {
|
||||
return this.db_;
|
||||
}
|
||||
|
||||
api() {
|
||||
return this.api_;
|
||||
}
|
||||
|
||||
loadParentAndItem(change) {
|
||||
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 });
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return Folder.load(change.item_id).then((folder) => {
|
||||
return Promise.resolve({ parent: null, item: folder });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
processState_uploadChanges() {
|
||||
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() {
|
||||
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;
|
||||
|
||||
if (state == 'uploadChanges') {
|
||||
this.processState_uploadChanges();
|
||||
} else if (state == 'downloadChanges') {
|
||||
this.processState('idle');
|
||||
//this.processState_downloadChanges();
|
||||
} else if (state == 'idle') {
|
||||
// Nothing
|
||||
} else {
|
||||
throw new Error('Invalid state: ' . state);
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
Log.info('Sync: start');
|
||||
|
||||
if (this.state() != 'idle') {
|
||||
Log.info("Sync: cannot start synchronizer because synchronization already in progress. State: " + this.state());
|
||||
return;
|
||||
}
|
||||
|
||||
// if (!this.api().session()) {
|
||||
// Log.info("Sync: cannot start synchronizer because user is not logged in.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.processState('uploadChanges');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Synchronizer };
|
@ -3,6 +3,7 @@ import { isNode } from 'src/env.js';
|
||||
import { stringify } from 'query-string';
|
||||
|
||||
if (isNode()) {
|
||||
// TODO: doesn't work in React-Native - FormData gets set to "undefined"
|
||||
// Needs to be in a variable otherwise ReactNative will try to load this module (and fails due to
|
||||
// missing node modules), even if isNode() is false.
|
||||
let modulePath = 'src/shim.js';
|
||||
@ -29,6 +30,7 @@ class WebApi {
|
||||
constructor(baseUrl) {
|
||||
this.baseUrl_ = baseUrl;
|
||||
this.session_ = null;
|
||||
this.retryInterval_ = 500;
|
||||
}
|
||||
|
||||
setSession(v) {
|
||||
@ -88,8 +90,33 @@ class WebApi {
|
||||
return cmd.join(' ');
|
||||
}
|
||||
|
||||
exec(method, path, query, data) {
|
||||
delay(milliseconds) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, milliseconds);
|
||||
});
|
||||
}
|
||||
|
||||
exec(method, path, query, data) {
|
||||
return this.execNoRetry(method, path, query, data).then((data) => {
|
||||
this.retryInterval_ = 500;
|
||||
return data;
|
||||
}).catch((error) => {
|
||||
if (error.errno == 'ECONNRESET') {
|
||||
this.retryInterval_ += 500;
|
||||
console.warn('Got error ' + error.errno + '. Retrying in ' + this.retryInterval_);
|
||||
return this.delay(this.retryInterval_).then(() => {
|
||||
return this.exec(method, path, query, data);
|
||||
});
|
||||
} else {
|
||||
this.retryInterval_ = 500;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
execNoRetry(method, path, query, data) {
|
||||
if (this.session_) {
|
||||
query = query ? Object.assign({}, query) : {};
|
||||
if (!query.session) query.session = this.session_;
|
||||
@ -97,34 +124,29 @@ class WebApi {
|
||||
|
||||
let r = this.makeRequest(method, path, query, data);
|
||||
|
||||
//Log.debug(WebApi.toCurl(r, data));
|
||||
Log.debug(WebApi.toCurl(r, data));
|
||||
//console.info(WebApi.toCurl(r, data));
|
||||
|
||||
fetch(r.url, r.options).then(function(response) {
|
||||
return fetch(r.url, r.options).then((response) => {
|
||||
let responseClone = response.clone();
|
||||
|
||||
if (!response.ok) {
|
||||
return responseClone.text().then(function(text) {
|
||||
reject(new WebApiError('HTTP ' + response.status + ': ' + response.statusText + ': ' + text));
|
||||
return responseClone.text().then((text) => {
|
||||
throw new WebApiError('HTTP ' + response.status + ': ' + response.statusText + ': ' + text);
|
||||
});
|
||||
}
|
||||
|
||||
return response.json().then(function(data) {
|
||||
return response.json().then((data) => {
|
||||
if (data && data.error) {
|
||||
reject(new WebApiError(data));
|
||||
throw new WebApiError(data);
|
||||
} else {
|
||||
resolve(data);
|
||||
return data;
|
||||
}
|
||||
}).catch(function(error) {
|
||||
responseClone.text().then(function(text) {
|
||||
reject(new WebApiError('Cannot parse JSON: ' + text));
|
||||
}).catch((error) => {
|
||||
return responseClone.text().then((text) => {
|
||||
throw new WebApiError('Cannot parse JSON: ' + text);
|
||||
});
|
||||
});
|
||||
}).then(function(data) {
|
||||
resolve(data);
|
||||
}).catch(function(error) {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,6 @@
|
||||
"sensio/distribution-bundle": "^5.0",
|
||||
"sensio/framework-extra-bundle": "^3.0.2",
|
||||
"incenteev/composer-parameter-handler": "^2.0",
|
||||
|
||||
"illuminate/database": "*",
|
||||
"yetanotherape/diff-match-patch": "*"
|
||||
},
|
||||
|
@ -15,11 +15,13 @@ class SynchronizerController extends ApiController {
|
||||
* @Route("/synchronizer")
|
||||
*/
|
||||
public function allAction(Request $request) {
|
||||
$lastChangeId = (int)$request->query->get('rev_id');
|
||||
|
||||
if (!$this->user() || !$this->session()) throw new UnauthorizedException();
|
||||
|
||||
$actions = Change::changesDoneAfterId($this->user()->id, $this->session()->client_id, $lastChangeId);
|
||||
$lastChangeId = (int)$request->query->get('rev_id');
|
||||
$limit = (int)$request->query->get('limit');
|
||||
//$curl 'http://192.168.1.3/synchronizer?rev_id=6973&session=02d0e9ca42cbbc2d35efb1bc790b9eec'
|
||||
|
||||
$actions = Change::changesDoneAfterId($this->user()->id, $this->session()->client_id, $lastChangeId, $limit);
|
||||
return static::successResponse($actions);
|
||||
}
|
||||
|
||||
|
@ -14,11 +14,26 @@ class Diff {
|
||||
return self::$dmp_;
|
||||
}
|
||||
|
||||
// Temporary fix to go around diffmatchpach bug:
|
||||
// https://github.com/yetanotherape/diff-match-patch/issues/9
|
||||
static private function encodingFix($s) {
|
||||
return $s;
|
||||
return iconv('UTF-8', 'ISO-8859-1//IGNORE', $s);
|
||||
}
|
||||
|
||||
static public function decodeFix($s) {
|
||||
return $s;
|
||||
return iconv('ISO-8859-1', 'UTF-8', $s);
|
||||
}
|
||||
|
||||
static public function diff($from, $to) {
|
||||
$from = self::encodingFix($from);
|
||||
$to = self::encodingFix($to);
|
||||
return self::dmp()->patch_toText(self::dmp()->patch_make($from, $to));
|
||||
}
|
||||
|
||||
static public function patch($from, $diff) {
|
||||
$from = self::encodingFix($from);
|
||||
return self::dmp()->patch_apply(self::dmp()->patch_fromText($diff), $from);
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ class Change extends BaseModel {
|
||||
'type' => array('create', 'update', 'delete'),
|
||||
);
|
||||
|
||||
static public function changesDoneAfterId($userId, $clientId, $fromChangeId) {
|
||||
static public function changesDoneAfterId($userId, $clientId, $fromChangeId, $limit = null) {
|
||||
// Simplification:
|
||||
//
|
||||
// - If create, update, delete => return nothing
|
||||
@ -19,7 +19,8 @@ class Change extends BaseModel {
|
||||
// - If update, update, update => return last
|
||||
|
||||
// $limit = 10000;
|
||||
$limit = 100;
|
||||
if ((int)$limit <= 0) $limit = 100;
|
||||
if ($limit > 1000) $limit = 1000;
|
||||
$changes = self::where('id', '>', $fromChangeId)
|
||||
->where('user_id', '=', $userId)
|
||||
->where('client_id', '!=', $clientId)
|
||||
|
@ -6,6 +6,19 @@ require_once dirname(__FILE__) . '/TestUtils.php';
|
||||
require_once dirname(__FILE__) . '/BaseTestCase.php';
|
||||
require_once dirname(__FILE__) . '/BaseControllerTestCase.php';
|
||||
|
||||
|
||||
|
||||
// use DiffMatchPatch\DiffMatchPatch;
|
||||
// $dmp = new DiffMatchPatch();
|
||||
// $diff = $dmp->patch_make('car', 'car 🚘');
|
||||
// var_dump($dmp->patch_toText($diff));
|
||||
// var_dump($dmp->patch_apply($diff, 'car'));
|
||||
|
||||
// //$dmp->patch_toText($dmp->patch_make($from, $to));
|
||||
// die();
|
||||
|
||||
|
||||
|
||||
$dbConfig = array(
|
||||
'dbName' => 'notes_test',
|
||||
'user' => 'root',
|
||||
|
@ -31,6 +31,7 @@ try {
|
||||
'error' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
'type' => $errorType,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
);
|
||||
if ($errorType == 'NotFoundHttpException') {
|
||||
header('HTTP/1.1 404 Not found');
|
||||
|
Loading…
Reference in New Issue
Block a user