mbkbucket 3.5.1 → 3.7.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.
- package/.env.example +14 -14
- package/.mbkbucket.config.example.json +14 -14
- package/LICENSE +20 -20
- package/README.md +377 -350
- package/index.js +30 -30
- package/lib/cli.js +5 -4
- package/lib/public/bucketadmin.css +1695 -1420
- package/lib/public/bucketadmin.js +1229 -1229
- package/lib/public/mbkbucket-helper-demo.html +1184 -1184
- package/lib/public/mbkbucket-helper.css +1235 -1235
- package/lib/public/mbkbucket-helper.js +1557 -1557
- package/lib/src/app.js +109 -109
- package/lib/src/cli/auth-helper.js +141 -150
- package/lib/src/config/index.js +110 -112
- package/lib/src/config/validation.js +73 -73
- package/lib/src/controllers/api.controller.js +410 -410
- package/lib/src/controllers/dashboard.controller.js +49 -48
- package/lib/src/controllers/info.controller.js +33 -34
- package/lib/src/controllers/view.controller.js +231 -233
- package/lib/src/middleware/async-handler.js +19 -23
- package/lib/src/middleware/bucket-resolver.js +37 -37
- package/lib/src/middleware/error-handler.js +50 -54
- package/lib/src/middleware/rate-limiter.js +22 -22
- package/lib/src/middleware/security.js +21 -27
- package/lib/src/routes/api.routes.js +53 -40
- package/lib/src/routes/index.js +60 -21
- package/lib/src/routes/info.routes.js +9 -9
- package/lib/src/routes/view.routes.js +25 -20
- package/lib/src/services/s3.service.js +594 -603
- package/lib/src/utils/errors.js +46 -54
- package/lib/src/utils/helpers.js +212 -251
- package/lib/src/utils/logger.js +17 -17
- package/package.json +83 -83
- package/views/bucket.handlebars +196 -196
- package/views/bucketadmin_alerts.handlebars +15 -15
- package/views/bucketadmin_delete_modal.handlebars +36 -36
- package/views/bucketadmin_filelist_skeleton.handlebars +32 -32
- package/views/bucketadmin_newfolder_modal.handlebars +29 -29
- package/views/bucketadmin_preview_modal.handlebars +40 -40
- package/views/bucketadmin_upload_modal.handlebars +54 -54
- package/views/bucketadmincontent.handlebars +141 -141
- package/views/mbkbucket_info.handlebars +232 -232
|
@@ -1,48 +1,49 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { resolveBucketName, getAvailableBucketNames } from "../services/s3.service.js";
|
|
4
|
-
import { renderPage, mbkautheVar } from "mbkauthe";
|
|
5
|
-
import { commonHandlebarsHelpers } from "../utils/helpers.js";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Render the main bucket dashboard page.
|
|
9
|
-
*/
|
|
10
|
-
export function renderBucketDashboard(req, res) {
|
|
11
|
-
const allBuckets = getAvailableBucketNames();
|
|
12
|
-
const configuredDefaultBucket = mbkautheVar?.bucket;
|
|
13
|
-
const isPortal = mbkautheVar?.APP_NAME === "portal";
|
|
14
|
-
const bucketfilePath = isPortal ? "bucketportal.handlebars" : "bucket.handlebars";
|
|
15
|
-
|
|
16
|
-
const mainLayoutPath = path.resolve(process.cwd(), "views", "layouts", "main");
|
|
17
|
-
const hasMainLayout = fs.existsSync(`${mainLayoutPath}.handlebars`) || fs.existsSync(mainLayoutPath);
|
|
18
|
-
const portalLayout = (isPortal && hasMainLayout)
|
|
19
|
-
|
|
20
|
-
let selectedBucket;
|
|
21
|
-
let error = req.query.error;
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
selectedBucket = resolveBucketName(req.query.bucket);
|
|
25
|
-
} catch (err) {
|
|
26
|
-
selectedBucket = mbkautheVar?.bucket;
|
|
27
|
-
error ||= err.message;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const bucketOptions = allBuckets.map(name => ({
|
|
31
|
-
name,
|
|
32
|
-
isSelected: name === selectedBucket,
|
|
33
|
-
isDefault: name === configuredDefaultBucket
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
renderPage(req, res, bucketfilePath, portalLayout
|
|
37
|
-
page: "Admin Bucket",
|
|
38
|
-
helpers: commonHandlebarsHelpers,
|
|
39
|
-
|
|
40
|
-
bucketvar: selectedBucket,
|
|
41
|
-
selectedBucket,
|
|
42
|
-
bucketOptions,
|
|
43
|
-
APP_NAME: mbkautheVar?.APP_NAME,
|
|
44
|
-
message: req.query.message,
|
|
45
|
-
error,
|
|
46
|
-
availableBuckets: allBuckets
|
|
47
|
-
});
|
|
48
|
-
}
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { resolveBucketName, getAvailableBucketNames } from "../services/s3.service.js";
|
|
4
|
+
import { renderPage, mbkautheVar } from "mbkauthe";
|
|
5
|
+
import { commonHandlebarsHelpers } from "../utils/helpers.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render the main bucket dashboard page.
|
|
9
|
+
*/
|
|
10
|
+
export function renderBucketDashboard(req, res) {
|
|
11
|
+
const allBuckets = getAvailableBucketNames();
|
|
12
|
+
const configuredDefaultBucket = mbkautheVar?.bucket;
|
|
13
|
+
const isPortal = mbkautheVar?.APP_NAME === "portal";
|
|
14
|
+
const bucketfilePath = isPortal ? "bucketportal.handlebars" : "bucket.handlebars";
|
|
15
|
+
|
|
16
|
+
const mainLayoutPath = path.resolve(process.cwd(), "views", "layouts", "main");
|
|
17
|
+
const hasMainLayout = fs.existsSync(`${mainLayoutPath}.handlebars`) || fs.existsSync(mainLayoutPath);
|
|
18
|
+
const portalLayout = (isPortal && hasMainLayout) && mainLayoutPath;
|
|
19
|
+
|
|
20
|
+
let selectedBucket;
|
|
21
|
+
let error = req.query.error;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
selectedBucket = resolveBucketName(req.query.bucket);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
selectedBucket = mbkautheVar?.bucket;
|
|
27
|
+
error ||= err.message;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const bucketOptions = allBuckets.map(name => ({
|
|
31
|
+
name,
|
|
32
|
+
isSelected: name === selectedBucket,
|
|
33
|
+
isDefault: name === configuredDefaultBucket
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
renderPage(req, res, bucketfilePath, Boolean(portalLayout), {
|
|
37
|
+
page: "Admin Bucket",
|
|
38
|
+
helpers: commonHandlebarsHelpers,
|
|
39
|
+
layout: portalLayout || false,
|
|
40
|
+
bucketvar: selectedBucket,
|
|
41
|
+
selectedBucket,
|
|
42
|
+
bucketOptions,
|
|
43
|
+
APP_NAME: mbkautheVar?.APP_NAME,
|
|
44
|
+
message: req.query.message,
|
|
45
|
+
error,
|
|
46
|
+
availableBuckets: allBuckets
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -1,34 +1,33 @@
|
|
|
1
|
-
import { renderPage, mbkautheVar } from "mbkauthe";
|
|
2
|
-
import { packageJson, getLatestVersion } from "../config/index.js";
|
|
3
|
-
|
|
4
|
-
async function getInfoData() {
|
|
5
|
-
let latestVersion = 'unknown';
|
|
6
|
-
try {
|
|
7
|
-
latestVersion = (await getLatestVersion()) || 'unknown';
|
|
8
|
-
} catch (err) {
|
|
9
|
-
console.error("[mbkbucket] Error fetching latest version:", err);
|
|
10
|
-
}
|
|
11
|
-
return {
|
|
12
|
-
CurrentVersion: packageJson.version,
|
|
13
|
-
latestVersion,
|
|
14
|
-
APP_NAME: mbkautheVar?.APP_NAME
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Render the mbkbucket info page (Handlebars).
|
|
20
|
-
*/
|
|
21
|
-
export async function infoPage(req, res) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
1
|
+
import { renderPage, mbkautheVar } from "mbkauthe";
|
|
2
|
+
import { packageJson, getLatestVersion } from "../config/index.js";
|
|
3
|
+
|
|
4
|
+
async function getInfoData() {
|
|
5
|
+
let latestVersion = 'unknown';
|
|
6
|
+
try {
|
|
7
|
+
latestVersion = (await getLatestVersion()) || 'unknown';
|
|
8
|
+
} catch (err) {
|
|
9
|
+
console.error("[mbkbucket] Error fetching latest version:", err);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
CurrentVersion: packageJson.version,
|
|
13
|
+
latestVersion,
|
|
14
|
+
APP_NAME: mbkautheVar?.APP_NAME
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render the mbkbucket info page (Handlebars).
|
|
20
|
+
*/
|
|
21
|
+
export async function infoPage(req, res) {
|
|
22
|
+
renderPage(req, res, "mbkbucket_info.handlebars", false, {
|
|
23
|
+
page: "MBK Bucket Info",
|
|
24
|
+
...(await getInfoData())
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return bucket info as JSON.
|
|
30
|
+
*/
|
|
31
|
+
export async function infoJson(_req, res) {
|
|
32
|
+
res.json(await getInfoData());
|
|
33
|
+
}
|
|
@@ -1,233 +1,231 @@
|
|
|
1
|
-
import { downloadFile, getFileMetadata, ensureKeyHasAppPrefix, generateSignedUrl, resolveBucketName } from "../services/s3.service.js";
|
|
2
|
-
import { mbkbucketVar } from "../config/index.js";
|
|
3
|
-
import { escapeHtml, getBaseName, getFileExt, getMimeType, normalizeKeyParam, destroyStream, VIEWABLE_TYPES, VIEWABLE_TYPES_SORTED, STATIC_ASSET_TYPES, VIDEO_TYPES, AUDIO_TYPES, buildCacheControl, parseRangeHeader, renderPlayerPage } from "../utils/helpers.js";
|
|
4
|
-
import { createLogger } from "#logger";
|
|
5
|
-
|
|
6
|
-
const debugView = createLogger('view');
|
|
7
|
-
|
|
8
|
-
function attachStreamCleanup(req, res, stream, timeoutMs = 120000) {
|
|
9
|
-
const cleanup = () => destroyStream(stream);
|
|
10
|
-
|
|
11
|
-
stream.on('error', (err) => {
|
|
12
|
-
console.error('Stream error:', err);
|
|
13
|
-
cleanup();
|
|
14
|
-
if (!res.headersSent) res.status(500).end('Stream error');
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
req.on('close', cleanup);
|
|
18
|
-
req.on('aborted', cleanup);
|
|
19
|
-
req.setTimeout(timeoutMs, () => {
|
|
20
|
-
console.error('Request timeout for stream');
|
|
21
|
-
cleanup();
|
|
22
|
-
if (!res.headersSent) res.status(408).end('Request timeout');
|
|
23
|
-
});
|
|
24
|
-
res.on('finish', cleanup);
|
|
25
|
-
res.on('close', cleanup);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName, publicCache = false } = {}) {
|
|
29
|
-
const fileName = getBaseName(keyToUse);
|
|
30
|
-
const fileExtension = getFileExt(fileName);
|
|
31
|
-
|
|
32
|
-
debugView('Request for %s (type: %s, range: %s)', fileName, fileExtension, req.headers.range || 'none');
|
|
33
|
-
|
|
34
|
-
if (!VIEWABLE_TYPES.has(fileExtension)) {
|
|
35
|
-
return res.status(415).json({
|
|
36
|
-
message: 'File type not supported for viewing',
|
|
37
|
-
supportedTypes: VIEWABLE_TYPES_SORTED
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const ifNoneMatch = req.headers['if-none-match'];
|
|
42
|
-
const ifModifiedSince = req.headers['if-modified-since'];
|
|
43
|
-
const rangeHeader = req.headers.range;
|
|
44
|
-
|
|
45
|
-
const isStaticAsset = STATIC_ASSET_TYPES.has(fileExtension);
|
|
46
|
-
const isVideo = VIDEO_TYPES.has(fileExtension);
|
|
47
|
-
const isAudio = AUDIO_TYPES.has(fileExtension);
|
|
48
|
-
const isPdf = fileExtension === 'pdf';
|
|
49
|
-
const supportsRanges = isVideo || isAudio || isPdf;
|
|
50
|
-
const contentType = getMimeType(fileName);
|
|
51
|
-
|
|
52
|
-
// Fast path for range requests (video / audio / PDF streaming)
|
|
53
|
-
if (supportsRanges && rangeHeader) {
|
|
54
|
-
debugView('Processing range request: %s for %s', rangeHeader, fileName);
|
|
55
|
-
let meta;
|
|
56
|
-
try {
|
|
57
|
-
meta = await getFileMetadata(keyToUse, bucketName);
|
|
58
|
-
} catch (err) {
|
|
59
|
-
console.error('[mbkbucket] serveFileInline metadata error:', err);
|
|
60
|
-
return res.status(500).json({ message: 'Failed to fetch file metadata', error: err.message });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (!meta?.exists) {
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
res.
|
|
88
|
-
res.setHeader('Content-
|
|
89
|
-
res.setHeader('
|
|
90
|
-
res.setHeader('
|
|
91
|
-
res.setHeader('
|
|
92
|
-
res.setHeader('
|
|
93
|
-
res.setHeader('
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
res.setHeader('
|
|
135
|
-
res.setHeader('Content-
|
|
136
|
-
res.setHeader('
|
|
137
|
-
res.setHeader('X-
|
|
138
|
-
res.setHeader('
|
|
139
|
-
if (
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (error.name === '
|
|
168
|
-
res.status(
|
|
169
|
-
} else
|
|
170
|
-
res.status(
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
res.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
}
|
|
1
|
+
import { downloadFile, getFileMetadata, ensureKeyHasAppPrefix, generateSignedUrl, resolveBucketName } from "../services/s3.service.js";
|
|
2
|
+
import { mbkbucketVar } from "../config/index.js";
|
|
3
|
+
import { escapeHtml, getBaseName, getFileExt, getMimeType, normalizeKeyParam, destroyStream, VIEWABLE_TYPES, VIEWABLE_TYPES_SORTED, STATIC_ASSET_TYPES, VIDEO_TYPES, AUDIO_TYPES, buildCacheControl, parseRangeHeader, renderPlayerPage } from "../utils/helpers.js";
|
|
4
|
+
import { createLogger } from "#logger";
|
|
5
|
+
|
|
6
|
+
const debugView = createLogger('view');
|
|
7
|
+
|
|
8
|
+
function attachStreamCleanup(req, res, stream, timeoutMs = 120000) {
|
|
9
|
+
const cleanup = () => destroyStream(stream);
|
|
10
|
+
|
|
11
|
+
stream.on('error', (err) => {
|
|
12
|
+
console.error('Stream error:', err);
|
|
13
|
+
cleanup();
|
|
14
|
+
if (!res.headersSent) res.status(500).end('Stream error');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
req.on('close', cleanup);
|
|
18
|
+
req.on('aborted', cleanup);
|
|
19
|
+
req.setTimeout(timeoutMs, () => {
|
|
20
|
+
console.error('Request timeout for stream');
|
|
21
|
+
cleanup();
|
|
22
|
+
if (!res.headersSent) res.status(408).end('Request timeout');
|
|
23
|
+
});
|
|
24
|
+
res.on('finish', cleanup);
|
|
25
|
+
res.on('close', cleanup);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName, publicCache = false } = {}) {
|
|
29
|
+
const fileName = getBaseName(keyToUse);
|
|
30
|
+
const fileExtension = getFileExt(fileName);
|
|
31
|
+
|
|
32
|
+
debugView('Request for %s (type: %s, range: %s)', fileName, fileExtension, req.headers.range || 'none');
|
|
33
|
+
|
|
34
|
+
if (!VIEWABLE_TYPES.has(fileExtension)) {
|
|
35
|
+
return res.status(415).json({
|
|
36
|
+
message: 'File type not supported for viewing',
|
|
37
|
+
supportedTypes: VIEWABLE_TYPES_SORTED
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ifNoneMatch = req.headers['if-none-match'];
|
|
42
|
+
const ifModifiedSince = req.headers['if-modified-since'];
|
|
43
|
+
const rangeHeader = req.headers.range;
|
|
44
|
+
|
|
45
|
+
const isStaticAsset = STATIC_ASSET_TYPES.has(fileExtension);
|
|
46
|
+
const isVideo = VIDEO_TYPES.has(fileExtension);
|
|
47
|
+
const isAudio = AUDIO_TYPES.has(fileExtension);
|
|
48
|
+
const isPdf = fileExtension === 'pdf';
|
|
49
|
+
const supportsRanges = isVideo || isAudio || isPdf;
|
|
50
|
+
const contentType = getMimeType(fileName);
|
|
51
|
+
|
|
52
|
+
// Fast path for range requests (video / audio / PDF streaming)
|
|
53
|
+
if (supportsRanges && rangeHeader) {
|
|
54
|
+
debugView('Processing range request: %s for %s', rangeHeader, fileName);
|
|
55
|
+
let meta;
|
|
56
|
+
try {
|
|
57
|
+
meta = await getFileMetadata(keyToUse, bucketName);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error('[mbkbucket] serveFileInline metadata error:', err);
|
|
60
|
+
return res.status(500).json({ message: 'Failed to fetch file metadata', error: err.message });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!meta?.exists) return res.status(404).json({ message: 'File not found', key: keyToUse });
|
|
64
|
+
|
|
65
|
+
const etag = `"${meta.LastModified?.getTime() || Date.now()}-${meta.ContentLength || 0}"`;
|
|
66
|
+
if (ifNoneMatch === etag) return res.status(304).end();
|
|
67
|
+
|
|
68
|
+
const total = Number(meta.ContentLength);
|
|
69
|
+
if (total) {
|
|
70
|
+
const range = parseRangeHeader(rangeHeader, total);
|
|
71
|
+
if (!range) {
|
|
72
|
+
res.status(416).setHeader('Content-Range', `bytes */${total}`);
|
|
73
|
+
return res.end();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { start, end, size: chunksize } = range;
|
|
77
|
+
try {
|
|
78
|
+
const rangeResult = await downloadFile(keyToUse, { range: `bytes=${start}-${end}`, bucketName });
|
|
79
|
+
if (typeof rangeResult.ContentLength === 'number' && rangeResult.ContentLength !== chunksize) {
|
|
80
|
+
debugView('Range size mismatch for %s: expected=%s got=%s', keyToUse, chunksize, rangeResult.ContentLength);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
debugView('Serving range bytes %s-%s/%s (%s bytes) for %s', start, end, total, chunksize, fileName);
|
|
84
|
+
|
|
85
|
+
res.status(206);
|
|
86
|
+
res.setHeader('Content-Range', `bytes ${start}-${end}/${total}`);
|
|
87
|
+
res.setHeader('Content-Length', chunksize);
|
|
88
|
+
res.setHeader('Content-Type', contentType);
|
|
89
|
+
res.setHeader('Cache-Control', buildCacheControl(contentType, { supportsRanges: true, publicCache }));
|
|
90
|
+
res.setHeader('ETag', etag);
|
|
91
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
92
|
+
if (noindex) res.setHeader('X-Robots-Tag', 'noindex, nofollow');
|
|
93
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
94
|
+
if (meta.LastModified) res.setHeader('Last-Modified', meta.LastModified.toUTCString());
|
|
95
|
+
|
|
96
|
+
attachStreamCleanup(req, res, rangeResult.Body);
|
|
97
|
+
return rangeResult.Body.pipe(res);
|
|
98
|
+
} catch (rangeError) {
|
|
99
|
+
console.error('Range request failed:', rangeError);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Standard path: full-file download
|
|
105
|
+
const downloadOptions = {
|
|
106
|
+
...(ifNoneMatch && { ifNoneMatch }),
|
|
107
|
+
...(ifModifiedSince && { ifModifiedSince: new Date(ifModifiedSince) }),
|
|
108
|
+
bucketName
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (isPdf) debugView('Serving full PDF (no range header) for %s', fileName);
|
|
112
|
+
|
|
113
|
+
let result;
|
|
114
|
+
try {
|
|
115
|
+
result = await downloadFile(keyToUse, downloadOptions);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (err.name === 'NoSuchKey' || err.name === 'NotFound') {
|
|
118
|
+
return res.status(404).json({ message: 'File not found', key: keyToUse });
|
|
119
|
+
}
|
|
120
|
+
if (err.name === 'AccessDenied') {
|
|
121
|
+
return res.status(403).json({ message: 'Access denied' });
|
|
122
|
+
}
|
|
123
|
+
console.error('[mbkbucket] serveFileInline download error:', err);
|
|
124
|
+
return res.status(500).json({ message: 'Failed to fetch file', error: err.message });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (result?.notModified) return res.status(304).end();
|
|
128
|
+
|
|
129
|
+
const etag = `"${result.LastModified?.getTime() || Date.now()}-${result.ContentLength || 0}"`;
|
|
130
|
+
if (ifNoneMatch === etag) return res.status(304).end();
|
|
131
|
+
|
|
132
|
+
res.setHeader('Content-Type', contentType);
|
|
133
|
+
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
|
|
134
|
+
res.setHeader('ETag', etag);
|
|
135
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
136
|
+
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
137
|
+
if (noindex) res.setHeader('X-Robots-Tag', 'noindex, nofollow');
|
|
138
|
+
if (result.ContentLength) res.setHeader('Content-Length', result.ContentLength);
|
|
139
|
+
if (result.LastModified) res.setHeader('Last-Modified', result.LastModified.toUTCString());
|
|
140
|
+
if (fileExtension === 'html' || fileExtension === 'htm') {
|
|
141
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';");
|
|
142
|
+
}
|
|
143
|
+
if (supportsRanges) res.setHeader('Accept-Ranges', 'bytes');
|
|
144
|
+
res.setHeader('Cache-Control', buildCacheControl(contentType, { isStaticAsset, supportsRanges, publicCache }));
|
|
145
|
+
res.status(200);
|
|
146
|
+
|
|
147
|
+
const timeoutDuration = result.ContentLength > 50 * 1024 * 1024 ? 300000 : 120000;
|
|
148
|
+
attachStreamCleanup(req, res, result.Body, timeoutDuration);
|
|
149
|
+
result.Body.pipe(res);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function viewFile(req, res) {
|
|
153
|
+
try {
|
|
154
|
+
const key = normalizeKeyParam(req.params.key);
|
|
155
|
+
let keyToUse;
|
|
156
|
+
try {
|
|
157
|
+
keyToUse = ensureKeyHasAppPrefix(key);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return res.status(400).json({ message: e.message });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await serveFileInline(req, res, keyToUse, { noindex: true, bucketName: req.activeBucket, publicCache: false });
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error("Error viewing file:", error);
|
|
165
|
+
if (error.name === 'NoSuchKey' || error.name === 'NotFound') {
|
|
166
|
+
res.status(404).json({ message: "File not found", key: req.params.key });
|
|
167
|
+
} else if (error.name === 'AccessDenied') {
|
|
168
|
+
res.status(403).json({ message: "Access denied" });
|
|
169
|
+
} else {
|
|
170
|
+
res.status(500).json({ message: "View failed", error: error.message });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function playerPage(req, res) {
|
|
176
|
+
try {
|
|
177
|
+
const key = normalizeKeyParam(req.params.key);
|
|
178
|
+
let keyToUse;
|
|
179
|
+
try {
|
|
180
|
+
keyToUse = ensureKeyHasAppPrefix(key);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
return res.status(400).send(e.message);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const html = renderPlayerPage(
|
|
186
|
+
escapeHtml(getBaseName(keyToUse)),
|
|
187
|
+
encodeURIComponent(keyToUse),
|
|
188
|
+
`?bucket=${encodeURIComponent(req.activeBucket)}`
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
192
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
193
|
+
return res.status(200).send(html);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error('Error rendering player page:', err);
|
|
196
|
+
return res.status(500).send('Failed to render player');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function publicView(req, res) {
|
|
201
|
+
try {
|
|
202
|
+
const bucketName = resolveBucketName();
|
|
203
|
+
const key = normalizeKeyParam(req.params.key);
|
|
204
|
+
|
|
205
|
+
if (!key || key.endsWith('/')) {
|
|
206
|
+
return res.status(403).json({ message: 'Directory indexing is not allowed' });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let keyToUse;
|
|
210
|
+
try {
|
|
211
|
+
keyToUse = ensureKeyHasAppPrefix(key);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return res.status(400).json({ message: e.message });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!mbkbucketVar?.p_view_inline) {
|
|
217
|
+
try {
|
|
218
|
+
const signed = await generateSignedUrl(keyToUse, 'getObject', 3600, bucketName);
|
|
219
|
+
return res.redirect(302, signed.url);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error('[mbkbucket] p_view signed URL error:', err);
|
|
222
|
+
return res.status(500).json({ message: 'Failed to generate access URL' });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await serveFileInline(req, res, keyToUse, { noindex: true, bucketName, publicCache: true });
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error('[mbkbucket] p_view error:', error);
|
|
229
|
+
res.status(500).json({ message: 'Internal server error', error: error.message });
|
|
230
|
+
}
|
|
231
|
+
}
|