mbkbucket 2.0.0 → 2.1.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 +4 -2
- package/LICENSE +161 -335
- package/README.md +5 -4
- package/index.js +9 -9
- package/lib/config/index.js +27 -11
- package/lib/config/validation.js +5 -2
- package/lib/debug.js +12 -0
- package/lib/routes/api.js +53 -63
- package/lib/routes/index.js +2 -2
- package/lib/routes/view.js +40 -20
- package/lib/s3.js +44 -29
- package/package.json +12 -6
- package/public/bucketadmin.css +124 -1
- package/public/bucketadmin.js +182 -88
- package/views/bucket.handlebars +0 -1
- package/views/bucketadmincontent.handlebars +15 -15
package/lib/s3.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { S3Client, ListObjectsV2Command, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, ListMultipartUploadsCommand } from '@aws-sdk/client-s3';
|
|
2
2
|
import dotenv from "dotenv";
|
|
3
3
|
import { mbkautheVar } from "mbkauthe";
|
|
4
|
+
import { createLogger } from "./debug.js";
|
|
4
5
|
|
|
5
6
|
dotenv.config();
|
|
7
|
+
const debugS3 = createLogger('s3');
|
|
6
8
|
|
|
7
9
|
function parseJsonEnv(envVar, fallback = {}) {
|
|
8
10
|
if (!envVar) return fallback;
|
|
@@ -18,9 +20,15 @@ function parseJsonEnv(envVar, fallback = {}) {
|
|
|
18
20
|
|
|
19
21
|
// Parse BucketConnection JSON that contains all bucket configurations
|
|
20
22
|
const allBucketConfigs = parseJsonEnv(process.env.BucketConnection, {});
|
|
23
|
+
const availableBucketNames = Object.freeze(Object.keys(allBucketConfigs || {}));
|
|
24
|
+
const hasBucketConfigs = availableBucketNames.length > 0;
|
|
25
|
+
|
|
26
|
+
function escapeRegExp(value) {
|
|
27
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
}
|
|
21
29
|
|
|
22
30
|
export function getAvailableBucketNames() {
|
|
23
|
-
return
|
|
31
|
+
return availableBucketNames;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
// Helper to get default bucket name
|
|
@@ -31,9 +39,8 @@ function getDefaultBucketName() {
|
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
// Second priority: use first bucket in BucketConnection
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return buckets[0];
|
|
42
|
+
if (availableBucketNames.length > 0) {
|
|
43
|
+
return availableBucketNames[0];
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
// Fallback: return null (will cause error in getBucketConfig)
|
|
@@ -49,12 +56,12 @@ export function resolveBucketName(bucketName) {
|
|
|
49
56
|
throw new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.');
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
if (!
|
|
59
|
+
if (!hasBucketConfigs) {
|
|
53
60
|
throw new Error('BucketConnection environment variable is not set or empty. Please configure it with your bucket connections.');
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
if (!allBucketConfigs[candidate]) {
|
|
57
|
-
throw new Error(`Bucket '${candidate}' not found in BucketConnection. Available buckets: ${
|
|
64
|
+
throw new Error(`Bucket '${candidate}' not found in BucketConnection. Available buckets: ${availableBucketNames.join(', ')}`);
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
return candidate;
|
|
@@ -69,7 +76,9 @@ export function getAppName() {
|
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
function isRootModeApp(appName) {
|
|
72
|
-
|
|
79
|
+
const app = String(appName || '').toLowerCase();
|
|
80
|
+
const isRootApp = app === 'portal' || app === 'mbkbucket';
|
|
81
|
+
return isRootApp;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
export function ensureKeyHasAppPrefix(key = '') {
|
|
@@ -96,7 +105,7 @@ export function ensureKeyHasAppPrefix(key = '') {
|
|
|
96
105
|
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
97
106
|
|
|
98
107
|
// Collapse repeated app prefixes (app/app/... -> app/...)
|
|
99
|
-
const appPrefixRegex = new RegExp(`^(?:${app}\/)+`);
|
|
108
|
+
const appPrefixRegex = new RegExp(`^(?:${escapeRegExp(app)}\/)+`);
|
|
100
109
|
if (!cleaned.startsWith(`${app}/`)) {
|
|
101
110
|
cleaned = `${app}/${cleaned}`;
|
|
102
111
|
} else {
|
|
@@ -137,7 +146,7 @@ export function ensurePrefix(prefix = '') {
|
|
|
137
146
|
|
|
138
147
|
if (p.startsWith(`${app}/`)) {
|
|
139
148
|
// collapse repeated app/ sequences
|
|
140
|
-
const appPrefixRegex = new RegExp(`^(?:${app}\/)+`);
|
|
149
|
+
const appPrefixRegex = new RegExp(`^(?:${escapeRegExp(app)}\/)+`);
|
|
141
150
|
return p.replace(appPrefixRegex, `${app}/`);
|
|
142
151
|
}
|
|
143
152
|
return `${app}/${p}`;
|
|
@@ -157,7 +166,7 @@ export function getBucketConfig(bucketName) {
|
|
|
157
166
|
if (_configCache.has(cacheKey)) return _configCache.get(cacheKey);
|
|
158
167
|
|
|
159
168
|
// Look up bucket config from BucketConnection JSON
|
|
160
|
-
if (!
|
|
169
|
+
if (!hasBucketConfigs) {
|
|
161
170
|
console.error(`[mbkbucket] ❌ BucketConnection is not configured or failed to parse`);
|
|
162
171
|
console.error(`[mbkbucket] Please set BucketConnection in your .env file with this format:`);
|
|
163
172
|
console.error(`[mbkbucket] BucketConnection={"r2":{"BUCKET_NAME":"...","ACCESS_KEY_ID":"...","SECRET_ACCESS_KEY":"...","ENDPOINT":"https://..."}}`);
|
|
@@ -170,7 +179,7 @@ export function getBucketConfig(bucketName) {
|
|
|
170
179
|
const cfg = allBucketConfigs[bucketName];
|
|
171
180
|
|
|
172
181
|
if (!cfg) {
|
|
173
|
-
throw new Error(`Bucket '${bucketName}' not found in BucketConnection. Available buckets: ${
|
|
182
|
+
throw new Error(`Bucket '${bucketName}' not found in BucketConnection. Available buckets: ${availableBucketNames.join(', ')}`);
|
|
174
183
|
}
|
|
175
184
|
|
|
176
185
|
_configCache.set(cacheKey, cfg);
|
|
@@ -250,10 +259,10 @@ async function checkHealth() {
|
|
|
250
259
|
// Quick diagnostics at startup
|
|
251
260
|
(async () => {
|
|
252
261
|
try {
|
|
253
|
-
|
|
262
|
+
debugS3('mbkautheVar snapshot: %O', { APP_NAME: mbkautheVar?.APP_NAME, bucket: mbkautheVar?.bucket });
|
|
254
263
|
const health = await checkHealth();
|
|
255
264
|
if (health.status === 'healthy') {
|
|
256
|
-
|
|
265
|
+
debugS3('Connected to bucket: %s (%sms)', health.bucket, health.responseTime);
|
|
257
266
|
} else {
|
|
258
267
|
console.error(`[mbkbucket] S3 connection failed:`, health.error);
|
|
259
268
|
}
|
|
@@ -286,11 +295,12 @@ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
|
286
295
|
const config = getBucketConfig(bucketName);
|
|
287
296
|
const bucket = config.BUCKET_NAME;
|
|
288
297
|
|
|
289
|
-
|
|
298
|
+
debugS3('Uploading file to bucket=%s key=%s', bucket, keyToUpload);
|
|
290
299
|
|
|
291
300
|
// Add default metadata (supports metadata)
|
|
301
|
+
const uploadedAt = new Date().toISOString();
|
|
292
302
|
const defaultMetadata = {
|
|
293
|
-
'uploaded-at':
|
|
303
|
+
'uploaded-at': uploadedAt,
|
|
294
304
|
'file-size': fileBuffer.length.toString(),
|
|
295
305
|
'upload-source': 'web-portal',
|
|
296
306
|
...metadata
|
|
@@ -321,7 +331,7 @@ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
|
321
331
|
fileSize: fileBuffer.length,
|
|
322
332
|
key: keyToUpload,
|
|
323
333
|
contentType,
|
|
324
|
-
uploadedAt
|
|
334
|
+
uploadedAt
|
|
325
335
|
};
|
|
326
336
|
} catch (error) {
|
|
327
337
|
console.error(`Upload failed for key ${key}:`, error);
|
|
@@ -361,7 +371,7 @@ export async function downloadFile(key, options = {}) {
|
|
|
361
371
|
|
|
362
372
|
// Log range requests for debugging
|
|
363
373
|
if (range) {
|
|
364
|
-
|
|
374
|
+
debugS3('Range request for %s: %s', keyToDownload, range);
|
|
365
375
|
}
|
|
366
376
|
|
|
367
377
|
const command = new GetObjectCommand({
|
|
@@ -374,9 +384,7 @@ export async function downloadFile(key, options = {}) {
|
|
|
374
384
|
...(responseContentType && { ResponseContentType: responseContentType }),
|
|
375
385
|
});
|
|
376
386
|
|
|
377
|
-
const startTime = Date.now();
|
|
378
387
|
const result = await client.send(command);
|
|
379
|
-
const downloadTime = Date.now() - startTime;
|
|
380
388
|
|
|
381
389
|
return {
|
|
382
390
|
...result,
|
|
@@ -446,6 +454,8 @@ export async function deleteFiles(keys, bucketName) {
|
|
|
446
454
|
// S3 allows max 1000 objects per delete request
|
|
447
455
|
const maxBatchSize = 1000;
|
|
448
456
|
const results = [];
|
|
457
|
+
let deletedCount = 0;
|
|
458
|
+
const errors = [];
|
|
449
459
|
|
|
450
460
|
for (let i = 0; i < keys.length; i += maxBatchSize) {
|
|
451
461
|
const batch = keys.slice(i, i + maxBatchSize).map(k => ({ Key: ensureKeyHasAppPrefix(k) }));
|
|
@@ -460,12 +470,14 @@ export async function deleteFiles(keys, bucketName) {
|
|
|
460
470
|
|
|
461
471
|
const result = await client.send(command);
|
|
462
472
|
results.push(result);
|
|
473
|
+
deletedCount += result.Deleted?.length || 0;
|
|
474
|
+
if (result.Errors?.length) errors.push(...result.Errors);
|
|
463
475
|
}
|
|
464
476
|
|
|
465
477
|
return {
|
|
466
478
|
results,
|
|
467
|
-
deletedCount
|
|
468
|
-
errors
|
|
479
|
+
deletedCount,
|
|
480
|
+
errors,
|
|
469
481
|
deletedAt: new Date().toISOString()
|
|
470
482
|
};
|
|
471
483
|
} catch (error) {
|
|
@@ -600,19 +612,21 @@ export async function getFileMetadata(key, bucketName) {
|
|
|
600
612
|
});
|
|
601
613
|
|
|
602
614
|
const result = await client.send(command);
|
|
615
|
+
const queriedAt = new Date().toISOString();
|
|
603
616
|
|
|
604
617
|
return {
|
|
605
618
|
...result,
|
|
606
619
|
key: keyToCheck,
|
|
607
620
|
exists: true,
|
|
608
|
-
queriedAt
|
|
621
|
+
queriedAt
|
|
609
622
|
};
|
|
610
623
|
} catch (error) {
|
|
624
|
+
const queriedAt = new Date().toISOString();
|
|
611
625
|
if (error.name === 'NotFound' || error.name === 'NoSuchKey') {
|
|
612
626
|
return {
|
|
613
627
|
key,
|
|
614
628
|
exists: false,
|
|
615
|
-
queriedAt
|
|
629
|
+
queriedAt
|
|
616
630
|
};
|
|
617
631
|
}
|
|
618
632
|
|
|
@@ -655,13 +669,14 @@ export async function createMultipartUpload(key, contentType = 'application/octe
|
|
|
655
669
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
656
670
|
const cfg = getBucketConfig(bucketName);
|
|
657
671
|
const client = getBucketClient(bucketName);
|
|
672
|
+
const uploadedAt = new Date().toISOString();
|
|
658
673
|
const command = new CreateMultipartUploadCommand({
|
|
659
674
|
Bucket: cfg.BUCKET_NAME,
|
|
660
675
|
Key: keyToUse,
|
|
661
676
|
ContentType: contentType,
|
|
662
677
|
Metadata: {
|
|
663
678
|
'upload-source': 'web-portal',
|
|
664
|
-
'uploaded-at':
|
|
679
|
+
'uploaded-at': uploadedAt,
|
|
665
680
|
...metadata
|
|
666
681
|
}
|
|
667
682
|
});
|
|
@@ -803,7 +818,7 @@ export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefi
|
|
|
803
818
|
const uploads = await listIncompleteMultipartUploads(prefix, bucketName);
|
|
804
819
|
|
|
805
820
|
if (!uploads.length) {
|
|
806
|
-
|
|
821
|
+
debugS3('No incomplete multipart uploads found');
|
|
807
822
|
return { abortedCount: 0, uploads: [] };
|
|
808
823
|
}
|
|
809
824
|
|
|
@@ -815,22 +830,22 @@ export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefi
|
|
|
815
830
|
});
|
|
816
831
|
|
|
817
832
|
if (!uploadsToAbort.length) {
|
|
818
|
-
|
|
833
|
+
debugS3('No incomplete uploads older than %s days', olderThanDays);
|
|
819
834
|
return { abortedCount: 0, uploads: [] };
|
|
820
835
|
}
|
|
821
836
|
|
|
822
|
-
|
|
837
|
+
debugS3('Found %s incomplete uploads to clean up', uploadsToAbort.length);
|
|
823
838
|
|
|
824
839
|
const aborted = [];
|
|
825
840
|
for (const upload of uploadsToAbort) {
|
|
826
841
|
try {
|
|
827
|
-
await abortMultipartUpload(upload.Key, upload.UploadId);
|
|
842
|
+
await abortMultipartUpload(upload.Key, upload.UploadId, bucketName);
|
|
828
843
|
aborted.push({
|
|
829
844
|
key: upload.Key,
|
|
830
845
|
uploadId: upload.UploadId,
|
|
831
846
|
initiated: upload.Initiated
|
|
832
847
|
});
|
|
833
|
-
|
|
848
|
+
debugS3('Aborted incomplete upload: %s (initiated: %s)', upload.Key, upload.Initiated);
|
|
834
849
|
} catch (err) {
|
|
835
850
|
console.error(`[mbkbucket] Failed to abort upload ${upload.Key}:`, err);
|
|
836
851
|
}
|
package/package.json
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkbucket",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Flexible S3/R2 bucket management library and Express router for mbktech.org applications, providing upload, download, listing, and multipart utilities with built-in configuration validation and health checks",
|
|
5
5
|
"author": "Muhammad Bin Khalid",
|
|
6
6
|
"homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
|
|
7
|
-
"license": "
|
|
7
|
+
"license": "LGPL-3.0-only",
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"types": "index.d.ts",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"dev": "cross-env
|
|
12
|
+
"dev": "cross-env NODE_ENV=dev nodemon index.js",
|
|
13
13
|
"test": "node --test"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@aws-sdk/client-s3": "^3.968.0",
|
|
17
17
|
"@aws-sdk/s3-request-presigner": "^3.1000.0",
|
|
18
18
|
"compression": "^1.8.0",
|
|
19
|
+
"debug": "^2.6.9",
|
|
19
20
|
"dotenv": "^16.6.1",
|
|
20
21
|
"express": "^4.22.1",
|
|
21
22
|
"express-handlebars": "^8.0.3",
|
|
22
23
|
"express-rate-limit": "^7.0.1",
|
|
23
|
-
"mbkauthe": "
|
|
24
|
+
"mbkauthe": "5.1.1",
|
|
24
25
|
"multer": "^2.0.2",
|
|
25
26
|
"pg": "^8.16.3"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
|
-
"nodemon": "^
|
|
29
|
+
"nodemon": "^3.1.14",
|
|
29
30
|
"cross-env": "^7.0.3"
|
|
31
|
+
},
|
|
32
|
+
"overrides": {
|
|
33
|
+
"csurf": {
|
|
34
|
+
"cookie": "^0.7.0"
|
|
35
|
+
}
|
|
30
36
|
},
|
|
31
37
|
"repository": {
|
|
32
38
|
"type": "git",
|
|
@@ -49,4 +55,4 @@
|
|
|
49
55
|
"mbkauthe",
|
|
50
56
|
"mbktech.org"
|
|
51
57
|
]
|
|
52
|
-
}
|
|
58
|
+
}
|
package/public/bucketadmin.css
CHANGED
|
@@ -731,9 +731,18 @@
|
|
|
731
731
|
align-items: center;
|
|
732
732
|
gap: 0.5rem;
|
|
733
733
|
font-size: 0.9rem;
|
|
734
|
+
min-width: 0;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.breadcrumb-nav .breadcrumb-item::before {
|
|
738
|
+
display: none !important;
|
|
739
|
+
content: none !important;
|
|
734
740
|
}
|
|
735
741
|
|
|
736
742
|
.breadcrumb-link {
|
|
743
|
+
background: transparent;
|
|
744
|
+
border: 0;
|
|
745
|
+
padding: 0;
|
|
737
746
|
color: var(--primary-color-ml);
|
|
738
747
|
cursor: pointer;
|
|
739
748
|
transition: all 0.2s ease;
|
|
@@ -754,6 +763,8 @@
|
|
|
754
763
|
.breadcrumb-current {
|
|
755
764
|
color: var(--dark-color-ml);
|
|
756
765
|
font-weight: 600;
|
|
766
|
+
min-width: 0;
|
|
767
|
+
overflow-wrap: anywhere;
|
|
757
768
|
}
|
|
758
769
|
|
|
759
770
|
.file-icon {
|
|
@@ -2048,6 +2059,19 @@
|
|
|
2048
2059
|
.breadcrumb-nav {
|
|
2049
2060
|
padding: 0.5rem 1rem;
|
|
2050
2061
|
font-size: 0.85rem;
|
|
2062
|
+
gap: 0.35rem;
|
|
2063
|
+
overflow: visible;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
.breadcrumb-item {
|
|
2067
|
+
max-width: 100%;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
.breadcrumb-current,
|
|
2071
|
+
.breadcrumb-link {
|
|
2072
|
+
max-width: calc(100vw - 4rem);
|
|
2073
|
+
white-space: normal;
|
|
2074
|
+
overflow-wrap: anywhere;
|
|
2051
2075
|
}
|
|
2052
2076
|
|
|
2053
2077
|
.file-size {
|
|
@@ -2172,4 +2196,103 @@
|
|
|
2172
2196
|
.dtm {
|
|
2173
2197
|
display: block;
|
|
2174
2198
|
}
|
|
2175
|
-
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
.table-cell-center {
|
|
2202
|
+
text-align: center;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
.table-select-col {
|
|
2206
|
+
width: 48px;
|
|
2207
|
+
text-align: center;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
.file-name-cell,
|
|
2211
|
+
.file-name-heading {
|
|
2212
|
+
padding-left: 1.5rem;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
.file-row-main {
|
|
2216
|
+
display: flex;
|
|
2217
|
+
align-items: center;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
.folder-name {
|
|
2221
|
+
font-weight: 700;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
.folder-leading-icon {
|
|
2225
|
+
margin-right: 0.5rem;
|
|
2226
|
+
color: #f59e0b;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
.muted-cell-value {
|
|
2230
|
+
color: var(--gray-color-ml);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
.path-display-icon {
|
|
2234
|
+
margin-right: 4px;
|
|
2235
|
+
opacity: 0.5;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
.copy-btn {
|
|
2239
|
+
color: #fff;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
.view-btn.is-disabled {
|
|
2243
|
+
opacity: 0.5;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
.sortable-heading {
|
|
2247
|
+
cursor: pointer;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
.bulk-actions-controls {
|
|
2251
|
+
float: right;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
.toast-icon {
|
|
2255
|
+
font-size: 1.25rem;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
.toast-message {
|
|
2259
|
+
flex: 1;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
.empty-state-error-icon {
|
|
2263
|
+
color: var(--danger-color-ml);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
.empty-state-folder-icon {
|
|
2267
|
+
font-size: 4rem;
|
|
2268
|
+
color: #cbd5e0;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
.upload-progress-cell {
|
|
2272
|
+
min-width: 220px;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
.upload-progress-track {
|
|
2276
|
+
height: 8px;
|
|
2277
|
+
margin-bottom: 6px;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
.upload-progress-bar {
|
|
2281
|
+
height: 8px;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
.progress-bar-empty {
|
|
2285
|
+
width: 0%;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
.is-hidden {
|
|
2289
|
+
display: none !important;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
.bucket-switcher-status:not(.is-hidden) {
|
|
2293
|
+
display: inline;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
.file-list-card-body {
|
|
2297
|
+
padding: 0;
|
|
2298
|
+
}
|