mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
All: Started Nextcloud support
This commit is contained in:
parent
e355f4e49b
commit
6a7d368184
@ -66,6 +66,35 @@ process.stdout.on('error', function( err ) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// async function main() {
|
||||
// const WebDavApi = require('lib/WebDavApi');
|
||||
// const api = new WebDavApi('http://nextcloud.local/remote.php/dav/files/admin/Joplin', { username: 'admin', password: '123456' });
|
||||
// const { FileApiDriverWebDav } = new require('lib/file-api-driver-webdav');
|
||||
// const driver = new FileApiDriverWebDav(api);
|
||||
|
||||
// //await driver.stat('testing.txt');
|
||||
// const stat = await driver.stat('testing.txt');
|
||||
// console.info(stat);
|
||||
|
||||
// //await api.execPropFind('');
|
||||
|
||||
// //const stat = await driver.stat('testing.txt');
|
||||
// //console.info(stat);
|
||||
// }
|
||||
|
||||
// main().catch((error) => { console.error(error); });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
application.start(process.argv).catch((error) => {
|
||||
console.error(_('Fatal error:'));
|
||||
console.error(error);
|
||||
|
14
CliClient/package-lock.json
generated
14
CliClient/package-lock.json
generated
@ -2144,6 +2144,20 @@
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
|
||||
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
|
||||
},
|
||||
"xregexp": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.2.0.tgz",
|
||||
|
@ -59,6 +59,7 @@
|
||||
"tkwidgets": "^0.5.21",
|
||||
"uuid": "^3.0.1",
|
||||
"word-wrap": "^1.2.3",
|
||||
"xml2js": "^0.4.19",
|
||||
"yargs-parser": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
210
ReactNativeClient/lib/WebDavApi.js
Normal file
210
ReactNativeClient/lib/WebDavApi.js
Normal file
@ -0,0 +1,210 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const parseXmlString = require('xml2js').parseString;
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
|
||||
// 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
|
||||
// implementations use the same namespaces. If not, extra processing can be done in `nameProcessor`, for
|
||||
// example to convert a custom namespace to "d:" so that it can be used by the rest of the code.
|
||||
// In general, we should only deal with things in "d:", which is the standard DAV namespace.
|
||||
|
||||
class WebDavApi {
|
||||
|
||||
constructor(baseUrl, options) {
|
||||
this.logger_ = new Logger();
|
||||
this.baseUrl_ = baseUrl.replace(/\/+$/, ""); // Remove last trailing slashes
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
setLogger(l) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
authToken() {
|
||||
if (!this.options_.username || !this.options_.password) return null;
|
||||
return (new Buffer(this.options_.username + ':' + this.options_.password)).toString('base64');
|
||||
}
|
||||
|
||||
baseUrl() {
|
||||
return this.baseUrl_;
|
||||
}
|
||||
|
||||
davNs(json) {
|
||||
|
||||
}
|
||||
|
||||
async xmlToJson(xml) {
|
||||
|
||||
const nameProcessor = (name) => {
|
||||
// const idx = name.indexOf(':');
|
||||
// if (idx >= 0) {
|
||||
// if (name.indexOf('xmlns:') !== 0) name = name.substr(idx + 1);
|
||||
// }
|
||||
return name.toLowerCase();
|
||||
};
|
||||
|
||||
const options = {
|
||||
tagNameProcessors: [nameProcessor],
|
||||
attrNameProcessors: [nameProcessor],
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
parseXmlString(xml, options, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
valueFromJson(json, keys, type) {
|
||||
let output = json;
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (!output || !output[key]) return null;
|
||||
output = output[key];
|
||||
}
|
||||
|
||||
if (type === 'string') {
|
||||
if (typeof output !== 'string') return null;
|
||||
return output;
|
||||
}
|
||||
|
||||
if (type === 'object') {
|
||||
if (!Array.isArray(output) && typeof output === 'object') return output;
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
stringFromJson(json, keys) {
|
||||
return this.valueFromJson(json, keys, 'string');
|
||||
// let output = json;
|
||||
// for (let i = 0; i < keys.length; i++) {
|
||||
// const key = keys[i];
|
||||
// if (!output || !output[key]) return null;
|
||||
// output = output[key];
|
||||
// }
|
||||
// if (typeof output !== 'string') return null;
|
||||
// return output;
|
||||
}
|
||||
|
||||
objectFromJson(json, keys) {
|
||||
return this.valueFromJson(json, keys, 'object');
|
||||
// let output = json;
|
||||
// for (let i = 0; i < keys.length; i++) {
|
||||
// const key = keys[i];
|
||||
// if (!output || !output[key]) return null;
|
||||
// output = output[key];
|
||||
// }
|
||||
// if (!Array.isArray(output) && typeof output === 'object') return output;
|
||||
// return null;
|
||||
}
|
||||
|
||||
// isDirectory(propStat) {
|
||||
// try {
|
||||
// return propStat[0]['d:prop'][0]['d:resourcetype'][0]['d:collection'];
|
||||
// } catch (error) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
async execPropFind(path, fields = null) {
|
||||
if (fields === null) fields = ['d:getlastmodified'];
|
||||
|
||||
let fieldsXml = '';
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
fieldsXml += '<' + fields[i] + '/>';
|
||||
}
|
||||
|
||||
// To find all available properties:
|
||||
//
|
||||
const body=`<?xml version="1.0" encoding="utf-8" ?>
|
||||
<propfind xmlns="DAV:">
|
||||
<propname/>
|
||||
</propfind>`;
|
||||
|
||||
// const body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
// <d:propfind xmlns:d="DAV:">
|
||||
// <d:prop xmlns:oc="http://owncloud.org/ns">
|
||||
// ` + fieldsXml + `
|
||||
// </d:prop>
|
||||
// </d:propfind>`;
|
||||
|
||||
return this.exec('PROPFIND', path, body);
|
||||
}
|
||||
|
||||
// curl -u admin:123456 'http://nextcloud.local/remote.php/dav/files/admin/' -X PROPFIND --data '<?xml version="1.0" encoding="UTF-8"?>
|
||||
// <d:propfind xmlns:d="DAV:">
|
||||
// <d:prop xmlns:oc="http://owncloud.org/ns">
|
||||
// <d:getlastmodified/>
|
||||
// </d:prop>
|
||||
// </d:propfind>'
|
||||
|
||||
async exec(method, path = '', body = null, headers = null) {
|
||||
if (headers === null) headers = {};
|
||||
|
||||
const authToken = this.authToken();
|
||||
|
||||
if (authToken) headers['Authorization'] = 'Basic ' + authToken;
|
||||
|
||||
const fetchOptions = {};
|
||||
fetchOptions.headers = headers;
|
||||
fetchOptions.method = method;
|
||||
if (body) fetchOptions.body = body;
|
||||
|
||||
const url = this.baseUrl() + '/' + path;
|
||||
|
||||
const response = await shim.fetch(url, fetchOptions);
|
||||
const responseText = await response.text();
|
||||
const responseJson = await this.xmlToJson(responseText);
|
||||
|
||||
if (!responseJson) throw new Error('Could not parse response: ' + responseText);
|
||||
|
||||
if (responseJson['d:error']) {
|
||||
const code = responseJson['d:error']['s:exception'] ? responseJson['d:error']['s:exception'].join(' ') : null;
|
||||
const message = responseJson['d:error']['s:message'] ? responseJson['d:error']['s:message'].join("\n") : responseText;
|
||||
throw new JoplinError(message, code);
|
||||
}
|
||||
|
||||
return responseJson;
|
||||
|
||||
|
||||
// //console.info(JSON.stringify(responseJson['d:multistatus']['d:response']));
|
||||
|
||||
|
||||
// body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
// <d:propfind xmlns:d="DAV:">
|
||||
// <d:prop xmlns:oc="http://owncloud.org/ns">
|
||||
// <d:getlastmodified/>
|
||||
// </d:prop>
|
||||
// </d:propfind>`;
|
||||
|
||||
// const authToken = this.authToken();
|
||||
// const url = 'http://nextcloud.local/remote.php/dav/files/admin/Joplin';
|
||||
// const fetchOptions = {
|
||||
// method: 'PROPFIND',
|
||||
// headers: {
|
||||
// 'Authorization': 'Basic ' + authToken,
|
||||
// },
|
||||
// body: body,
|
||||
// };
|
||||
|
||||
// console.info(url, fetchOptions);
|
||||
|
||||
// const response = await shim.fetch(url, fetchOptions);
|
||||
|
||||
// console.info(await response.text());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = WebDavApi;
|
@ -44,8 +44,6 @@ class FileApiDriverLocal {
|
||||
path: stat.path,
|
||||
created_time: stat.birthtime.getTime(),
|
||||
updated_time: stat.mtime.getTime(),
|
||||
created_time_orig: stat.birthtime,
|
||||
updated_time_orig: stat.mtime,
|
||||
isDir: stat.isDirectory(),
|
||||
};
|
||||
}
|
||||
|
124
ReactNativeClient/lib/file-api-driver-webdav.js
Normal file
124
ReactNativeClient/lib/file-api-driver-webdav.js
Normal file
@ -0,0 +1,124 @@
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
class FileApiDriverWebDav {
|
||||
|
||||
constructor(api) {
|
||||
this.api_ = api;
|
||||
}
|
||||
|
||||
api() {
|
||||
return this.api_;
|
||||
}
|
||||
|
||||
async stat(path) {
|
||||
const result = await this.api().execPropFind(path, [
|
||||
'd:getlastmodified',
|
||||
'd:resourcetype',
|
||||
]);
|
||||
|
||||
return this.metadataFromStat_(result, path);
|
||||
}
|
||||
|
||||
metadataFromStat_(stat, 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 lastModifiedString = this.api().stringFromJson(stat, ['d:multistatus', 'd:response', 0, 'd:propstat', 0, 'd:prop', 0, 'd:getlastmodified', 0]);
|
||||
|
||||
if (!lastModifiedString) throw new Error('Could not get lastModified date: ' + JSON.stringify(stat));
|
||||
|
||||
const lastModifiedDate = new Date(lastModifiedString);
|
||||
if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + lastModifiedString);
|
||||
|
||||
return {
|
||||
path: path,
|
||||
created_time: lastModifiedDate.getTime(),
|
||||
updated_time: lastModifiedDate.getTime(),
|
||||
isDir: isCollection === '',
|
||||
};
|
||||
}
|
||||
|
||||
metadataFromStats_(stats) {
|
||||
let output = [];
|
||||
for (let i = 0; i < stats.length; i++) {
|
||||
const mdStat = this.metadataFromStat_(stats[i]);
|
||||
output.push(mdStat);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async setTimestamp(path, timestampMs) {
|
||||
|
||||
}
|
||||
|
||||
async delta(path, options) {
|
||||
// const itemIds = await options.allItemIdsHandler();
|
||||
|
||||
// try {
|
||||
// const stats = await this.fsDriver().readDirStats(path);
|
||||
// let output = this.metadataFromStats_(stats);
|
||||
|
||||
// if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
|
||||
|
||||
// let deletedItems = [];
|
||||
// for (let i = 0; i < itemIds.length; i++) {
|
||||
// const itemId = itemIds[i];
|
||||
// let found = false;
|
||||
// for (let j = 0; j < output.length; j++) {
|
||||
// const item = output[j];
|
||||
// if (BaseItem.pathToId(item.path) == itemId) {
|
||||
// found = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!found) {
|
||||
// deletedItems.push({
|
||||
// path: BaseItem.systemPath(itemId),
|
||||
// isDeleted: true,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// output = output.concat(deletedItems);
|
||||
|
||||
// return {
|
||||
// hasMore: false,
|
||||
// context: null,
|
||||
// items: output,
|
||||
// };
|
||||
// } catch(error) {
|
||||
// throw this.fsErrorToJsError_(error, path);
|
||||
// }
|
||||
}
|
||||
|
||||
async list(path, options) {
|
||||
|
||||
}
|
||||
|
||||
async get(path, options) {
|
||||
|
||||
}
|
||||
|
||||
async mkdir(path) {
|
||||
|
||||
}
|
||||
|
||||
async put(path, content, options = null) {
|
||||
|
||||
}
|
||||
|
||||
async delete(path) {
|
||||
|
||||
}
|
||||
|
||||
async move(oldPath, newPath) {
|
||||
|
||||
}
|
||||
|
||||
format() {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { FileApiDriverWebDav };
|
Loading…
Reference in New Issue
Block a user