UNPKG

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
/* 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;