@autonomys/file-server 1.6.2 → 1.6.4-beta.0

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.
@@ -1,6 +1,9 @@
1
1
  import { Request, Response } from 'express';
2
2
  import { ByteRange, DownloadMetadata, DownloadOptions } from '../models.js';
3
- export declare const handleDownloadResponseHeaders: (req: Request, res: Response, metadata: DownloadMetadata, { byteRange, rawMode }: DownloadOptions) => void;
3
+ export type DownloadHeaderResult = {
4
+ shouldDecompressBody: boolean;
5
+ };
6
+ export declare const handleDownloadResponseHeaders: (req: Request, res: Response, metadata: DownloadMetadata, { byteRange, rawMode }: DownloadOptions) => DownloadHeaderResult;
4
7
  export declare const handleS3DownloadResponseHeaders: (req: Request, res: Response, metadata: DownloadMetadata) => void;
5
8
  export declare const getByteRange: (req: Request) => ByteRange | undefined;
6
9
  //# sourceMappingURL=headers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/http/headers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AA8C3E,eAAO,MAAM,6BAA6B,GACxC,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,UAAU,gBAAgB,EAC1B,wBAA4C,eAAe,SAoC5D,CAAA;AAED,eAAO,MAAM,+BAA+B,GAC1C,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,UAAU,gBAAgB,SAS3B,CAAA;AAED,eAAO,MAAM,YAAY,GAAI,KAAK,OAAO,KAAG,SAAS,GAAG,SAgBvD,CAAA"}
1
+ {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/http/headers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAkF3E,MAAM,MAAM,oBAAoB,GAAG;IACjC,oBAAoB,EAAE,OAAO,CAAA;CAC9B,CAAA;AAED,eAAO,MAAM,6BAA6B,GACxC,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,UAAU,gBAAgB,EAC1B,wBAA4C,eAAe,KAC1D,oBAmEF,CAAA;AAED,eAAO,MAAM,+BAA+B,GAC1C,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,UAAU,gBAAgB,SAS3B,CAAA;AAED,eAAO,MAAM,YAAY,GAAI,KAAK,OAAO,KAAG,SAAS,GAAG,SAgBvD,CAAA"}
@@ -1,65 +1,122 @@
1
1
  import { CompressionAlgorithm, EncryptionAlgorithm } from '@autonomys/auto-dag-data';
2
+ import { inferMimeType } from '../utils.js';
3
+ // Generic mimetypes that should trigger extension-based fallback
4
+ const GENERIC_MIME_TYPES = new Set(['application/octet-stream', 'binary/octet-stream']);
5
+ // Get the best mimetype for a file, falling back to extension-based inference
6
+ // when the stored mimetype is missing or generic
7
+ const getMimeType = (metadata) => {
8
+ var _a;
9
+ const storedMime = (_a = metadata.mimeType) === null || _a === void 0 ? void 0 : _a.toLowerCase();
10
+ // If we have a meaningful mimetype, use it
11
+ if (storedMime && !GENERIC_MIME_TYPES.has(storedMime)) {
12
+ return metadata.mimeType;
13
+ }
14
+ // Otherwise, infer from filename extension
15
+ return inferMimeType(metadata.name);
16
+ };
17
+ // Helper to create an ASCII-safe fallback for filename parameter (RFC 2183/6266)
18
+ const toAsciiFallback = (name) => name
19
+ .replace(/[^\x20-\x7E]+/g, '_') // replace non-ASCII with underscore
20
+ .replace(/["\\]/g, '\\$&'); // escape quotes and backslashes
21
+ // RFC 5987 encoding for filename* parameter
22
+ const rfc5987Encode = (str) => encodeURIComponent(str)
23
+ .replace(/['()]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
24
+ .replace(/\*/g, '%2A');
2
25
  // Check if this is actually a document navigation (based on headers only)
3
26
  // Used to determine if browser will auto-decompress Content-Encoding
4
27
  const isDocumentNavigation = (req) => {
5
28
  const destHeader = req.headers['sec-fetch-dest'];
6
29
  const dest = (Array.isArray(destHeader) ? destHeader[0] : (destHeader !== null && destHeader !== void 0 ? destHeader : '')).toLowerCase();
7
30
  if (dest && dest !== 'document')
8
- return false; // e.g. <img>, fetch(), etc.
31
+ return false; // e.g. <img>, <video>, fetch(), etc.
9
32
  const modeHeader = req.headers['sec-fetch-mode'];
10
33
  const mode = (Array.isArray(modeHeader) ? modeHeader[0] : (modeHeader !== null && modeHeader !== void 0 ? modeHeader : '')).toLowerCase();
11
34
  if (mode && mode !== 'navigate')
12
- return false; // programmatic fetch
35
+ return false; // programmatic fetch / subresource
13
36
  return true;
14
37
  };
15
- const isInlineDocument = (req) => {
16
- // Check explicit query parameters - treat presence as boolean flag
38
+ // Decide if this file type is something browsers can usually render inline via URL
39
+ const isPreviewableInline = (metadata) => {
40
+ if (metadata.isEncrypted)
41
+ return false;
42
+ const mimeType = getMimeType(metadata).toLowerCase();
43
+ const directDisplayTypes = ['image/', 'video/', 'audio/'];
44
+ return (directDisplayTypes.some((type) => mimeType.startsWith(type)) || mimeType === 'application/pdf');
45
+ };
46
+ const isInlineDisposition = (req, metadata) => {
47
+ // Explicit query overrides - treat presence as boolean flag
17
48
  // ?download or ?download=true triggers attachment, ?download=false is ignored
18
49
  if (req.query.download === 'true' || req.query.download === '')
19
50
  return false;
20
51
  // ?inline or ?inline=true triggers inline, ?inline=false is ignored
21
52
  if (req.query.inline === 'true' || req.query.inline === '')
22
53
  return true;
23
- // Fall back to header-based detection
54
+ // Folders (served as zip) should default to attachment
55
+ if (metadata.type !== 'file')
56
+ return false;
57
+ // For media / PDFs that browsers can render directly, prefer inline even for subresources
58
+ if (isPreviewableInline(metadata))
59
+ return true;
60
+ // Fallback to header-based detection: top-level document navigations are inline
24
61
  return isDocumentNavigation(req);
25
62
  };
26
- // Helper to create an ASCII-safe fallback for filename parameter (RFC 2183/6266)
27
- const toAsciiFallback = (name) => name
28
- .replace(/[^\x20-\x7E]+/g, '_') // replace non-ASCII with underscore
29
- .replace(/["\\]/g, '\\$&'); // escape quotes and backslashes
30
- // RFC 5987 encoding for filename* parameter
31
- const rfc5987Encode = (str) => encodeURIComponent(str)
32
- .replace(/['()]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
33
- .replace(/\*/g, '%2A');
34
- const buildDisposition = (req, filename) => {
63
+ const buildDisposition = (req, metadata, filename) => {
35
64
  const fallbackName = toAsciiFallback(filename || 'download');
36
65
  const encoded = rfc5987Encode(filename || 'download');
37
- const type = isInlineDocument(req) ? 'inline' : 'attachment';
66
+ const type = isInlineDisposition(req, metadata) ? 'inline' : 'attachment';
38
67
  return `${type}; filename="${fallbackName}"; filename*=UTF-8''${encoded}`;
39
68
  };
40
69
  export const handleDownloadResponseHeaders = (req, res, metadata, { byteRange = undefined, rawMode = false }) => {
41
70
  var _a;
42
71
  const baseName = metadata.name || 'download';
43
72
  const fileName = metadata.type === 'file' ? baseName : `${baseName}.zip`;
73
+ let shouldDecompressBody = false;
44
74
  if (metadata.type === 'file') {
45
- const contentType = (!metadata.isEncrypted && !rawMode && metadata.mimeType) || 'application/octet-stream';
75
+ const contentType = !metadata.isEncrypted && !rawMode ? getMimeType(metadata) : 'application/octet-stream';
46
76
  res.set('Content-Type', contentType);
47
- const compressedButNoEncrypted = metadata.isCompressed && !metadata.isEncrypted;
77
+ const compressedButNotEncrypted = metadata.isCompressed && !metadata.isEncrypted;
48
78
  // Only set Content-Encoding for document navigations where browsers auto-decompress
49
- // Don't set it for <img>, fetch(), etc. as browsers won't auto-decompress those
79
+ // Don't set it for <img>, <video>, fetch(), etc. as browsers won't auto-decompress those
50
80
  const shouldHandleEncoding = req.query.ignoreEncoding
51
81
  ? req.query.ignoreEncoding !== 'true'
52
82
  : isDocumentNavigation(req);
53
- if (compressedButNoEncrypted && shouldHandleEncoding && !rawMode && !byteRange) {
83
+ const mimeType = contentType.toLowerCase();
84
+ const isMediaType = mimeType.startsWith('video/') || mimeType.startsWith('audio/');
85
+ const mustDecompress = compressedButNotEncrypted &&
86
+ (!shouldHandleEncoding || rawMode || byteRange != null || isMediaType);
87
+ // Always advertise range support if we have size, even for compressed media
88
+ // because we'll decompress them on-the-fly to support seeking
89
+ const canAdvertiseRanges = metadata.size != null;
90
+ if (canAdvertiseRanges) {
91
+ res.set('Accept-Ranges', 'bytes');
92
+ }
93
+ else {
94
+ res.set('Accept-Ranges', 'none');
95
+ }
96
+ if (compressedButNotEncrypted &&
97
+ shouldHandleEncoding &&
98
+ !rawMode &&
99
+ !byteRange &&
100
+ !isMediaType) {
54
101
  res.set('Content-Encoding', 'deflate');
55
102
  }
56
- if (byteRange) {
103
+ else if (mustDecompress) {
104
+ shouldDecompressBody = true;
105
+ }
106
+ if (mustDecompress) {
107
+ // When decompressing, we can't know the output size upfront
108
+ // Don't set Content-Length - this will use chunked transfer encoding
109
+ // Also don't advertise ranges since we can't seek in decompressed stream
110
+ res.set('Accept-Ranges', 'none');
111
+ }
112
+ else if (byteRange && metadata.size != null) {
113
+ // For range requests on non-compressed content
57
114
  res.status(206);
58
- res.set('Content-Range', `bytes ${byteRange[0]}-${byteRange[1]}/${metadata.size}`);
59
115
  const upperBound = (_a = byteRange[1]) !== null && _a !== void 0 ? _a : Number(metadata.size) - 1;
116
+ res.set('Content-Range', `bytes ${byteRange[0]}-${upperBound}/${metadata.size}`);
60
117
  res.set('Content-Length', (upperBound - byteRange[0] + 1).toString());
61
118
  }
62
- else if (metadata.size) {
119
+ else if (metadata.size != null) {
63
120
  res.set('Content-Length', metadata.size.toString());
64
121
  }
65
122
  }
@@ -67,7 +124,8 @@ export const handleDownloadResponseHeaders = (req, res, metadata, { byteRange =
67
124
  const contentType = metadata.isEncrypted ? 'application/octet-stream' : 'application/zip';
68
125
  res.set('Content-Type', contentType);
69
126
  }
70
- res.set('Content-Disposition', buildDisposition(req, fileName));
127
+ res.set('Content-Disposition', buildDisposition(req, metadata, fileName));
128
+ return { shouldDecompressBody };
71
129
  };
72
130
  export const handleS3DownloadResponseHeaders = (req, res, metadata) => {
73
131
  if (metadata.isEncrypted) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@autonomys/file-server",
3
3
  "packageManager": "yarn@4.7.0",
4
- "version": "1.6.2",
4
+ "version": "1.6.4-beta.0",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -48,19 +48,19 @@
48
48
  "typescript": "^5.8.3"
49
49
  },
50
50
  "dependencies": {
51
- "@autonomys/asynchronous": "^1.6.2",
52
- "@autonomys/auto-dag-data": "^1.6.2",
53
- "@autonomys/auto-utils": "^1.6.2",
51
+ "@autonomys/asynchronous": "^1.6.4-beta.0",
52
+ "@autonomys/auto-dag-data": "^1.6.4-beta.0",
53
+ "@autonomys/auto-utils": "^1.6.4-beta.0",
54
54
  "@keyvhq/sqlite": "^2.1.7",
55
55
  "cache-manager": "^6.4.2",
56
56
  "express": "^4.19.2",
57
57
  "jszip": "^3.10.1",
58
58
  "keyv": "^5.3.2",
59
- "mime-types": "^3.0.1",
59
+ "mime-types": "^3.0.2",
60
60
  "process": "^0.11.10",
61
61
  "stream": "^0.0.3",
62
62
  "uuid": "^11.1.0",
63
63
  "zod": "^3.24.2"
64
64
  },
65
- "gitHead": "910aff6c92f736b87f7986780d667035b9c72217"
65
+ "gitHead": "d43ceff5387fef39f5fafe1e50552eae0b5a8ec3"
66
66
  }