mbkbucket 3.5.0 → 3.6.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 +649 -649
- package/lib/public/bucketadmin.css +1420 -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 +150 -150
- package/lib/src/config/index.js +112 -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 +48 -48
- package/lib/src/controllers/info.controller.js +34 -34
- package/lib/src/controllers/view.controller.js +233 -233
- package/lib/src/middleware/async-handler.js +23 -23
- package/lib/src/middleware/bucket-resolver.js +37 -37
- package/lib/src/middleware/error-handler.js +54 -54
- package/lib/src/middleware/rate-limiter.js +22 -22
- package/lib/src/middleware/security.js +27 -27
- package/lib/src/routes/api.routes.js +54 -40
- package/lib/src/routes/index.js +40 -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 +603 -603
- package/lib/src/utils/errors.js +54 -54
- package/lib/src/utils/helpers.js +251 -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
package/lib/src/utils/errors.js
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Centralized error classification for API and View routes.
|
|
3
|
-
* Maps error messages to HTTP status codes and machine-readable error codes.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const API_ERROR_RULES = [
|
|
7
|
-
{ match: 'bucketconnection environment variable is not set', status: 503, code: 'BUCKET_CONFIG_ERROR' },
|
|
8
|
-
{ match: ['no bucket selected', 'not found in bucketconnection'], status: 400, code: 'INVALID_BUCKET' },
|
|
9
|
-
{ match: 'already exists', status: 409, code: 'CONFLICT' },
|
|
10
|
-
{ match: 'not found', status: 404, code: 'NOT_FOUND' },
|
|
11
|
-
{ match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
|
|
12
|
-
{ match: ['required', 'invalid', 'must be'], status: 400, code: 'VALIDATION_ERROR' },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const VIEW_ERROR_RULES = [
|
|
16
|
-
{ match: ['no bucket selected', 'not found in bucketconnection'], status: 400, code: 'INVALID_BUCKET' },
|
|
17
|
-
{ match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
|
|
18
|
-
{ match: 'not found', status: 404, code: 'NOT_FOUND' },
|
|
19
|
-
{ match: ['invalid', 'required'], status: 400, code: 'VALIDATION_ERROR' },
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
function matchError(rules, error, fallbackMessage) {
|
|
23
|
-
const message = String(error?.message || fallbackMessage);
|
|
24
|
-
const lower = message.toLowerCase();
|
|
25
|
-
const found = rules.find(r => Array.isArray(r.match) ? r.match.some(m => lower.includes(m)) : lower.includes(r.match));
|
|
26
|
-
return { status: found?.status ?? 500, code: found?.code ?? 'INTERNAL_ERROR', message };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function classifyApiError(error, fallbackMessage = 'Request failed') {
|
|
30
|
-
return matchError(API_ERROR_RULES, error, fallbackMessage);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function classifyViewError(error, fallbackMessage = 'Request failed') {
|
|
34
|
-
return matchError(VIEW_ERROR_RULES, error, fallbackMessage);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function sendApiError(res, error, fallbackMessage = 'Request failed') {
|
|
38
|
-
const { status, code, message } = classifyApiError(error, fallbackMessage);
|
|
39
|
-
return res.status(status).json({
|
|
40
|
-
success: false,
|
|
41
|
-
code,
|
|
42
|
-
error: {
|
|
43
|
-
code,
|
|
44
|
-
message,
|
|
45
|
-
},
|
|
46
|
-
message,
|
|
47
|
-
timestamp: new Date().toISOString(),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function sendViewError(res, error, fallbackMessage = 'Request failed') {
|
|
52
|
-
const { status, code, message } = classifyViewError(error, fallbackMessage);
|
|
53
|
-
return res.status(status).json({ code, message });
|
|
54
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Centralized error classification for API and View routes.
|
|
3
|
+
* Maps error messages to HTTP status codes and machine-readable error codes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const API_ERROR_RULES = [
|
|
7
|
+
{ match: 'bucketconnection environment variable is not set', status: 503, code: 'BUCKET_CONFIG_ERROR' },
|
|
8
|
+
{ match: ['no bucket selected', 'not found in bucketconnection'], status: 400, code: 'INVALID_BUCKET' },
|
|
9
|
+
{ match: 'already exists', status: 409, code: 'CONFLICT' },
|
|
10
|
+
{ match: 'not found', status: 404, code: 'NOT_FOUND' },
|
|
11
|
+
{ match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
|
|
12
|
+
{ match: ['required', 'invalid', 'must be'], status: 400, code: 'VALIDATION_ERROR' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const VIEW_ERROR_RULES = [
|
|
16
|
+
{ match: ['no bucket selected', 'not found in bucketconnection'], status: 400, code: 'INVALID_BUCKET' },
|
|
17
|
+
{ match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
|
|
18
|
+
{ match: 'not found', status: 404, code: 'NOT_FOUND' },
|
|
19
|
+
{ match: ['invalid', 'required'], status: 400, code: 'VALIDATION_ERROR' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function matchError(rules, error, fallbackMessage) {
|
|
23
|
+
const message = String(error?.message || fallbackMessage);
|
|
24
|
+
const lower = message.toLowerCase();
|
|
25
|
+
const found = rules.find(r => Array.isArray(r.match) ? r.match.some(m => lower.includes(m)) : lower.includes(r.match));
|
|
26
|
+
return { status: found?.status ?? 500, code: found?.code ?? 'INTERNAL_ERROR', message };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function classifyApiError(error, fallbackMessage = 'Request failed') {
|
|
30
|
+
return matchError(API_ERROR_RULES, error, fallbackMessage);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function classifyViewError(error, fallbackMessage = 'Request failed') {
|
|
34
|
+
return matchError(VIEW_ERROR_RULES, error, fallbackMessage);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sendApiError(res, error, fallbackMessage = 'Request failed') {
|
|
38
|
+
const { status, code, message } = classifyApiError(error, fallbackMessage);
|
|
39
|
+
return res.status(status).json({
|
|
40
|
+
success: false,
|
|
41
|
+
code,
|
|
42
|
+
error: {
|
|
43
|
+
code,
|
|
44
|
+
message,
|
|
45
|
+
},
|
|
46
|
+
message,
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sendViewError(res, error, fallbackMessage = 'Request failed') {
|
|
52
|
+
const { status, code, message } = classifyViewError(error, fallbackMessage);
|
|
53
|
+
return res.status(status).json({ code, message });
|
|
54
|
+
}
|
package/lib/src/utils/helpers.js
CHANGED
|
@@ -1,251 +1,251 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared helper utilities used across controllers, services, and CLI.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const HTML_ESCAPE_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
6
|
-
|
|
7
|
-
export function escapeHtml(value = '') {
|
|
8
|
-
return String(value ?? '').replace(/[&<>"']/g, c => HTML_ESCAPE_MAP[c]);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function getBaseName(path = '') {
|
|
12
|
-
const value = String(path ?? '');
|
|
13
|
-
const lastSlashIndex = value.lastIndexOf('/');
|
|
14
|
-
return lastSlashIndex === -1 ? value : value.slice(lastSlashIndex + 1);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function getFileExt(fileName = '') {
|
|
18
|
-
const name = getBaseName(fileName);
|
|
19
|
-
const dotIndex = name.lastIndexOf('.');
|
|
20
|
-
return dotIndex === -1 ? '' : name.slice(dotIndex + 1).toLowerCase();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function trimSlashes(str = '') {
|
|
24
|
-
return String(str ?? '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function trimLeadingSlashes(str = '') {
|
|
28
|
-
return String(str ?? '').replace(/^\/+/, '');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function getFolderPath(key = '') {
|
|
32
|
-
const s = String(key ?? '');
|
|
33
|
-
const lastSlash = s.lastIndexOf('/');
|
|
34
|
-
return lastSlash === -1 ? '' : s.slice(0, lastSlash + 1);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function normalizeKeyParam(rawKey) {
|
|
38
|
-
return Array.isArray(rawKey) ? rawKey.join('/') : String(rawKey || '');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function buildKey(prefix, fileName) {
|
|
42
|
-
const cleanPrefix = trimSlashes(prefix);
|
|
43
|
-
const cleanName = trimLeadingSlashes(fileName);
|
|
44
|
-
return cleanPrefix ? `${cleanPrefix}/${cleanName}` : cleanName;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function nowIso() {
|
|
48
|
-
return new Date().toISOString();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function formatBytes(bytes) {
|
|
52
|
-
if (!bytes || bytes === 0) return '0 B';
|
|
53
|
-
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
54
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
55
|
-
return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function formatDate(iso) {
|
|
59
|
-
try {
|
|
60
|
-
return new Date(iso).toLocaleString();
|
|
61
|
-
} catch {
|
|
62
|
-
return iso || '—';
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function streamToBuffer(stream) {
|
|
67
|
-
if (Buffer.isBuffer(stream)) return stream;
|
|
68
|
-
const chunks = [];
|
|
69
|
-
if (stream?.[Symbol.asyncIterator] || stream?.transformToWeb) {
|
|
70
|
-
for await (const chunk of stream) {
|
|
71
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
72
|
-
}
|
|
73
|
-
return Buffer.concat(chunks);
|
|
74
|
-
}
|
|
75
|
-
if (stream?.getReader) {
|
|
76
|
-
const reader = stream.getReader();
|
|
77
|
-
while (true) {
|
|
78
|
-
const { done, value } = await reader.read();
|
|
79
|
-
if (done) break;
|
|
80
|
-
chunks.push(Buffer.isBuffer(value) ? value : Buffer.from(value));
|
|
81
|
-
}
|
|
82
|
-
return Buffer.concat(chunks);
|
|
83
|
-
}
|
|
84
|
-
return Buffer.concat(chunks);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function destroyStream(stream) {
|
|
88
|
-
if (stream && typeof stream.destroy === 'function') {
|
|
89
|
-
stream.removeAllListeners?.();
|
|
90
|
-
stream.destroy();
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
// MIME type & file extension constants
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
|
|
98
|
-
export const VIEWABLE_TYPES = new Set([
|
|
99
|
-
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico',
|
|
100
|
-
'mp4', 'webm', 'ogg', 'avi', 'mov',
|
|
101
|
-
'mp3', 'wav', 'flac', 'aac', 'm4a',
|
|
102
|
-
'txt', 'md', 'json', 'xml', 'csv', 'log',
|
|
103
|
-
'js', 'ts', 'html', 'htm', 'css', 'php', 'py', 'java', 'cpp', 'c', 'h',
|
|
104
|
-
'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml',
|
|
105
|
-
'toml', 'ini', 'conf', 'pdf'
|
|
106
|
-
]);
|
|
107
|
-
|
|
108
|
-
export const VIEWABLE_TYPES_SORTED = [...VIEWABLE_TYPES].sort();
|
|
109
|
-
export const STATIC_ASSET_TYPES = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'css', 'js']);
|
|
110
|
-
export const VIDEO_TYPES = new Set(['mp4', 'webm', 'ogg', 'avi', 'mov']);
|
|
111
|
-
export const AUDIO_TYPES = new Set(['mp3', 'wav', 'flac', 'aac', 'm4a']);
|
|
112
|
-
|
|
113
|
-
export const MIME_TYPES = {
|
|
114
|
-
// Images
|
|
115
|
-
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',
|
|
116
|
-
gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
|
|
117
|
-
bmp: 'image/bmp', ico: 'image/x-icon',
|
|
118
|
-
// Videos
|
|
119
|
-
mp4: 'video/mp4', webm: 'video/webm', ogg: 'video/ogg',
|
|
120
|
-
avi: 'video/x-msvideo', mov: 'video/quicktime',
|
|
121
|
-
// Audio
|
|
122
|
-
mp3: 'audio/mpeg', wav: 'audio/wav', flac: 'audio/flac',
|
|
123
|
-
aac: 'audio/aac', m4a: 'audio/mp4',
|
|
124
|
-
// Text / Code
|
|
125
|
-
txt: 'text/plain; charset=utf-8', md: 'text/markdown; charset=utf-8',
|
|
126
|
-
json: 'application/json; charset=utf-8', xml: 'application/xml; charset=utf-8',
|
|
127
|
-
csv: 'text/csv; charset=utf-8', log: 'text/plain; charset=utf-8',
|
|
128
|
-
js: 'text/javascript; charset=utf-8', ts: 'text/typescript; charset=utf-8',
|
|
129
|
-
html: 'text/html; charset=utf-8', htm: 'text/html; charset=utf-8',
|
|
130
|
-
css: 'text/css; charset=utf-8', php: 'text/x-php; charset=utf-8',
|
|
131
|
-
py: 'text/x-python; charset=utf-8', java: 'text/x-java-source; charset=utf-8',
|
|
132
|
-
cpp: 'text/x-c++src; charset=utf-8', c: 'text/x-csrc; charset=utf-8',
|
|
133
|
-
h: 'text/x-chdr; charset=utf-8', cs: 'text/x-csharp; charset=utf-8',
|
|
134
|
-
rb: 'text/x-ruby; charset=utf-8', go: 'text/x-go; charset=utf-8',
|
|
135
|
-
rs: 'text/x-rust; charset=utf-8', sql: 'text/x-sql; charset=utf-8',
|
|
136
|
-
sh: 'text/x-shellscript; charset=utf-8', bat: 'text/x-msdos-batch; charset=utf-8',
|
|
137
|
-
ps1: 'text/x-powershell; charset=utf-8', yaml: 'text/x-yaml; charset=utf-8',
|
|
138
|
-
yml: 'text/x-yaml; charset=utf-8', toml: 'text/x-toml; charset=utf-8',
|
|
139
|
-
ini: 'text/x-ini; charset=utf-8', conf: 'text/x-conf; charset=utf-8',
|
|
140
|
-
// Archives & Documents
|
|
141
|
-
zip: 'application/zip', gz: 'application/gzip', tar: 'application/x-tar',
|
|
142
|
-
pdf: 'application/pdf'
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
export function getMimeType(fileName, fallback = 'application/octet-stream') {
|
|
146
|
-
return MIME_TYPES[getFileExt(fileName)] || fallback;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function isSensitiveTextType(contentType = '') {
|
|
150
|
-
const type = String(contentType ?? '').toLowerCase();
|
|
151
|
-
return type.startsWith('text/') || /json|xml|javascript|typescript/.test(type);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function buildCacheControl(contentType, { isStaticAsset = false, supportsRanges = false, publicCache = false } = {}) {
|
|
155
|
-
if (publicCache) {
|
|
156
|
-
if (isStaticAsset) return 'public, max-age=3600, immutable';
|
|
157
|
-
if (supportsRanges) return 'public, max-age=86400, must-revalidate';
|
|
158
|
-
return 'public, max-age=300, must-revalidate';
|
|
159
|
-
}
|
|
160
|
-
if (isSensitiveTextType(contentType)) return 'private, no-store';
|
|
161
|
-
return supportsRanges ? 'private, max-age=1800, must-revalidate' : 'private, max-age=300, must-revalidate';
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
// Range header parsing
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
|
|
168
|
-
const DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024; // 8 MB
|
|
169
|
-
|
|
170
|
-
export function parseRangeHeader(rangeHeader, totalSize) {
|
|
171
|
-
if (!totalSize || totalSize <= 0) return null;
|
|
172
|
-
|
|
173
|
-
const match = /^bytes=(\d*)-(\d*)$/.exec(String(rangeHeader ?? '').trim());
|
|
174
|
-
if (!match) return null;
|
|
175
|
-
|
|
176
|
-
const [, startStr, endStr] = match;
|
|
177
|
-
let start;
|
|
178
|
-
let end;
|
|
179
|
-
|
|
180
|
-
if (startStr === '') {
|
|
181
|
-
const suffix = parseInt(endStr, 10);
|
|
182
|
-
if (isNaN(suffix) || suffix <= 0) return null;
|
|
183
|
-
start = Math.max(0, totalSize - suffix);
|
|
184
|
-
end = totalSize - 1;
|
|
185
|
-
} else {
|
|
186
|
-
start = parseInt(startStr, 10);
|
|
187
|
-
if (isNaN(start) || start < 0) return null;
|
|
188
|
-
if (endStr === '') {
|
|
189
|
-
end = Math.min(start + DEFAULT_CHUNK_SIZE - 1, totalSize - 1);
|
|
190
|
-
} else {
|
|
191
|
-
end = parseInt(endStr, 10);
|
|
192
|
-
if (isNaN(end) || end < start) return null;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (start >= totalSize || start > end || end >= totalSize) return null;
|
|
197
|
-
return { start, end, total: totalSize, size: end - start + 1 };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ---------------------------------------------------------------------------
|
|
201
|
-
// Player page HTML template
|
|
202
|
-
// ---------------------------------------------------------------------------
|
|
203
|
-
|
|
204
|
-
export function renderPlayerPage(safeFileName, encodedKey, bucketQuery) {
|
|
205
|
-
return `<!doctype html>
|
|
206
|
-
<html lang="en">
|
|
207
|
-
<head>
|
|
208
|
-
<meta charset="utf-8">
|
|
209
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
210
|
-
<title>Preview: ${safeFileName}</title>
|
|
211
|
-
<style>
|
|
212
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
213
|
-
body { margin: 0; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #000; color: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
|
|
214
|
-
.player-container { width: 100%; max-width: 100vw; max-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
|
|
215
|
-
video { width: 100%; height: auto; max-width: 100%; max-height: calc(100vh - 40px); object-fit: contain; background: #000; }
|
|
216
|
-
.file-name { position: fixed; top: 10px; left: 10px; background: rgba(0, 0, 0, 0.8); padding: 8px 16px; border-radius: 4px; font-size: 14px; z-index: 10; }
|
|
217
|
-
</style>
|
|
218
|
-
</head>
|
|
219
|
-
<body>
|
|
220
|
-
<div class="file-name">${safeFileName}</div>
|
|
221
|
-
<div class="player-container">
|
|
222
|
-
<video controls preload="metadata" src="/mbkbucket/view/${encodedKey}${bucketQuery}">
|
|
223
|
-
Your browser does not support the video tag.
|
|
224
|
-
</video>
|
|
225
|
-
</div>
|
|
226
|
-
</body>
|
|
227
|
-
</html>`;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ---------------------------------------------------------------------------
|
|
231
|
-
// Common Handlebars helpers
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
|
|
234
|
-
export const commonHandlebarsHelpers = {
|
|
235
|
-
eq: (a, b) => a === b,
|
|
236
|
-
neq: (a, b) => a !== b,
|
|
237
|
-
or: (...args) => args.slice(0, -1).some(Boolean),
|
|
238
|
-
and: (...args) => args.slice(0, -1).every(Boolean),
|
|
239
|
-
not: (val) => !val,
|
|
240
|
-
gt: (a, b) => a > b,
|
|
241
|
-
gte: (a, b) => a >= b,
|
|
242
|
-
lt: (a, b) => a < b,
|
|
243
|
-
lte: (a, b) => a <= b,
|
|
244
|
-
includes: (arr, val) => Array.isArray(arr) && arr.includes(val),
|
|
245
|
-
json: (obj) => JSON.stringify(obj, null, 2),
|
|
246
|
-
jsonStringify: (obj) => JSON.stringify(obj),
|
|
247
|
-
getInitials: (username) => {
|
|
248
|
-
if (!username) return '?';
|
|
249
|
-
return username.split(/[._]/).map(p => p.charAt(0)).join('').toUpperCase().slice(0, 2);
|
|
250
|
-
},
|
|
251
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper utilities used across controllers, services, and CLI.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const HTML_ESCAPE_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
6
|
+
|
|
7
|
+
export function escapeHtml(value = '') {
|
|
8
|
+
return String(value ?? '').replace(/[&<>"']/g, c => HTML_ESCAPE_MAP[c]);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getBaseName(path = '') {
|
|
12
|
+
const value = String(path ?? '');
|
|
13
|
+
const lastSlashIndex = value.lastIndexOf('/');
|
|
14
|
+
return lastSlashIndex === -1 ? value : value.slice(lastSlashIndex + 1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getFileExt(fileName = '') {
|
|
18
|
+
const name = getBaseName(fileName);
|
|
19
|
+
const dotIndex = name.lastIndexOf('.');
|
|
20
|
+
return dotIndex === -1 ? '' : name.slice(dotIndex + 1).toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function trimSlashes(str = '') {
|
|
24
|
+
return String(str ?? '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function trimLeadingSlashes(str = '') {
|
|
28
|
+
return String(str ?? '').replace(/^\/+/, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getFolderPath(key = '') {
|
|
32
|
+
const s = String(key ?? '');
|
|
33
|
+
const lastSlash = s.lastIndexOf('/');
|
|
34
|
+
return lastSlash === -1 ? '' : s.slice(0, lastSlash + 1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizeKeyParam(rawKey) {
|
|
38
|
+
return Array.isArray(rawKey) ? rawKey.join('/') : String(rawKey || '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildKey(prefix, fileName) {
|
|
42
|
+
const cleanPrefix = trimSlashes(prefix);
|
|
43
|
+
const cleanName = trimLeadingSlashes(fileName);
|
|
44
|
+
return cleanPrefix ? `${cleanPrefix}/${cleanName}` : cleanName;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function nowIso() {
|
|
48
|
+
return new Date().toISOString();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatBytes(bytes) {
|
|
52
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
53
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
54
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
55
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatDate(iso) {
|
|
59
|
+
try {
|
|
60
|
+
return new Date(iso).toLocaleString();
|
|
61
|
+
} catch {
|
|
62
|
+
return iso || '—';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function streamToBuffer(stream) {
|
|
67
|
+
if (Buffer.isBuffer(stream)) return stream;
|
|
68
|
+
const chunks = [];
|
|
69
|
+
if (stream?.[Symbol.asyncIterator] || stream?.transformToWeb) {
|
|
70
|
+
for await (const chunk of stream) {
|
|
71
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
72
|
+
}
|
|
73
|
+
return Buffer.concat(chunks);
|
|
74
|
+
}
|
|
75
|
+
if (stream?.getReader) {
|
|
76
|
+
const reader = stream.getReader();
|
|
77
|
+
while (true) {
|
|
78
|
+
const { done, value } = await reader.read();
|
|
79
|
+
if (done) break;
|
|
80
|
+
chunks.push(Buffer.isBuffer(value) ? value : Buffer.from(value));
|
|
81
|
+
}
|
|
82
|
+
return Buffer.concat(chunks);
|
|
83
|
+
}
|
|
84
|
+
return Buffer.concat(chunks);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function destroyStream(stream) {
|
|
88
|
+
if (stream && typeof stream.destroy === 'function') {
|
|
89
|
+
stream.removeAllListeners?.();
|
|
90
|
+
stream.destroy();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// MIME type & file extension constants
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export const VIEWABLE_TYPES = new Set([
|
|
99
|
+
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico',
|
|
100
|
+
'mp4', 'webm', 'ogg', 'avi', 'mov',
|
|
101
|
+
'mp3', 'wav', 'flac', 'aac', 'm4a',
|
|
102
|
+
'txt', 'md', 'json', 'xml', 'csv', 'log',
|
|
103
|
+
'js', 'ts', 'html', 'htm', 'css', 'php', 'py', 'java', 'cpp', 'c', 'h',
|
|
104
|
+
'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml',
|
|
105
|
+
'toml', 'ini', 'conf', 'pdf'
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
export const VIEWABLE_TYPES_SORTED = [...VIEWABLE_TYPES].sort();
|
|
109
|
+
export const STATIC_ASSET_TYPES = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'css', 'js']);
|
|
110
|
+
export const VIDEO_TYPES = new Set(['mp4', 'webm', 'ogg', 'avi', 'mov']);
|
|
111
|
+
export const AUDIO_TYPES = new Set(['mp3', 'wav', 'flac', 'aac', 'm4a']);
|
|
112
|
+
|
|
113
|
+
export const MIME_TYPES = {
|
|
114
|
+
// Images
|
|
115
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',
|
|
116
|
+
gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
|
|
117
|
+
bmp: 'image/bmp', ico: 'image/x-icon',
|
|
118
|
+
// Videos
|
|
119
|
+
mp4: 'video/mp4', webm: 'video/webm', ogg: 'video/ogg',
|
|
120
|
+
avi: 'video/x-msvideo', mov: 'video/quicktime',
|
|
121
|
+
// Audio
|
|
122
|
+
mp3: 'audio/mpeg', wav: 'audio/wav', flac: 'audio/flac',
|
|
123
|
+
aac: 'audio/aac', m4a: 'audio/mp4',
|
|
124
|
+
// Text / Code
|
|
125
|
+
txt: 'text/plain; charset=utf-8', md: 'text/markdown; charset=utf-8',
|
|
126
|
+
json: 'application/json; charset=utf-8', xml: 'application/xml; charset=utf-8',
|
|
127
|
+
csv: 'text/csv; charset=utf-8', log: 'text/plain; charset=utf-8',
|
|
128
|
+
js: 'text/javascript; charset=utf-8', ts: 'text/typescript; charset=utf-8',
|
|
129
|
+
html: 'text/html; charset=utf-8', htm: 'text/html; charset=utf-8',
|
|
130
|
+
css: 'text/css; charset=utf-8', php: 'text/x-php; charset=utf-8',
|
|
131
|
+
py: 'text/x-python; charset=utf-8', java: 'text/x-java-source; charset=utf-8',
|
|
132
|
+
cpp: 'text/x-c++src; charset=utf-8', c: 'text/x-csrc; charset=utf-8',
|
|
133
|
+
h: 'text/x-chdr; charset=utf-8', cs: 'text/x-csharp; charset=utf-8',
|
|
134
|
+
rb: 'text/x-ruby; charset=utf-8', go: 'text/x-go; charset=utf-8',
|
|
135
|
+
rs: 'text/x-rust; charset=utf-8', sql: 'text/x-sql; charset=utf-8',
|
|
136
|
+
sh: 'text/x-shellscript; charset=utf-8', bat: 'text/x-msdos-batch; charset=utf-8',
|
|
137
|
+
ps1: 'text/x-powershell; charset=utf-8', yaml: 'text/x-yaml; charset=utf-8',
|
|
138
|
+
yml: 'text/x-yaml; charset=utf-8', toml: 'text/x-toml; charset=utf-8',
|
|
139
|
+
ini: 'text/x-ini; charset=utf-8', conf: 'text/x-conf; charset=utf-8',
|
|
140
|
+
// Archives & Documents
|
|
141
|
+
zip: 'application/zip', gz: 'application/gzip', tar: 'application/x-tar',
|
|
142
|
+
pdf: 'application/pdf'
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export function getMimeType(fileName, fallback = 'application/octet-stream') {
|
|
146
|
+
return MIME_TYPES[getFileExt(fileName)] || fallback;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function isSensitiveTextType(contentType = '') {
|
|
150
|
+
const type = String(contentType ?? '').toLowerCase();
|
|
151
|
+
return type.startsWith('text/') || /json|xml|javascript|typescript/.test(type);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function buildCacheControl(contentType, { isStaticAsset = false, supportsRanges = false, publicCache = false } = {}) {
|
|
155
|
+
if (publicCache) {
|
|
156
|
+
if (isStaticAsset) return 'public, max-age=3600, immutable';
|
|
157
|
+
if (supportsRanges) return 'public, max-age=86400, must-revalidate';
|
|
158
|
+
return 'public, max-age=300, must-revalidate';
|
|
159
|
+
}
|
|
160
|
+
if (isSensitiveTextType(contentType)) return 'private, no-store';
|
|
161
|
+
return supportsRanges ? 'private, max-age=1800, must-revalidate' : 'private, max-age=300, must-revalidate';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Range header parsing
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
const DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024; // 8 MB
|
|
169
|
+
|
|
170
|
+
export function parseRangeHeader(rangeHeader, totalSize) {
|
|
171
|
+
if (!totalSize || totalSize <= 0) return null;
|
|
172
|
+
|
|
173
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(String(rangeHeader ?? '').trim());
|
|
174
|
+
if (!match) return null;
|
|
175
|
+
|
|
176
|
+
const [, startStr, endStr] = match;
|
|
177
|
+
let start;
|
|
178
|
+
let end;
|
|
179
|
+
|
|
180
|
+
if (startStr === '') {
|
|
181
|
+
const suffix = parseInt(endStr, 10);
|
|
182
|
+
if (isNaN(suffix) || suffix <= 0) return null;
|
|
183
|
+
start = Math.max(0, totalSize - suffix);
|
|
184
|
+
end = totalSize - 1;
|
|
185
|
+
} else {
|
|
186
|
+
start = parseInt(startStr, 10);
|
|
187
|
+
if (isNaN(start) || start < 0) return null;
|
|
188
|
+
if (endStr === '') {
|
|
189
|
+
end = Math.min(start + DEFAULT_CHUNK_SIZE - 1, totalSize - 1);
|
|
190
|
+
} else {
|
|
191
|
+
end = parseInt(endStr, 10);
|
|
192
|
+
if (isNaN(end) || end < start) return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (start >= totalSize || start > end || end >= totalSize) return null;
|
|
197
|
+
return { start, end, total: totalSize, size: end - start + 1 };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Player page HTML template
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
export function renderPlayerPage(safeFileName, encodedKey, bucketQuery) {
|
|
205
|
+
return `<!doctype html>
|
|
206
|
+
<html lang="en">
|
|
207
|
+
<head>
|
|
208
|
+
<meta charset="utf-8">
|
|
209
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
210
|
+
<title>Preview: ${safeFileName}</title>
|
|
211
|
+
<style>
|
|
212
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
213
|
+
body { margin: 0; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #000; color: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
|
|
214
|
+
.player-container { width: 100%; max-width: 100vw; max-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
|
|
215
|
+
video { width: 100%; height: auto; max-width: 100%; max-height: calc(100vh - 40px); object-fit: contain; background: #000; }
|
|
216
|
+
.file-name { position: fixed; top: 10px; left: 10px; background: rgba(0, 0, 0, 0.8); padding: 8px 16px; border-radius: 4px; font-size: 14px; z-index: 10; }
|
|
217
|
+
</style>
|
|
218
|
+
</head>
|
|
219
|
+
<body>
|
|
220
|
+
<div class="file-name">${safeFileName}</div>
|
|
221
|
+
<div class="player-container">
|
|
222
|
+
<video controls preload="metadata" src="/mbkbucket/view/${encodedKey}${bucketQuery}">
|
|
223
|
+
Your browser does not support the video tag.
|
|
224
|
+
</video>
|
|
225
|
+
</div>
|
|
226
|
+
</body>
|
|
227
|
+
</html>`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Common Handlebars helpers
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
export const commonHandlebarsHelpers = {
|
|
235
|
+
eq: (a, b) => a === b,
|
|
236
|
+
neq: (a, b) => a !== b,
|
|
237
|
+
or: (...args) => args.slice(0, -1).some(Boolean),
|
|
238
|
+
and: (...args) => args.slice(0, -1).every(Boolean),
|
|
239
|
+
not: (val) => !val,
|
|
240
|
+
gt: (a, b) => a > b,
|
|
241
|
+
gte: (a, b) => a >= b,
|
|
242
|
+
lt: (a, b) => a < b,
|
|
243
|
+
lte: (a, b) => a <= b,
|
|
244
|
+
includes: (arr, val) => Array.isArray(arr) && arr.includes(val),
|
|
245
|
+
json: (obj) => JSON.stringify(obj, null, 2),
|
|
246
|
+
jsonStringify: (obj) => JSON.stringify(obj),
|
|
247
|
+
getInitials: (username) => {
|
|
248
|
+
if (!username) return '?';
|
|
249
|
+
return username.split(/[._]/).map(p => p.charAt(0)).join('').toUpperCase().slice(0, 2);
|
|
250
|
+
},
|
|
251
|
+
};
|
package/lib/src/utils/logger.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import createDebug from "debug";
|
|
2
|
-
import dotenv from "dotenv";
|
|
3
|
-
|
|
4
|
-
dotenv.config();
|
|
5
|
-
|
|
6
|
-
if (process.env.DEBUG) {
|
|
7
|
-
createDebug.enable(process.env.DEBUG);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Creates a namespaced debug logger (e.g., `mbkbucket:s3`, `mbkbucket:api`).
|
|
12
|
-
* @param {string} namespace
|
|
13
|
-
* @returns {import("debug").Debugger}
|
|
14
|
-
*/
|
|
15
|
-
export function createLogger(namespace) {
|
|
16
|
-
return createDebug(`mbkbucket:${namespace}`);
|
|
17
|
-
}
|
|
1
|
+
import createDebug from "debug";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
|
|
4
|
+
dotenv.config();
|
|
5
|
+
|
|
6
|
+
if (process.env.DEBUG) {
|
|
7
|
+
createDebug.enable(process.env.DEBUG);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a namespaced debug logger (e.g., `mbkbucket:s3`, `mbkbucket:api`).
|
|
12
|
+
* @param {string} namespace
|
|
13
|
+
* @returns {import("debug").Debugger}
|
|
14
|
+
*/
|
|
15
|
+
export function createLogger(namespace) {
|
|
16
|
+
return createDebug(`mbkbucket:${namespace}`);
|
|
17
|
+
}
|