storage-explorer 0.1.0 → 1.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/README.md +14 -0
- package/bunfig.toml +2 -0
- package/cli.js +98 -0
- package/dist/chunk-fa0pf3pw.js +11 -0
- package/dist/chunk-fa0pf3pw.js.map +24 -0
- package/dist/chunk-veptbhs8.css +1 -0
- package/dist/index.html +1 -1
- package/package.json +16 -2
- package/src/App.tsx +390 -0
- package/src/features/buckets/BucketPanel.tsx +93 -0
- package/src/features/objects/ObjectExplorer.tsx +129 -0
- package/src/features/objects/PathBreadcrumb.tsx +50 -0
- package/src/features/profiles/ProfileSidebar.tsx +167 -0
- package/src/frontend.tsx +26 -0
- package/{dist/chunk-js4y3bna.css → src/index.css} +134 -111
- package/src/index.html +13 -0
- package/src/index.ts +22 -0
- package/src/logo.svg +1 -0
- package/src/server/http/response.ts +12 -0
- package/src/server/routes/s3Routes.ts +17 -0
- package/src/server/s3/client.ts +14 -0
- package/src/server/s3/handlers.ts +96 -0
- package/src/server/s3/mappers.ts +73 -0
- package/src/server/s3/types.ts +15 -0
- package/src/server/s3/validate.ts +103 -0
- package/src/shared/api/s3Api.ts +56 -0
- package/src/shared/hooks/useProfilesStorage.ts +175 -0
- package/src/shared/types/s3.ts +42 -0
- package/dist/chunk-vtsn1g38.js +0 -1022
- package/dist/index-3xfxtfws.js +0 -238
- package/dist/index-3xfxtfws.js.map +0 -24
- package/dist/index-67w6q0ny.css +0 -1
- package/dist/index-9t8tyk25.js +0 -238
- package/dist/index-9t8tyk25.js.map +0 -24
- package/dist/index-b7b12360.css +0 -1
- package/dist/index-bz8f0q85.js +0 -238
- package/dist/index-bz8f0q85.js.map +0 -18
- package/dist/index-vw9287sb.js +0 -238
- package/dist/index-vw9287sb.js.map +0 -18
- package/dist/index-xde44bqw.css +0 -1
- package/dist/index.js +0 -29485
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
/* src/index.css */
|
|
2
1
|
:root {
|
|
2
|
+
font-family: "Segoe UI", "Avenir Next", "Helvetica Neue", sans-serif;
|
|
3
|
+
line-height: 1.4;
|
|
4
|
+
font-weight: 400;
|
|
3
5
|
color: #152234;
|
|
6
|
+
background: #f6f8fb;
|
|
4
7
|
--bg-top: #f9f3ea;
|
|
5
8
|
--bg-bottom: #e8eff8;
|
|
6
9
|
--panel: #ffffffd8;
|
|
@@ -12,10 +15,6 @@
|
|
|
12
15
|
--danger: #b9414f;
|
|
13
16
|
--ok: #1f8b4c;
|
|
14
17
|
--radius: 14px;
|
|
15
|
-
background: #f6f8fb;
|
|
16
|
-
font-family: Segoe UI, Avenir Next, Helvetica Neue, sans-serif;
|
|
17
|
-
font-weight: 400;
|
|
18
|
-
line-height: 1.4;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
* {
|
|
@@ -23,20 +22,23 @@
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
body {
|
|
26
|
-
|
|
27
|
-
background: linear-gradient(168deg, var(--bg-top), var(--bg-bottom));
|
|
25
|
+
margin: 0;
|
|
28
26
|
min-width: 320px;
|
|
29
27
|
min-height: 100vh;
|
|
30
|
-
|
|
28
|
+
color: var(--text);
|
|
29
|
+
background: linear-gradient(168deg, var(--bg-top), var(--bg-bottom));
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
body
|
|
32
|
+
body::before {
|
|
34
33
|
content: "";
|
|
35
34
|
position: fixed;
|
|
35
|
+
inset: 0;
|
|
36
36
|
z-index: -1;
|
|
37
37
|
pointer-events: none;
|
|
38
|
-
background:
|
|
39
|
-
|
|
38
|
+
background:
|
|
39
|
+
radial-gradient(circle at 20% 18%, #ffffffaa 0, transparent 36%),
|
|
40
|
+
radial-gradient(circle at 85% 12%, #d9e9fa88 0, transparent 28%),
|
|
41
|
+
radial-gradient(circle at 70% 80%, #f8e8d6aa 0, transparent 35%);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
#root {
|
|
@@ -45,17 +47,17 @@ body:before {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
.app-shell {
|
|
50
|
+
width: min(1320px, 100% - 2rem);
|
|
51
|
+
margin: 1rem auto;
|
|
48
52
|
display: grid;
|
|
49
53
|
grid-template-columns: 360px minmax(0, 1fr);
|
|
50
54
|
gap: 1rem;
|
|
51
|
-
width: min(1320px, 100% - 2rem);
|
|
52
|
-
margin: 1rem auto;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
.main-column {
|
|
56
58
|
display: grid;
|
|
57
|
-
align-content: start;
|
|
58
59
|
gap: 1rem;
|
|
60
|
+
align-content: start;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
.panel {
|
|
@@ -68,56 +70,61 @@ body:before {
|
|
|
68
70
|
|
|
69
71
|
.panel-step {
|
|
70
72
|
display: inline-block;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
margin-bottom: 0.6rem;
|
|
74
|
+
padding: 0.2rem 0.55rem;
|
|
73
75
|
border-radius: 999px;
|
|
74
|
-
|
|
75
|
-
padding: .2rem .55rem;
|
|
76
|
-
font-size: .73rem;
|
|
76
|
+
font-size: 0.73rem;
|
|
77
77
|
font-weight: 600;
|
|
78
|
+
color: var(--accent);
|
|
79
|
+
background: var(--accent-soft);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
.profile-panel {
|
|
81
|
-
position: sticky;
|
|
82
|
-
overflow: auto;
|
|
83
|
-
max-height: calc(100vh - 2rem);
|
|
84
83
|
padding: 1rem;
|
|
84
|
+
position: sticky;
|
|
85
85
|
top: 1rem;
|
|
86
|
+
max-height: calc(100vh - 2rem);
|
|
87
|
+
overflow: auto;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
.explorer-panel,
|
|
90
|
+
.explorer-panel,
|
|
91
|
+
.object-panel {
|
|
89
92
|
padding: 1rem;
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
.panel-header h1,
|
|
93
|
-
|
|
95
|
+
.panel-header h1,
|
|
96
|
+
.explorer-header h2,
|
|
97
|
+
.objects-header h3 {
|
|
94
98
|
margin: 0;
|
|
99
|
+
letter-spacing: 0.01em;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
.panel-header p,
|
|
102
|
+
.panel-header p,
|
|
103
|
+
.explorer-header p,
|
|
104
|
+
.objects-header p {
|
|
105
|
+
margin: 0.35rem 0 0;
|
|
98
106
|
color: var(--muted);
|
|
99
|
-
|
|
100
|
-
font-size: .92rem;
|
|
107
|
+
font-size: 0.92rem;
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
.profiles-toolbar {
|
|
111
|
+
margin-top: 1rem;
|
|
104
112
|
display: flex;
|
|
113
|
+
align-items: center;
|
|
105
114
|
justify-content: space-between;
|
|
106
|
-
align-items: center;
|
|
107
|
-
margin-top: 1rem;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
.profiles-list {
|
|
118
|
+
margin-top: 0.65rem;
|
|
111
119
|
display: grid;
|
|
112
|
-
gap: .5rem;
|
|
113
|
-
margin-top: .65rem;
|
|
120
|
+
gap: 0.5rem;
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
.profile-card {
|
|
117
124
|
border: 1px solid var(--panel-border);
|
|
118
|
-
overflow: hidden;
|
|
119
|
-
background: #fff;
|
|
120
125
|
border-radius: 10px;
|
|
126
|
+
background: #fff;
|
|
127
|
+
overflow: hidden;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
.profile-card.active {
|
|
@@ -126,14 +133,14 @@ body:before {
|
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
.profile-select {
|
|
136
|
+
width: 100%;
|
|
137
|
+
border: 0;
|
|
138
|
+
background: transparent;
|
|
129
139
|
text-align: left;
|
|
140
|
+
padding: 0.65rem 0.75rem;
|
|
130
141
|
display: grid;
|
|
142
|
+
gap: 0.25rem;
|
|
131
143
|
cursor: pointer;
|
|
132
|
-
background: none;
|
|
133
|
-
border: 0;
|
|
134
|
-
gap: .25rem;
|
|
135
|
-
width: 100%;
|
|
136
|
-
padding: .65rem .75rem;
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
.profile-name {
|
|
@@ -142,46 +149,49 @@ body:before {
|
|
|
142
149
|
|
|
143
150
|
.profile-endpoint {
|
|
144
151
|
color: var(--muted);
|
|
152
|
+
font-size: 0.84rem;
|
|
145
153
|
overflow-wrap: anywhere;
|
|
146
|
-
font-size: .84rem;
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
.delete-button {
|
|
157
|
+
width: 100%;
|
|
150
158
|
border: 0;
|
|
151
159
|
border-top: 1px solid var(--panel-border);
|
|
160
|
+
background: #fff;
|
|
152
161
|
color: var(--danger);
|
|
162
|
+
padding: 0.45rem 0.75rem;
|
|
153
163
|
cursor: pointer;
|
|
154
|
-
background: #fff;
|
|
155
|
-
width: 100%;
|
|
156
|
-
padding: .45rem .75rem;
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
.profile-form {
|
|
160
|
-
display: grid;
|
|
161
|
-
gap: .7rem;
|
|
162
167
|
margin-top: 1rem;
|
|
168
|
+
display: grid;
|
|
169
|
+
gap: 0.7rem;
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
.profile-form label {
|
|
166
173
|
display: grid;
|
|
167
|
-
gap: .35rem;
|
|
168
|
-
font-size: .9rem;
|
|
174
|
+
gap: 0.35rem;
|
|
175
|
+
font-size: 0.9rem;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
|
-
input,
|
|
178
|
+
input,
|
|
179
|
+
button {
|
|
172
180
|
font: inherit;
|
|
173
181
|
}
|
|
174
182
|
|
|
175
|
-
.profile-form input,
|
|
183
|
+
.profile-form input,
|
|
184
|
+
.manual-bucket-row input {
|
|
176
185
|
border: 1px solid var(--panel-border);
|
|
186
|
+
border-radius: 9px;
|
|
187
|
+
padding: 0.55rem 0.65rem;
|
|
177
188
|
color: var(--text);
|
|
178
189
|
background: #fff;
|
|
179
|
-
border-radius: 9px;
|
|
180
190
|
flex: 1;
|
|
181
|
-
padding: .55rem .65rem;
|
|
182
191
|
}
|
|
183
192
|
|
|
184
|
-
.profile-form input:focus,
|
|
193
|
+
.profile-form input:focus,
|
|
194
|
+
.manual-bucket-row input:focus {
|
|
185
195
|
outline: 2px solid #0000;
|
|
186
196
|
border-color: var(--accent);
|
|
187
197
|
box-shadow: 0 0 0 2px #146c9430;
|
|
@@ -189,7 +199,7 @@ input, button {
|
|
|
189
199
|
|
|
190
200
|
.secret-row {
|
|
191
201
|
display: flex;
|
|
192
|
-
gap: .5rem;
|
|
202
|
+
gap: 0.5rem;
|
|
193
203
|
}
|
|
194
204
|
|
|
195
205
|
.secret-row input {
|
|
@@ -198,7 +208,7 @@ input, button {
|
|
|
198
208
|
|
|
199
209
|
.checkbox-row {
|
|
200
210
|
grid-template-columns: auto 1fr;
|
|
201
|
-
align-items:
|
|
211
|
+
align-items: center;
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
.checkbox-row input {
|
|
@@ -208,15 +218,17 @@ input, button {
|
|
|
208
218
|
|
|
209
219
|
.form-actions {
|
|
210
220
|
display: flex;
|
|
221
|
+
gap: 0.55rem;
|
|
211
222
|
flex-wrap: wrap;
|
|
212
|
-
gap: .55rem;
|
|
213
223
|
}
|
|
214
224
|
|
|
215
|
-
.primary-button,
|
|
216
|
-
|
|
225
|
+
.primary-button,
|
|
226
|
+
.secondary-button,
|
|
227
|
+
.ghost-button {
|
|
217
228
|
border-radius: 9px;
|
|
218
|
-
padding: .55rem .85rem;
|
|
219
|
-
|
|
229
|
+
padding: 0.55rem 0.85rem;
|
|
230
|
+
cursor: pointer;
|
|
231
|
+
transition: 0.15s ease;
|
|
220
232
|
}
|
|
221
233
|
|
|
222
234
|
.primary-button {
|
|
@@ -231,28 +243,35 @@ input, button {
|
|
|
231
243
|
|
|
232
244
|
.secondary-button {
|
|
233
245
|
border: 1px solid var(--panel-border);
|
|
234
|
-
color: var(--text);
|
|
235
246
|
background: #fff;
|
|
247
|
+
color: var(--text);
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
.ghost-button {
|
|
239
251
|
border: 1px solid var(--panel-border);
|
|
240
|
-
color: var(--muted);
|
|
241
252
|
background: #fff;
|
|
253
|
+
color: var(--muted);
|
|
242
254
|
}
|
|
243
255
|
|
|
244
|
-
.secondary-button:hover,
|
|
256
|
+
.secondary-button:hover,
|
|
257
|
+
.ghost-button:hover,
|
|
258
|
+
.delete-button:hover,
|
|
259
|
+
.bucket-item:hover,
|
|
260
|
+
.object-row.folder:hover,
|
|
261
|
+
.breadcrumb-link:hover {
|
|
245
262
|
background: var(--accent-soft);
|
|
246
263
|
}
|
|
247
264
|
|
|
248
265
|
button:disabled {
|
|
249
|
-
opacity: .58;
|
|
266
|
+
opacity: 0.58;
|
|
250
267
|
cursor: not-allowed;
|
|
251
268
|
}
|
|
252
269
|
|
|
253
|
-
.status-ok,
|
|
254
|
-
|
|
255
|
-
|
|
270
|
+
.status-ok,
|
|
271
|
+
.status-error,
|
|
272
|
+
.empty-copy {
|
|
273
|
+
margin: 0.75rem 0 0;
|
|
274
|
+
font-size: 0.88rem;
|
|
256
275
|
}
|
|
257
276
|
|
|
258
277
|
.status-ok {
|
|
@@ -263,59 +282,62 @@ button:disabled {
|
|
|
263
282
|
color: var(--danger);
|
|
264
283
|
}
|
|
265
284
|
|
|
266
|
-
.empty-copy,
|
|
285
|
+
.empty-copy,
|
|
286
|
+
.muted,
|
|
287
|
+
.helper-copy {
|
|
267
288
|
color: var(--muted);
|
|
268
289
|
}
|
|
269
290
|
|
|
270
|
-
.explorer-header,
|
|
291
|
+
.explorer-header,
|
|
292
|
+
.objects-header {
|
|
271
293
|
display: flex;
|
|
272
294
|
justify-content: space-between;
|
|
273
|
-
align-items:
|
|
295
|
+
align-items: flex-start;
|
|
274
296
|
gap: 1rem;
|
|
275
297
|
}
|
|
276
298
|
|
|
277
299
|
.manual-bucket-form {
|
|
300
|
+
margin-top: 0.9rem;
|
|
278
301
|
display: grid;
|
|
302
|
+
gap: 0.45rem;
|
|
279
303
|
border: 1px solid var(--panel-border);
|
|
280
|
-
background: #fff;
|
|
281
304
|
border-radius: 10px;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
padding: .7rem;
|
|
305
|
+
background: #fff;
|
|
306
|
+
padding: 0.7rem;
|
|
285
307
|
}
|
|
286
308
|
|
|
287
309
|
.manual-bucket-form label {
|
|
310
|
+
font-size: 0.88rem;
|
|
288
311
|
color: var(--muted);
|
|
289
|
-
font-size: .88rem;
|
|
290
312
|
}
|
|
291
313
|
|
|
292
314
|
.manual-bucket-row {
|
|
293
315
|
display: flex;
|
|
294
|
-
|
|
295
|
-
|
|
316
|
+
gap: 0.5rem;
|
|
317
|
+
align-items: center;
|
|
296
318
|
}
|
|
297
319
|
|
|
298
320
|
.helper-copy {
|
|
299
321
|
margin: 0;
|
|
300
|
-
font-size: .82rem;
|
|
322
|
+
font-size: 0.82rem;
|
|
301
323
|
}
|
|
302
324
|
|
|
303
325
|
.buckets-grid {
|
|
326
|
+
margin-top: 0.8rem;
|
|
304
327
|
display: grid;
|
|
305
328
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
306
|
-
gap: .5rem;
|
|
307
|
-
margin-top: .8rem;
|
|
329
|
+
gap: 0.5rem;
|
|
308
330
|
}
|
|
309
331
|
|
|
310
332
|
.bucket-item {
|
|
311
333
|
border: 1px solid var(--panel-border);
|
|
334
|
+
border-radius: 10px;
|
|
335
|
+
background: #fff;
|
|
336
|
+
padding: 0.7rem;
|
|
312
337
|
text-align: left;
|
|
313
338
|
display: grid;
|
|
339
|
+
gap: 0.2rem;
|
|
314
340
|
cursor: pointer;
|
|
315
|
-
background: #fff;
|
|
316
|
-
border-radius: 10px;
|
|
317
|
-
gap: .2rem;
|
|
318
|
-
padding: .7rem;
|
|
319
341
|
}
|
|
320
342
|
|
|
321
343
|
.bucket-item.active {
|
|
@@ -329,21 +351,21 @@ button:disabled {
|
|
|
329
351
|
|
|
330
352
|
.objects-actions {
|
|
331
353
|
display: flex;
|
|
354
|
+
gap: 0.5rem;
|
|
332
355
|
flex-wrap: wrap;
|
|
333
|
-
gap: .5rem;
|
|
334
356
|
}
|
|
335
357
|
|
|
336
358
|
.path-breadcrumb {
|
|
359
|
+
margin-top: 0.8rem;
|
|
337
360
|
border: 1px solid var(--panel-border);
|
|
338
|
-
display: flex;
|
|
339
|
-
background: #fff;
|
|
340
361
|
border-radius: 10px;
|
|
362
|
+
background: #fff;
|
|
363
|
+
padding: 0.55rem 0.7rem;
|
|
364
|
+
display: flex;
|
|
341
365
|
flex-wrap: wrap;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
padding: .55rem .7rem;
|
|
346
|
-
font-size: .88rem;
|
|
366
|
+
gap: 0.1rem;
|
|
367
|
+
align-items: center;
|
|
368
|
+
font-size: 0.88rem;
|
|
347
369
|
}
|
|
348
370
|
|
|
349
371
|
.path-breadcrumb.muted {
|
|
@@ -351,46 +373,46 @@ button:disabled {
|
|
|
351
373
|
}
|
|
352
374
|
|
|
353
375
|
.breadcrumb-link {
|
|
354
|
-
cursor: pointer;
|
|
355
|
-
color: var(--text);
|
|
356
|
-
background: none;
|
|
357
376
|
border: 0;
|
|
358
377
|
border-radius: 6px;
|
|
359
|
-
padding: .2rem .4rem;
|
|
378
|
+
padding: 0.2rem 0.4rem;
|
|
379
|
+
background: transparent;
|
|
380
|
+
cursor: pointer;
|
|
381
|
+
color: var(--text);
|
|
360
382
|
}
|
|
361
383
|
|
|
362
384
|
.breadcrumb-segment {
|
|
363
385
|
display: inline-flex;
|
|
364
|
-
align-items:
|
|
386
|
+
align-items: center;
|
|
365
387
|
}
|
|
366
388
|
|
|
367
389
|
.breadcrumb-separator {
|
|
368
390
|
color: var(--muted);
|
|
369
|
-
margin: 0 .05rem;
|
|
391
|
+
margin: 0 0.05rem;
|
|
370
392
|
}
|
|
371
393
|
|
|
372
394
|
.objects-list {
|
|
395
|
+
margin-top: 0.8rem;
|
|
373
396
|
border: 1px solid var(--panel-border);
|
|
374
|
-
overflow: auto;
|
|
375
|
-
background: #fff;
|
|
376
397
|
border-radius: 10px;
|
|
398
|
+
background: #fff;
|
|
399
|
+
padding: 0.5rem;
|
|
377
400
|
min-height: 320px;
|
|
378
|
-
|
|
379
|
-
padding: .5rem;
|
|
401
|
+
overflow: auto;
|
|
380
402
|
}
|
|
381
403
|
|
|
382
404
|
.object-row {
|
|
405
|
+
border: 0;
|
|
406
|
+
border-bottom: 1px solid #e7edf6;
|
|
407
|
+
width: 100%;
|
|
408
|
+
background: #fff;
|
|
383
409
|
color: var(--text);
|
|
384
410
|
display: grid;
|
|
385
411
|
grid-template-columns: minmax(0, 1fr) 140px 210px;
|
|
412
|
+
gap: 0.8rem;
|
|
413
|
+
padding: 0.65rem;
|
|
386
414
|
text-align: left;
|
|
387
|
-
|
|
388
|
-
border: 0;
|
|
389
|
-
border-bottom: 1px solid #e7edf6;
|
|
390
|
-
align-items: center;
|
|
391
|
-
gap: .8rem;
|
|
392
|
-
width: 100%;
|
|
393
|
-
padding: .65rem;
|
|
415
|
+
align-items: center;
|
|
394
416
|
}
|
|
395
417
|
|
|
396
418
|
.object-row:last-child {
|
|
@@ -407,12 +429,12 @@ button:disabled {
|
|
|
407
429
|
|
|
408
430
|
.object-meta {
|
|
409
431
|
color: var(--muted);
|
|
432
|
+
font-size: 0.82rem;
|
|
410
433
|
text-align: right;
|
|
411
434
|
white-space: nowrap;
|
|
412
|
-
font-size: .82rem;
|
|
413
435
|
}
|
|
414
436
|
|
|
415
|
-
@media (width
|
|
437
|
+
@media (max-width: 1020px) {
|
|
416
438
|
.app-shell {
|
|
417
439
|
grid-template-columns: 1fr;
|
|
418
440
|
}
|
|
@@ -422,7 +444,8 @@ button:disabled {
|
|
|
422
444
|
max-height: none;
|
|
423
445
|
}
|
|
424
446
|
|
|
425
|
-
.explorer-header,
|
|
447
|
+
.explorer-header,
|
|
448
|
+
.objects-header {
|
|
426
449
|
flex-direction: column;
|
|
427
450
|
}
|
|
428
451
|
|
package/src/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="./logo.svg" />
|
|
7
|
+
<title>S3 Explorer</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="./frontend.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { serve } from "bun";
|
|
2
|
+
import index from "./index.html";
|
|
3
|
+
import { s3Routes } from "./server/routes/s3Routes";
|
|
4
|
+
|
|
5
|
+
const port = Number(process.env.PORT || 3000);
|
|
6
|
+
const hostname = process.env.HOST || "127.0.0.1";
|
|
7
|
+
|
|
8
|
+
const server = serve({
|
|
9
|
+
port,
|
|
10
|
+
hostname,
|
|
11
|
+
routes: {
|
|
12
|
+
...s3Routes,
|
|
13
|
+
"/*": index,
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
development: process.env.NODE_ENV !== "production" && {
|
|
17
|
+
hmr: true,
|
|
18
|
+
console: true,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
console.log(`Server running at ${server.url}`);
|
package/src/logo.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70"><title>Bun Logo</title><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" data-name="Bottom Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" data-name="Light Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2" data-name="Background"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" data-name="Background" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" data-name="Outline" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" data-name="Outline" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" data-name="Right Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" data-name="Left Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ApiErrorPayload = {
|
|
2
|
+
message: string;
|
|
3
|
+
code?: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function ok<T>(data: T): Response {
|
|
7
|
+
return Response.json({ ok: true, data });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function fail(status: number, error: ApiErrorPayload): Response {
|
|
11
|
+
return Response.json({ ok: false, error }, { status });
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listBucketsHandler,
|
|
3
|
+
listObjectsHandler,
|
|
4
|
+
testConnectionHandler,
|
|
5
|
+
} from "../s3/handlers";
|
|
6
|
+
|
|
7
|
+
export const s3Routes = {
|
|
8
|
+
"/api/s3/test-connection": {
|
|
9
|
+
POST: testConnectionHandler,
|
|
10
|
+
},
|
|
11
|
+
"/api/s3/list-buckets": {
|
|
12
|
+
POST: listBucketsHandler,
|
|
13
|
+
},
|
|
14
|
+
"/api/s3/list-objects": {
|
|
15
|
+
POST: listObjectsHandler,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import type { S3ProfileInput } from "./types";
|
|
3
|
+
|
|
4
|
+
export function createS3Client(profile: S3ProfileInput): S3Client {
|
|
5
|
+
return new S3Client({
|
|
6
|
+
region: profile.region,
|
|
7
|
+
endpoint: profile.endpoint,
|
|
8
|
+
forcePathStyle: profile.forcePathStyle,
|
|
9
|
+
credentials: {
|
|
10
|
+
accessKeyId: profile.accessKeyId,
|
|
11
|
+
secretAccessKey: profile.secretAccessKey,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ListBucketsCommand,
|
|
3
|
+
ListObjectsV2Command,
|
|
4
|
+
S3ServiceException,
|
|
5
|
+
} from "@aws-sdk/client-s3";
|
|
6
|
+
import { fail, ok } from "../http/response";
|
|
7
|
+
import { createS3Client } from "./client";
|
|
8
|
+
import { mapListBucketsResult, mapListObjectsResult, mapS3Error } from "./mappers";
|
|
9
|
+
import { invalidProfileError, parseListObjectsInput, parseProfileFromBody } from "./validate";
|
|
10
|
+
|
|
11
|
+
async function parseJsonBody(req: Request): Promise<unknown> {
|
|
12
|
+
try {
|
|
13
|
+
return await req.json();
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function testConnectionHandler(req: Request): Promise<Response> {
|
|
20
|
+
const body = await parseJsonBody(req);
|
|
21
|
+
const profile = parseProfileFromBody(body);
|
|
22
|
+
|
|
23
|
+
if (!profile) {
|
|
24
|
+
return fail(400, invalidProfileError());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const client = createS3Client(profile);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await client.send(new ListBucketsCommand({}));
|
|
31
|
+
return ok({
|
|
32
|
+
connected: true,
|
|
33
|
+
message: "Connection successful.",
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (
|
|
37
|
+
err instanceof S3ServiceException &&
|
|
38
|
+
(err.name === "AccessDenied" || err.name === "AccessDeniedException")
|
|
39
|
+
) {
|
|
40
|
+
return ok({
|
|
41
|
+
connected: true,
|
|
42
|
+
limitedPermissions: true,
|
|
43
|
+
message:
|
|
44
|
+
"Connected, but this key cannot list buckets. You can still browse known buckets.",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return fail(400, mapS3Error(err));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function listBucketsHandler(req: Request): Promise<Response> {
|
|
53
|
+
const body = await parseJsonBody(req);
|
|
54
|
+
const profile = parseProfileFromBody(body);
|
|
55
|
+
|
|
56
|
+
if (!profile) {
|
|
57
|
+
return fail(400, invalidProfileError());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const client = createS3Client(profile);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const result = await client.send(new ListBucketsCommand({}));
|
|
64
|
+
return ok(mapListBucketsResult(result));
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return fail(400, mapS3Error(err));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function listObjectsHandler(req: Request): Promise<Response> {
|
|
71
|
+
const body = await parseJsonBody(req);
|
|
72
|
+
const parsed = parseListObjectsInput(body);
|
|
73
|
+
|
|
74
|
+
if (!parsed.ok) {
|
|
75
|
+
return fail(400, parsed.error);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { profile, bucket, prefix, continuationToken, maxKeys } = parsed.data;
|
|
79
|
+
const client = createS3Client(profile);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const result = await client.send(
|
|
83
|
+
new ListObjectsV2Command({
|
|
84
|
+
Bucket: bucket,
|
|
85
|
+
Prefix: prefix,
|
|
86
|
+
ContinuationToken: continuationToken,
|
|
87
|
+
Delimiter: "/",
|
|
88
|
+
MaxKeys: maxKeys,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return ok(mapListObjectsResult(result, bucket, prefix));
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return fail(400, mapS3Error(err));
|
|
95
|
+
}
|
|
96
|
+
}
|