2018-01-25 21:01:14 +02:00
|
|
|
const { basicDelta } = require('lib/file-api');
|
2020-10-21 01:23:55 +02:00
|
|
|
const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils');
|
2018-02-02 01:40:05 +02:00
|
|
|
const JoplinError = require('lib/JoplinError');
|
2018-01-21 19:01:37 +02:00
|
|
|
|
2019-07-29 15:43:53 +02:00
|
|
|
class FileApiDriverWebDav {
|
2018-01-21 19:01:37 +02:00
|
|
|
constructor(api) {
|
|
|
|
this.api_ = api;
|
|
|
|
}
|
|
|
|
|
|
|
|
api() {
|
|
|
|
return this.api_;
|
|
|
|
}
|
|
|
|
|
2018-02-07 21:46:07 +02:00
|
|
|
requestRepeatCount() {
|
|
|
|
return 3;
|
|
|
|
}
|
|
|
|
|
2019-09-25 20:58:15 +02:00
|
|
|
lastRequests() {
|
|
|
|
return this.api().lastRequests();
|
|
|
|
}
|
|
|
|
|
|
|
|
clearLastRequests() {
|
|
|
|
return this.api().clearLastRequests();
|
|
|
|
}
|
|
|
|
|
2018-01-21 19:01:37 +02:00
|
|
|
async stat(path) {
|
2018-01-25 21:01:14 +02:00
|
|
|
try {
|
2019-07-29 15:43:53 +02:00
|
|
|
const result = await this.api().execPropFind(path, 0, ['d:getlastmodified', 'd:resourcetype']);
|
2018-01-21 19:01:37 +02:00
|
|
|
|
2018-01-25 21:01:14 +02:00
|
|
|
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;
|
|
|
|
}
|
2018-01-21 19:01:37 +02:00
|
|
|
}
|
|
|
|
|
2018-01-25 21:01:14 +02:00
|
|
|
statFromResource_(resource, path) {
|
2018-05-05 16:25:37 +02:00
|
|
|
// WebDAV implementations are always slightly different from one server to another but, at the minimum,
|
2018-02-14 21:08:07 +02:00
|
|
|
// a resource should have a propstat key - if not it's probably an error.
|
|
|
|
const propStat = this.api().arrayFromJson(resource, ['d:propstat']);
|
2019-09-19 23:51:18 +02:00
|
|
|
if (!Array.isArray(propStat)) throw new Error(`Invalid WebDAV resource format: ${JSON.stringify(resource)}`);
|
2018-02-14 21:08:07 +02:00
|
|
|
|
2018-06-21 20:00:20 +02:00
|
|
|
// Disabled for now to try to fix this: https://github.com/laurent22/joplin/issues/624
|
|
|
|
//
|
|
|
|
// const httpStatusLine = this.api().stringFromJson(resource, ['d:propstat',0,'d:status', 0]);
|
|
|
|
// if ( typeof httpStatusLine === 'string' && httpStatusLine.indexOf('404') >= 0 ) throw new JoplinError(resource, 404);
|
2018-05-23 03:08:14 +02:00
|
|
|
|
2018-02-14 21:08:07 +02:00
|
|
|
const resourceTypes = this.api().resourcePropByName(resource, 'array', 'd:resourcetype');
|
|
|
|
let isDir = false;
|
|
|
|
if (Array.isArray(resourceTypes)) {
|
|
|
|
for (let i = 0; i < resourceTypes.length; i++) {
|
|
|
|
const t = resourceTypes[i];
|
|
|
|
if (typeof t === 'object' && 'd:collection' in t) {
|
|
|
|
isDir = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-18 12:46:12 +02:00
|
|
|
let lastModifiedString = null;
|
|
|
|
|
|
|
|
try {
|
|
|
|
lastModifiedString = this.api().resourcePropByName(resource, 'string', 'd:getlastmodified');
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code === 'stringNotFound') {
|
|
|
|
// OK - the logic to handle this is below
|
|
|
|
} else {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
2018-01-21 19:01:37 +02:00
|
|
|
|
2018-02-14 21:08:07 +02:00
|
|
|
// Note: Not all WebDAV servers return a getlastmodified date (eg. Seafile, which doesn't return the
|
|
|
|
// property for folders) so we can only throw an error if it's a file.
|
2019-09-19 23:51:18 +02:00
|
|
|
if (!lastModifiedString && !isDir) throw new Error(`Could not get lastModified date for resource: ${JSON.stringify(resource)}`);
|
2018-02-14 21:08:07 +02:00
|
|
|
const lastModifiedDate = lastModifiedString ? new Date(lastModifiedString) : new Date();
|
2019-09-19 23:51:18 +02:00
|
|
|
if (isNaN(lastModifiedDate.getTime())) throw new Error(`Invalid date: ${lastModifiedString}`);
|
2018-01-21 19:01:37 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
path: path,
|
|
|
|
updated_time: lastModifiedDate.getTime(),
|
2018-02-14 21:08:07 +02:00
|
|
|
isDir: isDir,
|
2018-01-21 19:01:37 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-09-13 00:16:42 +02:00
|
|
|
async setTimestamp() {
|
2018-01-22 21:06:50 +02:00
|
|
|
throw new Error('Not implemented'); // Not needed anymore
|
2018-01-21 19:01:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async delta(path, options) {
|
2020-05-21 10:14:33 +02:00
|
|
|
const getDirStats = async path => {
|
2020-08-02 13:28:50 +02:00
|
|
|
const result = await this.list(path, { includeDirs: false });
|
2018-01-25 23:15:58 +02:00
|
|
|
return result.items;
|
2018-01-25 21:01:14 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
return await basicDelta(path, getDirStats, options);
|
2018-01-21 19:01:37 +02:00
|
|
|
}
|
|
|
|
|
2018-02-02 01:40:05 +02:00
|
|
|
// 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 {
|
2019-09-19 23:51:18 +02:00
|
|
|
throw new Error(`href ${href} not in baseUrl ${baseUrl} nor relativeBaseUrl ${relativeBaseUrl}`);
|
2018-02-02 01:40:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return rtrimSlashes(ltrimSlashes(output));
|
|
|
|
}
|
|
|
|
|
|
|
|
statsFromResources_(resources) {
|
2018-01-29 22:51:14 +02:00
|
|
|
const relativeBaseUrl = this.api().relativeBaseUrl();
|
2018-02-02 01:40:05 +02:00
|
|
|
const baseUrl = this.api().baseUrl();
|
2020-03-14 01:46:14 +02:00
|
|
|
const output = [];
|
2018-02-02 01:40:05 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-09-13 00:16:42 +02:00
|
|
|
async list(path) {
|
2020-08-02 13:28:50 +02:00
|
|
|
// See mkdir() call for explanation about trailing slash
|
|
|
|
const result = await this.api().execPropFind(!path.endsWith('/') ? `${path}/` : path, 1, ['d:getlastmodified', 'd:resourcetype']);
|
2018-01-25 23:15:58 +02:00
|
|
|
|
2018-02-02 01:40:05 +02:00
|
|
|
const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']);
|
2020-08-02 13:28:50 +02:00
|
|
|
|
|
|
|
const stats = this.statsFromResources_(resources).map((stat) => {
|
|
|
|
if (path && stat.path.indexOf(`${path}/`) === 0) {
|
|
|
|
const s = stat.path.substr(path.length + 1);
|
|
|
|
if (s.split('/').length === 1) {
|
|
|
|
return {
|
|
|
|
...stat,
|
|
|
|
path: stat.path.substr(path.length + 1),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return stat;
|
|
|
|
}).filter((stat) => {
|
|
|
|
return stat.path !== rtrimSlashes(path);
|
|
|
|
});
|
2018-01-25 23:15:58 +02:00
|
|
|
|
|
|
|
return {
|
2018-01-29 22:51:14 +02:00
|
|
|
items: stats,
|
2018-01-25 23:15:58 +02:00
|
|
|
hasMore: false,
|
|
|
|
context: null,
|
|
|
|
};
|
2018-01-21 19:01:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async get(path, options) {
|
2018-01-23 22:10:20 +02:00
|
|
|
if (!options) options = {};
|
|
|
|
if (!options.responseFormat) options.responseFormat = 'text';
|
|
|
|
try {
|
2018-02-02 01:40:05 +02:00
|
|
|
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.
|
2019-07-30 09:35:42 +02:00
|
|
|
if (response === 'The specified file doesn\'t exist.') throw new JoplinError(response, 404);
|
2018-02-02 01:40:05 +02:00
|
|
|
return response;
|
2018-01-23 22:10:20 +02:00
|
|
|
} catch (error) {
|
|
|
|
if (error.code !== 404) throw error;
|
2018-03-24 21:35:10 +02:00
|
|
|
return null;
|
2018-01-23 22:10:20 +02:00
|
|
|
}
|
2018-01-21 19:01:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async mkdir(path) {
|
2018-01-23 22:10:20 +02:00
|
|
|
try {
|
2018-05-22 16:55:38 +02:00
|
|
|
// RFC wants this, and so does NGINX. Not having the trailing slash means that some
|
|
|
|
// WebDAV implementations will redirect to a URL with "/". However, when doing so
|
|
|
|
// in React Native, the auth headers, etc. are lost so we need to avoid this.
|
|
|
|
// https://github.com/facebook/react-native/issues/929
|
2019-09-19 23:51:18 +02:00
|
|
|
if (!path.endsWith('/')) path = `${path}/`;
|
2018-01-23 22:10:20 +02:00
|
|
|
await this.api().exec('MKCOL', path);
|
|
|
|
} catch (error) {
|
2018-02-02 01:40:05 +02:00
|
|
|
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;
|
|
|
|
}
|
2019-07-29 15:43:53 +02:00
|
|
|
|
2018-02-02 01:40:05 +02:00
|
|
|
throw error;
|
2018-01-23 22:10:20 +02:00
|
|
|
}
|
2018-01-21 19:01:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async put(path, content, options = null) {
|
2018-02-12 20:15:22 +02:00
|
|
|
return await this.api().exec('PUT', path, content, null, options);
|
2018-01-21 19:01:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async delete(path) {
|
2018-01-23 22:10:20 +02:00
|
|
|
try {
|
|
|
|
await this.api().exec('DELETE', path);
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code !== 404) throw error;
|
|
|
|
}
|
2018-01-21 19:01:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async move(oldPath, newPath) {
|
2018-01-30 22:10:36 +02:00
|
|
|
await this.api().exec('MOVE', oldPath, null, {
|
2019-09-19 23:51:18 +02:00
|
|
|
Destination: `${this.api().baseUrl()}/${newPath}`,
|
2019-07-29 15:43:53 +02:00
|
|
|
Overwrite: 'T',
|
2018-01-30 22:10:36 +02:00
|
|
|
});
|
2018-01-21 19:01:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
format() {
|
|
|
|
throw new Error('Not supported');
|
|
|
|
}
|
|
|
|
|
2018-01-25 23:15:58 +02:00
|
|
|
async clearRoot() {
|
|
|
|
await this.delete('');
|
|
|
|
await this.mkdir('');
|
|
|
|
}
|
2018-01-21 19:01:37 +02:00
|
|
|
}
|
|
|
|
|
2018-05-22 16:55:38 +02:00
|
|
|
module.exports = { FileApiDriverWebDav };
|