db-to-cloud
Version:
Synchronize your database with a cloud drive i.e. Dropbox, Google Drive, OneDrive, Github, etc.
197 lines (182 loc) • 4.77 kB
JavaScript
/* global self */
const {dirname, basename} = require("path");
const {createRequest} = require("../request");
const {createXMLParser} = require("../util");
function arrayify(o) {
return Array.isArray(o) ? o : [o];
}
function createDrive({
username,
password,
url,
fetch = (typeof self !== "undefined" ? self : global).fetch,
DOMParser = (typeof self !== "undefined" ? self : global).DOMParser,
parseXML = createXMLParser(DOMParser)
}) {
if (!url.endsWith("/")) {
url += "/";
}
let lockToken;
const request = createRequest({fetch, username, password});
return {
name: "webdav",
get,
put,
post,
delete: delete_,
list,
// acquireLock,
// releaseLock
};
async function requestDAV({path, ...args}) {
if (lockToken) {
args.headers = args.headers || {};
args.headers["If"] = `(${lockToken})`;
}
const text = await request({
path: `${url}${path}`,
...args
});
if (args.format || typeof text !== "string" || !text) return text;
const result = await parseXML(text);
if (result.error) {
throw new Error(`Failed requesting DAV at ${url}${path}: ${JSON.stringify(result.error)}`);
}
if (result.multistatus) {
result.multistatus.response = arrayify(result.multistatus.response);
for (const r of result.multistatus.response) {
if (r.error) {
throw new Error(`Failed requesting DAV at ${url}${path}: ${r.href} ${r.error}`);
}
}
}
return result;
}
async function list(file) {
if (!file.endsWith("/")) {
file += "/";
}
const result = await requestDAV({
method: "PROPFIND",
path: file,
contentType: "application/xml",
body:
`<?xml version="1.0" encoding="utf-8" ?>
<propfind xmlns="DAV:">
<allprop/>
</propfind>`,
headers: {
"Depth": "1"
}
});
const files = [];
for (const entry of arrayify(result.multistatus.response)) {
if (arrayify(entry.propstat).some(s => s.prop.resourcetype && s.prop.resourcetype.collection !== undefined)) {
continue;
}
// FIXME: what is the proper way to get the filename from entry?
// NOTE: some servers may treat `@` and `%40` as the same path, we may have to normalize them here and there.
const base = `${url}${file}`;
const absUrl = new URL(entry.href, base).href;
const name = basename(absUrl);
files.push(name);
}
return files;
}
async function get(file) {
return await requestDAV({
method: "GET",
path: file,
format: "text"
});
}
async function put(file, data) {
return await withDir(
dirname(file),
() => requestDAV({
method: "PUT",
path: file,
contentType: "application/octet-stream",
body: data
})
);
}
async function withDir(dir, cb) {
try {
return await cb();
} catch (err) {
if (err.code !== 409 && err.code !== 404 || dir === ".") {
throw err;
}
}
await withDir(dirname(dir), () =>
requestDAV({
method: "MKCOL",
path: dir
})
);
return await cb();
}
async function post(file, data) {
try {
return await withDir(
dirname(file),
() => requestDAV({
method: "PUT",
path: file,
body: data,
contentType: "octet-stream",
headers: {
// FIXME: seems webdav-server doesn't support etag, what about others?
"If-None-Match": "*"
}
})
);
} catch (err) {
if (err.code === 412) {
err.code = "EEXIST";
}
throw err;
}
}
async function delete_(file) {
// FIXME: support deleting collections?
// FIXME: handle errors?
try {
await requestDAV({
method: "DELETE",
path: file
});
} catch (err) {
if (err.code === 404) return;
throw err;
}
}
// async function acquireLock(mins) {
// const r = await requestDAV({
// method: "LOCK",
// path: "",
// body:
// `<?xml version="1.0" encoding="utf-8" ?>
// <lockinfo xmlns='DAV:'>
// <lockscope><exclusive/></lockscope>
// <locktype><write/></locktype>
// </lockinfo> `,
// headers: {
// "Timeout": `Second-${mins * 60}`
// },
// raw: true
// });
// lockToken = r.headers.get("Lock-Token");
// }
// async function releaseLock() {
// await requestDAV({
// method: "UNLOCK",
// path: "",
// headers: {
// "Lock-Token": lockToken
// }
// });
// }
}
module.exports = createDrive;