@autonomys/file-server 1.6.0 → 1.6.2-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,6 @@
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, options: DownloadOptions) => void;
3
+ export declare const handleDownloadResponseHeaders: (req: Request, res: Response, metadata: DownloadMetadata, { byteRange, rawMode }: DownloadOptions) => void;
4
4
  export declare const handleS3DownloadResponseHeaders: (req: Request, res: Response, metadata: DownloadMetadata) => void;
5
5
  export declare const getByteRange: (req: Request) => ByteRange | undefined;
6
6
  //# 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;AAS3E,eAAO,MAAM,6BAA6B,GACxC,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,UAAU,gBAAgB,EAC1B,SAAS,eAAe,SAsBzB,CAAA;AA+CD,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;AAiG3E,eAAO,MAAM,6BAA6B,GACxC,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,UAAU,gBAAgB,EAC1B,wBAA4C,eAAe,SAyC5D,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,45 +1,124 @@
1
1
  import { CompressionAlgorithm, EncryptionAlgorithm } from '@autonomys/auto-dag-data';
2
- const isExpectedDocument = (req) => {
3
- return (req.headers['sec-fetch-site'] === 'none' ||
4
- (req.headers['sec-fetch-site'] === 'same-site' && req.headers['sec-fetch-mode'] === 'navigate'));
2
+ // Check if this is actually a document navigation (based on headers only)
3
+ // Used to determine if browser will auto-decompress Content-Encoding
4
+ const isDocumentNavigation = (req) => {
5
+ const destHeader = req.headers['sec-fetch-dest'];
6
+ const dest = (Array.isArray(destHeader) ? destHeader[0] : (destHeader !== null && destHeader !== void 0 ? destHeader : '')).toLowerCase();
7
+ if (dest && dest !== 'document')
8
+ return false; // e.g. <img>, <video>, fetch(), etc.
9
+ const modeHeader = req.headers['sec-fetch-mode'];
10
+ const mode = (Array.isArray(modeHeader) ? modeHeader[0] : (modeHeader !== null && modeHeader !== void 0 ? modeHeader : '')).toLowerCase();
11
+ if (mode && mode !== 'navigate')
12
+ return false; // programmatic fetch / subresource
13
+ return true;
5
14
  };
6
- export const handleDownloadResponseHeaders = (req, res, metadata, options) => {
7
- const safeName = encodeURIComponent(metadata.name || 'download');
8
- const documentExpected = isExpectedDocument(req);
9
- const shouldHandleEncoding = req.query.ignoreEncoding
10
- ? req.query.ignoreEncoding !== 'true'
11
- : documentExpected;
12
- const isEncrypted = metadata.isEncrypted;
13
- if (metadata.type === 'file') {
14
- setFileResponseHeaders(res, metadata, isEncrypted, documentExpected, shouldHandleEncoding, safeName, options);
15
- }
16
- else {
17
- setFolderResponseHeaders(res, isEncrypted, documentExpected, safeName);
18
- }
15
+ // Decide if this file type is something browsers can usually render inline via URL
16
+ // (mirrors the logic in canDisplayDirectly but on DownloadMetadata)
17
+ const isPreviewableInline = (metadata) => {
18
+ var _a, _b, _c, _d, _e;
19
+ if (metadata.isEncrypted)
20
+ return false;
21
+ const mimeType = (_b = (_a = metadata.mimeType) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : '';
22
+ const extension = (_e = (_d = (_c = metadata.name) === null || _c === void 0 ? void 0 : _c.split('.').pop()) === null || _d === void 0 ? void 0 : _d.toLowerCase()) !== null && _e !== void 0 ? _e : '';
23
+ const directDisplayTypes = ['image/', 'video/', 'audio/'];
24
+ const directDisplayExtensions = [
25
+ // Images
26
+ 'jpg',
27
+ 'jpeg',
28
+ 'png',
29
+ 'gif',
30
+ 'svg',
31
+ 'webp',
32
+ 'bmp',
33
+ 'ico',
34
+ // Videos
35
+ 'mp4',
36
+ 'webm',
37
+ 'avi',
38
+ 'mov',
39
+ 'mkv',
40
+ 'flv',
41
+ 'wmv',
42
+ // Audio
43
+ 'mp3',
44
+ 'wav',
45
+ 'ogg',
46
+ 'flac',
47
+ 'm4a',
48
+ 'aac',
49
+ // PDFs
50
+ 'pdf',
51
+ ];
52
+ return (directDisplayTypes.some((type) => mimeType.startsWith(type)) ||
53
+ mimeType === 'application/pdf' ||
54
+ directDisplayExtensions.includes(extension));
55
+ };
56
+ const isInlineDisposition = (req, metadata) => {
57
+ // Explicit query overrides - treat presence as boolean flag
58
+ // ?download or ?download=true triggers attachment, ?download=false is ignored
59
+ if (req.query.download === 'true' || req.query.download === '')
60
+ return false;
61
+ // ?inline or ?inline=true triggers inline, ?inline=false is ignored
62
+ if (req.query.inline === 'true' || req.query.inline === '')
63
+ return true;
64
+ // Folders (served as zip) should default to attachment
65
+ if (metadata.type !== 'file')
66
+ return false;
67
+ // For media / PDFs that browsers can render directly, prefer inline even for subresources
68
+ if (isPreviewableInline(metadata))
69
+ return true;
70
+ // Fallback to header-based detection: top-level document navigations are inline
71
+ return isDocumentNavigation(req);
19
72
  };
20
- const setFileResponseHeaders = (res, metadata, isEncrypted, isExpectedDocument, shouldHandleEncoding, safeName, { byteRange = undefined, rawMode = false }) => {
73
+ // Helper to create an ASCII-safe fallback for filename parameter (RFC 2183/6266)
74
+ const toAsciiFallback = (name) => name
75
+ .replace(/[^\x20-\x7E]+/g, '_') // replace non-ASCII with underscore
76
+ .replace(/["\\]/g, '\\$&'); // escape quotes and backslashes
77
+ // RFC 5987 encoding for filename* parameter
78
+ const rfc5987Encode = (str) => encodeURIComponent(str)
79
+ .replace(/['()]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
80
+ .replace(/\*/g, '%2A');
81
+ const buildDisposition = (req, metadata, filename) => {
82
+ const fallbackName = toAsciiFallback(filename || 'download');
83
+ const encoded = rfc5987Encode(filename || 'download');
84
+ const type = isInlineDisposition(req, metadata) ? 'inline' : 'attachment';
85
+ return `${type}; filename="${fallbackName}"; filename*=UTF-8''${encoded}`;
86
+ };
87
+ export const handleDownloadResponseHeaders = (req, res, metadata, { byteRange = undefined, rawMode = false }) => {
21
88
  var _a;
22
- const contentType = (!isEncrypted && !rawMode && metadata.mimeType) || 'application/octet-stream';
23
- res.set('Content-Type', contentType);
24
- res.set('Content-Disposition', `${isExpectedDocument ? 'inline' : 'attachment'}; filename="${safeName}"`);
25
- const compressedButNoEncrypted = metadata.isCompressed && !isEncrypted;
26
- if (compressedButNoEncrypted && shouldHandleEncoding && !rawMode && !byteRange) {
27
- res.set('Content-Encoding', 'deflate');
28
- }
29
- if (byteRange) {
30
- res.status(206);
31
- res.set('Content-Range', `bytes ${byteRange[0]}-${byteRange[1]}/${metadata.size}`);
32
- const upperBound = (_a = byteRange[1]) !== null && _a !== void 0 ? _a : Number(metadata.size) - 1;
33
- res.set('Content-Length', (upperBound - byteRange[0] + 1).toString());
89
+ const baseName = metadata.name || 'download';
90
+ const fileName = metadata.type === 'file' ? baseName : `${baseName}.zip`;
91
+ if (metadata.type === 'file') {
92
+ const contentType = (!metadata.isEncrypted && !rawMode && metadata.mimeType) || 'application/octet-stream';
93
+ res.set('Content-Type', contentType);
94
+ // Advertise range support for files so browsers like Chrome can seek in media
95
+ if (metadata.size != null) {
96
+ res.set('Accept-Ranges', 'bytes');
97
+ }
98
+ const compressedButNotEncrypted = metadata.isCompressed && !metadata.isEncrypted;
99
+ // Only set Content-Encoding for document navigations where browsers auto-decompress
100
+ // Don't set it for <img>, <video>, fetch(), etc. as browsers won't auto-decompress those
101
+ const shouldHandleEncoding = req.query.ignoreEncoding
102
+ ? req.query.ignoreEncoding !== 'true'
103
+ : isDocumentNavigation(req);
104
+ if (compressedButNotEncrypted && shouldHandleEncoding && !rawMode && !byteRange) {
105
+ res.set('Content-Encoding', 'deflate');
106
+ }
107
+ if (byteRange) {
108
+ res.status(206);
109
+ res.set('Content-Range', `bytes ${byteRange[0]}-${byteRange[1]}/${metadata.size}`);
110
+ const upperBound = (_a = byteRange[1]) !== null && _a !== void 0 ? _a : Number(metadata.size) - 1;
111
+ res.set('Content-Length', (upperBound - byteRange[0] + 1).toString());
112
+ }
113
+ else if (metadata.size) {
114
+ res.set('Content-Length', metadata.size.toString());
115
+ }
34
116
  }
35
- else if (metadata.size) {
36
- res.set('Content-Length', metadata.size.toString());
117
+ else {
118
+ const contentType = metadata.isEncrypted ? 'application/octet-stream' : 'application/zip';
119
+ res.set('Content-Type', contentType);
37
120
  }
38
- };
39
- const setFolderResponseHeaders = (res, isEncrypted, isExpectedDocument, safeName) => {
40
- const contentType = isEncrypted ? 'application/octet-stream' : 'application/zip';
41
- res.set('Content-Type', contentType);
42
- res.set('Content-Disposition', `${isExpectedDocument ? 'inline' : 'attachment'}; filename="${safeName}.zip"`);
121
+ res.set('Content-Disposition', buildDisposition(req, metadata, fileName));
43
122
  };
44
123
  export const handleS3DownloadResponseHeaders = (req, res, metadata) => {
45
124
  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.0",
4
+ "version": "1.6.2-beta.0",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -19,7 +19,8 @@
19
19
  "node": ">=20.8.0"
20
20
  },
21
21
  "scripts": {
22
- "build": "yarn tsc"
22
+ "build": "yarn tsc",
23
+ "test": "node --experimental-vm-modules ../../../node_modules/jest/bin/jest.js --config ./jest.config.cjs"
23
24
  },
24
25
  "main": "./dist/index.js",
25
26
  "types": "./dist/index.d.ts",
@@ -47,9 +48,9 @@
47
48
  "typescript": "^5.8.3"
48
49
  },
49
50
  "dependencies": {
50
- "@autonomys/asynchronous": "^1.6.0",
51
- "@autonomys/auto-dag-data": "^1.6.0",
52
- "@autonomys/auto-utils": "^1.6.0",
51
+ "@autonomys/asynchronous": "^1.6.2-beta.0",
52
+ "@autonomys/auto-dag-data": "^1.6.2-beta.0",
53
+ "@autonomys/auto-utils": "^1.6.2-beta.0",
53
54
  "@keyvhq/sqlite": "^2.1.7",
54
55
  "cache-manager": "^6.4.2",
55
56
  "express": "^4.19.2",
@@ -61,5 +62,5 @@
61
62
  "uuid": "^11.1.0",
62
63
  "zod": "^3.24.2"
63
64
  },
64
- "gitHead": "a0a448db8d1a26006c4123ad9a0e53a8c9293255"
65
+ "gitHead": "ee6436b04e85cb1a3da9c58250c3505f6c18d22b"
65
66
  }