1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-29 22:48:10 +02:00

All: Made WebDAV driver more generic to support services other than Nextcloud and added WebDAV sync target

This commit is contained in:
Laurent Cozic
2018-02-01 23:40:05 +00:00
parent c52da82447
commit 30ff81064f
8 changed files with 186 additions and 36 deletions

View File

@@ -6,6 +6,7 @@ const Entities = require('html-entities').AllHtmlEntities;
const html_entity_decode = (new Entities()).decode;
const { shim } = require('lib/shim');
const { basename } = require('lib/path-utils');
const JoplinError = require('lib/JoplinError');
class FileApiDriverWebDav {
@@ -67,8 +68,41 @@ class FileApiDriverWebDav {
return await basicDelta(path, getDirStats, options);
}
async list(path, options) {
// A file href, as found in the result of a PROPFIND, can be either an absolute URL or a
// relative URL (an absolute URL minus the protocol and domain), while the sync algorithm
// works with paths relative to the base URL.
hrefToRelativePath_(href, baseUrl, relativeBaseUrl) {
let output = '';
if (href.indexOf(baseUrl) === 0) {
output = href.substr(baseUrl.length);
} else if (href.indexOf(relativeBaseUrl) === 0) {
output = href.substr(relativeBaseUrl.length);
} else {
throw new Error('href ' + href + ' not in baseUrl ' + baseUrl + ' nor relativeBaseUrl ' + relativeBaseUrl);
}
return rtrimSlashes(ltrimSlashes(output));
}
statsFromResources_(resources) {
const relativeBaseUrl = this.api().relativeBaseUrl();
const baseUrl = this.api().baseUrl();
let output = [];
for (let i = 0; i < resources.length; i++) {
const resource = resources[i];
const href = this.api().stringFromJson(resource, ['d:href', 0]);
const path = this.hrefToRelativePath_(href, baseUrl, relativeBaseUrl);
// if (href.indexOf(relativeBaseUrl) !== 0) throw new Error('Path "' + href + '" not inside base URL: ' + relativeBaseUrl);
// 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;
}
async list(path, options) {
// const relativeBaseUrl = this.api().relativeBaseUrl();
// function parsePropFindXml(xmlString) {
// return new Promise(async (resolve, reject) => {
@@ -176,39 +210,53 @@ class FileApiDriverWebDav {
// instead of being processed by xml2json like the other WebDAV responses. This is over 2 times faster
// and it means the mobile app does not freeze during sync.
async function parsePropFindXml2(xmlString) {
const regex = /<d:response>[\S\s]*?<d:href>([\S\s]*?)<\/d:href>[\S\s]*?<d:getlastmodified>(.*?)<\/d:getlastmodified>/g;
// async function parsePropFindXml2(xmlString) {
// const regex = /<d:response>[\S\s]*?<d:href>([\S\s]*?)<\/d:href>[\S\s]*?<d:getlastmodified>(.*?)<\/d:getlastmodified>/g;
let output = [];
let match = null;
// let output = [];
// let match = null;
while (match = regex.exec(xmlString)) {
const href = html_entity_decode(match[1]);
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)));
// while (match = regex.exec(xmlString)) {
// const href = html_entity_decode(match[1]);
// 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
// if (!path) continue; // The list of resources includes the root dir too, which we don't want
const lastModifiedDate = new Date(match[2]);
if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + match[2]);
// const lastModifiedDate = new Date(match[2]);
// if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + match[2]);
output.push({
path: path,
updated_time: lastModifiedDate.getTime(),
created_time: lastModifiedDate.getTime(),
isDir: !BaseItem.isSystemPath(path),
});
}
// output.push({
// path: path,
// updated_time: lastModifiedDate.getTime(),
// created_time: lastModifiedDate.getTime(),
// isDir: !BaseItem.isSystemPath(path),
// });
// }
return output;
}
// return output;
// }
const resultXml = await this.api().execPropFind(path, 1, [
// const resultXml = await this.api().execPropFind(path, 1, [
// 'd:getlastmodified',
// //'d:resourcetype', // Include this to use parsePropFindXml()
// ], { responseFormat: 'text' });
// const stats = await parsePropFindXml2(resultXml);
// return {
// items: stats,
// hasMore: false,
// context: null,
// };
const result = await this.api().execPropFind(path, 1, [
'd:getlastmodified',
//'d:resourcetype', // Include this to use parsePropFindXml()
], { responseFormat: 'text' });
'd:resourcetype',
]);
const stats = await parsePropFindXml2(resultXml);
const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']);
const stats = this.statsFromResources_(resources)
return {
items: stats,
@@ -221,7 +269,13 @@ class FileApiDriverWebDav {
if (!options) options = {};
if (!options.responseFormat) options.responseFormat = 'text';
try {
return await this.api().exec('GET', path, null, null, options);
const response = await this.api().exec('GET', path, null, null, options);
// This is awful but instead of a 404 Not Found, Microsoft IIS returns an HTTP code 200
// with a response body "The specified file doesn't exist." for non-existing files,
// so we need to check for this.
if (response === "The specified file doesn't exist.") throw new JoplinError(response, 404);
return response;
} catch (error) {
if (error.code !== 404) throw error;
}
@@ -231,7 +285,17 @@ class FileApiDriverWebDav {
try {
await this.api().exec('MKCOL', path);
} catch (error) {
if (error.code !== 405) throw error; // 405 means that the collection already exists (Method Not Allowed)
if (error.code === 405) return; // 405 means that the collection already exists (Method Not Allowed)
// 409 should only be returned if a parent path does not exists (eg. when trying to create a/b/c when a/b does not exist)
// however non-compliant servers (eg. Microsoft IIS) also return this code when the directory already exists. So here, if
// we get this code, verify that indeed the directory already exists and exit if it does.
if (error.code === 409) {
const stat = await this.stat(path);
if (stat) return;
}
throw error;
}
}
@@ -275,6 +339,7 @@ class FileApiDriverWebDav {
async move(oldPath, newPath) {
await this.api().exec('MOVE', oldPath, null, {
'Destination': this.api().baseUrl() + '/' + newPath,
'Overwrite': 'T',
});
}