@filebox/webdav-server 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,591 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/operations/commands.ts
8
+ var commands_exports = {};
9
+ __export(commands_exports, {
10
+ Copy: () => copy_default,
11
+ Delete: () => delete_default,
12
+ Get: () => get_default,
13
+ Head: () => head_default,
14
+ Mkcol: () => mkcol_default,
15
+ Move: () => move_default,
16
+ NotImplemented: () => not_implemented_default,
17
+ Options: () => options_default,
18
+ Propfind: () => propfind_default,
19
+ Proppatch: () => proppatch_default,
20
+ Put: () => put_default
21
+ });
22
+
23
+ // src/operations/get.ts
24
+ async function loadProxy() {
25
+ const proxy = await import("@filebox/proxy");
26
+ return proxy.default || proxy;
27
+ }
28
+ var get_default = async (ctx) => {
29
+ try {
30
+ const decodedPath = decodeURIComponent(ctx.path);
31
+ const stat = await ctx.driver.stat(decodedPath);
32
+ if (!stat || !stat.file) {
33
+ return {
34
+ status: "405",
35
+ body: "405 Method not allowed"
36
+ };
37
+ }
38
+ const data = await ctx.driver.download(decodedPath);
39
+ if (!data || !data.length || !data[0].url) {
40
+ return {
41
+ status: "404",
42
+ body: "file not found"
43
+ };
44
+ }
45
+ const proxy = await loadProxy();
46
+ return proxy(ctx.req, {
47
+ ...data[0],
48
+ fileName: decodedPath
49
+ });
50
+ } catch (_) {
51
+ return {
52
+ status: "500",
53
+ body: "Internal Server Error"
54
+ };
55
+ }
56
+ };
57
+
58
+ // src/operations/put.ts
59
+ import { basename, dirname } from "pathe";
60
+ import fs from "node:fs";
61
+ import path from "node:path";
62
+ var put_default = async (ctx) => {
63
+ try {
64
+ const rawPath = decodeURIComponent(ctx.path);
65
+ const name = basename(rawPath);
66
+ const upath = dirname(rawPath);
67
+ const contentLength = parseInt(ctx.req.headers["content-length"] || 0);
68
+ const mimeType = ctx.req.headers["content-type"] || "application/octet-stream";
69
+ const tempDir = ctx.config?.temp;
70
+ if (!tempDir) {
71
+ return { status: "500", body: "temp dir not configured" };
72
+ }
73
+ const tempFile = path.join(tempDir, `webdav-upload-${Date.now()}-${name}`);
74
+ const uploadStream = ctx.req._webdavStream || (ctx.req.readable && !ctx.req.readableEnded ? ctx.req : null);
75
+ if (uploadStream && contentLength > 0) {
76
+ await new Promise((resolve, reject) => {
77
+ const ws = fs.createWriteStream(tempFile);
78
+ uploadStream.pipe(ws);
79
+ ws.on("finish", resolve);
80
+ ws.on("error", reject);
81
+ uploadStream.on("error", reject);
82
+ });
83
+ } else {
84
+ fs.writeFileSync(tempFile, "");
85
+ }
86
+ const tempStat = fs.statSync(tempFile);
87
+ if (tempStat.size === 0 && contentLength > 0) {
88
+ return { status: "502", body: "upload failed: empty file" };
89
+ }
90
+ try {
91
+ const result = await ctx.driver.upload(upath, {
92
+ name,
93
+ size: tempStat.size,
94
+ absolutePath: tempFile,
95
+ mimeType
96
+ });
97
+ if (result && typeof result.upload === "function") {
98
+ await result.upload();
99
+ }
100
+ } catch (error) {
101
+ return { status: "502", body: String(error) };
102
+ } finally {
103
+ try {
104
+ fs.unlinkSync(tempFile);
105
+ } catch (_) {
106
+ }
107
+ }
108
+ return { status: "200" };
109
+ } catch (err) {
110
+ return { status: "502", body: String(err?.message || err) };
111
+ }
112
+ };
113
+
114
+ // src/operations/head.ts
115
+ var head_default = async (ctx) => {
116
+ try {
117
+ console.log(ctx.path, "head", 22222);
118
+ const stat = ctx.driver.stat(ctx.path);
119
+ const headers = {
120
+ "Content-Type": "application/octet-stream",
121
+ "Content-Length": stat.size,
122
+ "Last-Modified": stat.mtime,
123
+ "Accept-Ranges": "bytes"
124
+ };
125
+ return {
126
+ status: "200",
127
+ headers,
128
+ body: null
129
+ };
130
+ } catch (error) {
131
+ console.log(error);
132
+ return {
133
+ status: "404"
134
+ };
135
+ }
136
+ };
137
+
138
+ // src/operations/move.ts
139
+ import { basename as basename2, dirname as dirname2 } from "pathe";
140
+ var move_default = async (ctx) => {
141
+ try {
142
+ const dst = new URL(ctx.req.headers?.destination).pathname.replace(
143
+ ctx.base,
144
+ ""
145
+ );
146
+ const src = ctx.path;
147
+ if (!dst) {
148
+ return {
149
+ status: "400",
150
+ body: "Missing source or destination URI"
151
+ };
152
+ }
153
+ const normalizedSrc = src.endsWith("/") ? src.slice(0, -1) : src;
154
+ const normalizedDst = dst.endsWith("/") ? dst.slice(0, -1) : dst;
155
+ if (normalizedSrc === normalizedDst) {
156
+ return { status: "403" };
157
+ }
158
+ const isRename = normalizedSrc.substring(0, normalizedSrc.lastIndexOf("/")) === normalizedDst.substring(0, normalizedDst.lastIndexOf("/"));
159
+ if (isRename) {
160
+ await ctx.driver?.rename(normalizedSrc, basename2(normalizedDst));
161
+ } else {
162
+ await ctx.driver?.move(normalizedSrc, dirname2(normalizedDst));
163
+ }
164
+ return {
165
+ status: "201"
166
+ };
167
+ } catch (_) {
168
+ return {
169
+ status: "502",
170
+ body: null
171
+ };
172
+ }
173
+ };
174
+
175
+ // src/operations/copy.ts
176
+ import { basename as basename3, dirname as dirname3 } from "pathe";
177
+ var copy_default = async (ctx) => {
178
+ try {
179
+ const dst = new URL(ctx.req.headers?.destination).pathname.replace(
180
+ ctx.base,
181
+ ""
182
+ );
183
+ const src = ctx.path;
184
+ if (src === dst) {
185
+ return { status: "403" };
186
+ }
187
+ const stat = await ctx.driver.stat(src);
188
+ if (!stat || !stat.file) {
189
+ return { status: "404" };
190
+ }
191
+ const data = await ctx.driver.download(src);
192
+ if (!data || !data.length) {
193
+ return { status: "404" };
194
+ }
195
+ const upath = dirname3(dst);
196
+ const name = basename3(dst);
197
+ const resp = await fetch(data[0].url);
198
+ await ctx.driver.upload(upath, {
199
+ name,
200
+ size: parseInt(resp.headers.get("content-length") || "0"),
201
+ data: resp.body,
202
+ mimeType: stat.mime || "application/octet-stream"
203
+ });
204
+ return { status: "201" };
205
+ } catch (_) {
206
+ return { status: "502" };
207
+ }
208
+ };
209
+
210
+ // src/operations/mkcol.ts
211
+ var mkcol_default = async (ctx) => {
212
+ try {
213
+ await ctx.driver.mkdir(ctx.path);
214
+ return {
215
+ status: "201",
216
+ body: null
217
+ };
218
+ } catch (_) {
219
+ return {
220
+ status: "500",
221
+ body: null
222
+ };
223
+ }
224
+ };
225
+
226
+ // src/operations/delete.ts
227
+ var delete_default = async (ctx) => {
228
+ try {
229
+ await ctx.driver.remove(ctx.path);
230
+ return {
231
+ status: "204"
232
+ };
233
+ } catch (_) {
234
+ return {
235
+ status: "502"
236
+ };
237
+ }
238
+ };
239
+
240
+ // src/operations/options.ts
241
+ var options_default = async (ctx) => {
242
+ const dav = [1];
243
+ if (ctx.allows?.includes("LOCK")) {
244
+ dav.push(2);
245
+ }
246
+ return {
247
+ headers: {
248
+ // For Microsoft clients
249
+ "MS-Author-Via": "DAV",
250
+ DAV: dav.join(", "),
251
+ Allow: ctx.allows?.join(", ") || ""
252
+ },
253
+ status: "200"
254
+ };
255
+ };
256
+
257
+ // src/operations/propfind.ts
258
+ import xml2js from "xml2js";
259
+
260
+ // src/operations/shared.ts
261
+ import { parseStringPromise, processors } from "xml2js";
262
+ var parseBody = (req, charset) => {
263
+ return new Promise((resolve, reject) => {
264
+ const data = [];
265
+ const stream = req._webdavStream || req;
266
+ stream.on("data", (chunk) => {
267
+ data.push(chunk);
268
+ }).on("error", reject).on("end", () => resolve(Buffer.concat(data).toString(charset)));
269
+ });
270
+ };
271
+ var parseXML = async (req) => {
272
+ const txt = await parseBody(req);
273
+ if (!txt.trim()) return null;
274
+ return await parseStringPromise(txt, {
275
+ // explicitChildren: true,
276
+ explicitArray: false,
277
+ tagNameProcessors: [processors.stripPrefix]
278
+ });
279
+ };
280
+ var shared_default = parseXML;
281
+
282
+ // src/operations/propfind.ts
283
+ var DEFAULT_PROPS = [
284
+ "displayname",
285
+ "getcontentlength",
286
+ "resourcetype",
287
+ "getcontenttype",
288
+ "creationdate",
289
+ "getlastmodified"
290
+ ];
291
+ var propParse = (data) => {
292
+ if (!data) {
293
+ return {
294
+ ns: { prefix: "D", uri: "DAV:" },
295
+ prop: [...DEFAULT_PROPS]
296
+ };
297
+ }
298
+ let prop = [...DEFAULT_PROPS];
299
+ const ns = { prefix: "D", uri: "DAV:" };
300
+ if (data.propfind.hasOwnProperty("prop")) {
301
+ if (data.propfind.prop.hasOwnProperty("allprop")) {
302
+ return {
303
+ ns,
304
+ prop: [...DEFAULT_PROPS]
305
+ };
306
+ }
307
+ prop = Object.keys(data.propfind.prop);
308
+ }
309
+ return { ns, prop };
310
+ };
311
+ var convData = (files, options) => {
312
+ const {
313
+ path: path2,
314
+ base = "",
315
+ depth,
316
+ prop,
317
+ ns: { prefix, uri }
318
+ } = options;
319
+ files = files.data || files.list || files;
320
+ return files.map((file) => {
321
+ const item = {};
322
+ for (const key of prop) {
323
+ if (key === "displayname") {
324
+ item[key] = file.name.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
325
+ }
326
+ if (key === "getcontentlength") {
327
+ item[key] = file.byte || 0;
328
+ }
329
+ if (key === "resourcetype") {
330
+ item[key] = file.type === "folder" ? { collection: "" } : "";
331
+ }
332
+ if (key == "getcontenttype") {
333
+ item[key] = file.mime;
334
+ }
335
+ if (key === "getetag") {
336
+ const stamp = file.etag || file.hash || file.mtime || file.updatedAt || "";
337
+ item[key] = `"${Buffer.from(`${file.name || ""}:${file.byte || 0}:${stamp}`).toString("base64url")}"`;
338
+ }
339
+ if (key === "creationdate" && file.ctime) {
340
+ item[key] = new Date(file.ctime).toUTCString();
341
+ }
342
+ if (key === "getlastmodified" && file.mtime) {
343
+ item[key] = new Date(file.mtime).toUTCString();
344
+ }
345
+ if (key === "quota-available-bytes") {
346
+ item[key] = -1;
347
+ }
348
+ if (key === "quota-used-bytes") {
349
+ item[key] = -1;
350
+ }
351
+ }
352
+ const isSelf = file._self === true;
353
+ let href = (isSelf ? base + path2 : base + path2 + "/" + encodeURIComponent(file.name)).replace(/\/{2,}/g, "/");
354
+ if (file.type === "folder" && !href.endsWith("/")) href += "/";
355
+ return {
356
+ href,
357
+ propstat: {
358
+ status: "HTTP/1.1 200 OK",
359
+ prop: item
360
+ }
361
+ };
362
+ });
363
+ };
364
+ var fixNs = (data, prefix) => {
365
+ if (!prefix) return data;
366
+ Object.keys(data).forEach((key) => {
367
+ const val = data[key];
368
+ if (key != "$" && prefix) {
369
+ if (Array.isArray(val) || typeof val == "object") {
370
+ fixNs(val, prefix);
371
+ }
372
+ delete data[key];
373
+ data[`${prefix}:${key}`] = val;
374
+ } else {
375
+ if (val.xmlns) {
376
+ val[`xmlns:${prefix}`] = val.xmlns;
377
+ delete val.xmlns;
378
+ }
379
+ }
380
+ });
381
+ return data;
382
+ };
383
+ var createXML = (data, options) => {
384
+ const {
385
+ ns: { prefix, uri }
386
+ } = options;
387
+ const obj = {
388
+ multistatus: {
389
+ response: convData(data || [], options)
390
+ }
391
+ };
392
+ if (uri) {
393
+ obj.multistatus.$ = {
394
+ xmlns: uri
395
+ };
396
+ }
397
+ const builder = new xml2js.Builder({
398
+ renderOpts: { pretty: false },
399
+ xmldec: { version: "1.0", encoding: "UTF-8" }
400
+ });
401
+ const xml = builder.buildObject(fixNs(obj, prefix));
402
+ return xml;
403
+ };
404
+ var propfind_default = async (ctx) => {
405
+ const options = Object.assign(
406
+ {
407
+ path: ctx.path,
408
+ base: ctx.base,
409
+ depth: ctx.depth
410
+ },
411
+ propParse(await shared_default(ctx.req))
412
+ );
413
+ let data = {};
414
+ data.item = await ctx.driver.stat(ctx.path);
415
+ if (ctx.depth == "1") {
416
+ try {
417
+ data.files = await ctx.driver.list(ctx.path);
418
+ } catch (error) {
419
+ return { status: "500" };
420
+ }
421
+ }
422
+ if (!data) return { status: "404" };
423
+ if (data.error) {
424
+ if (data.error.code == 401) {
425
+ return {
426
+ headers: {
427
+ "WWW-Authenticate": `Basic realm="ShareList WebDAV"`
428
+ },
429
+ status: "401"
430
+ };
431
+ } else {
432
+ return {
433
+ status: "404"
434
+ };
435
+ }
436
+ }
437
+ const selfItem = Object.assign({}, data.item, { _self: true });
438
+ if (ctx.depth == "0") {
439
+ return {
440
+ status: "207",
441
+ headers: {
442
+ "content-type": 'text/xml; charset="utf-8"'
443
+ },
444
+ body: createXML([selfItem], options)
445
+ };
446
+ } else if (ctx.depth == "1") {
447
+ const listResult = data.files;
448
+ const children = Array.isArray(listResult) ? listResult : Array.isArray(listResult && listResult.data) ? listResult.data : Array.isArray(listResult && listResult.list) ? listResult.list : [];
449
+ return {
450
+ status: "207",
451
+ headers: {
452
+ // "content-type": 'text/xml; charset="utf-8"',
453
+ "content-type": 'application/xml; charset="utf-8"'
454
+ },
455
+ body: createXML([selfItem].concat(children), options)
456
+ };
457
+ } else if (ctx.depth == "infinity") {
458
+ return {
459
+ status: "404"
460
+ };
461
+ }
462
+ };
463
+
464
+ // src/operations/proppatch.ts
465
+ var proppatch_default = async (ctx) => {
466
+ return {
467
+ status: "200"
468
+ };
469
+ };
470
+
471
+ // src/operations/not-implemented.ts
472
+ var not_implemented_default = async (ctx) => {
473
+ return {
474
+ status: "405 Method not allowed",
475
+ headers: {
476
+ Allow: Object.keys(commands_exports).join(", ")
477
+ }
478
+ };
479
+ };
480
+
481
+ // src/context.ts
482
+ var createContext = (req, base, allows, temp) => {
483
+ const authorization = req.headers?.authorization?.split(" ")[1];
484
+ const path2 = new URL(req.url, `http://${req.headers.host}`).pathname;
485
+ const ctx = {
486
+ req,
487
+ depth: req.headers?.depth || 0,
488
+ method: (req.method || "").toLowerCase(),
489
+ path: path2.replace(base, "").replace("/k", ""),
490
+ // 临时这么做
491
+ base,
492
+ config: {
493
+ temp
494
+ },
495
+ auth: { user: void 0, pass: void 0 },
496
+ allows,
497
+ get(field) {
498
+ const req2 = this.req;
499
+ switch (field = field.toLowerCase()) {
500
+ case "referer":
501
+ case "referrer":
502
+ return req2.headers.referrer || req2.headers.referer || "";
503
+ default:
504
+ return req2.headers[field] || "";
505
+ }
506
+ }
507
+ };
508
+ if (authorization) {
509
+ const pairs = Buffer.from(authorization, "base64").toString("utf8").split(":");
510
+ ctx.auth = { user: pairs[0], pass: pairs[1] };
511
+ }
512
+ return ctx;
513
+ };
514
+ var context_default = createContext;
515
+
516
+ // src/index.ts
517
+ var StatusCodes = {
518
+ 200: "OK",
519
+ 201: "Created",
520
+ 204: "No Content",
521
+ 207: "Multi Status",
522
+ 302: "Moved Temporarily",
523
+ 401: "Unauthorized",
524
+ 403: "Forbidden",
525
+ 404: "Not Found",
526
+ 409: "Conflict",
527
+ 423: "Locked",
528
+ 500: "Internal Server Error"
529
+ };
530
+ var VirtualDriver = {
531
+ async get() {
532
+ return {
533
+ status: "200",
534
+ body: "Hello World!",
535
+ headers: {
536
+ "Content-Type": "application/json"
537
+ }
538
+ };
539
+ }
540
+ // 其他方法...
541
+ };
542
+ var WebDAVServer = class {
543
+ constructor({ driver, base, redirect, auth } = { redirect: false, auth: () => true }) {
544
+ this.methods = {};
545
+ this.driver = driver || VirtualDriver;
546
+ this.base = base || "";
547
+ this.auth = auth;
548
+ this.config = { redirect };
549
+ const commands = commands_exports;
550
+ for (const k in commands) {
551
+ if (k === "NotImplemented") {
552
+ this.unknownMethod = commands[k];
553
+ } else {
554
+ this.methods[k.toLowerCase()] = commands[k];
555
+ }
556
+ }
557
+ this.allows = Object.keys(this.methods).map((i) => i.toUpperCase());
558
+ }
559
+ async request(req, options) {
560
+ const ctx = context_default(
561
+ req,
562
+ options?.base || this.base,
563
+ this.allows,
564
+ options.temp
565
+ );
566
+ if (!(ctx.method == "options" && !ctx.path) && !this?.auth(ctx.auth.user, ctx.auth.pass)) {
567
+ return {
568
+ headers: {
569
+ "X-WebDAV-Status": `401 ${StatusCodes[401]}`,
570
+ "www-Authenticate": 'Basic realm="Restricted"'
571
+ },
572
+ status: "401"
573
+ };
574
+ }
575
+ ctx.driver = options.driver;
576
+ ctx.config = { ...this.config, temp: options.temp };
577
+ const method = this.methods[ctx.method] || this.unknownMethod;
578
+ const res = await method(ctx);
579
+ res.headers = res.headers ?? {};
580
+ if (res.status) {
581
+ }
582
+ return res;
583
+ }
584
+ };
585
+ var index_default = {
586
+ WebDAVServer
587
+ };
588
+ export {
589
+ WebDAVServer,
590
+ index_default as default
591
+ };
package/package.json CHANGED
@@ -1,28 +1,36 @@
1
1
  {
2
2
  "name": "@filebox/webdav-server",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "WebDAV server adapter for FileBox drivers",
5
- "main": "./index.js",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
6
9
  "exports": {
7
10
  ".": {
8
- "require": "./index.js",
9
- "default": "./index.js"
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs",
14
+ "default": "./dist/index.mjs"
10
15
  }
11
16
  },
12
17
  "files": [
13
- "index.js",
14
- "context.js",
15
- "operations",
16
- "LICENSE",
17
- "README.md"
18
+ "dist"
18
19
  ],
19
20
  "publishConfig": {
20
21
  "access": "public"
21
22
  },
22
23
  "license": "MIT",
24
+ "scripts": {
25
+ "build": "node build.mjs",
26
+ "prepack": "node build.mjs"
27
+ },
23
28
  "dependencies": {
24
29
  "@filebox/proxy": "^1.0.0",
25
30
  "pathe": "^1.1.2",
26
31
  "xml2js": "^0.6.0"
32
+ },
33
+ "devDependencies": {
34
+ "esbuild": "^0.24.0"
27
35
  }
28
36
  }
package/context.js DELETED
@@ -1,37 +0,0 @@
1
- const createContext = (req, base, allows, temp) => {
2
- const authorization = req.headers?.authorization?.split(" ")[1];
3
-
4
- const path = new URL(req.url, `http://${req.headers.host}`).pathname;
5
- const ctx = {
6
- req: req,
7
- depth: req.headers?.depth || 0,
8
- method: (req.method || "").toLowerCase(),
9
- path: path.replace(base, "").replace("/k", ""), // 临时这么做
10
- base,
11
- config: {
12
- temp,
13
- },
14
- auth: { user: undefined, pass: undefined },
15
- allows,
16
- get(field) {
17
- const req = this.req;
18
- switch ((field = field.toLowerCase())) {
19
- case "referer":
20
- case "referrer":
21
- return req.headers.referrer || req.headers.referer || "";
22
- default:
23
- return req.headers[field] || "";
24
- }
25
- },
26
- };
27
-
28
- if (authorization) {
29
- const pairs = Buffer.from(authorization, "base64")
30
- .toString("utf8")
31
- .split(":");
32
- ctx.auth = { user: pairs[0], pass: pairs[1] };
33
- }
34
- return ctx;
35
- };
36
-
37
- module.exports = createContext;