mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Got Nextcloud sync to work in Electron
This commit is contained in:
parent
6ade09c228
commit
1cc27f2509
@ -72,18 +72,36 @@ process.stdout.on('error', function( err ) {
|
|||||||
|
|
||||||
// async function main() {
|
// async function main() {
|
||||||
// const WebDavApi = require('lib/WebDavApi');
|
// const WebDavApi = require('lib/WebDavApi');
|
||||||
// const api = new WebDavApi('http://nextcloud.local/remote.php/dav/files/admin/Joplin', { username: 'admin', password: '123456' });
|
// const api = new WebDavApi('http://nextcloud.local/remote.php/dav/files/admin/Joplin', { username: 'admin', password: '1234567' });
|
||||||
// const { FileApiDriverWebDav } = new require('lib/file-api-driver-webdav');
|
// const { FileApiDriverWebDav } = new require('lib/file-api-driver-webdav');
|
||||||
// const driver = new FileApiDriverWebDav(api);
|
// const driver = new FileApiDriverWebDav(api);
|
||||||
|
|
||||||
// //await driver.stat('testing.txt');
|
// const stat = await driver.stat('');
|
||||||
// const stat = await driver.stat('testing.txt');
|
|
||||||
// console.info(stat);
|
// console.info(stat);
|
||||||
|
|
||||||
// //await api.execPropFind('');
|
|
||||||
|
|
||||||
// // const stat = await driver.stat('testing.txt');
|
// // const stat = await driver.stat('testing.txt');
|
||||||
// // console.info(stat);
|
// // console.info(stat);
|
||||||
|
|
||||||
|
|
||||||
|
// // const content = await driver.get('testing.txta');
|
||||||
|
// // console.info(content);
|
||||||
|
|
||||||
|
// // const content = await driver.get('testing.txta', { target: 'file', path: '/var/www/joplin/CliClient/testing-file.txt' });
|
||||||
|
// // console.info(content);
|
||||||
|
|
||||||
|
// // const content = await driver.mkdir('newdir5');
|
||||||
|
// // console.info(content);
|
||||||
|
|
||||||
|
// //await driver.put('myfile4.md', 'this is my content');
|
||||||
|
|
||||||
|
// // await driver.put('testimg.jpg', null, { source: 'file', path: '/mnt/d/test.jpg' });
|
||||||
|
|
||||||
|
// // await driver.delete('myfile4.md');
|
||||||
|
|
||||||
|
// // const deltaResult = await driver.delta('', {
|
||||||
|
// // allItemIdsHandler: () => { return []; }
|
||||||
|
// // });
|
||||||
|
// // console.info(deltaResult);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// main().catch((error) => { console.error(error); });
|
// main().catch((error) => { console.error(error); });
|
||||||
|
16
ElectronClient/app/package-lock.json
generated
16
ElectronClient/app/package-lock.json
generated
@ -5334,6 +5334,22 @@
|
|||||||
"integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=",
|
"integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"xml2js": {
|
||||||
|
"version": "0.4.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
|
||||||
|
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
|
||||||
|
"requires": {
|
||||||
|
"sax": "1.2.4",
|
||||||
|
"xmlbuilder": "9.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"xmlbuilder": {
|
||||||
|
"version": "9.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
|
||||||
|
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"xmlbuilder": {
|
"xmlbuilder": {
|
||||||
"version": "8.2.2",
|
"version": "8.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
|
||||||
|
@ -84,6 +84,7 @@
|
|||||||
"string-padding": "^1.0.2",
|
"string-padding": "^1.0.2",
|
||||||
"string-to-stream": "^1.1.0",
|
"string-to-stream": "^1.1.0",
|
||||||
"tcp-port-used": "^0.1.2",
|
"tcp-port-used": "^0.1.2",
|
||||||
"uuid": "^3.1.0"
|
"uuid": "^3.1.0",
|
||||||
|
"xml2js": "^0.4.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,12 +26,14 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
|||||||
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||||
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
||||||
|
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
||||||
const EncryptionService = require('lib/services/EncryptionService');
|
const EncryptionService = require('lib/services/EncryptionService');
|
||||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||||
|
|
||||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||||
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
||||||
|
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
||||||
|
|
||||||
class BaseApplication {
|
class BaseApplication {
|
||||||
|
|
||||||
|
@ -30,6 +30,10 @@ class BaseSyncTarget {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authRouteName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static id() {
|
static id() {
|
||||||
throw new Error('id() not implemented');
|
throw new Error('id() not implemented');
|
||||||
}
|
}
|
||||||
|
65
ReactNativeClient/lib/SyncTargetNextcloud.js
Normal file
65
ReactNativeClient/lib/SyncTargetNextcloud.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||||
|
const { _ } = require('lib/locale.js');
|
||||||
|
const Setting = require('lib/models/Setting.js');
|
||||||
|
const { FileApi } = require('lib/file-api.js');
|
||||||
|
const { Synchronizer } = require('lib/synchronizer.js');
|
||||||
|
const WebDavApi = require('lib/WebDavApi');
|
||||||
|
const { FileApiDriverWebDav } = new require('lib/file-api-driver-webdav');
|
||||||
|
|
||||||
|
class SyncTargetNextcloud extends BaseSyncTarget {
|
||||||
|
|
||||||
|
static id() {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(db, options = null) {
|
||||||
|
super(db, options);
|
||||||
|
// this.authenticated_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static targetName() {
|
||||||
|
return 'nextcloud';
|
||||||
|
}
|
||||||
|
|
||||||
|
static label() {
|
||||||
|
return _('Nextcloud');
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthenticated() {
|
||||||
|
return true;
|
||||||
|
//return this.authenticated_;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initFileApi() {
|
||||||
|
const options = {
|
||||||
|
baseUrl: () => Setting.value('sync.5.path'),
|
||||||
|
username: () => Setting.value('sync.5.username'),
|
||||||
|
password: () => Setting.value('sync.5.password'),
|
||||||
|
};
|
||||||
|
|
||||||
|
//const api = new WebDavApi('http://nextcloud.local/remote.php/dav/files/admin/Joplin', { username: 'admin', password: '123456' });
|
||||||
|
const api = new WebDavApi(options);
|
||||||
|
const driver = new FileApiDriverWebDav(api);
|
||||||
|
|
||||||
|
// this.authenticated_ = true;
|
||||||
|
// try {
|
||||||
|
// await driver.stat('');
|
||||||
|
// } catch (error) {
|
||||||
|
// console.info(error);
|
||||||
|
// this.authenticated_ = false;
|
||||||
|
// if (error.code !== 401) throw error;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const fileApi = new FileApi('', driver);
|
||||||
|
fileApi.setSyncTargetId(SyncTargetNextcloud.id());
|
||||||
|
fileApi.setLogger(this.logger());
|
||||||
|
return fileApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initSynchronizer() {
|
||||||
|
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SyncTargetNextcloud;
|
@ -9,15 +9,15 @@ const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
|
|||||||
|
|
||||||
class SyncTargetOneDrive extends BaseSyncTarget {
|
class SyncTargetOneDrive extends BaseSyncTarget {
|
||||||
|
|
||||||
|
static id() {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(db, options = null) {
|
constructor(db, options = null) {
|
||||||
super(db, options);
|
super(db, options);
|
||||||
this.api_ = null;
|
this.api_ = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static id() {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
static targetName() {
|
static targetName() {
|
||||||
return 'onedrive';
|
return 'onedrive';
|
||||||
}
|
}
|
||||||
@ -38,6 +38,10 @@ class SyncTargetOneDrive extends BaseSyncTarget {
|
|||||||
return parameters().oneDrive;
|
return parameters().oneDrive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authRouteName() {
|
||||||
|
return 'OneDriveLogin';
|
||||||
|
}
|
||||||
|
|
||||||
api() {
|
api() {
|
||||||
if (this.api_) return this.api_;
|
if (this.api_) return this.api_;
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@ const { Logger } = require('lib/logger.js');
|
|||||||
const { shim } = require('lib/shim.js');
|
const { shim } = require('lib/shim.js');
|
||||||
const parseXmlString = require('xml2js').parseString;
|
const parseXmlString = require('xml2js').parseString;
|
||||||
const JoplinError = require('lib/JoplinError');
|
const JoplinError = require('lib/JoplinError');
|
||||||
|
const urlParser = require("url");
|
||||||
|
const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils.js');
|
||||||
|
|
||||||
// Note that the d: namespace (the DAV namespace) is specific to Nextcloud. The RFC for example uses "D:" however
|
// Note that the d: namespace (the DAV namespace) is specific to Nextcloud. The RFC for example uses "D:" however
|
||||||
// we make all the tags and attributes lowercase so we handle both the Nextcloud style and RFC. Hopefully other
|
// we make all the tags and attributes lowercase so we handle both the Nextcloud style and RFC. Hopefully other
|
||||||
@ -11,10 +13,11 @@ const JoplinError = require('lib/JoplinError');
|
|||||||
|
|
||||||
class WebDavApi {
|
class WebDavApi {
|
||||||
|
|
||||||
constructor(baseUrl, options) {
|
constructor(options) {
|
||||||
this.logger_ = new Logger();
|
this.logger_ = new Logger();
|
||||||
this.baseUrl_ = baseUrl.replace(/\/+$/, ""); // Remove last trailing slashes
|
|
||||||
this.options_ = options;
|
this.options_ = options;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLogger(l) {
|
setLogger(l) {
|
||||||
@ -26,12 +29,17 @@ class WebDavApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authToken() {
|
authToken() {
|
||||||
if (!this.options_.username || !this.options_.password) return null;
|
if (!this.options_.username() || !this.options_.password()) return null;
|
||||||
return (new Buffer(this.options_.username + ':' + this.options_.password)).toString('base64');
|
return (new Buffer(this.options_.username() + ':' + this.options_.password())).toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
baseUrl() {
|
baseUrl() {
|
||||||
return this.baseUrl_;
|
return this.options_.baseUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
relativeBaseUrl() {
|
||||||
|
const url = urlParser.parse(this.baseUrl(), true);
|
||||||
|
return url.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
async xmlToJson(xml) {
|
async xmlToJson(xml) {
|
||||||
@ -62,9 +70,14 @@ class WebDavApi {
|
|||||||
|
|
||||||
valueFromJson(json, keys, type) {
|
valueFromJson(json, keys, type) {
|
||||||
let output = json;
|
let output = json;
|
||||||
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
const key = keys[i];
|
const key = keys[i];
|
||||||
if (!output || !output[key]) return null;
|
|
||||||
|
// console.info(key, typeof key, typeof output, typeof output === 'object' && (key in output), Array.isArray(output));
|
||||||
|
|
||||||
|
if (typeof key === 'number' && !Array.isArray(output)) return null;
|
||||||
|
if (typeof key === 'string' && (typeof output !== 'object' || !(key in output))) return null;
|
||||||
output = output[key];
|
output = output[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +91,10 @@ class WebDavApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'array') {
|
||||||
|
return Array.isArray(output) ? output : null;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +106,11 @@ class WebDavApi {
|
|||||||
return this.valueFromJson(json, keys, 'object');
|
return this.valueFromJson(json, keys, 'object');
|
||||||
}
|
}
|
||||||
|
|
||||||
async execPropFind(path, fields = null) {
|
arrayFromJson(json, keys) {
|
||||||
|
return this.valueFromJson(json, keys, 'array');
|
||||||
|
}
|
||||||
|
|
||||||
|
async execPropFind(path, depth, fields = null) {
|
||||||
if (fields === null) fields = ['d:getlastmodified'];
|
if (fields === null) fields = ['d:getlastmodified'];
|
||||||
|
|
||||||
let fieldsXml = '';
|
let fieldsXml = '';
|
||||||
@ -111,7 +132,7 @@ class WebDavApi {
|
|||||||
</d:prop>
|
</d:prop>
|
||||||
</d:propfind>`;
|
</d:propfind>`;
|
||||||
|
|
||||||
return this.exec('PROPFIND', path, body);
|
return this.exec('PROPFIND', path, body, { 'Depth': depth });
|
||||||
}
|
}
|
||||||
|
|
||||||
// curl -u admin:123456 'http://nextcloud.local/remote.php/dav/files/admin/' -X PROPFIND --data '<?xml version="1.0" encoding="UTF-8"?>
|
// curl -u admin:123456 'http://nextcloud.local/remote.php/dav/files/admin/' -X PROPFIND --data '<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@ -151,33 +172,45 @@ class WebDavApi {
|
|||||||
|
|
||||||
const responseText = await response.text();
|
const responseText = await response.text();
|
||||||
|
|
||||||
|
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
||||||
|
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
||||||
|
const shortResponseText = () => {
|
||||||
|
return (responseText + '').substr(0, 1024);
|
||||||
|
}
|
||||||
|
|
||||||
let responseJson_ = null;
|
let responseJson_ = null;
|
||||||
const loadResponseJson = async () => {
|
const loadResponseJson = async () => {
|
||||||
if (!responseText) return null;
|
if (!responseText) return null;
|
||||||
if (responseJson_) return responseJson_;
|
if (responseJson_) return responseJson_;
|
||||||
responseJson_ = await this.xmlToJson(responseText);
|
responseJson_ = await this.xmlToJson(responseText);
|
||||||
if (!responseJson_) throw new JoplinError('Cannot parse JSON response: ' + responseText, response.status);
|
if (!responseJson_) throw new JoplinError('Cannot parse JSON response: ' + shortResponseText(), response.status);
|
||||||
return responseJson_;
|
return responseJson_;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// When using fetchBlob we only get a string (not xml or json) back
|
// When using fetchBlob we only get a string (not xml or json) back
|
||||||
if (options.target === 'file') throw new JoplinError(responseText, response.status);
|
if (options.target === 'file') throw new JoplinError(shortResponseText(), response.status);
|
||||||
|
|
||||||
const json = await loadResponseJson();
|
const json = await loadResponseJson();
|
||||||
|
|
||||||
if (json['d:error']) {
|
if (json['d:error']) {
|
||||||
const code = json['d:error']['s:exception'] ? json['d:error']['s:exception'].join(' ') : response.status;
|
const code = json['d:error']['s:exception'] ? json['d:error']['s:exception'].join(' ') : response.status;
|
||||||
const message = json['d:error']['s:message'] ? json['d:error']['s:message'].join("\n") : responseText;
|
const message = json['d:error']['s:message'] ? json['d:error']['s:message'].join("\n") : shortResponseText();
|
||||||
throw new JoplinError(message + ' (' + code + ')', response.status);
|
throw new JoplinError(message + ' (' + code + ')', response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new JoplinError(responseText, response.status);
|
throw new JoplinError(shortResponseText(), response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.responseFormat === 'text') return responseText;
|
if (options.responseFormat === 'text') return responseText;
|
||||||
|
|
||||||
return await loadResponseJson();
|
const output = await loadResponseJson();
|
||||||
|
|
||||||
|
// Check that we didn't get for example an HTML page (as an error) instead of the JSON response
|
||||||
|
// null responses are possible, for example for DELETE calls
|
||||||
|
if (output !== null && typeof output === 'object' && !('d:multistatus' in output)) throw new Error('Not a valid JSON response: ' + shortResponseText());
|
||||||
|
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -37,13 +37,18 @@ shared.synchronize_press = async function(comp) {
|
|||||||
const action = comp.props.syncStarted ? 'cancel' : 'start';
|
const action = comp.props.syncStarted ? 'cancel' : 'start';
|
||||||
|
|
||||||
if (!reg.syncTarget().isAuthenticated()) {
|
if (!reg.syncTarget().isAuthenticated()) {
|
||||||
|
if (reg.syncTarget().authRouteName()) {
|
||||||
comp.props.dispatch({
|
comp.props.dispatch({
|
||||||
type: 'NAV_GO',
|
type: 'NAV_GO',
|
||||||
routeName: 'OneDriveLogin',
|
routeName: reg.syncTarget().authRouteName(),
|
||||||
});
|
});
|
||||||
return 'auth';
|
return 'auth';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reg.logger().info('Not authentified with sync target - please check your credential.');
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
|
||||||
let sync = null;
|
let sync = null;
|
||||||
try {
|
try {
|
||||||
sync = await reg.syncTarget().synchronizer();
|
sync = await reg.syncTarget().synchronizer();
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
const BaseItem = require('lib/models/BaseItem.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
|
const { basicDelta } = require('lib/file-api');
|
||||||
|
const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils.js');
|
||||||
|
|
||||||
class FileApiDriverWebDav {
|
class FileApiDriverWebDav {
|
||||||
|
|
||||||
@ -12,19 +14,25 @@ class FileApiDriverWebDav {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async stat(path) {
|
async stat(path) {
|
||||||
const result = await this.api().execPropFind(path, [
|
try {
|
||||||
|
const result = await this.api().execPropFind(path, 0, [
|
||||||
'd:getlastmodified',
|
'd:getlastmodified',
|
||||||
'd:resourcetype',
|
'd:resourcetype',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return this.metadataFromStat_(result, path);
|
const resource = this.api().objectFromJson(result, ['d:multistatus', 'd:response', 0]);
|
||||||
|
return this.statFromResource_(resource, path);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 404) return null;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataFromStat_(stat, path) {
|
statFromResource_(resource, path) {
|
||||||
const isCollection = this.api().stringFromJson(stat, ['d:multistatus', 'd:response', 0, 'd:propstat', 0, 'd:prop', 0, 'd:resourcetype', 0, 'd:collection', 0]);
|
const isCollection = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:resourcetype', 0, 'd:collection', 0]);
|
||||||
const lastModifiedString = this.api().stringFromJson(stat, ['d:multistatus', 'd:response', 0, 'd:propstat', 0, 'd:prop', 0, 'd:getlastmodified', 0]);
|
const lastModifiedString = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getlastmodified', 0]);
|
||||||
|
|
||||||
if (!lastModifiedString) throw new Error('Could not get lastModified date: ' + JSON.stringify(stat));
|
if (!lastModifiedString) throw new Error('Could not get lastModified date: ' + JSON.stringify(resource));
|
||||||
|
|
||||||
const lastModifiedDate = new Date(lastModifiedString);
|
const lastModifiedDate = new Date(lastModifiedString);
|
||||||
if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + lastModifiedString);
|
if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + lastModifiedString);
|
||||||
@ -37,11 +45,17 @@ class FileApiDriverWebDav {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataFromStats_(stats) {
|
statsFromResources_(resources) {
|
||||||
|
const relativeBaseUrl = this.api().relativeBaseUrl();
|
||||||
let output = [];
|
let output = [];
|
||||||
for (let i = 0; i < stats.length; i++) {
|
for (let i = 0; i < resources.length; i++) {
|
||||||
const mdStat = this.metadataFromStat_(stats[i]);
|
const resource = resources[i];
|
||||||
output.push(mdStat);
|
const href = this.api().stringFromJson(resource, ['d:href', 0]);
|
||||||
|
if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
|
||||||
|
const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
|
||||||
|
if (path === '') continue; // The list of resources includes the root dir too, which we don't want
|
||||||
|
const stat = this.statFromResource_(resources[i], path);
|
||||||
|
output.push(stat);
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
@ -51,7 +65,17 @@ class FileApiDriverWebDav {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delta(path, options) {
|
async delta(path, options) {
|
||||||
|
const getDirStats = async (path) => {
|
||||||
|
const result = await this.api().execPropFind(path, 1, [
|
||||||
|
'd:getlastmodified',
|
||||||
|
'd:resourcetype',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']);
|
||||||
|
return this.statsFromResources_(resources);
|
||||||
|
};
|
||||||
|
|
||||||
|
return await basicDelta(path, getDirStats, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async list(path, options) {
|
async list(path, options) {
|
||||||
|
@ -139,9 +139,9 @@ function basicDeltaContextFromOptions_(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This is the basic delta algorithm, which can be used in case the cloud service does not have
|
// This is the basic delta algorithm, which can be used in case the cloud service does not have
|
||||||
// a built-on delta API. OneDrive and Dropbox have one for example, but Nextcloud and obviously
|
// a built-in delta API. OneDrive and Dropbox have one for example, but Nextcloud and obviously
|
||||||
// the file system do not.
|
// the file system do not.
|
||||||
async function basicDelta(path, getStatFn, options) {
|
async function basicDelta(path, getDirStatFn, options) {
|
||||||
const outputLimit = 1000;
|
const outputLimit = 1000;
|
||||||
const itemIds = await options.allItemIdsHandler();
|
const itemIds = await options.allItemIdsHandler();
|
||||||
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
|
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
|
||||||
@ -156,7 +156,7 @@ async function basicDelta(path, getStatFn, options) {
|
|||||||
|
|
||||||
// Stats are cached until all items have been processed (until hasMore is false)
|
// Stats are cached until all items have been processed (until hasMore is false)
|
||||||
if (newContext.statsCache === null) {
|
if (newContext.statsCache === null) {
|
||||||
newContext.statsCache = await getStatFn(path);
|
newContext.statsCache = await getDirStatFn(path);
|
||||||
newContext.statsCache.sort(function(a, b) {
|
newContext.statsCache.sort(function(a, b) {
|
||||||
return a.updated_time - b.updated_time;
|
return a.updated_time - b.updated_time;
|
||||||
});
|
});
|
||||||
@ -196,6 +196,8 @@ async function basicDelta(path, getStatFn, options) {
|
|||||||
if (output.length >= outputLimit) break;
|
if (output.length >= outputLimit) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find out which items have been deleted on the sync target by comparing the items
|
||||||
|
// we have to the items on the target.
|
||||||
let deletedItems = [];
|
let deletedItems = [];
|
||||||
for (let i = 0; i < itemIds.length; i++) {
|
for (let i = 0; i < itemIds.length; i++) {
|
||||||
if (output.length + deletedItems.length >= outputLimit) break;
|
if (output.length + deletedItems.length >= outputLimit) break;
|
||||||
|
@ -82,6 +82,9 @@ class Setting extends BaseModel {
|
|||||||
return SyncTargetRegistry.idAndLabelPlainObject();
|
return SyncTargetRegistry.idAndLabelPlainObject();
|
||||||
}},
|
}},
|
||||||
'sync.2.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('filesystem') }, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: () => _('The path to synchronise with when file system synchronisation is enabled. See `sync.target`.') },
|
'sync.2.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('filesystem') }, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: () => _('The path to synchronise with when file system synchronisation is enabled. See `sync.target`.') },
|
||||||
|
'sync.5.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud directory URL to synchronise with') },
|
||||||
|
'sync.5.username': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud username') },
|
||||||
|
'sync.5.password': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud password') },
|
||||||
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
'sync.1.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
'sync.1.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||||
|
@ -45,4 +45,12 @@ function toSystemSlashes(path, os) {
|
|||||||
return path.replace(/\\/g, "/");
|
return path.replace(/\\/g, "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { basename, dirname, filename, isHidden, fileExtension, safeFileExtension, toSystemSlashes };
|
function rtrimSlashes(path) {
|
||||||
|
return path.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ltrimSlashes(path) {
|
||||||
|
return path.replace(/^\/+/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { basename, dirname, filename, isHidden, fileExtension, safeFileExtension, toSystemSlashes, rtrimSlashes, ltrimSlashes };
|
Loading…
Reference in New Issue
Block a user