mbkbucket 3.8.3 → 4.0.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 +15 -14
- package/README.md +561 -353
- package/index.js +32 -30
- package/lib/public/bucketadmin.css +2042 -1708
- package/lib/public/bucketadmin.js +1622 -1286
- package/lib/public/mbkbucket-helper-demo.html +4 -24
- package/lib/public/mbkbucket-helper.css +28 -34
- package/lib/public/mbkbucket-helper.js +4 -3
- package/lib/src/config/validation.js +86 -73
- package/lib/src/controllers/api.controller.js +459 -410
- package/lib/src/controllers/dashboard.controller.js +69 -49
- package/lib/src/controllers/info.controller.js +34 -3
- package/lib/src/controllers/view.controller.js +233 -231
- package/lib/src/core/storage/errors.js +62 -0
- package/lib/src/core/storage/gdrive-auth.js +193 -0
- package/lib/src/core/storage/gdrive-provider.js +656 -0
- package/lib/src/core/storage/index.js +14 -0
- package/lib/src/core/storage/models.js +80 -0
- package/lib/src/core/storage/path-strategy.js +65 -0
- package/lib/src/core/storage/s3-provider.js +491 -0
- package/lib/src/core/storage/storage-manager.js +203 -0
- package/lib/src/core/storage/storage-provider.js +217 -0
- package/lib/src/middleware/bucket-resolver.js +37 -37
- package/lib/src/routes/api.routes.js +5 -1
- package/lib/src/routes/index.js +60 -60
- package/lib/src/routes/view.routes.js +28 -25
- package/lib/src/services/s3.service.js +42 -594
- package/lib/src/services/storage.service.js +213 -0
- package/lib/src/utils/errors.js +56 -46
- package/package.json +1 -1
- package/views/bucket.hbs +47 -71
- package/views/bucketadmin_move_modal.hbs +35 -0
- package/views/bucketadmin_preview_modal.hbs +108 -40
- package/views/bucketadmincontent.hbs +193 -144
- package/views/mbkbucket_info.hbs +134 -232
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MBKBucket - Provider-Independent Storage Service
|
|
3
|
+
* Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import dotenv from "dotenv";
|
|
8
|
+
import { storageManager } from "../core/storage/storage-manager.js";
|
|
9
|
+
import { PathStrategy } from "../core/storage/path-strategy.js";
|
|
10
|
+
import { StorageValidationError } from "../core/storage/errors.js";
|
|
11
|
+
import { createLogger } from "#logger";
|
|
12
|
+
import { trimSlashes } from "#helpers";
|
|
13
|
+
|
|
14
|
+
dotenv.config();
|
|
15
|
+
const debugStorage = createLogger('storage-service');
|
|
16
|
+
|
|
17
|
+
export const getAvailableStorageNames = () => storageManager.getAvailableConnectionNames();
|
|
18
|
+
export const getAvailableBucketNames = getAvailableStorageNames;
|
|
19
|
+
|
|
20
|
+
export function resolveStorageName(name) {
|
|
21
|
+
return storageManager.resolveConnectionName(name);
|
|
22
|
+
}
|
|
23
|
+
export const resolveBucketName = resolveStorageName;
|
|
24
|
+
|
|
25
|
+
export const getAppName = () => PathStrategy.getAppName();
|
|
26
|
+
export const validateKeySafety = (str, isKey = false) => PathStrategy.validateSafety(str, isKey);
|
|
27
|
+
export const ensureKeyHasAppPrefix = (key = '') => PathStrategy.applyKeyPrefix(key);
|
|
28
|
+
export const ensurePrefix = (prefix = '') => PathStrategy.applyPrefix(prefix);
|
|
29
|
+
|
|
30
|
+
export function getStorageProvider(connectionName) {
|
|
31
|
+
return storageManager.getProvider(connectionName);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getStorageConfig(connectionName) {
|
|
35
|
+
return storageManager.getConnectionConfig(connectionName);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Health Checks
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export async function checkHealth(connectionName) {
|
|
43
|
+
return storageManager.checkHealth(connectionName);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runHealthCheck(connectionName) {
|
|
47
|
+
try {
|
|
48
|
+
const health = await checkHealth(connectionName);
|
|
49
|
+
if (health.status === 'healthy') {
|
|
50
|
+
debugStorage('Connected to storage: %s (%sms)', health.bucket || health.name || 'default', health.responseTime);
|
|
51
|
+
} else {
|
|
52
|
+
console.error('[mbkbucket] Storage connection failed:', health.error);
|
|
53
|
+
}
|
|
54
|
+
return health;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error("[mbkbucket] Storage connection test error:", err.message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Unified Storage Operations
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
65
|
+
if (!key || !fileBuffer) throw new StorageValidationError('Key and file buffer are required');
|
|
66
|
+
const keyToUpload = ensureKeyHasAppPrefix(key);
|
|
67
|
+
const provider = getStorageProvider(options.bucketName || options.connectionName);
|
|
68
|
+
return provider.uploadFile(keyToUpload, fileBuffer, contentType, options);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function downloadFile(key, options = {}) {
|
|
72
|
+
if (!key) throw new StorageValidationError('Key is required');
|
|
73
|
+
const keyToDownload = ensureKeyHasAppPrefix(key);
|
|
74
|
+
const provider = getStorageProvider(options.bucketName || options.connectionName);
|
|
75
|
+
return provider.downloadFile(keyToDownload, options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function deleteFile(key, connectionName) {
|
|
79
|
+
if (!key) throw new StorageValidationError('Key is required');
|
|
80
|
+
const keyToDelete = ensureKeyHasAppPrefix(key);
|
|
81
|
+
const provider = getStorageProvider(connectionName);
|
|
82
|
+
return provider.deleteFile(keyToDelete);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function deleteFiles(keys, connectionName) {
|
|
86
|
+
if (!keys || !Array.isArray(keys) || !keys.length) {
|
|
87
|
+
throw new StorageValidationError('Keys array is required and must not be empty');
|
|
88
|
+
}
|
|
89
|
+
const keysToDelete = keys.map(k => ensureKeyHasAppPrefix(k));
|
|
90
|
+
const provider = getStorageProvider(connectionName);
|
|
91
|
+
return provider.deleteFiles(keysToDelete);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function deleteFolder(prefix, connectionName) {
|
|
95
|
+
if (!prefix) throw new StorageValidationError('Prefix is required');
|
|
96
|
+
const effectivePrefix = `${ensurePrefix(trimSlashes(prefix))}/`;
|
|
97
|
+
const provider = getStorageProvider(connectionName);
|
|
98
|
+
return provider.deleteFolder(effectivePrefix);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function createFolder(prefix, connectionName, options = {}) {
|
|
102
|
+
const effectivePrefix = ensurePrefix(prefix);
|
|
103
|
+
const provider = getStorageProvider(connectionName);
|
|
104
|
+
return provider.createFolder(effectivePrefix, options);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function listfiles(prefix = '', options = {}) {
|
|
108
|
+
const effectivePrefix = ensurePrefix(prefix);
|
|
109
|
+
const provider = getStorageProvider(options.bucketName || options.connectionName);
|
|
110
|
+
return provider.listFiles(effectivePrefix, options);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function getFileMetadata(key, connectionName) {
|
|
114
|
+
if (!key) throw new StorageValidationError('Key is required');
|
|
115
|
+
const keyToCheck = ensureKeyHasAppPrefix(key);
|
|
116
|
+
const provider = getStorageProvider(connectionName);
|
|
117
|
+
return provider.getFileMetadata(keyToCheck);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function fileExists(key, connectionName) {
|
|
121
|
+
try {
|
|
122
|
+
const meta = await getFileMetadata(key, connectionName);
|
|
123
|
+
return Boolean(meta?.exists);
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function getFileSize(key, connectionName) {
|
|
130
|
+
try {
|
|
131
|
+
const meta = await getFileMetadata(key, connectionName);
|
|
132
|
+
return meta?.exists ? (meta.ContentLength ?? null) : null;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function generateSignedUrl(key, operation = 'getObject', expiresIn = 3600, connectionName) {
|
|
139
|
+
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
140
|
+
const provider = getStorageProvider(connectionName);
|
|
141
|
+
return provider.generateSignedUrl(keyToUse, operation, expiresIn);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function copyFile(sourceKey, destKey, options = {}, connectionName) {
|
|
145
|
+
const src = ensureKeyHasAppPrefix(sourceKey);
|
|
146
|
+
const dst = ensureKeyHasAppPrefix(destKey);
|
|
147
|
+
const provider = getStorageProvider(connectionName);
|
|
148
|
+
return provider.copyFile(src, dst, options);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function moveFile(sourceKey, destKey, options = {}, connectionName) {
|
|
152
|
+
const src = ensureKeyHasAppPrefix(sourceKey);
|
|
153
|
+
const dst = ensureKeyHasAppPrefix(destKey);
|
|
154
|
+
const provider = getStorageProvider(connectionName);
|
|
155
|
+
return provider.moveFile(src, dst, options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Multipart Upload Helpers (For S3 / compatible providers)
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
export async function createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}, connectionName) {
|
|
163
|
+
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
164
|
+
const provider = getStorageProvider(connectionName);
|
|
165
|
+
if (typeof provider.createMultipartUpload !== 'function') {
|
|
166
|
+
throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
|
|
167
|
+
}
|
|
168
|
+
return provider.createMultipartUpload(keyToUse, contentType, metadata);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function uploadPart(key, uploadId, partNumber, buffer, connectionName) {
|
|
172
|
+
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
173
|
+
const provider = getStorageProvider(connectionName);
|
|
174
|
+
if (typeof provider.uploadPart !== 'function') {
|
|
175
|
+
throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
|
|
176
|
+
}
|
|
177
|
+
return provider.uploadPart(keyToUse, uploadId, partNumber, buffer);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function completeMultipartUpload(key, uploadId, parts, connectionName) {
|
|
181
|
+
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
182
|
+
const provider = getStorageProvider(connectionName);
|
|
183
|
+
if (typeof provider.completeMultipartUpload !== 'function') {
|
|
184
|
+
throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
|
|
185
|
+
}
|
|
186
|
+
return provider.completeMultipartUpload(keyToUse, uploadId, parts);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function abortMultipartUpload(key, uploadId, connectionName) {
|
|
190
|
+
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
191
|
+
const provider = getStorageProvider(connectionName);
|
|
192
|
+
if (typeof provider.abortMultipartUpload !== 'function') {
|
|
193
|
+
throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
|
|
194
|
+
}
|
|
195
|
+
return provider.abortMultipartUpload(keyToUse, uploadId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function listIncompleteMultipartUploads(prefix = '', connectionName) {
|
|
199
|
+
const effectivePrefix = ensurePrefix(prefix || '');
|
|
200
|
+
const provider = getStorageProvider(connectionName);
|
|
201
|
+
if (typeof provider.listIncompleteMultipartUploads !== 'function') {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
return provider.listIncompleteMultipartUploads(effectivePrefix);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefix = '', connectionName) {
|
|
208
|
+
const provider = getStorageProvider(connectionName);
|
|
209
|
+
if (typeof provider.cleanupIncompleteMultipartUploads !== 'function') {
|
|
210
|
+
return { abortedCount: 0, uploads: [] };
|
|
211
|
+
}
|
|
212
|
+
return provider.cleanupIncompleteMultipartUploads(olderThanDays, prefix);
|
|
213
|
+
}
|
package/lib/src/utils/errors.js
CHANGED
|
@@ -1,46 +1,56 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
{ match: '
|
|
10
|
-
{ match: 'not found', status:
|
|
11
|
-
{ match: '
|
|
12
|
-
{ match:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{ match: 'not found', status:
|
|
19
|
-
{ match:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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": "
|
|
3
|
+
"version": "4.0.0",
|
|
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",
|
package/views/bucket.hbs
CHANGED
|
@@ -22,86 +22,63 @@
|
|
|
22
22
|
<style>
|
|
23
23
|
:root {
|
|
24
24
|
--primary-color-ml: #0f766e;
|
|
25
|
-
--primary-light-color-ml: #
|
|
26
|
-
--primary-dark-color-ml: #
|
|
27
|
-
--secondary-color-ml: #
|
|
28
|
-
--dark-color-ml: #
|
|
29
|
-
--light-color-ml: #
|
|
30
|
-
--gray-color-ml: #
|
|
31
|
-
--gray-light-color-ml: #
|
|
32
|
-
--danger-color-ml: #
|
|
33
|
-
--warning-color-ml: #
|
|
34
|
-
--success-color-ml: #
|
|
35
|
-
--info-color-ml: #
|
|
25
|
+
--primary-light-color-ml: #f0fdfa;
|
|
26
|
+
--primary-dark-color-ml: #115e59;
|
|
27
|
+
--secondary-color-ml: #14b8a6;
|
|
28
|
+
--dark-color-ml: #0f172a;
|
|
29
|
+
--light-color-ml: #f8fafc;
|
|
30
|
+
--gray-color-ml: #64748b;
|
|
31
|
+
--gray-light-color-ml: #e2e8f0;
|
|
32
|
+
--danger-color-ml: #ef4444;
|
|
33
|
+
--warning-color-ml: #f59e0b;
|
|
34
|
+
--success-color-ml: #10b981;
|
|
35
|
+
--info-color-ml: #0284c7;
|
|
36
36
|
|
|
37
37
|
--primary: var(--primary-color-ml);
|
|
38
38
|
--primary-lt: var(--primary-light-color-ml);
|
|
39
39
|
--primary-dk: var(--primary-dark-color-ml);
|
|
40
|
-
--primary-hover: #
|
|
40
|
+
--primary-hover: #134e4a;
|
|
41
41
|
|
|
42
42
|
--body-bg: var(--light-color-ml);
|
|
43
|
-
--bg-surface:
|
|
44
|
-
--bg-surface-secondary:
|
|
45
|
-
--bg-surface-tertiary: #
|
|
43
|
+
--bg-surface: #ffffff;
|
|
44
|
+
--bg-surface-secondary: #f8fafc;
|
|
45
|
+
--bg-surface-tertiary: #f1f5f9;
|
|
46
46
|
|
|
47
|
-
--border-color:
|
|
48
|
-
--border-color-light:
|
|
47
|
+
--border-color: #e2e8f0;
|
|
48
|
+
--border-color-light: #f1f5f9;
|
|
49
49
|
|
|
50
50
|
--text: var(--dark-color-ml);
|
|
51
|
-
--text-secondary: #
|
|
51
|
+
--text-secondary: #475569;
|
|
52
52
|
--text-muted: var(--gray-color-ml);
|
|
53
|
-
--text-disabled: #
|
|
53
|
+
--text-disabled: #94a3b8;
|
|
54
54
|
|
|
55
|
-
--font-family: "
|
|
56
|
-
--font-family-headings: "
|
|
57
|
-
--font-mono:
|
|
55
|
+
--font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif;
|
|
56
|
+
--font-family-headings: "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif;
|
|
57
|
+
--font-mono: "Segoe UI Mono", "Cascadia Code", Consolas, "Courier New", monospace;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
* {
|
|
61
61
|
margin: 0;
|
|
62
62
|
padding: 0;
|
|
63
63
|
box-sizing: border-box;
|
|
64
|
+
font-family: inherit;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
body, button, input, select, textarea {
|
|
68
|
+
font-family: var(--font-family);
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
body {
|
|
67
72
|
font-family: var(--font-family);
|
|
68
|
-
background:
|
|
69
|
-
|
|
70
|
-
radial-gradient(circle at 86% 8%, rgba(244, 114, 38, 0.12), transparent 40%),
|
|
71
|
-
linear-gradient(160deg, #eef7f2 0, #f8fbf7 52%, #eaf2ef 100%);
|
|
72
|
-
color: var(--dark-color-ml);
|
|
73
|
+
background: var(--body-bg);
|
|
74
|
+
color: var(--text);
|
|
73
75
|
display: flex;
|
|
74
76
|
min-height: 100vh;
|
|
75
77
|
overflow-x: hidden;
|
|
76
|
-
line-height: 1.
|
|
78
|
+
line-height: 1.5;
|
|
77
79
|
font-size: 14px;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
body::before,
|
|
83
|
-
body::after {
|
|
84
|
-
content: "";
|
|
85
|
-
position: fixed;
|
|
86
|
-
border-radius: 999px;
|
|
87
|
-
pointer-events: none;
|
|
88
|
-
z-index: -1;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
body::before {
|
|
92
|
-
width: 260px;
|
|
93
|
-
height: 260px;
|
|
94
|
-
left: -80px;
|
|
95
|
-
top: 18vh;
|
|
96
|
-
background: radial-gradient(circle, rgba(15, 118, 110, 0.16) 0, rgba(15, 118, 110, 0) 70%);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
body::after {
|
|
100
|
-
width: 300px;
|
|
101
|
-
height: 300px;
|
|
102
|
-
right: -120px;
|
|
103
|
-
bottom: 10vh;
|
|
104
|
-
background: radial-gradient(circle, rgba(251, 146, 60, 0.18) 0, rgba(251, 146, 60, 0) 72%);
|
|
80
|
+
-webkit-font-smoothing: antialiased;
|
|
81
|
+
-moz-osx-font-smoothing: grayscale;
|
|
105
82
|
}
|
|
106
83
|
|
|
107
84
|
.main-content {
|
|
@@ -113,33 +90,32 @@
|
|
|
113
90
|
}
|
|
114
91
|
|
|
115
92
|
.top-nav {
|
|
116
|
-
background:
|
|
117
|
-
|
|
118
|
-
padding: .75rem 1.5rem;
|
|
93
|
+
background: #ffffff;
|
|
94
|
+
padding: 0.75rem 1.5rem;
|
|
119
95
|
display: flex;
|
|
120
96
|
align-items: center;
|
|
121
97
|
justify-content: space-between;
|
|
122
|
-
|
|
98
|
+
border-bottom: 1px solid var(--border-color);
|
|
123
99
|
z-index: 90;
|
|
124
|
-
border-bottom: 1px solid rgba(255, 255, 255, .75);
|
|
125
100
|
gap: 1rem;
|
|
126
101
|
}
|
|
127
102
|
|
|
128
103
|
.logo {
|
|
129
104
|
font-family: var(--font-family-headings);
|
|
130
|
-
font-size: 1.
|
|
105
|
+
font-size: 1.25rem;
|
|
131
106
|
font-weight: 700;
|
|
132
|
-
color:
|
|
107
|
+
color: var(--primary-dk);
|
|
133
108
|
display: flex;
|
|
134
109
|
align-items: center;
|
|
135
|
-
gap: .
|
|
110
|
+
gap: 0.6rem;
|
|
136
111
|
cursor: pointer;
|
|
137
112
|
text-decoration: none;
|
|
113
|
+
letter-spacing: -0.01em;
|
|
138
114
|
}
|
|
139
115
|
|
|
140
116
|
.logo img {
|
|
141
|
-
width:
|
|
142
|
-
height:
|
|
117
|
+
width: 28px;
|
|
118
|
+
height: 28px;
|
|
143
119
|
}
|
|
144
120
|
|
|
145
121
|
.content-area {
|
|
@@ -149,7 +125,7 @@
|
|
|
149
125
|
}
|
|
150
126
|
|
|
151
127
|
.content-area::-webkit-scrollbar {
|
|
152
|
-
width:
|
|
128
|
+
width: 6px;
|
|
153
129
|
}
|
|
154
130
|
|
|
155
131
|
.content-area::-webkit-scrollbar-track {
|
|
@@ -157,20 +133,20 @@
|
|
|
157
133
|
}
|
|
158
134
|
|
|
159
135
|
.content-area::-webkit-scrollbar-thumb {
|
|
160
|
-
background:
|
|
161
|
-
border-radius:
|
|
136
|
+
background: #cbd5e1;
|
|
137
|
+
border-radius: 3px;
|
|
162
138
|
}
|
|
163
139
|
|
|
164
140
|
.content-area::-webkit-scrollbar-thumb:hover {
|
|
165
|
-
background:
|
|
141
|
+
background: #94a3b8;
|
|
166
142
|
}
|
|
167
143
|
|
|
168
144
|
@media (max-width: 768px) {
|
|
169
145
|
.top-nav {
|
|
170
|
-
padding: .
|
|
146
|
+
padding: 0.6rem 1rem;
|
|
171
147
|
}
|
|
172
148
|
.content-area {
|
|
173
|
-
padding: 0.
|
|
149
|
+
padding: 0.75rem;
|
|
174
150
|
}
|
|
175
151
|
}
|
|
176
152
|
</style>
|
|
@@ -0,0 +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>
|