mbkbucket 4.0.0 → 4.0.1
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 +15 -15
- package/README.md +561 -561
- package/index.js +32 -32
- package/lib/cli.js +239 -239
- package/lib/public/bucketadmin.css +2042 -2042
- package/lib/public/bucketadmin.js +1622 -1622
- package/lib/src/config/validation.js +86 -86
- package/lib/src/controllers/api.controller.js +459 -459
- package/lib/src/controllers/dashboard.controller.js +69 -69
- package/lib/src/controllers/view.controller.js +233 -233
- package/lib/src/core/storage/errors.js +62 -62
- package/lib/src/core/storage/gdrive-auth.js +193 -193
- package/lib/src/core/storage/gdrive-provider.js +656 -656
- package/lib/src/core/storage/index.js +14 -14
- package/lib/src/core/storage/models.js +80 -80
- package/lib/src/core/storage/path-strategy.js +65 -65
- package/lib/src/core/storage/s3-provider.js +491 -491
- package/lib/src/core/storage/storage-manager.js +203 -203
- package/lib/src/core/storage/storage-provider.js +217 -217
- package/lib/src/middleware/bucket-resolver.js +37 -37
- package/lib/src/routes/index.js +60 -60
- package/lib/src/routes/view.routes.js +28 -28
- package/lib/src/services/s3.service.js +42 -42
- package/lib/src/services/storage.service.js +213 -213
- package/lib/src/utils/errors.js +56 -56
- package/package.json +2 -3
- package/views/bucketadmin_move_modal.hbs +35 -35
- package/views/bucketadmin_preview_modal.hbs +108 -108
- package/views/bucketadmincontent.hbs +193 -193
- package/views/mbkbucket_info.hbs +134 -134
package/lib/src/utils/errors.js
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Centralized error classification for API and View routes.
|
|
3
|
-
* Maps typed StorageErrors and error messages to HTTP status codes and machine-readable error codes.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { StorageError } from "../core/storage/errors.js";
|
|
7
|
-
|
|
8
|
-
const API_ERROR_RULES = [
|
|
9
|
-
{ match: 'bucketconnection environment variable is not set', status: 503, code: 'BUCKET_CONFIG_ERROR' },
|
|
10
|
-
{ match: ['no bucket selected', 'not found in bucketconnection', 'no storage connection'], status: 400, code: 'INVALID_BUCKET' },
|
|
11
|
-
{ match: 'already exists', status: 409, code: 'CONFLICT' },
|
|
12
|
-
{ match: 'not found', status: 404, code: 'NOT_FOUND' },
|
|
13
|
-
{ match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
|
|
14
|
-
{ match: ['required', 'invalid', 'must be', 'path traversal'], status: 400, code: 'VALIDATION_ERROR' },
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
const VIEW_ERROR_RULES = [
|
|
18
|
-
{ match: ['no bucket selected', 'not found in bucketconnection', 'no storage connection'], status: 400, code: 'INVALID_BUCKET' },
|
|
19
|
-
{ match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
|
|
20
|
-
{ match: 'not found', status: 404, code: 'NOT_FOUND' },
|
|
21
|
-
{ match: ['invalid', 'required', 'path traversal'], status: 400, code: 'VALIDATION_ERROR' },
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
function matchError(rules, error, fallbackMessage) {
|
|
25
|
-
if (error instanceof StorageError) {
|
|
26
|
-
return {
|
|
27
|
-
status: error.status,
|
|
28
|
-
code: error.code,
|
|
29
|
-
message: error.message || fallbackMessage
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const message = String(error?.message || fallbackMessage);
|
|
34
|
-
const lower = message.toLowerCase();
|
|
35
|
-
const found = rules.find(r => Array.isArray(r.match) ? r.match.some(m => lower.includes(m)) : lower.includes(r.match));
|
|
36
|
-
return { status: found?.status ?? 500, code: found?.code ?? 'INTERNAL_ERROR', message };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const classifyApiError = (error, fallbackMessage = 'Request failed') => matchError(API_ERROR_RULES, error, fallbackMessage);
|
|
40
|
-
export const classifyViewError = (error, fallbackMessage = 'Request failed') => matchError(VIEW_ERROR_RULES, error, fallbackMessage);
|
|
41
|
-
|
|
42
|
-
export function sendApiError(res, error, fallbackMessage = 'Request failed') {
|
|
43
|
-
const { status, code, message } = classifyApiError(error, fallbackMessage);
|
|
44
|
-
return res.status(status).json({
|
|
45
|
-
success: false,
|
|
46
|
-
code,
|
|
47
|
-
error: { code, message },
|
|
48
|
-
message,
|
|
49
|
-
timestamp: new Date().toISOString(),
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function sendViewError(res, error, fallbackMessage = 'Request failed') {
|
|
54
|
-
const { status, code, message } = classifyViewError(error, fallbackMessage);
|
|
55
|
-
return res.status(status).json({ code, message });
|
|
56
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Centralized error classification for API and View routes.
|
|
3
|
+
* Maps typed StorageErrors and error messages to HTTP status codes and machine-readable error codes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StorageError } from "../core/storage/errors.js";
|
|
7
|
+
|
|
8
|
+
const API_ERROR_RULES = [
|
|
9
|
+
{ match: 'bucketconnection environment variable is not set', status: 503, code: 'BUCKET_CONFIG_ERROR' },
|
|
10
|
+
{ match: ['no bucket selected', 'not found in bucketconnection', 'no storage connection'], status: 400, code: 'INVALID_BUCKET' },
|
|
11
|
+
{ match: 'already exists', status: 409, code: 'CONFLICT' },
|
|
12
|
+
{ match: 'not found', status: 404, code: 'NOT_FOUND' },
|
|
13
|
+
{ match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
|
|
14
|
+
{ match: ['required', 'invalid', 'must be', 'path traversal'], status: 400, code: 'VALIDATION_ERROR' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const VIEW_ERROR_RULES = [
|
|
18
|
+
{ match: ['no bucket selected', 'not found in bucketconnection', 'no storage connection'], status: 400, code: 'INVALID_BUCKET' },
|
|
19
|
+
{ match: 'access denied', status: 403, code: 'ACCESS_DENIED' },
|
|
20
|
+
{ match: 'not found', status: 404, code: 'NOT_FOUND' },
|
|
21
|
+
{ match: ['invalid', 'required', 'path traversal'], status: 400, code: 'VALIDATION_ERROR' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function matchError(rules, error, fallbackMessage) {
|
|
25
|
+
if (error instanceof StorageError) {
|
|
26
|
+
return {
|
|
27
|
+
status: error.status,
|
|
28
|
+
code: error.code,
|
|
29
|
+
message: error.message || fallbackMessage
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const message = String(error?.message || fallbackMessage);
|
|
34
|
+
const lower = message.toLowerCase();
|
|
35
|
+
const found = rules.find(r => Array.isArray(r.match) ? r.match.some(m => lower.includes(m)) : lower.includes(r.match));
|
|
36
|
+
return { status: found?.status ?? 500, code: found?.code ?? 'INTERNAL_ERROR', message };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const classifyApiError = (error, fallbackMessage = 'Request failed') => matchError(API_ERROR_RULES, error, fallbackMessage);
|
|
40
|
+
export const classifyViewError = (error, fallbackMessage = 'Request failed') => matchError(VIEW_ERROR_RULES, error, fallbackMessage);
|
|
41
|
+
|
|
42
|
+
export function sendApiError(res, error, fallbackMessage = 'Request failed') {
|
|
43
|
+
const { status, code, message } = classifyApiError(error, fallbackMessage);
|
|
44
|
+
return res.status(status).json({
|
|
45
|
+
success: false,
|
|
46
|
+
code,
|
|
47
|
+
error: { code, message },
|
|
48
|
+
message,
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function sendViewError(res, error, fallbackMessage = 'Request failed') {
|
|
54
|
+
const { status, code, message } = classifyViewError(error, fallbackMessage);
|
|
55
|
+
return res.status(status).json({ code, message });
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkbucket",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Flexible S3/R2 bucket management — library, Express router, and standalone server CLI for mbktech.org applications. Provides upload, download, listing, multipart utilities, signed URLs, and a standalone server runner with built-in configuration validation and health checks",
|
|
5
5
|
"author": "Muhammad Bin Khalid",
|
|
6
6
|
"homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"start": "node lib/cli.js",
|
|
16
|
-
"dev": "cross-env NODE_ENV=dev
|
|
16
|
+
"dev": "cross-env NODE_ENV=dev node --watch index.js",
|
|
17
17
|
"publish": "node scripts/publish.js",
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/supertest": "^7.2.1",
|
|
38
38
|
"cross-env": "^7.0.3",
|
|
39
|
-
"nodemon": "^3.1.14",
|
|
40
39
|
"supertest": "^7.3.1",
|
|
41
40
|
"vite": "^8.3.2",
|
|
42
41
|
"vitest": "5.0.3"
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
<!-- Move / Rename Modal -->
|
|
2
|
-
<div class="modal" id="moveModal" tabindex="-1" aria-labelledby="moveModalLabel" aria-hidden="true">
|
|
3
|
-
<div class="modal-dialog modal-dialog-centered">
|
|
4
|
-
<div class="modal-content">
|
|
5
|
-
<div class="modal-header">
|
|
6
|
-
<h5 class="modal-title" id="moveModalLabel">
|
|
7
|
-
<svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="modal-title-icon"><path d="M11 2L14 5L11 8M14 5H5C3.34315 5 2 6.34315 2 8V14"/></svg>
|
|
8
|
-
<span id="moveModalTitle">Move or Rename</span>
|
|
9
|
-
</h5>
|
|
10
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="closeMoveModal()">×</button>
|
|
11
|
-
</div>
|
|
12
|
-
<div class="modal-body">
|
|
13
|
-
<form id="moveForm" onsubmit="event.preventDefault(); submitMoveItem();">
|
|
14
|
-
<div class="modal-form-row mb-3">
|
|
15
|
-
<label class="form-label text-muted small">Current Path</label>
|
|
16
|
-
<div class="p-2 rounded text-truncate font-mono small" id="moveSourcePathPreview" style="background: var(--bg-surface-secondary); border: 1px solid var(--border-color); color: var(--dark-color-ml);"></div>
|
|
17
|
-
<input type="hidden" id="moveSourceKeyInput">
|
|
18
|
-
<input type="hidden" id="moveIsCopyInput" value="false">
|
|
19
|
-
</div>
|
|
20
|
-
<div class="modal-form-row mb-3">
|
|
21
|
-
<label for="moveDestNameInput" class="form-label" id="moveDestNameLabel">New Name or Path</label>
|
|
22
|
-
<input type="text" id="moveDestNameInput" class="form-control" autocomplete="off" required>
|
|
23
|
-
<div class="form-text" id="moveHelpText">Enter the new file name or folder destination path.</div>
|
|
24
|
-
</div>
|
|
25
|
-
</form>
|
|
26
|
-
</div>
|
|
27
|
-
<div class="modal-footer">
|
|
28
|
-
<button type="button" class="btn btn-outline" onclick="closeMoveModal()">Cancel</button>
|
|
29
|
-
<button type="button" class="btn btn-primary" id="submitMoveBtn" onclick="submitMoveItem()">
|
|
30
|
-
<span id="moveSubmitBtnText">Save Changes</span>
|
|
31
|
-
</button>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
1
|
+
<!-- Move / Rename Modal -->
|
|
2
|
+
<div class="modal" id="moveModal" tabindex="-1" aria-labelledby="moveModalLabel" aria-hidden="true">
|
|
3
|
+
<div class="modal-dialog modal-dialog-centered">
|
|
4
|
+
<div class="modal-content">
|
|
5
|
+
<div class="modal-header">
|
|
6
|
+
<h5 class="modal-title" id="moveModalLabel">
|
|
7
|
+
<svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="modal-title-icon"><path d="M11 2L14 5L11 8M14 5H5C3.34315 5 2 6.34315 2 8V14"/></svg>
|
|
8
|
+
<span id="moveModalTitle">Move or Rename</span>
|
|
9
|
+
</h5>
|
|
10
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="closeMoveModal()">×</button>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="modal-body">
|
|
13
|
+
<form id="moveForm" onsubmit="event.preventDefault(); submitMoveItem();">
|
|
14
|
+
<div class="modal-form-row mb-3">
|
|
15
|
+
<label class="form-label text-muted small">Current Path</label>
|
|
16
|
+
<div class="p-2 rounded text-truncate font-mono small" id="moveSourcePathPreview" style="background: var(--bg-surface-secondary); border: 1px solid var(--border-color); color: var(--dark-color-ml);"></div>
|
|
17
|
+
<input type="hidden" id="moveSourceKeyInput">
|
|
18
|
+
<input type="hidden" id="moveIsCopyInput" value="false">
|
|
19
|
+
</div>
|
|
20
|
+
<div class="modal-form-row mb-3">
|
|
21
|
+
<label for="moveDestNameInput" class="form-label" id="moveDestNameLabel">New Name or Path</label>
|
|
22
|
+
<input type="text" id="moveDestNameInput" class="form-control" autocomplete="off" required>
|
|
23
|
+
<div class="form-text" id="moveHelpText">Enter the new file name or folder destination path.</div>
|
|
24
|
+
</div>
|
|
25
|
+
</form>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="modal-footer">
|
|
28
|
+
<button type="button" class="btn btn-outline" onclick="closeMoveModal()">Cancel</button>
|
|
29
|
+
<button type="button" class="btn btn-primary" id="submitMoveBtn" onclick="submitMoveItem()">
|
|
30
|
+
<span id="moveSubmitBtnText">Save Changes</span>
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
<!-- Enhanced Preview Modal -->
|
|
2
|
-
<div class="modal" id="previewModal" role="dialog" aria-modal="true" aria-labelledby="previewModalLabel" tabindex="-1">
|
|
3
|
-
<div class="modal-dialog modal-dialog-centered preview-modal-dialog" role="document">
|
|
4
|
-
<div class="modal-content preview-modal-content">
|
|
5
|
-
<div class="modal-header preview-modal-header">
|
|
6
|
-
<div class="preview-header-left">
|
|
7
|
-
<h5 class="modal-title" id="previewModalLabel">
|
|
8
|
-
<svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="modal-title-icon"><path d="M1 8s3-5 7-5 7 5 7 5-3 5-7 5-7-5-7-5z"/><circle cx="8" cy="8" r="2.5"/></svg>
|
|
9
|
-
<span id="previewFileName">Preview</span>
|
|
10
|
-
</h5>
|
|
11
|
-
<div class="preview-tabs ms-3" role="tablist">
|
|
12
|
-
<button type="button" class="preview-tab active" id="tab-preview-btn" data-preview-tab="preview">Preview</button>
|
|
13
|
-
<button type="button" class="preview-tab" id="tab-info-btn" data-preview-tab="info">File Info</button>
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
<div class="preview-header-controls">
|
|
17
|
-
<div id="previewExtraControls" class="d-inline-flex align-items-center gap-1"></div>
|
|
18
|
-
<button id="previewFullscreenBtn" class="btn btn-icon btn-outline btn-sm" type="button" aria-label="Toggle fullscreen" title="Fullscreen">
|
|
19
|
-
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 5V2h3M14 5V2h-3M2 11v3h3M14 11v3h-3"/></svg>
|
|
20
|
-
</button>
|
|
21
|
-
<button type="button" class="btn-close" aria-label="Close preview" id="previewCloseBtn" onclick="closePreviewModal()">×</button>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
<div class="modal-body p-0 preview-modal-body">
|
|
25
|
-
<!-- Preview Tab View -->
|
|
26
|
-
<div class="preview-tab-pane active" id="pane-preview">
|
|
27
|
-
<div class="preview-content" id="previewContent" aria-live="polite">
|
|
28
|
-
<div class="empty-preview">
|
|
29
|
-
<div class="spinner-border" role="status"></div>
|
|
30
|
-
<div class="modal-header-spacing mt-2">Loading preview...</div>
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<!-- Info & Metadata Tab View -->
|
|
36
|
-
<div class="preview-tab-pane" id="pane-info" style="display: none;">
|
|
37
|
-
<div class="p-4 file-info-pane">
|
|
38
|
-
<div class="row g-3">
|
|
39
|
-
<div class="col-md-6">
|
|
40
|
-
<div class="info-card">
|
|
41
|
-
<div class="info-label">File Key</div>
|
|
42
|
-
<div class="info-value font-mono text-break" id="infoFileKey">—</div>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
<div class="col-md-3">
|
|
46
|
-
<div class="info-card">
|
|
47
|
-
<div class="info-label">Size</div>
|
|
48
|
-
<div class="info-value font-semibold" id="infoFileSize">—</div>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
<div class="col-md-3">
|
|
52
|
-
<div class="info-card">
|
|
53
|
-
<div class="info-label">MIME Type</div>
|
|
54
|
-
<div class="info-value" id="infoMimeType">—</div>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
<div class="col-md-6">
|
|
58
|
-
<div class="info-card">
|
|
59
|
-
<div class="info-label">Last Modified</div>
|
|
60
|
-
<div class="info-value" id="infoLastModified">—</div>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
<div class="col-md-6">
|
|
64
|
-
<div class="info-card">
|
|
65
|
-
<div class="info-label">ETag / Checksum</div>
|
|
66
|
-
<div class="info-value font-mono text-break" id="infoETag">—</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
<div class="col-md-12" id="infoPublicLinkCard" style="{{#unless publicViewEnabled}}display: none;{{/unless}}">
|
|
70
|
-
<div class="info-card">
|
|
71
|
-
<div class="info-label">Public View URL</div>
|
|
72
|
-
<div class="info-value font-mono text-break d-flex align-items-center justify-content-between gap-2">
|
|
73
|
-
<span id="infoPublicUrl" class="text-truncate" style="user-select: all;">—</span>
|
|
74
|
-
<button type="button" class="btn btn-outline btn-sm flex-shrink-0" onclick="copyCurrentPreviewPublicLink()" title="Copy Public URL">
|
|
75
|
-
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 2h8a1 1 0 011 1v8M3 5h8a1 1 0 011 1v8a1 1 0 01-1 1H3a1 1 0 01-1-1V6a1 1 0 011-1z"/></svg>
|
|
76
|
-
Copy
|
|
77
|
-
</button>
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
<div class="modal-footer preview-footer">
|
|
86
|
-
<div class="preview-actions">
|
|
87
|
-
<button type="button" class="btn btn-outline btn-sm" id="previewCopyLinkBtn" onclick="copyCurrentPreviewLink()">
|
|
88
|
-
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 2h8a1 1 0 011 1v8M3 5h8a1 1 0 011 1v8a1 1 0 01-1 1H3a1 1 0 01-1-1V6a1 1 0 011-1z"/></svg>
|
|
89
|
-
Copy Link
|
|
90
|
-
</button>
|
|
91
|
-
<button type="button" class="btn btn-outline btn-sm" id="previewCopyPublicLinkBtn" onclick="copyCurrentPreviewPublicLink()" title="Copy publicly accessible link" style="{{#unless publicViewEnabled}}display: none;{{/unless}}">
|
|
92
|
-
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6.25"/><path d="M1.75 8h12.5M8 1.75a10.5 10.5 0 010 12.5 10.5 10.5 0 010-12.5z"/></svg>
|
|
93
|
-
Copy Public Link
|
|
94
|
-
</button>
|
|
95
|
-
<a id="previewOpenBtn" class="btn btn-outline btn-sm" target="_blank" rel="noopener noreferrer" href="#">
|
|
96
|
-
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 3H3a1 1 0 00-1 1v9a1 1 0 001 1h9a1 1 0 001-1v-3M10 2h4v4M9 7l5-5"/></svg>
|
|
97
|
-
Open in new tab
|
|
98
|
-
</a>
|
|
99
|
-
<a id="previewDownloadBtn" class="btn btn-primary btn-sm" download href="#">
|
|
100
|
-
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M5 8l3 3 3-3"/><line x1="8" y1="2" x2="8" y2="11"/></svg>
|
|
101
|
-
Download
|
|
102
|
-
</a>
|
|
103
|
-
<button type="button" class="btn btn-outline btn-sm" id="previewCloseFooter" onclick="closePreviewModal()">Close</button>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
1
|
+
<!-- Enhanced Preview Modal -->
|
|
2
|
+
<div class="modal" id="previewModal" role="dialog" aria-modal="true" aria-labelledby="previewModalLabel" tabindex="-1">
|
|
3
|
+
<div class="modal-dialog modal-dialog-centered preview-modal-dialog" role="document">
|
|
4
|
+
<div class="modal-content preview-modal-content">
|
|
5
|
+
<div class="modal-header preview-modal-header">
|
|
6
|
+
<div class="preview-header-left">
|
|
7
|
+
<h5 class="modal-title" id="previewModalLabel">
|
|
8
|
+
<svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="modal-title-icon"><path d="M1 8s3-5 7-5 7 5 7 5-3 5-7 5-7-5-7-5z"/><circle cx="8" cy="8" r="2.5"/></svg>
|
|
9
|
+
<span id="previewFileName">Preview</span>
|
|
10
|
+
</h5>
|
|
11
|
+
<div class="preview-tabs ms-3" role="tablist">
|
|
12
|
+
<button type="button" class="preview-tab active" id="tab-preview-btn" data-preview-tab="preview">Preview</button>
|
|
13
|
+
<button type="button" class="preview-tab" id="tab-info-btn" data-preview-tab="info">File Info</button>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="preview-header-controls">
|
|
17
|
+
<div id="previewExtraControls" class="d-inline-flex align-items-center gap-1"></div>
|
|
18
|
+
<button id="previewFullscreenBtn" class="btn btn-icon btn-outline btn-sm" type="button" aria-label="Toggle fullscreen" title="Fullscreen">
|
|
19
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 5V2h3M14 5V2h-3M2 11v3h3M14 11v3h-3"/></svg>
|
|
20
|
+
</button>
|
|
21
|
+
<button type="button" class="btn-close" aria-label="Close preview" id="previewCloseBtn" onclick="closePreviewModal()">×</button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="modal-body p-0 preview-modal-body">
|
|
25
|
+
<!-- Preview Tab View -->
|
|
26
|
+
<div class="preview-tab-pane active" id="pane-preview">
|
|
27
|
+
<div class="preview-content" id="previewContent" aria-live="polite">
|
|
28
|
+
<div class="empty-preview">
|
|
29
|
+
<div class="spinner-border" role="status"></div>
|
|
30
|
+
<div class="modal-header-spacing mt-2">Loading preview...</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- Info & Metadata Tab View -->
|
|
36
|
+
<div class="preview-tab-pane" id="pane-info" style="display: none;">
|
|
37
|
+
<div class="p-4 file-info-pane">
|
|
38
|
+
<div class="row g-3">
|
|
39
|
+
<div class="col-md-6">
|
|
40
|
+
<div class="info-card">
|
|
41
|
+
<div class="info-label">File Key</div>
|
|
42
|
+
<div class="info-value font-mono text-break" id="infoFileKey">—</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="col-md-3">
|
|
46
|
+
<div class="info-card">
|
|
47
|
+
<div class="info-label">Size</div>
|
|
48
|
+
<div class="info-value font-semibold" id="infoFileSize">—</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="col-md-3">
|
|
52
|
+
<div class="info-card">
|
|
53
|
+
<div class="info-label">MIME Type</div>
|
|
54
|
+
<div class="info-value" id="infoMimeType">—</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="col-md-6">
|
|
58
|
+
<div class="info-card">
|
|
59
|
+
<div class="info-label">Last Modified</div>
|
|
60
|
+
<div class="info-value" id="infoLastModified">—</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="col-md-6">
|
|
64
|
+
<div class="info-card">
|
|
65
|
+
<div class="info-label">ETag / Checksum</div>
|
|
66
|
+
<div class="info-value font-mono text-break" id="infoETag">—</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="col-md-12" id="infoPublicLinkCard" style="{{#unless publicViewEnabled}}display: none;{{/unless}}">
|
|
70
|
+
<div class="info-card">
|
|
71
|
+
<div class="info-label">Public View URL</div>
|
|
72
|
+
<div class="info-value font-mono text-break d-flex align-items-center justify-content-between gap-2">
|
|
73
|
+
<span id="infoPublicUrl" class="text-truncate" style="user-select: all;">—</span>
|
|
74
|
+
<button type="button" class="btn btn-outline btn-sm flex-shrink-0" onclick="copyCurrentPreviewPublicLink()" title="Copy Public URL">
|
|
75
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 2h8a1 1 0 011 1v8M3 5h8a1 1 0 011 1v8a1 1 0 01-1 1H3a1 1 0 01-1-1V6a1 1 0 011-1z"/></svg>
|
|
76
|
+
Copy
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="modal-footer preview-footer">
|
|
86
|
+
<div class="preview-actions">
|
|
87
|
+
<button type="button" class="btn btn-outline btn-sm" id="previewCopyLinkBtn" onclick="copyCurrentPreviewLink()">
|
|
88
|
+
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 2h8a1 1 0 011 1v8M3 5h8a1 1 0 011 1v8a1 1 0 01-1 1H3a1 1 0 01-1-1V6a1 1 0 011-1z"/></svg>
|
|
89
|
+
Copy Link
|
|
90
|
+
</button>
|
|
91
|
+
<button type="button" class="btn btn-outline btn-sm" id="previewCopyPublicLinkBtn" onclick="copyCurrentPreviewPublicLink()" title="Copy publicly accessible link" style="{{#unless publicViewEnabled}}display: none;{{/unless}}">
|
|
92
|
+
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6.25"/><path d="M1.75 8h12.5M8 1.75a10.5 10.5 0 010 12.5 10.5 10.5 0 010-12.5z"/></svg>
|
|
93
|
+
Copy Public Link
|
|
94
|
+
</button>
|
|
95
|
+
<a id="previewOpenBtn" class="btn btn-outline btn-sm" target="_blank" rel="noopener noreferrer" href="#">
|
|
96
|
+
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 3H3a1 1 0 00-1 1v9a1 1 0 001 1h9a1 1 0 001-1v-3M10 2h4v4M9 7l5-5"/></svg>
|
|
97
|
+
Open in new tab
|
|
98
|
+
</a>
|
|
99
|
+
<a id="previewDownloadBtn" class="btn btn-primary btn-sm" download href="#">
|
|
100
|
+
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M5 8l3 3 3-3"/><line x1="8" y1="2" x2="8" y2="11"/></svg>
|
|
101
|
+
Download
|
|
102
|
+
</a>
|
|
103
|
+
<button type="button" class="btn btn-outline btn-sm" id="previewCloseFooter" onclick="closePreviewModal()">Close</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|