From a25898fcf2b54b114573c7addeb98ddbcf9529a7 Mon Sep 17 00:00:00 2001 From: reruin Date: Sat, 9 Oct 2021 17:19:18 +0800 Subject: [PATCH] feat(webdav): release 0.1.0 --- packages/sharelist-webdav/package-lock.json | 289 ++++++++++++++++++ packages/sharelist-webdav/package.json | 22 ++ packages/sharelist-webdav/src/context.ts | 25 ++ packages/sharelist-webdav/src/index.ts | 58 ++++ .../src/operations/commands.ts | 31 ++ .../sharelist-webdav/src/operations/copy.ts | 23 ++ .../sharelist-webdav/src/operations/delete.ts | 14 + .../sharelist-webdav/src/operations/get.ts | 22 ++ .../sharelist-webdav/src/operations/head.ts | 3 + .../sharelist-webdav/src/operations/lock.ts | 3 + .../sharelist-webdav/src/operations/mkcol.ts | 24 ++ .../sharelist-webdav/src/operations/move.ts | 24 ++ .../src/operations/not-implemented.ts | 10 + .../src/operations/options.ts | 11 + .../sharelist-webdav/src/operations/post.ts | 3 + .../src/operations/propfind.ts | 171 +++++++++++ .../src/operations/proppatch.ts | 8 + .../sharelist-webdav/src/operations/put.ts | 19 ++ .../sharelist-webdav/src/operations/shared.ts | 25 ++ .../sharelist-webdav/src/operations/unlock.ts | 3 + packages/sharelist-webdav/src/types.ts | 54 ++++ packages/sharelist-webdav/tsconfig.json | 34 +++ 22 files changed, 876 insertions(+) create mode 100644 packages/sharelist-webdav/package-lock.json create mode 100644 packages/sharelist-webdav/package.json create mode 100644 packages/sharelist-webdav/src/context.ts create mode 100644 packages/sharelist-webdav/src/index.ts create mode 100644 packages/sharelist-webdav/src/operations/commands.ts create mode 100644 packages/sharelist-webdav/src/operations/copy.ts create mode 100644 packages/sharelist-webdav/src/operations/delete.ts create mode 100644 packages/sharelist-webdav/src/operations/get.ts create mode 100644 packages/sharelist-webdav/src/operations/head.ts create mode 100644 packages/sharelist-webdav/src/operations/lock.ts create mode 100644 packages/sharelist-webdav/src/operations/mkcol.ts create mode 100644 packages/sharelist-webdav/src/operations/move.ts create mode 100644 packages/sharelist-webdav/src/operations/not-implemented.ts create mode 100644 packages/sharelist-webdav/src/operations/options.ts create mode 100644 packages/sharelist-webdav/src/operations/post.ts create mode 100644 packages/sharelist-webdav/src/operations/propfind.ts create mode 100644 packages/sharelist-webdav/src/operations/proppatch.ts create mode 100644 packages/sharelist-webdav/src/operations/put.ts create mode 100644 packages/sharelist-webdav/src/operations/shared.ts create mode 100644 packages/sharelist-webdav/src/operations/unlock.ts create mode 100644 packages/sharelist-webdav/src/types.ts create mode 100644 packages/sharelist-webdav/tsconfig.json diff --git a/packages/sharelist-webdav/package-lock.json b/packages/sharelist-webdav/package-lock.json new file mode 100644 index 00000000..fc42766d --- /dev/null +++ b/packages/sharelist-webdav/package-lock.json @@ -0,0 +1,289 @@ +{ + "name": "@sharelist/webdav", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", + "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "dev": true, + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/body-parser": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==", + "dev": true + }, + "@types/cookies": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz", + "integrity": "sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/http-assert": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.2.tgz", + "integrity": "sha512-Ddzuzv/bB2prZnJKlS1sEYhaeT50wfJjhcTTTQLjEsEZJlk3XB4Xohieyq+P4VXIzg7lrQ1Spd/PfRnBpQsdqA==", + "dev": true + }, + "@types/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==", + "dev": true + }, + "@types/keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==", + "dev": true + }, + "@types/koa": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.4.tgz", + "integrity": "sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==", + "dev": true, + "requires": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", + "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "@types/node": { + "version": "16.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.1.tgz", + "integrity": "sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A==", + "dev": true + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/xml2js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", + "integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "acorn": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", + "dev": true + }, + "acorn-walk": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.1.1.tgz", + "integrity": "sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==", + "dev": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "ts-node": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", + "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "0.6.1", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + } + }, + "typescript": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "dev": true + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/packages/sharelist-webdav/package.json b/packages/sharelist-webdav/package.json new file mode 100644 index 00000000..e57ead6c --- /dev/null +++ b/packages/sharelist-webdav/package.json @@ -0,0 +1,22 @@ +{ + "name": "@sharelist/webdav", + "version": "0.1.1", + "repository": "https://github.com/reruin/sharelist", + "license": "MIT", + "main": "dist/index.js", + "scripts": { + "dev": "tsc -w -p .", + "build": "rm -rf dist && tsc -p .", + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path .", + "release": "node ../../scripts/release.js" + }, + "devDependencies": { + "@types/koa": "^2.13.4", + "@types/xml2js": "^0.4.9", + "ts-node": "^10.2.1", + "typescript": "^4.3.5" + }, + "dependencies": { + "xml2js": "^0.4.23" + } +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/context.ts b/packages/sharelist-webdav/src/context.ts new file mode 100644 index 00000000..bc3a507d --- /dev/null +++ b/packages/sharelist-webdav/src/context.ts @@ -0,0 +1,25 @@ +import http from 'http' +import { Context, WebDAVDepth } from './types' +import { parseXML } from './operations/shared' + +export default (req: http.IncomingMessage, base: string): Context => { + return { + req: req, + depth: req.headers?.depth as WebDAVDepth, + method: (req.method as string || '').toLowerCase(), + path: req.url?.replace(base, ''), + base, + config: {}, + get(field: string): string | undefined { + const req: http.IncomingMessage = this.req + switch (field = field.toLowerCase()) { + case 'referer': + case 'referrer': + return req.headers.referrer as string || req.headers.referer || '' + default: + return req.headers[field] as string || '' + } + } + } + +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/index.ts b/packages/sharelist-webdav/src/index.ts new file mode 100644 index 00000000..da23038c --- /dev/null +++ b/packages/sharelist-webdav/src/index.ts @@ -0,0 +1,58 @@ + +import { WebDAVMethod, WebDAVRequest, Driver, DriverMethod, Context, Response } from "./types" +import Commands from './operations/commands' +import { default as createContext } from './context' + +export type WebDAVServerOptions = { + driver?: Driver, + base?: string, + redirect: boolean +} + +const VirtualDriver: Driver = (actions: DriverMethod, options: any): any => { + console.log(`VirtualDriver ${actions}`, options) + return false +} + +export class WebDAVServer { + public methods: { [methodName: string]: WebDAVMethod } + + protected unknownMethod: WebDAVMethod | undefined + + protected driver: Driver | undefined + + protected base: string + + protected config: Record + + protected allow: string + + constructor({ driver, base, redirect }: WebDAVServerOptions = { redirect: false }) { + this.methods = {} + this.driver = driver || VirtualDriver + this.base = base || '' + this.config = { redirect } + const commands: { [key: string]: any } = Commands + for (const k in commands) + if (k === 'NotImplemented') + this.unknownMethod = commands[k] + else + this.methods[k.toLowerCase()] = commands[k] + this.allow = Object.keys(this.methods).map(i => i.toUpperCase()).join(', ') + } + + async request(req: WebDAVRequest): Promise { + const ctx: Context = createContext(req, this.base) + ctx.driver = this.driver + ctx.config = this.config + ctx.allow = this.allow + const method = this.methods[ctx.method] || this.unknownMethod + + const res = await method(ctx) + if (res.status) { + res.headers ||= {} + // res.headers['X-WebDAV-Status'] = res.status + } + return res + } +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/commands.ts b/packages/sharelist-webdav/src/operations/commands.ts new file mode 100644 index 00000000..5666661e --- /dev/null +++ b/packages/sharelist-webdav/src/operations/commands.ts @@ -0,0 +1,31 @@ +import Get from './get' +import Put from './put' +import Post from './post' +import Head from './head' +import Move from './move' +import Lock from './lock' +import Copy from './copy' +import Mkcol from './mkcol' +import Unlock from './unlock' +import Delete from './delete' +import Options from './options' +import Propfind from './propfind' +import Proppatch from './proppatch' +import NotImplemented from './not-implemented' + +export default { + NotImplemented, + Proppatch, + Propfind, + Options, + Delete, + // Unlock, + Mkcol, + // Copy, + // Lock, + Move, + // Head, + // Post, + Put, + Get +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/copy.ts b/packages/sharelist-webdav/src/operations/copy.ts new file mode 100644 index 00000000..01201e4f --- /dev/null +++ b/packages/sharelist-webdav/src/operations/copy.ts @@ -0,0 +1,23 @@ +import { Context, Response } from '../types' +import { URL } from 'url' + +export default async (ctx: Context): Promise => { + const dst = new URL(ctx.req.headers?.destination as string).pathname.replace(ctx.base, '') + const src = ctx.path + //The source URI and the destination URI are the same. + if (src === dst) { + return { status: '403' } + } + + const res = await ctx.driver?.('mv', src, dst, true) + if (res?.error) { + return { + status: res.error.code || '502' + } + } else { + return { + status: '201' + } + } + +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/delete.ts b/packages/sharelist-webdav/src/operations/delete.ts new file mode 100644 index 00000000..f9eebbaa --- /dev/null +++ b/packages/sharelist-webdav/src/operations/delete.ts @@ -0,0 +1,14 @@ +import { Context, Response } from '../types' + +export default async (ctx: Context): Promise => { + const res = await ctx.driver?.('rm', ctx.path) + if (res?.error) { + return { + status: res.error.code || '502' + } + } else { + return { + status: '204' + } + } +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/get.ts b/packages/sharelist-webdav/src/operations/get.ts new file mode 100644 index 00000000..e1f92ae4 --- /dev/null +++ b/packages/sharelist-webdav/src/operations/get.ts @@ -0,0 +1,22 @@ +import { parseXML } from './shared' +import { Context, Response } from '../types' + +const filterHeaders = (headers: Record): Record => { + const exclude = ['range', 'accept-encoding'] + const ret: Record = {} + Object.keys(headers).filter((i: string) => exclude.includes(i.toLocaleLowerCase())).forEach((key: string) => { + ret[key] = headers[key] + }) + return ret +} + +export default async (ctx: Context): Promise => { + const res = await ctx.driver?.('get', ctx.path, { reqHeaders: filterHeaders(ctx.req.headers) }) + if (res?.error) { + return { + status: res.error.code || '502' + } + } else { + return res as Response + } +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/head.ts b/packages/sharelist-webdav/src/operations/head.ts new file mode 100644 index 00000000..5c933b16 --- /dev/null +++ b/packages/sharelist-webdav/src/operations/head.ts @@ -0,0 +1,3 @@ +export default { + +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/lock.ts b/packages/sharelist-webdav/src/operations/lock.ts new file mode 100644 index 00000000..5c933b16 --- /dev/null +++ b/packages/sharelist-webdav/src/operations/lock.ts @@ -0,0 +1,3 @@ +export default { + +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/mkcol.ts b/packages/sharelist-webdav/src/operations/mkcol.ts new file mode 100644 index 00000000..5f75d09e --- /dev/null +++ b/packages/sharelist-webdav/src/operations/mkcol.ts @@ -0,0 +1,24 @@ +import { parseXML } from './shared' +import { Context, Response } from '../types' + +export default async (ctx: Context): Promise => { + const data = await ctx.driver?.('mkdir', ctx.path) + if (data?.error) { + if (data.error.code == 401) { + return { + headers: { + 'WWW-Authenticate': `Basic realm="ShareList WebDAV"` + }, + status: '401' + } + } else { + return { + status: '404' + } + } + } else { + return { + status: '201' + } + } +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/move.ts b/packages/sharelist-webdav/src/operations/move.ts new file mode 100644 index 00000000..8c574495 --- /dev/null +++ b/packages/sharelist-webdav/src/operations/move.ts @@ -0,0 +1,24 @@ +import { Context, Response } from '../types' +import { URL } from 'url' + +export default async (ctx: Context): Promise => { + console.log(ctx.req.headers) + const dst = new URL(ctx.req.headers?.destination as string).pathname.replace(ctx.base, '') + const src = ctx.path + //The source URI and the destination URI are the same. + if (src === dst) { + return { status: '403' } + } + + const res = await ctx.driver?.('mv', src, dst) + if (res?.error) { + return { + status: res.error.code || '502' + } + } else { + return { + status: '201' + } + } + +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/not-implemented.ts b/packages/sharelist-webdav/src/operations/not-implemented.ts new file mode 100644 index 00000000..981fc464 --- /dev/null +++ b/packages/sharelist-webdav/src/operations/not-implemented.ts @@ -0,0 +1,10 @@ +import { Context, Response } from '../types' +import commands from './commands' +export default async (ctx: Context): Promise => { + return { + status: '405 Method not allowed', + headers: { + 'Allow': Object.keys(commands).join(', ') + } + } +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/options.ts b/packages/sharelist-webdav/src/operations/options.ts new file mode 100644 index 00000000..a326c67b --- /dev/null +++ b/packages/sharelist-webdav/src/operations/options.ts @@ -0,0 +1,11 @@ +import { parseXML } from './shared' +import { Context, Response } from '../types' + +export default async (ctx: Context): Promise => { + return { + status: '200', + headers: { + 'allow': ctx.allow + } + } +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/post.ts b/packages/sharelist-webdav/src/operations/post.ts new file mode 100644 index 00000000..5c933b16 --- /dev/null +++ b/packages/sharelist-webdav/src/operations/post.ts @@ -0,0 +1,3 @@ +export default { + +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/propfind.ts b/packages/sharelist-webdav/src/operations/propfind.ts new file mode 100644 index 00000000..50d3c2c2 --- /dev/null +++ b/packages/sharelist-webdav/src/operations/propfind.ts @@ -0,0 +1,171 @@ +import { parseXML } from './shared' +import { Context, Response } from '../types' +import xml2js from 'xml2js' + +const DEFAULT_PROPS = [ + 'displayname', + 'getcontentlength', + 'resourcetype', + // 'getcontenttype', + 'creationdate', + 'getlastmodified' +] + +/** + * Parse props from webdav request + * + * @param {object} [data] + * @return {object|boolean} + */ +const propParse = (data: any) => { + if (!data) return false + + let prop = [...DEFAULT_PROPS] + const prefix = Object.keys(data.propfind.$).find(i => i.startsWith('xmlns:'))?.split(':')[1] || '' + const uri = data.propfind.$?.[`xmlns${prefix ? `:${prefix}` : ''}`] || '' + if (data.propfind.hasOwnProperty('prop')) { + prop = Object.keys(data.propfind.prop) + } + return { ns: { prefix, uri }, prop } +} + +/** + * Create webdav responese xml by data and props options + * + * @param {object} [data] file data + * @param {object} [options] + * @param {object} [options.props] Available props + * @param {object} [options.path] Current folder path + * @param {object} [options.ns] + * @return {string} XML string + */ + +const convData = (files: Array, options: any) => { + const { path, base = '', prop, ns: { prefix, uri } } = options + + return files.map((file: any) => { + const item: Record = {} + for (const key of prop) { + if (key == 'displayname') { + item[key] = file.name.replace(/&/g, '&').replace(/\ { + if (!prefix) return data + Object.keys(data).forEach((key: string) => { + const val = data[key] + if (key != '$') { + if (Array.isArray(val) || typeof val == 'object') { + fixNs(val, prefix) + } + delete data[key] + data[`${prefix}:${key}`] = val + } else { + if (val.xmlns) { + val[`xmlns:${prefix}`] = val.xmlns + delete val.xmlns + } + } + }) + return data +} + +const createXML = (data: any, options: any) => { + const { ns: { prefix, uri } } = options + + const obj = { + multistatus: { + $: { + "xmlns": uri + }, + response: convData(data?.files || [], options) + } + } + + const builder = new xml2js.Builder({ + renderOpts: { pretty: false }, + xmldec: { version: '1.0', encoding: 'UTF-8' } + }) + + const xml = builder.buildObject(fixNs(obj, prefix)) + return xml +} + +export default async (ctx: Context): Promise => { + const options = Object.assign({ + path: ctx.path, + base: ctx.base, + depth: ctx.depth, + }, propParse(await parseXML(ctx.req))) + + const data: any = ctx.depth == '0' ? await ctx.driver?.('stat', ctx.path) : await ctx.driver?.('ls', ctx.path) + + if (data.error) { + if (data.error.code == 401) { + // Windows seems to require this being the last header sent + // (changed according to PECL bug #3138) + return { + headers: { + 'WWW-Authenticate': `Basic realm="ShareList WebDAV"` + }, + status: '401' + } + } else { + return { + status: '404' + } + } + } + + //return itself + if (ctx.depth == '0') { + if (!data.files) { + return { + status: '207', + headers: { + 'content-type': 'text/xml; charset="utf-8"' + }, + body: createXML([data], options) + } + } + } + else if (ctx.depth == '1') { + if (data.files) { + return { + status: '207', + headers: { + 'content-type': 'text/xml; charset="utf-8"' + }, + body: createXML(data, options) + } + } + } + else if (ctx.depth == 'infinity') { + return { + status: '404' + } + } +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/proppatch.ts b/packages/sharelist-webdav/src/operations/proppatch.ts new file mode 100644 index 00000000..be376242 --- /dev/null +++ b/packages/sharelist-webdav/src/operations/proppatch.ts @@ -0,0 +1,8 @@ +import { parseXML } from './shared' +import { Context, Response } from '../types' + +export default async (ctx: Context): Promise => { + return { + status: '200' + } +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/put.ts b/packages/sharelist-webdav/src/operations/put.ts new file mode 100644 index 00000000..89f965f4 --- /dev/null +++ b/packages/sharelist-webdav/src/operations/put.ts @@ -0,0 +1,19 @@ +import { parseXML } from './shared' +import { Context, Response } from '../types' + +export default async (ctx: Context): Promise => { + console.log('>>', ctx.req.isPaused(), ctx.req.headers) + ctx.req.headers.connection = 'keep-alive' + const size = parseInt(ctx.req.headers['content-length'] || '0') + const res = await ctx.driver?.('upload', ctx.path, ctx.req, { size }) + + if (res?.error) { + return { + status: res.error.code || '502' + } + } else { + return { + status: '201' + } + } +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/operations/shared.ts b/packages/sharelist-webdav/src/operations/shared.ts new file mode 100644 index 00000000..8e5e86e5 --- /dev/null +++ b/packages/sharelist-webdav/src/operations/shared.ts @@ -0,0 +1,25 @@ +import * as http from 'http' +import { Readable } from 'stream' +import { parseStringPromise, processors } from 'xml2js' + +const saveStream = (stream: Readable, charset: BufferEncoding | undefined = 'utf8'): Promise => { + return new Promise((resolve, reject) => { + const data: Array = [] + stream + .on('data', chunk => { + data.push(chunk) + }) + .on('error', reject) + .on('end', () => resolve(Buffer.concat(data).toString(charset))) + }) +} + + +export const parseXML = async (req: http.IncomingMessage): Promise => { + const txt = await saveStream(req) + return await parseStringPromise(txt, { + // explicitChildren: true, + explicitArray: false, + tagNameProcessors: [processors.stripPrefix] + }) +} diff --git a/packages/sharelist-webdav/src/operations/unlock.ts b/packages/sharelist-webdav/src/operations/unlock.ts new file mode 100644 index 00000000..5c933b16 --- /dev/null +++ b/packages/sharelist-webdav/src/operations/unlock.ts @@ -0,0 +1,3 @@ +export default { + +} \ No newline at end of file diff --git a/packages/sharelist-webdav/src/types.ts b/packages/sharelist-webdav/src/types.ts new file mode 100644 index 00000000..23b78bcc --- /dev/null +++ b/packages/sharelist-webdav/src/types.ts @@ -0,0 +1,54 @@ +import http from 'http' + +export type DriverMethod = 'stat' | 'get' | 'ls' | 'rm' | 'mkdir' | 'upload' | 'mv' + +export type WebDAVDepth = "0" | "1" | "1,noroot" | "infinity" + +export type WebDAVRequest = http.IncomingMessage + +export type DriverMethodResponse = { + status?: string, + data?: any, + error?: { code?: string | number, message: string } +} + +export type Driver = { + // [key in DriverMethod]: (options: any) => any + (actions: DriverMethod, ...options: Array): DriverMethodResponse +} + +export interface Context { + req: http.IncomingMessage, + depth: WebDAVDepth, + method: string, + path: string | undefined, + base: string, + get(field: string): any, + driver?: Driver, + allow?: string, + config: Record +} + + +export interface WebDAVMethod { + (ctx: Context): any +} + +export type Response = { + status: string | number, + headers?: Record, + body?: any +} + +export const StatusCodes = { + 200: 'OK', + 201: 'Created', + 204: 'No Content', + 207: 'Multi Status', + 302: 'Moved Temporarily', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 409: 'Conflict', + 423: 'Locked' +} \ No newline at end of file diff --git a/packages/sharelist-webdav/tsconfig.json b/packages/sharelist-webdav/tsconfig.json new file mode 100644 index 00000000..6ea94866 --- /dev/null +++ b/packages/sharelist-webdav/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "es2015", + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + // "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "importHelpers": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "baseUrl": "./src", + "experimentalDecorators": true, + "lib": [ + "esnext", + "scripthost" + ], + "rootDir": "./src", + "outDir": "./dist", + "watch": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.vue", + "tests/**/*.ts", + "tests/**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file