1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-02 12:47:41 +02:00

Various changes

This commit is contained in:
Laurent Cozic 2017-06-11 22:11:14 +01:00
parent 5b9d50947c
commit 9cec2652e7
37 changed files with 1728 additions and 270 deletions

3
.gitignore vendored
View File

@ -29,4 +29,5 @@ app/data/uploads/
sparse_test.php sparse_test.php
INFO.md INFO.md
/web/env.php /web/env.php
sync_staging.sh sync_staging.sh
*.swp

211
CliClient/app/cmd.js Normal file
View 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();
});

View 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();

View File

@ -4,6 +4,7 @@ import { uuid } from 'src/uuid.js';
import moment from 'moment'; import moment from 'moment';
import { promiseChain } from 'src/promise-chain.js'; import { promiseChain } from 'src/promise-chain.js';
import { WebApi } from 'src/web-api.js' import { WebApi } from 'src/web-api.js'
import { folderItemFilename } from 'src/string-utils.js'
import jsSHA from "jssha"; import jsSHA from "jssha";
let webApi = new WebApi('http://joplin.local'); let webApi = new WebApi('http://joplin.local');
@ -519,13 +520,124 @@ function saveNoteToWebApi(note) {
delete data.tags; delete data.tags;
webApi.post('notes', null, data).then((r) => { webApi.post('notes', null, data).then((r) => {
console.info(r); //console.info(r);
}).catch((error) => { }).catch((error) => {
console.error("Error for note: " + note.title); console.error("Error for note: " + note.title);
console.error(error); 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) { function createNoteId(note) {
let shaObj = new jsSHA("SHA-256", "TEXT"); let shaObj = new jsSHA("SHA-256", "TEXT");
shaObj.update(note.title + '_' + note.body + "_" + note.created_time + "_" + note.updated_time + "_"); shaObj.update(note.title + '_' + note.body + "_" + note.created_time + "_" + note.updated_time + "_");
@ -533,7 +645,7 @@ function createNoteId(note) {
return hash.substr(0, 32); return hash.substr(0, 32);
} }
function importEnex(parentId, stream) { function importEnex(parentFolder, stream) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let options = {}; let options = {};
let strict = true; let strict = true;
@ -576,10 +688,16 @@ function importEnex(parentId, stream) {
firstAttachment = false; firstAttachment = false;
} }
note.parent_id = parentId; note.parent_id = parentFolder.id;
note.body = processMdArrayNewLines(result.lines); 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 ============================================================================'); // console.info('======== NOTE ============================================================================');
// let c = note.content; // let c = note.content;
@ -669,7 +787,7 @@ function importEnex(parentId, stream) {
if (notes.length >= 10) { if (notes.length >= 10) {
stream.pause(); stream.pause();
processNotes().then(() => { processNotes().then(() => {
//stream.resume(); stream.resume();
}).catch((error) => { }).catch((error) => {
console.info('Error processing note', error); console.info('Error processing note', error);
}); });
@ -722,8 +840,8 @@ function importEnex(parentId, stream) {
// TODO: make it persistent and random // TODO: make it persistent and random
const clientId = 'AB78AB78AB78AB78AB78AB78AB78AB78'; const clientId = 'AB78AB78AB78AB78AB78AB78AB78AB78';
//const folderTitle = 'Laurent'; const folderTitle = 'Laurent';
const folderTitle = 'Voiture'; //const folderTitle = 'Voiture';
webApi.post('sessions', null, { webApi.post('sessions', null, {
email: 'laurent@cozic.net', email: 'laurent@cozic.net',
@ -745,11 +863,15 @@ webApi.post('sessions', null, {
} }
return folder ? Promise.resolve(folder) : webApi.post('folders', null, { title: folderTitle }); return folder ? Promise.resolve(folder) : webApi.post('folders', null, { title: folderTitle });
}).then((folder) => {
return saveFolderToDisk(folder).then(() => {
return folder;
});
}).then((folder) => { }).then((folder) => {
let fileStream = fs.createReadStream('/mnt/c/Users/Laurent/Desktop/' + folderTitle + '.enex'); 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/afaire.enex');
//let fileStream = fs.createReadStream('/mnt/c/Users/Laurent/Desktop/testtags.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'); //console.info('DONE IMPORTING');
}).catch((error) => { }).catch((error) => {
console.error('Cannot import', error); console.error('Cannot import', error);

View File

@ -5,14 +5,19 @@
"dependencies": { "dependencies": {
"app-module-path": "^2.2.0", "app-module-path": "^2.2.0",
"form-data": "^2.1.4", "form-data": "^2.1.4",
"fs-extra": "^3.0.1",
"jssha": "^2.3.0", "jssha": "^2.3.0",
"mkdirp": "^0.5.1",
"moment": "^2.18.1", "moment": "^2.18.1",
"node-fetch": "^1.7.1", "node-fetch": "^1.7.1",
"promise": "^7.1.1", "promise": "^7.1.1",
"react": "16.0.0-alpha.6", "react": "16.0.0-alpha.6",
"sax": "^1.2.2", "sax": "^1.2.2",
"sprintf-js": "^1.1.1",
"sqlite3": "^3.1.8",
"string-to-stream": "^1.1.0", "string-to-stream": "^1.1.0",
"uuid": "^3.0.1" "uuid": "^3.0.1",
"vorpal": "^1.12.0"
}, },
"devDependencies": { "devDependencies": {
"babel-changed": "^7.0.0", "babel-changed": "^7.0.0",
@ -22,7 +27,8 @@
"query-string": "4.3.4" "query-string": "4.3.4"
}, },
"scripts": { "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" "clean": "babel-changed --reset"
} }
} }

View File

@ -4,4 +4,6 @@ CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
rm -f "$CLIENT_DIR/app/src" rm -f "$CLIENT_DIR/app/src"
ln -s "$CLIENT_DIR/../ReactNativeClient/src" "$CLIENT_DIR/app" 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

View File

@ -126,6 +126,7 @@ android {
} }
dependencies { dependencies {
compile project(':react-native-fs')
compile fileTree(dir: "libs", include: ["*.jar"]) compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1" compile "com.android.support:appcompat-v7:23.0.1"
compile "com.facebook.react:react-native:+" // From node_modules compile "com.facebook.react:react-native:+" // From node_modules

View File

@ -3,6 +3,7 @@ package com.awesomeproject;
import android.app.Application; import android.app.Application;
import com.facebook.react.ReactApplication; import com.facebook.react.ReactApplication;
import com.rnfs.RNFSPackage;
import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage; import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage; import com.facebook.react.shell.MainReactPackage;
@ -24,7 +25,8 @@ public class MainApplication extends Application implements ReactApplication {
protected List<ReactPackage> getPackages() { protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList( return Arrays.<ReactPackage>asList(
new SQLitePluginPackage(), new SQLitePluginPackage(),
new MainReactPackage() new MainReactPackage(),
new RNFSPackage()
); );
} }
}; };

View File

@ -1,4 +1,6 @@
rootProject.name = 'AwesomeProject' rootProject.name = 'AwesomeProject'
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
include ':app' include ':app'

View File

@ -5,7 +5,6 @@
}; };
objectVersion = 46; objectVersion = 46;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.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 */; }; 2DCD954D1E0B4F2C00145EB5 /* AwesomeProjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* AwesomeProjectTests.m */; };
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; }; 5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy 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>"; }; 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>"; }; 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>"; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -281,6 +283,7 @@
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */,
00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */,
139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */,
EA51DDC9EBFC469F8214B3AD /* libRNFS.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -447,6 +450,7 @@
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */,
00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */,
139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */,
8C2AA97067234408AD5BFD90 /* RNFS.xcodeproj */,
); );
name = Libraries; name = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@ -564,7 +568,7 @@
83CBB9F71A601CBA00E9B192 /* Project object */ = { 83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 0610; LastUpgradeCheck = 610;
ORGANIZATIONNAME = Facebook; ORGANIZATIONNAME = Facebook;
TargetAttributes = { TargetAttributes = {
00E356ED1AD99517003FC87E = { 00E356ED1AD99517003FC87E = {
@ -972,6 +976,14 @@
); );
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject.app/AwesomeProject"; 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; name = Debug;
}; };
@ -989,6 +1001,14 @@
); );
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject.app/AwesomeProject"; 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; name = Release;
}; };
@ -1007,6 +1027,10 @@
); );
PRODUCT_NAME = AwesomeProject; PRODUCT_NAME = AwesomeProject;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/../node_modules/react-native-fs/**",
);
}; };
name = Debug; name = Debug;
}; };
@ -1024,6 +1048,10 @@
); );
PRODUCT_NAME = AwesomeProject; PRODUCT_NAME = AwesomeProject;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/../node_modules/react-native-fs/**",
);
}; };
name = Release; name = Release;
}; };
@ -1050,6 +1078,14 @@
SDKROOT = appletvos; SDKROOT = appletvos;
TARGETED_DEVICE_FAMILY = 3; TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 9.2; TVOS_DEPLOYMENT_TARGET = 9.2;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/../node_modules/react-native-fs/**",
);
}; };
name = Debug; name = Debug;
}; };
@ -1076,6 +1112,14 @@
SDKROOT = appletvos; SDKROOT = appletvos;
TARGETED_DEVICE_FAMILY = 3; TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 9.2; TVOS_DEPLOYMENT_TARGET = 9.2;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/../node_modules/react-native-fs/**",
);
}; };
name = Release; name = Release;
}; };
@ -1097,6 +1141,10 @@
SDKROOT = appletvos; SDKROOT = appletvos;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject-tvOS.app/AwesomeProject-tvOS"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject-tvOS.app/AwesomeProject-tvOS";
TVOS_DEPLOYMENT_TARGET = 10.1; TVOS_DEPLOYMENT_TARGET = 10.1;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
}; };
name = Debug; name = Debug;
}; };
@ -1118,6 +1166,10 @@
SDKROOT = appletvos; SDKROOT = appletvos;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject-tvOS.app/AwesomeProject-tvOS"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AwesomeProject-tvOS.app/AwesomeProject-tvOS";
TVOS_DEPLOYMENT_TARGET = 10.1; TVOS_DEPLOYMENT_TARGET = 10.1;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
}; };
name = Release; name = Release;
}; };

View File

@ -7,12 +7,15 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"dropbox": "^2.5.4",
"form-data": "^2.1.4", "form-data": "^2.1.4",
"moment": "^2.18.1",
"node-fetch": "^1.7.1", "node-fetch": "^1.7.1",
"react": "16.0.0-alpha.6", "react": "16.0.0-alpha.6",
"react-native": "0.44.0", "react-native": "0.44.0",
"react-native-action-button": "^2.6.9", "react-native-action-button": "^2.6.9",
"react-native-checkbox": "^1.1.0", "react-native-checkbox": "^1.1.0",
"react-native-fs": "^2.3.3",
"react-native-popup-menu": "^0.7.4", "react-native-popup-menu": "^0.7.4",
"react-native-side-menu": "^0.20.1", "react-native-side-menu": "^0.20.1",
"react-native-vector-icons": "^2.0.3", "react-native-vector-icons": "^2.0.3",

View File

@ -72,7 +72,12 @@ class BaseModel {
} }
static load(id) { 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) { static applyPatch(model, patch) {
@ -143,43 +148,44 @@ class BaseModel {
options = this.modOptions(options); options = this.modOptions(options);
let isNew = options.isNew == 'auto' ? !o.id : options.isNew; let isNew = options.isNew == 'auto' ? !o.id : options.isNew;
let query = this.saveQuery(o, isNew);
return this.db().transaction((tx) => { let queries = [];
tx.executeSql(query.sql, query.params); let saveQuery = this.saveQuery(o, isNew);
let itemId = saveQuery.id;
if (options.trackChanges && this.trackChanges()) { queries.push(saveQuery);
// Cannot import this class the normal way due to cyclical dependencies between Change and BaseModel
// which are not handled by React Native. if (options.trackChanges && this.trackChanges()) {
const { Change } = require('src/models/change.js'); // Cannot import this class the normal way due to cyclical dependencies between Change and BaseModel
// which are not handled by React Native.
const { Change } = require('src/models/change.js');
if (isNew) {
let change = Change.newChange();
change.type = Change.TYPE_CREATE;
change.item_id = itemId;
change.item_type = this.itemType();
queries.push(Change.saveQuery(change));
} else {
for (let n in o) {
if (!o.hasOwnProperty(n)) continue;
if (n == 'id') continue;
if (isNew) {
let change = Change.newChange(); let change = Change.newChange();
change.type = Change.TYPE_CREATE; change.type = Change.TYPE_UPDATE;
change.item_id = query.id; change.item_id = itemId;
change.item_type = this.itemType(); change.item_type = this.itemType();
change.item_field = n;
let changeQuery = Change.saveQuery(change); queries.push(Change.saveQuery(change));
tx.executeSql(changeQuery.sql, changeQuery.params);
} else {
for (let n in o) {
if (!o.hasOwnProperty(n)) continue;
if (n == 'id') continue;
let change = Change.newChange();
change.type = Change.TYPE_UPDATE;
change.item_id = query.id;
change.item_type = this.itemType();
change.item_field = n;
let changeQuery = Change.saveQuery(change);
tx.executeSql(changeQuery.sql, changeQuery.params);
}
} }
} }
}).then((r) => { }
return this.db().transactionExecBatch(queries).then(() => {
o = Object.assign({}, o); o = Object.assign({}, o);
o.id = query.id; o.id = itemId;
return o; return o;
}).catch((error) => { }).catch((error) => {
Log.error('Cannot save model', error); Log.error('Cannot save model', error);
@ -220,5 +226,6 @@ BaseModel.ITEM_TYPE_FOLDER = 2;
BaseModel.tableInfo_ = null; BaseModel.tableInfo_ = null;
BaseModel.tableKeys_ = null; BaseModel.tableKeys_ = null;
BaseModel.db_ = null; BaseModel.db_ = null;
BaseModel.dispatch = function(o) {};
export { BaseModel }; export { BaseModel };

View File

@ -17,10 +17,6 @@ const styles = StyleSheet.create({
class ScreenHeaderComponent extends Component { class ScreenHeaderComponent extends Component {
static defaultProps = {
menuOptions: [],
};
showBackButton() { showBackButton() {
// Note: this is hardcoded for now because navigation.state doesn't tell whether // 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 // 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( const ScreenHeader = connect(
(state) => { (state) => {
return { user: state.user }; return { user: state.user };

View File

@ -61,7 +61,7 @@ class LoginScreenComponent extends React.Component {
Registry.api().setSession(session.id); Registry.api().setSession(session.id);
Registry.synchronizer().start(); //Registry.synchronizer().start();
}).catch((error) => { }).catch((error) => {
this.setState({ errorMessage: _('Could not login: %s)', error.message) }); this.setState({ errorMessage: _('Could not login: %s)', error.message) });
}); });

View 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 };

View 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 }

View File

@ -1,4 +1,3 @@
import SQLite from 'react-native-sqlite-storage';
import { Log } from 'src/log.js'; import { Log } from 'src/log.js';
import { uuid } from 'src/uuid.js'; import { uuid } from 'src/uuid.js';
import { promiseChain } from 'src/promise-chain.js'; import { promiseChain } from 'src/promise-chain.js';
@ -94,14 +93,15 @@ INSERT INTO version (version) VALUES (1);
class Database { class Database {
constructor() { constructor(driver) {
this.debugMode_ = false; this.debugMode_ = false;
this.initialized_ = false; this.initialized_ = false;
this.tableFields_ = null; this.tableFields_ = null;
this.driver_ = driver;
} }
setDebugEnabled(v) { setDebugEnabled(v) {
SQLite.DEBUG(v); this.driver_.setDebugEnabled(v);
this.debugMode_ = v; this.debugMode_ = v;
} }
@ -113,14 +113,43 @@ class Database {
return this.initialized_; return this.initialized_;
} }
open() { driver() {
this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-32.sqlite' }, (db) => { return this.driver_;
}
open(options) {
return this.driver().open(options).then((db) => {
Log.info('Database was open successfully'); Log.info('Database was open successfully');
}, (error) => { return this.initialize();
}).catch((error) => {
Log.error('Cannot open database: ', 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) { static enumId(type, s) {
@ -177,41 +206,7 @@ class Database {
logQuery(sql, params = null) { logQuery(sql, params = null) {
if (!this.debugMode()) return; if (!this.debugMode()) return;
//Log.debug('DB: ' + sql, params); 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);
} }
static insertQuery(tableName, data) { static insertQuery(tableName, data) {
@ -254,29 +249,53 @@ class Database {
} }
transaction(readyCallack) { transaction(readyCallack) {
return new Promise((resolve, reject) => { throw new Error('transaction() DEPRECATED');
this.db_.transaction( // return new Promise((resolve, reject) => {
readyCallack, // this.db_.transaction(
(error) => { reject(error); }, // readyCallack,
() => { resolve(); } // (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() { 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 = []; let chain = [];
for (let i = 0; i < tableResults.rows.length; i++) { for (let i = 0; i < tableRows.length; i++) {
let row = tableResults.rows.item(i); let tableName = tableRows[i].name;
let tableName = row.name;
if (tableName == 'android_metadata') continue; if (tableName == 'android_metadata') continue;
if (tableName == 'table_fields') continue; if (tableName == 'table_fields') continue;
chain.push(() => {
chain.push((queries) => { return this.selectAll('PRAGMA table_info("' + tableName + '")').then((pragmas) => {
if (!queries) queries = []; for (let i = 0; i < pragmas.length; i++) {
return this.exec('PRAGMA table_info("' + tableName + '")').then((pragmaResult) => { let item = pragmas[i];
for (let i = 0; i < pragmaResult.rows.length; i++) {
let item = pragmaResult.rows.item(i);
// In SQLite, if the default value is a string it has double quotes around it, so remove them here // In SQLite, if the default value is a string it has double quotes around it, so remove them here
let defaultValue = item.dflt_value; let defaultValue = item.dflt_value;
if (typeof defaultValue == 'string' && defaultValue.length >= 2 && defaultValue[0] == '"' && defaultValue[defaultValue.length - 1] == '"') { if (typeof defaultValue == 'string' && defaultValue.length >= 2 && defaultValue[0] == '"' && defaultValue[defaultValue.length - 1] == '"') {
@ -290,19 +309,13 @@ class Database {
}); });
queries.push(q); queries.push(q);
} }
return queries;
}); });
}); });
} }
return promiseChain(chain); return promiseChain(chain);
}).then((queries) => { }).then(() => {
return this.transaction((tx) => { return this.transactionExecBatch(queries);
tx.executeSql('DELETE FROM table_fields');
for (let i = 0; i < queries.length; i++) {
tx.executeSql(queries[i].sql, queries[i].params);
}
});
}); });
} }
@ -314,12 +327,13 @@ class Database {
// TODO: version update logic // TODO: version update logic
// TODO: only do this if db has been updated: // TODO: only do this if db has been updated:
return this.refreshTableFields(); // return this.refreshTableFields();
}).then(() => { }).then(() => {
return this.exec('SELECT * FROM table_fields').then((r) => { this.tableFields_ = {};
this.tableFields_ = {};
for (let i = 0; i < r.rows.length; i++) { return this.selectAll('SELECT * FROM table_fields').then((rows) => {
let row = r.rows.item(i); for (let i = 0; i < rows.length; i++) {
let row = rows[i];
if (!this.tableFields_[row.table_name]) this.tableFields_[row.table_name] = []; if (!this.tableFields_[row.table_name]) this.tableFields_[row.table_name] = [];
this.tableFields_[row.table_name].push({ this.tableFields_[row.table_name].push({
name: row.field_name, name: row.field_name,
@ -329,7 +343,6 @@ class Database {
} }
}); });
// }).then(() => { // }).then(() => {
// let p = this.exec('DELETE FROM notes').then(() => { // let p = this.exec('DELETE FROM notes').then(() => {
@ -348,11 +361,9 @@ class Database {
// return p; // return p;
}).catch((error) => { }).catch((error) => {
if (error && error.code != 0) { //console.info(error.code);
if (error && error.code != 0 && error.code != 'SQLITE_ERROR') {
Log.error(error); Log.error(error);
return; return;
} }
@ -364,19 +375,18 @@ class Database {
Log.info('Database is new - creating the schema...'); Log.info('Database is new - creating the schema...');
let statements = this.sqlStringToLines(structureSql) let queries = this.wrapQueries(this.sqlStringToLines(structureSql));
return this.transaction((tx) => { queries.push(this.wrapQuery('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumId('settings', 'string') + '")'));
for (let i = 0; i < statements.length; i++) { 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) + ')'));
tx.executeSql(statements[i]);
} return this.transactionExecBatch(queries).then(() => {
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(() => {
Log.info('Database schema created successfully'); Log.info('Database schema created successfully');
// Calling initialize() now that the db has been created will make it go through // Calling initialize() now that the db has been created will make it go through
// the normal db update process (applying any additional patch). // the normal db update process (applying any additional patch).
return this.refreshTableFields();
}).then(() => {
return this.initialize(); return this.initialize();
}) });
}); });
} }

View 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 };

View 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 };

View File

@ -16,6 +16,11 @@ class Geolocation {
} }
static currentPosition(options = null) { static currentPosition(options = null) {
if (typeof navigator === 'undefined') {
// TODO
return Promise.resolve(this.currentPosition_testResponse());
}
if (!options) options = {}; if (!options) options = {};
if (!('enableHighAccuracy' in options)) options.enableHighAccuracy = true; if (!('enableHighAccuracy' in options)) options.enableHighAccuracy = true;
if (!('timeout' in options)) options.timeout = 10000; if (!('timeout' in options)) options.timeout = 10000;

View File

@ -7,12 +7,12 @@ class Log {
} }
static level() { static level() {
return this.level_ === undefined ? Log.LEVEL_ERROR : this.level_; return this.level_ === undefined ? Log.LEVEL_DEBUG : this.level_;
} }
static debug(...o) { static debug(...o) {
if (Log.level() > Log.LEVEL_DEBUG) return; if (Log.level() > Log.LEVEL_DEBUG) return;
console.debug(...o); console.info(...o);
} }
static info(...o) { static info(...o) {

View File

@ -18,24 +18,20 @@ class Change extends BaseModel {
} }
static all() { static all() {
return this.db().selectAll('SELECT * FROM changes').then((r) => { return this.db().selectAll('SELECT * FROM changes');
let output = [];
for (let i = 0; i < r.rows.length; i++) {
output.push(r.rows.item(i));
}
return output;
});
} }
static deleteMultiple(ids) { static deleteMultiple(ids) {
if (ids.length == 0) return Promise.resolve(); if (ids.length == 0) return Promise.resolve();
return this.db().transaction((tx) => { console.warn('TODO: deleteMultiple: CHECK THAT IT WORKS');
let sql = '';
for (let i = 0; i < ids.length; i++) { let queries = [];
tx.executeSql('DELETE FROM changes WHERE id = ?', [ids[i]]); for (let i = 0; i < ids.length; i++) {
} queries.push(['DELETE FROM changes WHERE id = ?', [ids[i]]]);
}); }
return this.db().transactionExecBatch(queries);
} }
static mergeChanges(changes) { static mergeChanges(changes) {
@ -45,6 +41,7 @@ class Change extends BaseModel {
for (let i = 0; i < changes.length; i++) { for (let i = 0; i < changes.length; i++) {
let change = changes[i]; let change = changes[i];
let mergedChange = null;
if (itemChanges[change.item_id]) { if (itemChanges[change.item_id]) {
mergedChange = itemChanges[change.item_id]; mergedChange = itemChanges[change.item_id];

View File

@ -2,6 +2,7 @@ import { BaseModel } from 'src/base-model.js';
import { Log } from 'src/log.js'; import { Log } from 'src/log.js';
import { promiseChain } from 'src/promise-chain.js'; import { promiseChain } from 'src/promise-chain.js';
import { Note } from 'src/models/note.js'; import { Note } from 'src/models/note.js';
import { folderItemFilename } from 'src/string-utils.js'
import { _ } from 'src/locale.js'; import { _ } from 'src/locale.js';
class Folder extends BaseModel { class Folder extends BaseModel {
@ -10,6 +11,14 @@ class Folder extends BaseModel {
return 'folders'; return 'folders';
} }
static filename(folder) {
return folderItemFilename(folder);
}
static systemPath(parent, folder) {
return this.filename(folder);
}
static useUuid() { static useUuid() {
return true; return true;
} }
@ -21,7 +30,7 @@ class Folder extends BaseModel {
static trackChanges() { static trackChanges() {
return true; return true;
} }
static newFolder() { static newFolder() {
return { return {
id: null, id: null,
@ -30,10 +39,10 @@ class Folder extends BaseModel {
} }
static noteIds(id) { 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 = []; let output = [];
for (let i = 0; i < r.rows.length; i++) { for (let i = 0; i < rows.length; i++) {
let row = r.rows.item(i); let row = rows[i];
output.push(row.id); output.push(row.id);
} }
return output; return output;
@ -66,23 +75,25 @@ class Folder extends BaseModel {
}); });
} }
static loadNoteByField(folderId, field, value) {
return this.db().selectOne('SELECT * FROM notes WHERE `parent_id` = ? AND `' + field + '` = ?', [folderId, value]);
}
static all() { static all() {
return this.db().selectAll('SELECT * FROM folders').then((r) => { return this.db().selectAll('SELECT * FROM folders');
let output = [];
for (let i = 0; i < r.rows.length; i++) {
output.push(r.rows.item(i));
}
return output;
});
} }
static save(o, options = null) { static save(o, options = null) {
return super.save(o, options).then((folder) => { return Folder.loadByField('title', o.title).then((existingFolder) => {
this.dispatch({ if (existingFolder && existingFolder.id != o.id) throw new Error(_('A folder with title "%s" already exists', o.title));
type: 'FOLDERS_UPDATE_ONE',
folder: folder, return super.save(o, options).then((folder) => {
this.dispatch({
type: 'FOLDERS_UPDATE_ONE',
folder: folder,
});
return folder;
}); });
return folder;
}); });
} }

View File

@ -1,6 +1,9 @@
import { BaseModel } from 'src/base-model.js'; import { BaseModel } from 'src/base-model.js';
import { Log } from 'src/log.js'; import { Log } from 'src/log.js';
import { Folder } from 'src/models/folder.js';
import { Geolocation } from 'src/geolocation.js'; import { Geolocation } from 'src/geolocation.js';
import { folderItemFilename } from 'src/string-utils.js'
import moment from 'moment';
class Note extends BaseModel { class Note extends BaseModel {
@ -8,6 +11,42 @@ class Note extends BaseModel {
return 'notes'; 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() { static useUuid() {
return true; return true;
} }
@ -37,13 +76,7 @@ class Note extends BaseModel {
} }
static previews(parentId) { static previews(parentId) {
return this.db().selectAll('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE parent_id = ?', [parentId]).then((r) => { return this.db().selectAll('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE parent_id = ?', [parentId]);
let output = [];
for (let i = 0; i < r.rows.length; i++) {
output.push(r.rows.item(i));
}
return output;
});
} }
static preview(noteId) { static preview(noteId) {

View File

@ -27,10 +27,8 @@ class Setting extends BaseModel {
static load() { static load() {
this.cache_ = []; this.cache_ = [];
return this.db().selectAll('SELECT * FROM settings').then((r) => { return this.db().selectAll('SELECT * FROM settings').then((rows) => {
for (let i = 0; i < r.rows.length; i++) { this.cache_ = rows;
this.cache_.push(r.rows.item(i));
}
}); });
} }
@ -89,13 +87,13 @@ class Setting extends BaseModel {
clearTimeout(this.updateTimeoutId_); clearTimeout(this.updateTimeoutId_);
this.updateTimeoutId_ = null; this.updateTimeoutId_ = null;
return BaseModel.db().transaction((tx) => { let queries = [];
tx.executeSql('DELETE FROM settings'); queries.push('DELETE FROM settings');
for (let i = 0; i < this.cache_.length; i++) { for (let i = 0; i < this.cache_.length; i++) {
let q = Database.insertQuery(this.tableName(), this.cache_[i]); queries.push(Database.insertQuery(this.tableName(), this.cache_[i]));
tx.executeSql(q.sql, q.params); }
}
}).then(() => { return BaseModel.db().transactionExecBatch(queries).then(() => {
Log.info('Settings have been saved.'); Log.info('Settings have been saved.');
}).catch((error) => { }).catch((error) => {
Log.warn('Could not save settings', error); Log.warn('Could not save settings', error);

View File

@ -25,6 +25,7 @@ import { MenuContext } from 'react-native-popup-menu';
import { SideMenu } from 'src/components/side-menu.js'; import { SideMenu } from 'src/components/side-menu.js';
import { SideMenuContent } from 'src/components/side-menu-content.js'; import { SideMenuContent } from 'src/components/side-menu-content.js';
import { NoteFolderService } from 'src/services/note-folder-service.js'; import { NoteFolderService } from 'src/services/note-folder-service.js';
import { DatabaseDriverReactNative } from 'src/database-driver-react-native';
let defaultState = { let defaultState = {
notes: [], notes: [],
@ -89,8 +90,6 @@ const reducer = (state = defaultState, action) => {
// update it within the note array if it already exists. // update it within the note array if it already exists.
case 'NOTES_UPDATE_ONE': case 'NOTES_UPDATE_ONE':
Log.info('NOITTEOJTNEONTOE', action.note);
let newNotes = state.notes.splice(0); let newNotes = state.notes.splice(0);
var found = false; var found = false;
for (let i = 0; i < newNotes.length; i++) { for (let i = 0; i < newNotes.length; i++) {
@ -191,7 +190,7 @@ const AppNavigator = StackNavigator({
class AppComponent extends React.Component { class AppComponent extends React.Component {
componentDidMount() { componentDidMount() {
let db = new Database(); let db = new Database(new DatabaseDriverReactNative());
//db.setDebugEnabled(Registry.debugMode()); //db.setDebugEnabled(Registry.debugMode());
db.setDebugEnabled(false); db.setDebugEnabled(false);
@ -199,7 +198,7 @@ class AppComponent extends React.Component {
BaseModel.db_ = db; BaseModel.db_ = db;
NoteFolderService.dispatch = this.props.dispatch; NoteFolderService.dispatch = this.props.dispatch;
db.open().then(() => { db.open({ name: '/storage/emulated/0/Download/joplin-42.sqlite' }).then(() => {
Log.info('Database is ready.'); Log.info('Database is ready.');
Registry.setDb(db); Registry.setDb(db);
}).then(() => { }).then(() => {
@ -207,6 +206,21 @@ class AppComponent extends React.Component {
return Setting.load(); return Setting.load();
}).then(() => { }).then(() => {
let user = Setting.object('user'); 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('Client ID', Setting.value('clientId'));
Log.info('User', user); Log.info('User', user);
@ -241,9 +255,25 @@ class AppComponent extends React.Component {
// folderId: folder.id, // folderId: folder.id,
// }); // });
}).then(() => { }).then(() => {
let synchronizer = new Synchronizer(db, Registry.api());
Registry.setSynchronizer(synchronizer); var Dropbox = require('dropbox');
synchronizer.start(); 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) => { }).catch((error) => {
Log.error('Initialization error:', error); Log.error('Initialization error:', error);
}); });

View File

@ -31,21 +31,21 @@ class NoteFolderService extends BaseService {
output = item; output = item;
if (isNew && type == 'note') return Note.updateGeolocation(item.id); if (isNew && type == 'note') return Note.updateGeolocation(item.id);
}).then(() => { }).then(() => {
Registry.synchronizer().start(); // Registry.synchronizer().start();
return output; return output;
}); });
} }
static setField(type, itemId, fieldName, fieldValue, oldValue = undefined) { // static setField(type, itemId, fieldName, fieldValue, oldValue = undefined) {
// TODO: not really consistent as the promise will return 'null' while // // 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. // // this.save will return the note or folder. Currently not used, and maybe not needed.
if (oldValue !== undefined && fieldValue === oldValue) return Promise.resolve(); // if (oldValue !== undefined && fieldValue === oldValue) return Promise.resolve();
let item = { id: itemId }; // let item = { id: itemId };
item[fieldName] = fieldValue; // item[fieldName] = fieldValue;
let oldItem = { id: itemId }; // let oldItem = { id: itemId };
return this.save(type, item, oldItem); // return this.save(type, item, oldItem);
} // }
static openNoteList(folderId) { static openNoteList(folderId) {
return Note.previews(folderId).then((notes) => { return Note.previews(folderId).then((notes) => {

View 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 };

View File

@ -5,6 +5,10 @@ import { Folder } from 'src/models/folder.js';
import { Note } from 'src/models/note.js'; import { Note } from 'src/models/note.js';
import { BaseModel } from 'src/base-model.js'; import { BaseModel } from 'src/base-model.js';
import { promiseChain } from 'src/promise-chain.js'; import { promiseChain } from 'src/promise-chain.js';
import moment from 'moment';
const fs = require('fs');
const path = require('path');
class Synchronizer { class Synchronizer {
@ -26,63 +30,213 @@ class Synchronizer {
return this.api_; 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() { 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 mergedChanges = Change.mergeChanges(changes);
let chain = []; let chain = [];
let processedChangeIds = [];
for (let i = 0; i < mergedChanges.length; i++) { for (let i = 0; i < mergedChanges.length; i++) {
let c = mergedChanges[i]; let c = mergedChanges[i];
chain.push(() => { chain.push(() => {
let p = null; let p = null;
let ItemClass = null; let ItemClass = null;
let path = null;
if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) { if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) {
ItemClass = Folder; ItemClass = Folder;
path = 'folders';
} else if (c.item_type == BaseModel.ITEM_TYPE_NOTE) { } else if (c.item_type == BaseModel.ITEM_TYPE_NOTE) {
ItemClass = Note; ItemClass = Note;
path = 'notes';
} }
if (c.type == Change.TYPE_NOOP) { if (c.type == Change.TYPE_NOOP) {
p = Promise.resolve(); p = Promise.resolve();
} else if (c.type == Change.TYPE_CREATE) { } else if (c.type == Change.TYPE_CREATE) {
p = ItemClass.load(c.item_id).then((item) => { p = this.loadParentAndItem(c).then((result) => {
return this.api().put(path + '/' + item.id, null, item); 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));
}
});
}); });
} 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);
} }
// TODO: handle UPDATE
// TODO: handle DELETE
return p.then(() => { return p.then(() => {
processedChangeIds = processedChangeIds.concat(c.ids); processedChangeIds = processedChangeIds.concat(c.ids);
}).catch((error) => { }).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 // This is fine - trying to apply changes to an object that has been deleted
if (error.type == 'NotFoundException') { // if (error.type == 'NotFoundException') {
processedChangeIds = processedChangeIds.concat(c.ids); // processedChangeIds = processedChangeIds.concat(c.ids);
} else { // } else {
throw error; // throw error;
} // }
}); });
}); });
} }
return promiseChain(chain).catch((error) => { return promiseChain(chain);
Log.warn('Synchronization was interrupted due to an error:', error); }).catch((error) => {
}).then(() => { Log.warn('Synchronization was interrupted due to an error:', error);
Log.info('IDs to delete: ', processedChangeIds); }).then(() => {
Change.deleteMultiple(processedChangeIds); Log.info('IDs to delete: ', processedChangeIds);
}); // Change.deleteMultiple(processedChangeIds);
}).then(() => { }).then(() => {
this.processState('downloadChanges'); 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() { processState_downloadChanges() {
@ -150,9 +304,10 @@ class Synchronizer {
this.state_ = state; this.state_ = state;
if (state == 'uploadChanges') { if (state == 'uploadChanges') {
processState_uploadChanges(); return this.processState_uploadChanges();
} else if (state == 'downloadChanges') { } else if (state == 'downloadChanges') {
processState_downloadChanges(); return this.processState('idle');
//this.processState_downloadChanges();
} else if (state == 'idle') { } else if (state == 'idle') {
// Nothing // Nothing
} else { } else {
@ -168,12 +323,12 @@ class Synchronizer {
return; return;
} }
if (!this.api().session()) { // if (!this.api().session()) {
Log.info("Sync: cannot start synchronizer because user is not logged in."); // Log.info("Sync: cannot start synchronizer because user is not logged in.");
return; // return;
} // }
this.processState('uploadChanges'); return this.processState('uploadChanges');
} }
} }

View 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 };

View File

@ -3,6 +3,7 @@ import { isNode } from 'src/env.js';
import { stringify } from 'query-string'; import { stringify } from 'query-string';
if (isNode()) { 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 // 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. // missing node modules), even if isNode() is false.
let modulePath = 'src/shim.js'; let modulePath = 'src/shim.js';
@ -29,6 +30,7 @@ class WebApi {
constructor(baseUrl) { constructor(baseUrl) {
this.baseUrl_ = baseUrl; this.baseUrl_ = baseUrl;
this.session_ = null; this.session_ = null;
this.retryInterval_ = 500;
} }
setSession(v) { setSession(v) {
@ -88,42 +90,62 @@ class WebApi {
return cmd.join(' '); return cmd.join(' ');
} }
exec(method, path, query, data) { delay(milliseconds) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.session_) { setTimeout(() => {
query = query ? Object.assign({}, query) : {}; resolve();
if (!query.session) query.session = this.session_; }, 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_;
}
let r = this.makeRequest(method, path, query, data);
Log.debug(WebApi.toCurl(r, data));
//console.info(WebApi.toCurl(r, data));
return fetch(r.url, r.options).then((response) => {
let responseClone = response.clone();
if (!response.ok) {
return responseClone.text().then((text) => {
throw new WebApiError('HTTP ' + response.status + ': ' + response.statusText + ': ' + text);
});
} }
let r = this.makeRequest(method, path, query, data); return response.json().then((data) => {
if (data && data.error) {
//Log.debug(WebApi.toCurl(r, data)); throw new WebApiError(data);
//console.info(WebApi.toCurl(r, data)); } else {
return data;
fetch(r.url, r.options).then(function(response) {
let responseClone = response.clone();
if (!response.ok) {
return responseClone.text().then(function(text) {
reject(new WebApiError('HTTP ' + response.status + ': ' + response.statusText + ': ' + text));
});
} }
}).catch((error) => {
return response.json().then(function(data) { return responseClone.text().then((text) => {
if (data && data.error) { throw new WebApiError('Cannot parse JSON: ' + text);
reject(new WebApiError(data));
} else {
resolve(data);
}
}).catch(function(error) {
responseClone.text().then(function(text) {
reject(new WebApiError('Cannot parse JSON: ' + text));
});
}); });
}).then(function(data) {
resolve(data);
}).catch(function(error) {
reject(error);
}); });
}); });
} }

View File

@ -28,7 +28,6 @@
"sensio/distribution-bundle": "^5.0", "sensio/distribution-bundle": "^5.0",
"sensio/framework-extra-bundle": "^3.0.2", "sensio/framework-extra-bundle": "^3.0.2",
"incenteev/composer-parameter-handler": "^2.0", "incenteev/composer-parameter-handler": "^2.0",
"illuminate/database": "*", "illuminate/database": "*",
"yetanotherape/diff-match-patch": "*" "yetanotherape/diff-match-patch": "*"
}, },

View File

@ -15,11 +15,13 @@ class SynchronizerController extends ApiController {
* @Route("/synchronizer") * @Route("/synchronizer")
*/ */
public function allAction(Request $request) { public function allAction(Request $request) {
$lastChangeId = (int)$request->query->get('rev_id');
if (!$this->user() || !$this->session()) throw new UnauthorizedException(); 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); return static::successResponse($actions);
} }

View File

@ -14,11 +14,26 @@ class Diff {
return self::$dmp_; 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) { 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)); return self::dmp()->patch_toText(self::dmp()->patch_make($from, $to));
} }
static public function patch($from, $diff) { static public function patch($from, $diff) {
$from = self::encodingFix($from);
return self::dmp()->patch_apply(self::dmp()->patch_fromText($diff), $from); return self::dmp()->patch_apply(self::dmp()->patch_fromText($diff), $from);
} }

View File

@ -10,7 +10,7 @@ class Change extends BaseModel {
'type' => array('create', 'update', 'delete'), 'type' => array('create', 'update', 'delete'),
); );
static public function changesDoneAfterId($userId, $clientId, $fromChangeId) { static public function changesDoneAfterId($userId, $clientId, $fromChangeId, $limit = null) {
// Simplification: // Simplification:
// //
// - If create, update, delete => return nothing // - If create, update, delete => return nothing
@ -19,7 +19,8 @@ class Change extends BaseModel {
// - If update, update, update => return last // - If update, update, update => return last
// $limit = 10000; // $limit = 10000;
$limit = 100; if ((int)$limit <= 0) $limit = 100;
if ($limit > 1000) $limit = 1000;
$changes = self::where('id', '>', $fromChangeId) $changes = self::where('id', '>', $fromChangeId)
->where('user_id', '=', $userId) ->where('user_id', '=', $userId)
->where('client_id', '!=', $clientId) ->where('client_id', '!=', $clientId)

View File

@ -6,6 +6,19 @@ require_once dirname(__FILE__) . '/TestUtils.php';
require_once dirname(__FILE__) . '/BaseTestCase.php'; require_once dirname(__FILE__) . '/BaseTestCase.php';
require_once dirname(__FILE__) . '/BaseControllerTestCase.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( $dbConfig = array(
'dbName' => 'notes_test', 'dbName' => 'notes_test',
'user' => 'root', 'user' => 'root',

View File

@ -31,6 +31,7 @@ try {
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'code' => $e->getCode(), 'code' => $e->getCode(),
'type' => $errorType, 'type' => $errorType,
'trace' => $e->getTraceAsString(),
); );
if ($errorType == 'NotFoundHttpException') { if ($errorType == 'NotFoundHttpException') {
header('HTTP/1.1 404 Not found'); header('HTTP/1.1 404 Not found');