@server/next 0.49.1 → 0.49.3

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.
Files changed (3) hide show
  1. package/index.d.ts +13 -6
  2. package/index.js +74 -46
  3. package/package.json +2 -2
package/index.d.ts CHANGED
@@ -7,22 +7,29 @@ type FileInfo = {
7
7
  type: string | null;
8
8
  modified: Date;
9
9
  };
10
+ type ReadOptions = {
11
+ signal?: AbortSignal;
12
+ };
10
13
  type BucketFile = {
11
14
  readonly path: string;
12
15
  readonly name: string;
13
16
  readonly type?: string;
14
- exists(): Promise<boolean>;
15
- info?(): Promise<FileInfo | null>;
17
+ exists(opts?: ReadOptions): Promise<boolean>;
18
+ info?(opts?: ReadOptions): Promise<FileInfo | null>;
16
19
  write(content: string | Buffer | ReadableStream, options?: {
17
20
  type?: string;
18
- }): Promise<void>;
19
- stream(): ReadableStream;
21
+ } & ReadOptions): Promise<unknown>;
22
+ stream(opts?: ReadOptions): ReadableStream;
20
23
  slice?(start: number, end?: number): BucketFile;
21
- bytes(): Promise<Uint8Array>;
22
- remove(): Promise<void>;
24
+ bytes(opts?: ReadOptions): Promise<Uint8Array>;
25
+ remove?(opts?: ReadOptions): Promise<unknown>;
26
+ delete?(opts?: ReadOptions): Promise<unknown>;
23
27
  };
24
28
  type Bucket = {
25
29
  file(name: string): BucketFile;
30
+ create?(content: string | Buffer | ReadableStream, options?: {
31
+ type?: string;
32
+ } & ReadOptions): Promise<BucketFile>;
26
33
  folder?(prefix: string): Bucket;
27
34
  };
28
35
 
package/index.js CHANGED
@@ -762,26 +762,25 @@ function resolveUploads(up) {
762
762
  maxFiles: DEFAULT_FILES
763
763
  };
764
764
  }
765
- function getExt(filename) {
766
- const i = filename.lastIndexOf(".");
767
- if (i <= 0) return ".bin";
768
- return filename.slice(i).toLowerCase();
769
- }
770
765
  function validateFile(originalName, contentType, limits, sniffed) {
771
766
  const { fileType: fileType2 } = limits;
772
- if (!fileType2 || fileType2.length === 0) return;
773
767
  if (sniffed === null && isSniffable(contentType)) {
774
768
  throw errors_default.UPLOAD_TYPE_NOT_ALLOWED({
775
769
  name: originalName,
776
770
  type: contentType,
777
- allowed: fileType2
771
+ allowed: fileType2 ?? [contentType]
778
772
  });
779
773
  }
780
- const ext = getExt(originalName);
781
- const mime = contentType.toLowerCase();
782
- const allowed = fileType2.some(
783
- (t) => t.toLowerCase() === mime || t.toLowerCase() === ext
784
- );
774
+ if (!fileType2 || fileType2.length === 0) return;
775
+ const base = (value) => value.split(";")[0].trim().toLowerCase();
776
+ const type2 = base(contentType);
777
+ const allowed = fileType2.some((one) => {
778
+ const entry = one.trim().toLowerCase();
779
+ if (entry.endsWith("/*")) return type2.startsWith(entry.slice(0, -1));
780
+ if (entry.includes("/")) return base(entry) === type2;
781
+ const mapped = mimes_default[entry.replace(/^\./, "")];
782
+ return Boolean(mapped) && base(mapped) === type2;
783
+ });
785
784
  if (!allowed) {
786
785
  throw errors_default.UPLOAD_TYPE_NOT_ALLOWED({
787
786
  name: originalName,
@@ -2147,11 +2146,11 @@ async function assets(ctx) {
2147
2146
  try {
2148
2147
  const key = ctx.url.pathname.replace(/^\/+/, "");
2149
2148
  const file2 = ctx.options.public.file(key);
2149
+ const read2 = { signal: ctx.signal };
2150
2150
  const info = file2.info?.bind(file2);
2151
- const meta2 = info ? await info() : null;
2152
- if (info ? !meta2 : !await file2.exists()) return;
2153
- const ext = ctx.url.pathname.split(".").pop()?.toLowerCase();
2154
- const ctype = mimeOf(ctx.url.pathname) || meta2?.type || ext;
2151
+ const meta2 = info ? await info(read2) : null;
2152
+ if (info ? !meta2 : !await file2.exists(read2)) return;
2153
+ const ctype = mimeOf(ctx.url.pathname) || meta2?.type || void 0;
2155
2154
  const headers2 = {
2156
2155
  "cache-control": resolveCache(ctx.options.cache) ?? DEFAULT_CACHE
2157
2156
  };
@@ -2180,10 +2179,10 @@ async function assets(ctx) {
2180
2179
  ...headers2,
2181
2180
  "content-range": `bytes ${start}-${end}/${meta2.size}`,
2182
2181
  "content-length": String(end - start + 1)
2183
- }).send(file2.slice(start, end + 1).stream());
2182
+ }).send(file2.slice(start, end + 1).stream(read2));
2184
2183
  }
2185
2184
  }
2186
- return type(ctype).headers(headers2).send(file2.stream());
2185
+ return type(ctype).headers(headers2).send(file2.stream(read2));
2187
2186
  } catch {
2188
2187
  }
2189
2188
  }
@@ -2396,6 +2395,18 @@ async function socketUser(app, headers2, cookies2) {
2396
2395
  }
2397
2396
 
2398
2397
  // src/body/bodyParts.ts
2398
+ var extByMime = {};
2399
+ for (const ext in mimes_default) extByMime[mimes_default[ext].split(";")[0].trim()] = ext;
2400
+ function keyFor(type2) {
2401
+ const ext = extByMime[type2.split(";")[0].trim()];
2402
+ return `${createId()}${ext ? `.${ext}` : ""}`;
2403
+ }
2404
+ async function discard(file2) {
2405
+ try {
2406
+ await (file2.remove ? file2.remove() : file2.delete?.());
2407
+ } catch {
2408
+ }
2409
+ }
2399
2410
  var asIterable = (s) => s;
2400
2411
  function getMatching(string, regex) {
2401
2412
  const matches2 = string.match(regex);
@@ -2409,8 +2420,6 @@ function isProbablyText(buffer) {
2409
2420
  }
2410
2421
  return true;
2411
2422
  }
2412
- var extByMime = {};
2413
- for (const ext in mimes_default) extByMime[mimes_default[ext]] = ext;
2414
2423
  function addField(body, name, value) {
2415
2424
  if (body[name] === void 0) {
2416
2425
  body[name] = value;
@@ -2419,7 +2428,7 @@ function addField(body, name, value) {
2419
2428
  if (!Array.isArray(body[name])) body[name] = [body[name]];
2420
2429
  body[name].push(value);
2421
2430
  }
2422
- function makeFilePart(name, filename, declared, bucket2, limits, budget) {
2431
+ function makeFilePart(name, filename, declared, bucket2, limits, budget, signal) {
2423
2432
  return {
2424
2433
  kind: "file",
2425
2434
  name,
@@ -2428,13 +2437,14 @@ function makeFilePart(name, filename, declared, bucket2, limits, budget) {
2428
2437
  bucket: bucket2,
2429
2438
  limits,
2430
2439
  budget,
2440
+ signal,
2431
2441
  head: [],
2432
2442
  headSize: 0,
2433
2443
  opened: null,
2434
2444
  size: 0
2435
2445
  };
2436
2446
  }
2437
- function startPart(headerStr, bucket2, limits, budget) {
2447
+ function startPart(headerStr, bucket2, limits, budget, signal) {
2438
2448
  const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
2439
2449
  if (!name) return { kind: "skip" };
2440
2450
  const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
@@ -2447,7 +2457,7 @@ function startPart(headerStr, bucket2, limits, budget) {
2447
2457
  if (maxFiles != null && budget.files > maxFiles) {
2448
2458
  throw errors_default.UPLOAD_TOO_MANY_FILES({ limit: String(maxFiles) });
2449
2459
  }
2450
- return makeFilePart(name, filename, type2, bucket2, limits, budget);
2460
+ return makeFilePart(name, filename, type2, bucket2, limits, budget, signal);
2451
2461
  }
2452
2462
  async function abortFile(part, error) {
2453
2463
  if (part.opened) {
@@ -2457,8 +2467,7 @@ async function abortFile(part, error) {
2457
2467
  });
2458
2468
  } catch {
2459
2469
  }
2460
- await part.opened.file.remove().catch(() => {
2461
- });
2470
+ if (part.opened.handle) await discard(part.opened.handle);
2462
2471
  }
2463
2472
  throw error;
2464
2473
  }
@@ -2491,20 +2500,27 @@ function openFile(part) {
2491
2500
  const sniffed = sniff(head);
2492
2501
  const type2 = resolveType(sniffed, part.declared);
2493
2502
  validateFile(part.filename, type2, part.limits, sniffed);
2494
- const ext = sniffed ? extByMime[type2] : void 0;
2495
- const id = `${createId()}${ext ? `.${ext}` : ""}`;
2496
2503
  let controller;
2497
2504
  const readable = new ReadableStream({
2498
2505
  start(c) {
2499
2506
  controller = c;
2500
2507
  }
2501
2508
  });
2502
- const file2 = part.bucket.file(id);
2509
+ const write = { type: type2, signal: part.signal };
2510
+ if (part.bucket.create) {
2511
+ part.opened = {
2512
+ type: type2,
2513
+ controller,
2514
+ write: part.bucket.create(readable, write)
2515
+ };
2516
+ return;
2517
+ }
2518
+ const handle = part.bucket.file(keyFor(type2));
2503
2519
  part.opened = {
2504
2520
  type: type2,
2505
- file: file2,
2506
2521
  controller,
2507
- write: file2.write(readable, { type: type2 })
2522
+ handle,
2523
+ write: handle.write(readable, write).then(() => handle)
2508
2524
  };
2509
2525
  }
2510
2526
  async function feedPart(part, data) {
@@ -2548,11 +2564,10 @@ async function endPart(part, body) {
2548
2564
  }
2549
2565
  const opened = part.opened;
2550
2566
  opened.controller.close();
2551
- await opened.write;
2567
+ const file2 = await opened.write;
2552
2568
  const { minSize } = part.limits;
2553
2569
  if (minSize != null && part.size < parseBytes(minSize)) {
2554
- await opened.file.remove().catch(() => {
2555
- });
2570
+ await discard(file2);
2556
2571
  throw errors_default.UPLOAD_TOO_SMALL({
2557
2572
  name: part.filename,
2558
2573
  size: String(part.size),
@@ -2561,7 +2576,7 @@ async function endPart(part, body) {
2561
2576
  }
2562
2577
  addField(body, part.name, {
2563
2578
  name: part.filename,
2564
- path: opened.file.path,
2579
+ path: file2.path,
2565
2580
  type: opened.type,
2566
2581
  size: part.size
2567
2582
  });
@@ -2581,7 +2596,7 @@ function getBoundary(header) {
2581
2596
  return null;
2582
2597
  }
2583
2598
  var BREAK = Buffer.from("\r\n\r\n");
2584
- async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
2599
+ async function parseMultipart(stream, boundary, bucket2, limits, max = INF, signal) {
2585
2600
  const budget = { used: 0, max: INF, files: 0 };
2586
2601
  const delim = Buffer.from(`\r
2587
2602
  --${boundary}`);
@@ -2623,7 +2638,8 @@ async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
2623
2638
  buf.subarray(0, i).toString("utf-8"),
2624
2639
  bucket2,
2625
2640
  limits,
2626
- budget
2641
+ budget,
2642
+ signal
2627
2643
  );
2628
2644
  buf = buf.subarray(i + BREAK.length);
2629
2645
  state = "body";
@@ -2687,12 +2703,16 @@ function parseUrlEncoded(text) {
2687
2703
  }
2688
2704
  return out;
2689
2705
  }
2690
- async function streamRawToBucket(stream, type2, bucket2, limits) {
2691
- const part = makeFilePart("body", "upload", type2, bucket2, limits, {
2692
- used: 0,
2693
- max: INF,
2694
- files: 0
2695
- });
2706
+ async function streamRawToBucket(stream, type2, bucket2, limits, signal) {
2707
+ const part = makeFilePart(
2708
+ "body",
2709
+ "upload",
2710
+ type2,
2711
+ bucket2,
2712
+ limits,
2713
+ { used: 0, max: INF, files: 0 },
2714
+ signal
2715
+ );
2696
2716
  for await (const chunk of asIterable(stream)) {
2697
2717
  await feedPart(part, Buffer.from(chunk));
2698
2718
  }
@@ -2700,7 +2720,7 @@ async function streamRawToBucket(stream, type2, bucket2, limits) {
2700
2720
  await endPart(part, body);
2701
2721
  return part.size ? body.body : void 0;
2702
2722
  }
2703
- async function parseBody(input, contentType, dest, max = INF, length) {
2723
+ async function parseBody(input, contentType, dest, max = INF, length, signal) {
2704
2724
  const type2 = Array.isArray(contentType) ? contentType[0] : contentType;
2705
2725
  let bucket2;
2706
2726
  let limits = {};
@@ -2714,7 +2734,14 @@ async function parseBody(input, contentType, dest, max = INF, length) {
2714
2734
  if (type2 && /multipart\/form-data/i.test(type2)) {
2715
2735
  const boundary = getBoundary(type2);
2716
2736
  if (!boundary) throw errors_default.BODY_INVALID_MULTIPART();
2717
- return parseMultipart(toStream(input), boundary, bucket2, limits, max);
2737
+ return parseMultipart(
2738
+ toStream(input),
2739
+ boundary,
2740
+ bucket2,
2741
+ limits,
2742
+ max,
2743
+ signal
2744
+ );
2718
2745
  }
2719
2746
  if (!type2 || /^text\//i.test(type2)) {
2720
2747
  const buf = await toBuffer(input, max);
@@ -2742,7 +2769,7 @@ async function parseBody(input, contentType, dest, max = INF, length) {
2742
2769
  limit: String(maxFileSize)
2743
2770
  });
2744
2771
  }
2745
- return streamRawToBucket(toStream(input), type2, bucket2, limits);
2772
+ return streamRawToBucket(toStream(input), type2, bucket2, limits, signal);
2746
2773
  }
2747
2774
 
2748
2775
  // src/body/body.ts
@@ -2784,7 +2811,8 @@ async function resolveBody(ctx, mode = "parse", max = resolveMax(void 0)) {
2784
2811
  ctx.headers["content-type"],
2785
2812
  ctx.options.uploads,
2786
2813
  max,
2787
- Number.isFinite(declared) ? declared : void 0
2814
+ Number.isFinite(declared) ? declared : void 0,
2815
+ ctx.signal
2788
2816
  );
2789
2817
  if (size && !ctx.headers["content-length"]) {
2790
2818
  ctx.headers["content-length"] = String(size);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.49.1",
3
+ "version": "0.49.3",
4
4
  "description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
5
5
  "homepage": "https://server-js.com/",
6
6
  "repository": "github:franciscop/server-next",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "antarctic": "^0.6.0",
69
- "bucket": "^0.7.1"
69
+ "bucket": "^0.10.1"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/bun": "^1.3.0",