···
23
23
</a>
24
24
</p>
25
25
</div>
26
26
-
<p>
27
27
-
<small><em><strong>More options coming soon!</strong></em></small>
28
28
-
</p>
29
26
</main>
30
27
31
28
<style is:global>
···
37
37
</style>
38
38
39
39
<script>
40
40
-
import { S3Client } from "@bradenmacdonald/s3-lite-client";
41
41
-
import { computed, effect, type Signal, signal } from "spellcaster";
42
42
-
import { type Props, repeat, tags, text } from "spellcaster/hyperscript.js";
43
43
-
import * as IDB from "idb-keyval";
44
44
-
import * as URI from "uri-js";
45
45
-
import QS from "query-string";
40
40
+
import type { Actions } from "@scripts/input/s3/worker";
46
41
47
42
import type { Track } from "@applets/core/types.d.ts";
48
48
-
import { isAudioFile } from "@scripts/input/common";
49
43
import { register } from "@scripts/applet/common";
50
50
-
import manifest from "./_manifest.json";
51
51
-
52
52
-
type Bucket = {
53
53
-
accessKey: string;
54
54
-
bucketName: string;
55
55
-
host: string;
56
56
-
path: string;
57
57
-
region: string;
58
58
-
secretKey: string;
59
59
-
};
60
60
-
61
61
-
const ENCODINGS = {
62
62
-
"\+": "%2B",
63
63
-
"\!": "%21",
64
64
-
'\"': "%22",
65
65
-
"\#": "%23",
66
66
-
"\$": "%24",
67
67
-
"\&": "%26",
68
68
-
"'": "%27",
69
69
-
"\(": "%28",
70
70
-
"\)": "%29",
71
71
-
"\*": "%2A",
72
72
-
"\,": "%2C",
73
73
-
"\:": "%3A",
74
74
-
"\;": "%3B",
75
75
-
"\=": "%3D",
76
76
-
"\?": "%3F",
77
77
-
"\@": "%40",
78
78
-
};
44
44
+
import { endpoint, inIframe } from "@scripts/common";
79
45
80
46
////////////////////////////////////////////
81
47
// SETUP
82
48
////////////////////////////////////////////
83
83
-
const IDB_PREFIX = "@applets/input/s3";
84
84
-
const IDB_BUCKETS = `${IDB_PREFIX}/buckets`;
85
85
-
const SCHEME = manifest.input_properties.scheme;
49
49
+
const worker = endpoint<Actions>(
50
50
+
new Worker("../../../scripts/input/s3/worker", { type: "module" }),
51
51
+
);
86
52
87
53
// Register applet
88
54
const context = register();
89
55
90
56
////////////////////////////////////////////
91
91
-
// UI
92
92
-
////////////////////////////////////////////
93
93
-
const [buckets, setBuckets] = signal<Record<string, Bucket>>(await loadBuckets());
94
94
-
const [form, setForm] = signal<{
95
95
-
access_key?: string;
96
96
-
bucket_name?: string;
97
97
-
host?: string;
98
98
-
path?: string;
99
99
-
region?: string;
100
100
-
secret_key?: string;
101
101
-
}>({});
102
102
-
103
103
-
const bucketsMap = computed(() => {
104
104
-
return new Map(Object.entries(buckets()));
105
105
-
});
106
106
-
107
107
-
effect(() => {
108
108
-
saveBuckets(buckets());
109
109
-
});
110
110
-
111
111
-
////////////////////////////////////////////
112
112
-
// UI ~ BUCKETS
113
113
-
////////////////////////////////////////////
114
114
-
const Bucket = (bucket: Signal<Bucket>) => {
115
115
-
const onclick = () => {
116
116
-
const b = bucket();
117
117
-
const id = bucketId(b);
118
118
-
119
119
-
const col = { ...buckets() };
120
120
-
delete col[id];
121
121
-
122
122
-
setBuckets(col);
123
123
-
};
124
124
-
125
125
-
return tags.li({ onclick, style: "cursor: pointer" }, text(bucket().host));
126
126
-
};
127
127
-
128
128
-
const BucketList = computed(() => {
129
129
-
if (bucketsMap().size === 0) {
130
130
-
return tags.p({ id: "buckets" }, [tags.small({}, text("Nothing added so far."))]);
131
131
-
}
132
132
-
133
133
-
return tags.ul({ id: "buckets" }, repeat(bucketsMap, Bucket));
134
134
-
});
135
135
-
136
136
-
effect(() => {
137
137
-
document.querySelector("#buckets")?.replaceWith(BucketList());
138
138
-
});
139
139
-
140
140
-
////////////////////////////////////////////
141
141
-
// UI ~ FORM
142
142
-
////////////////////////////////////////////
143
143
-
function addBucket(event: Event) {
144
144
-
event.preventDefault();
145
145
-
146
146
-
const f = form();
147
147
-
148
148
-
const bucket: Bucket = {
149
149
-
accessKey: f.access_key || "",
150
150
-
bucketName: f.bucket_name || "",
151
151
-
host: f.host || "s3.amazonaws.com",
152
152
-
path: f.path || "/",
153
153
-
region: f.region || "us-east-1",
154
154
-
secretKey: f.secret_key || "",
155
155
-
};
156
156
-
157
157
-
setBuckets({
158
158
-
...buckets(),
159
159
-
[bucketId(bucket)]: bucket,
160
160
-
});
161
161
-
}
162
162
-
163
163
-
function Form() {
164
164
-
return tags.form({ onsubmit: addBucket }, [
165
165
-
tags.fieldset({ className: "grid" }, [
166
166
-
Input("access_key", "Access key", "r31w7m9c", { required: true }),
167
167
-
Input("secret_key", "Secret key", "v02g2l29", { required: true }),
168
168
-
]),
169
169
-
tags.fieldset({ className: "grid" }, [
170
170
-
Input("bucket_name", "Bucket name", "bucket", { required: true }),
171
171
-
Input("region", "Region", "us-east-1", { required: true }),
172
172
-
]),
173
173
-
tags.fieldset({ className: "grid" }, [
174
174
-
Input("host", "Host", "s3.amazonaws.com", { required: true }),
175
175
-
Input("path", "Path", "/"),
176
176
-
]),
177
177
-
tags.fieldset({ className: "grid" }, [tags.input({ type: "submit", value: "Connect" }, [])]),
178
178
-
]);
179
179
-
}
180
180
-
181
181
-
function Input(name: string, label: string, placeholder: string, opts: Props = {}) {
182
182
-
return tags.label({}, [
183
183
-
tags.span({}, [
184
184
-
tags.span({}, text(label)),
185
185
-
tags.small({}, text("required" in opts ? "" : " (optional)")),
186
186
-
]),
187
187
-
tags.input({
188
188
-
...opts,
189
189
-
name,
190
190
-
placeholder,
191
191
-
oninput: (event: InputEvent) => formInput(name, (event.target as HTMLInputElement).value),
192
192
-
}),
193
193
-
]);
194
194
-
}
195
195
-
196
196
-
function formInput(name: string, value: string) {
197
197
-
setForm({ ...form(), [name]: value });
198
198
-
}
199
199
-
200
200
-
// 🚀
201
201
-
document.querySelector("#form")?.replaceWith(Form());
202
202
-
203
203
-
////////////////////////////////////////////
204
57
// ACTIONS
205
58
////////////////////////////////////////////
206
59
const consult = async (fileUriOrScheme: string) => {
207
207
-
if (!navigator.onLine)
208
208
-
return { supported: false, reason: "Internet connection is not available" };
209
209
-
210
210
-
// TODO: Check if bucket is avail*able + CORS works?
211
211
-
return { supported: true };
60
60
+
return await worker.call.consult(fileUriOrScheme);
212
61
};
213
62
214
63
const contextualize = async (tracks: Track[]) => {
215
215
-
const b = bucketsFromTracks(tracks);
216
216
-
setBuckets({ ...buckets(), ...b });
64
64
+
const s = await worker.call.contextualize(tracks);
65
65
+
ui?.setBuckets({ ...ui?.buckets(), ...s });
217
66
};
218
67
219
68
const list = async (cachedTracks: Track[] = []) => {
220
220
-
const cache = cachedTracks.reduce((acc: Record<string, Track>, t: Track) => {
221
221
-
const uri = URI.parse(t.uri);
222
222
-
if (!uri.path) return acc;
223
223
-
return { ...acc, [URI.unescapeComponent(uri.path)]: t };
224
224
-
}, {});
225
225
-
226
226
-
const promises = Object.values(buckets()).map(async (bucket) => {
227
227
-
const client = createClient(bucket);
228
228
-
229
229
-
const list = await Array.fromAsync(
230
230
-
client.listObjects({
231
231
-
prefix: bucket.path.replace(/^\//, ""),
232
232
-
}),
233
233
-
);
234
234
-
235
235
-
return list
236
236
-
.filter((l) => isAudioFile(l.key))
237
237
-
.map((l) => {
238
238
-
const cachedTrack = cache[`/${l.key}`];
239
239
-
240
240
-
const id = cachedTrack?.id || crypto.randomUUID();
241
241
-
const stats = cachedTrack?.stats;
242
242
-
const tags = cachedTrack?.tags;
243
243
-
244
244
-
const track: Track = {
245
245
-
id,
246
246
-
stats,
247
247
-
tags,
248
248
-
uri: buildURI(bucket, l.key),
249
249
-
};
250
250
-
251
251
-
return track;
252
252
-
});
253
253
-
});
254
254
-
255
255
-
return (await Promise.all(promises)).flat(1);
69
69
+
return await worker.call.list(cachedTracks);
256
70
};
257
71
258
258
-
const resolve = async ({ method, uri }: { method: string; uri: string }) => {
259
259
-
const bucket = parseURI(uri);
260
260
-
if (!bucket) return undefined;
261
261
-
262
262
-
const client = createClient(bucket);
263
263
-
const parsedURI = URI.parse(uri);
264
264
-
const path = (
265
265
-
bucket.path.replace(/\/$/, "") + URI.unescapeComponent(parsedURI.path || "")
266
266
-
).replace(/^\//, "");
267
267
-
268
268
-
const expiresInSeconds = 60 * 60 * 24 * 7; // 7 days
269
269
-
const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds;
270
270
-
const url = await client.getPresignedUrl(method.toUpperCase() as any, path);
271
271
-
272
272
-
return { expiresAt: expiresAtSeconds, url };
72
72
+
const resolve = async (args: { method: string; uri: string }) => {
73
73
+
return await worker.call.resolve(args);
273
74
};
274
75
275
76
const mount = async () => {};
···
284
85
context.setActionHandler("unmount", unmount);
285
86
286
87
////////////////////////////////////////////
287
287
-
// 🛠️
88
88
+
// UI
288
89
////////////////////////////////////////////
289
289
-
function bucketsFromTracks(tracks: Track[]) {
290
290
-
return tracks.reduce((acc: Record<string, Bucket>, track: Track) => {
291
291
-
const bucket = parseURI(track.uri);
292
292
-
if (!bucket) return acc;
293
293
-
294
294
-
const id = bucketId(bucket);
295
295
-
if (acc[id]) return acc;
296
296
-
297
297
-
return { ...acc, [id]: bucket };
298
298
-
}, {});
299
299
-
}
300
300
-
301
301
-
function bucketId(bucket: Bucket) {
302
302
-
return `${bucket.accessKey}:${bucket.secretKey}@${bucket.host}`;
303
303
-
}
304
304
-
305
305
-
function buildURI(bucket: Bucket, path: string) {
306
306
-
return URI.serialize({
307
307
-
scheme: SCHEME,
308
308
-
userinfo: `${bucket.accessKey}:${bucket.secretKey}`,
309
309
-
host: bucket.host.replace(/^https?:\/\//, ""),
310
310
-
path: path,
311
311
-
query: QS.stringify({
312
312
-
bucketName: bucket.bucketName,
313
313
-
bucketPath: bucket.path,
314
314
-
region: bucket.region,
315
315
-
}),
316
316
-
});
317
317
-
}
318
318
-
319
319
-
function createClient(bucket: Bucket) {
320
320
-
return new S3Client({
321
321
-
bucket: bucket.bucketName,
322
322
-
endPoint: `http${bucket.host.startsWith("localhost") ? "" : "s"}://${bucket.host}`,
323
323
-
region: bucket.region,
324
324
-
pathStyle: false,
325
325
-
accessKey: bucket.accessKey,
326
326
-
secretKey: bucket.secretKey,
327
327
-
});
328
328
-
}
329
329
-
330
330
-
function encodeAwsUriComponent(a: string) {
331
331
-
return encodeURIComponent(a).replace(
332
332
-
/(\+|!|"|#|\$|&|'|\(|\)|\*|\+|,|:|;|=|\?|@)/gim,
333
333
-
(match) => (ENCODINGS as any)[match] ?? match,
334
334
-
);
335
335
-
}
336
336
-
337
337
-
async function loadBuckets() {
338
338
-
const i = await IDB.get(IDB_BUCKETS);
339
339
-
return i ? i : {};
340
340
-
}
341
341
-
342
342
-
function parseURI(uriString: string): Bucket | undefined {
343
343
-
const uri = URI.parse(uriString);
344
344
-
if (uri.scheme !== SCHEME) return undefined;
345
345
-
if (!uri.host) return undefined;
346
346
-
347
347
-
const [accessKey, secretKey] = uri.userinfo?.split(":") ?? [];
348
348
-
if (!accessKey || !secretKey) return undefined;
349
349
-
350
350
-
const qs = QS.parse(uri.query || "");
351
351
-
352
352
-
return {
353
353
-
accessKey,
354
354
-
bucketName: typeof qs.bucketName === "string" ? qs.bucketName : "",
355
355
-
host: uri.host,
356
356
-
path: qs.bucketPath === "string" ? qs.bucketPath : "/",
357
357
-
region: typeof qs.region === "string" ? qs.region : "",
358
358
-
secretKey,
359
359
-
};
360
360
-
}
361
361
-
362
362
-
async function saveBuckets(items: Record<string, Bucket>) {
363
363
-
await IDB.set(IDB_BUCKETS, items);
364
364
-
}
90
90
+
const ui = inIframe() ? undefined : await import("@scripts/input/s3/ui");
365
91
</script>
···
1
1
-
import { type FileSystemDirectoryHandle } from "native-file-system-adapter";
2
1
import * as IDB from "idb-keyval";
3
2
import * as URI from "uri-js";
4
3
import QS from "query-string";
···
1
1
-
import type { FileSystemDirectoryHandle } from "native-file-system-adapter";
2
2
-
3
1
export type Handles = Record<string, FileSystemDirectoryHandle>;
···
1
1
import { computed, effect, type Signal } from "spellcaster";
2
2
import { repeat, tags, text } from "spellcaster/hyperscript.js";
3
3
-
import { type FileSystemDirectoryHandle } from "native-file-system-adapter";
4
3
5
5
-
import { IDB_HANDLES } from "./constants";
6
4
import { mount, mounts, unmount } from "./mounting";
5
5
+
import { isSupported } from "./common";
7
6
8
7
////////////////////////////////////////////
9
8
// SIGNALS
10
9
////////////////////////////////////////////
11
10
12
11
// Mount button
13
13
-
document.getElementById("mount")?.addEventListener("click", () => mount());
12
12
+
document.getElementById("mount")?.addEventListener("click", () => {
13
13
+
if (isSupported()) mount();
14
14
+
else alert("The File System Access API is not supported on this platform.");
15
15
+
});
14
16
15
17
// Directories
16
18
const dirList = computed(() => {
···
1
1
-
import { type FileSystemDirectoryHandle } from "native-file-system-adapter";
2
1
import * as URI from "uri-js";
3
2
4
3
import type { Track } from "@applets/core/types.d.ts";
5
4
import { SCHEME } from "./constants";
6
6
-
import {
7
7
-
fetchHandles,
8
8
-
fetchHandlesList,
9
9
-
isSupported,
10
10
-
recursiveList,
11
11
-
trackHandleId,
12
12
-
} from "./common";
5
5
+
import { fetchHandles, fetchHandlesList, recursiveList, trackHandleId } from "./common";
13
6
import { expose } from "@scripts/common";
14
7
15
8
////////////////////////////////////////////
···
27
20
// Actions
28
21
29
22
export async function consult(fileUriOrScheme: string) {
30
30
-
if (!isSupported()) {
23
23
+
if (!self.FileSystemDirectoryHandle) {
31
24
return { supported: false, reason: "File System Access API is not supported" };
32
25
}
33
26
···
45
38
export async function contextualize(cachedTracks: Track[]) {}
46
39
47
40
export async function list(cachedTracks: Track[] = []) {
48
48
-
if (!isSupported()) {
49
49
-
return cachedTracks;
50
50
-
}
51
51
-
52
52
-
// Continue if supported
53
41
const handles = await fetchHandlesList();
54
42
55
43
// Recursive listing of all tracks of available handles
···
98
86
99
87
export async function resolve(args: { uri: string }) {
100
88
const fileUri = args.uri;
101
101
-
102
102
-
if (!isSupported()) {
103
103
-
return undefined;
104
104
-
}
105
89
106
90
const uri = URI.parse(fileUri);
107
91
if (uri.scheme !== SCHEME) return undefined;
···
1
1
+
import { S3Client } from "@bradenmacdonald/s3-lite-client";
2
2
+
import * as IDB from "idb-keyval";
3
3
+
import * as URI from "uri-js";
4
4
+
import QS from "query-string";
5
5
+
6
6
+
import type { Track } from "@applets/core/types.d.ts";
7
7
+
import { ENCODINGS, IDB_BUCKETS, SCHEME } from "./constants";
8
8
+
import type { Bucket } from "./types";
9
9
+
10
10
+
////////////////////////////////////////////
11
11
+
// 🛠️
12
12
+
////////////////////////////////////////////
13
13
+
export function bucketsFromTracks(tracks: Track[]) {
14
14
+
return tracks.reduce((acc: Record<string, Bucket>, track: Track) => {
15
15
+
const bucket = parseURI(track.uri);
16
16
+
if (!bucket) return acc;
17
17
+
18
18
+
const id = bucketId(bucket);
19
19
+
if (acc[id]) return acc;
20
20
+
21
21
+
return { ...acc, [id]: bucket };
22
22
+
}, {});
23
23
+
}
24
24
+
25
25
+
export function bucketId(bucket: Bucket) {
26
26
+
return `${bucket.accessKey}:${bucket.secretKey}@${bucket.host}`;
27
27
+
}
28
28
+
29
29
+
export function buildURI(bucket: Bucket, path: string) {
30
30
+
return URI.serialize({
31
31
+
scheme: SCHEME,
32
32
+
userinfo: `${bucket.accessKey}:${bucket.secretKey}`,
33
33
+
host: bucket.host.replace(/^https?:\/\//, ""),
34
34
+
path: path,
35
35
+
query: QS.stringify({
36
36
+
bucketName: bucket.bucketName,
37
37
+
bucketPath: bucket.path,
38
38
+
region: bucket.region,
39
39
+
}),
40
40
+
});
41
41
+
}
42
42
+
43
43
+
export function createClient(bucket: Bucket) {
44
44
+
return new S3Client({
45
45
+
bucket: bucket.bucketName,
46
46
+
endPoint: `http${bucket.host.startsWith("localhost") ? "" : "s"}://${bucket.host}`,
47
47
+
region: bucket.region,
48
48
+
pathStyle: false,
49
49
+
accessKey: bucket.accessKey,
50
50
+
secretKey: bucket.secretKey,
51
51
+
});
52
52
+
}
53
53
+
54
54
+
export function encodeAwsUriComponent(a: string) {
55
55
+
return encodeURIComponent(a).replace(
56
56
+
/(\+|!|"|#|\$|&|'|\(|\)|\*|\+|,|:|;|=|\?|@)/gim,
57
57
+
(match) => (ENCODINGS as any)[match] ?? match,
58
58
+
);
59
59
+
}
60
60
+
61
61
+
export async function loadBuckets(): Promise<Record<string, Bucket>> {
62
62
+
const i = await IDB.get(IDB_BUCKETS);
63
63
+
return i ? i : {};
64
64
+
}
65
65
+
66
66
+
export function parseURI(uriString: string): Bucket | undefined {
67
67
+
const uri = URI.parse(uriString);
68
68
+
if (uri.scheme !== SCHEME) return undefined;
69
69
+
if (!uri.host) return undefined;
70
70
+
71
71
+
const [accessKey, secretKey] = uri.userinfo?.split(":") ?? [];
72
72
+
if (!accessKey || !secretKey) return undefined;
73
73
+
74
74
+
const qs = QS.parse(uri.query || "");
75
75
+
76
76
+
return {
77
77
+
accessKey,
78
78
+
bucketName: typeof qs.bucketName === "string" ? qs.bucketName : "",
79
79
+
host: uri.host,
80
80
+
path: qs.bucketPath === "string" ? qs.bucketPath : "/",
81
81
+
region: typeof qs.region === "string" ? qs.region : "",
82
82
+
secretKey,
83
83
+
};
84
84
+
}
85
85
+
86
86
+
export async function saveBuckets(items: Record<string, Bucket>) {
87
87
+
await IDB.set(IDB_BUCKETS, items);
88
88
+
}
···
1
1
+
import manifest from "../../../pages/input/s3/_manifest.json";
2
2
+
3
3
+
export const IDB_PREFIX = "@applets/input/s3";
4
4
+
export const IDB_BUCKETS = `${IDB_PREFIX}/buckets`;
5
5
+
export const SCHEME = manifest.input_properties.scheme;
6
6
+
7
7
+
export const ENCODINGS = {
8
8
+
"\+": "%2B",
9
9
+
"\!": "%21",
10
10
+
'\"': "%22",
11
11
+
"\#": "%23",
12
12
+
"\$": "%24",
13
13
+
"\&": "%26",
14
14
+
"'": "%27",
15
15
+
"\(": "%28",
16
16
+
"\)": "%29",
17
17
+
"\*": "%2A",
18
18
+
"\,": "%2C",
19
19
+
"\:": "%3A",
20
20
+
"\;": "%3B",
21
21
+
"\=": "%3D",
22
22
+
"\?": "%3F",
23
23
+
"\@": "%40",
24
24
+
};
···
1
1
+
export type Bucket = {
2
2
+
accessKey: string;
3
3
+
bucketName: string;
4
4
+
host: string;
5
5
+
path: string;
6
6
+
region: string;
7
7
+
secretKey: string;
8
8
+
};
···
1
1
+
import { computed, effect, type Signal, signal } from "spellcaster";
2
2
+
import { type Props, repeat, tags, text } from "spellcaster/hyperscript.js";
3
3
+
4
4
+
import type { Bucket } from "./types";
5
5
+
import { bucketId, loadBuckets, saveBuckets } from "./common";
6
6
+
7
7
+
////////////////////////////////////////////
8
8
+
// UI
9
9
+
////////////////////////////////////////////
10
10
+
export const [buckets, setBuckets] = signal<Record<string, Bucket>>(await loadBuckets());
11
11
+
export const [form, setForm] = signal<{
12
12
+
access_key?: string;
13
13
+
bucket_name?: string;
14
14
+
host?: string;
15
15
+
path?: string;
16
16
+
region?: string;
17
17
+
secret_key?: string;
18
18
+
}>({});
19
19
+
20
20
+
export const bucketsMap = computed(() => {
21
21
+
return new Map(Object.entries(buckets()));
22
22
+
});
23
23
+
24
24
+
effect(() => {
25
25
+
saveBuckets(buckets());
26
26
+
});
27
27
+
28
28
+
////////////////////////////////////////////
29
29
+
// UI ~ BUCKETS
30
30
+
////////////////////////////////////////////
31
31
+
const Bucket = (bucket: Signal<Bucket>) => {
32
32
+
const onclick = () => {
33
33
+
const b = bucket();
34
34
+
const id = bucketId(b);
35
35
+
36
36
+
const col = { ...buckets() };
37
37
+
delete col[id];
38
38
+
39
39
+
setBuckets(col);
40
40
+
};
41
41
+
42
42
+
return tags.li({ onclick, style: "cursor: pointer" }, text(bucket().host));
43
43
+
};
44
44
+
45
45
+
const BucketList = computed(() => {
46
46
+
if (bucketsMap().size === 0) {
47
47
+
return tags.p({ id: "buckets" }, [tags.small({}, text("Nothing added so far."))]);
48
48
+
}
49
49
+
50
50
+
return tags.ul({ id: "buckets" }, repeat(bucketsMap, Bucket));
51
51
+
});
52
52
+
53
53
+
effect(() => {
54
54
+
document.querySelector("#buckets")?.replaceWith(BucketList());
55
55
+
});
56
56
+
57
57
+
////////////////////////////////////////////
58
58
+
// UI ~ FORM
59
59
+
////////////////////////////////////////////
60
60
+
function addBucket(event: Event) {
61
61
+
event.preventDefault();
62
62
+
63
63
+
const f = form();
64
64
+
65
65
+
const bucket: Bucket = {
66
66
+
accessKey: f.access_key || "",
67
67
+
bucketName: f.bucket_name || "",
68
68
+
host: f.host || "s3.amazonaws.com",
69
69
+
path: f.path || "/",
70
70
+
region: f.region || "us-east-1",
71
71
+
secretKey: f.secret_key || "",
72
72
+
};
73
73
+
74
74
+
setBuckets({
75
75
+
...buckets(),
76
76
+
[bucketId(bucket)]: bucket,
77
77
+
});
78
78
+
}
79
79
+
80
80
+
function Form() {
81
81
+
return tags.form({ onsubmit: addBucket }, [
82
82
+
tags.fieldset({ className: "grid" }, [
83
83
+
Input("access_key", "Access key", "r31w7m9c", { required: true }),
84
84
+
Input("secret_key", "Secret key", "v02g2l29", { required: true }),
85
85
+
]),
86
86
+
tags.fieldset({ className: "grid" }, [
87
87
+
Input("bucket_name", "Bucket name", "bucket", { required: true }),
88
88
+
Input("region", "Region", "us-east-1", { required: true }),
89
89
+
]),
90
90
+
tags.fieldset({ className: "grid" }, [
91
91
+
Input("host", "Host", "s3.amazonaws.com", { required: true }),
92
92
+
Input("path", "Path", "/"),
93
93
+
]),
94
94
+
tags.fieldset({ className: "grid" }, [tags.input({ type: "submit", value: "Connect" }, [])]),
95
95
+
]);
96
96
+
}
97
97
+
98
98
+
function Input(name: string, label: string, placeholder: string, opts: Props = {}) {
99
99
+
return tags.label({}, [
100
100
+
tags.span({}, [
101
101
+
tags.span({}, text(label)),
102
102
+
tags.small({}, text("required" in opts ? "" : " (optional)")),
103
103
+
]),
104
104
+
tags.input({
105
105
+
...opts,
106
106
+
name,
107
107
+
placeholder,
108
108
+
oninput: (event: InputEvent) => formInput(name, (event.target as HTMLInputElement).value),
109
109
+
}),
110
110
+
]);
111
111
+
}
112
112
+
113
113
+
function formInput(name: string, value: string) {
114
114
+
setForm({ ...form(), [name]: value });
115
115
+
}
116
116
+
117
117
+
// 🚀
118
118
+
document.querySelector("#form")?.replaceWith(Form());
···
1
1
+
import * as URI from "uri-js";
2
2
+
3
3
+
import type { Track } from "@applets/core/types.d.ts";
4
4
+
import { isAudioFile } from "@scripts/input/common";
5
5
+
import { bucketsFromTracks, buildURI, createClient, loadBuckets, parseURI } from "./common";
6
6
+
import { expose } from "@scripts/common";
7
7
+
8
8
+
////////////////////////////////////////////
9
9
+
// ACTIONS
10
10
+
////////////////////////////////////////////
11
11
+
const actions = expose({
12
12
+
consult,
13
13
+
contextualize,
14
14
+
list,
15
15
+
resolve,
16
16
+
});
17
17
+
18
18
+
export type Actions = typeof actions;
19
19
+
20
20
+
// Actions
21
21
+
22
22
+
async function consult(fileUriOrScheme: string) {
23
23
+
if (!navigator.onLine)
24
24
+
return { supported: false, reason: "Internet connection is not available" };
25
25
+
26
26
+
// TODO: Check if bucket is available + CORS works?
27
27
+
return { supported: true };
28
28
+
}
29
29
+
30
30
+
async function contextualize(tracks: Track[]) {
31
31
+
return bucketsFromTracks(tracks);
32
32
+
}
33
33
+
34
34
+
async function list(cachedTracks: Track[] = []) {
35
35
+
const cache = cachedTracks.reduce((acc: Record<string, Track>, t: Track) => {
36
36
+
const uri = URI.parse(t.uri);
37
37
+
if (!uri.path) return acc;
38
38
+
return { ...acc, [URI.unescapeComponent(uri.path)]: t };
39
39
+
}, {});
40
40
+
41
41
+
const buckets = await loadBuckets();
42
42
+
const promises = Object.values(buckets).map(async (bucket) => {
43
43
+
const client = createClient(bucket);
44
44
+
45
45
+
const list = await Array.fromAsync(
46
46
+
client.listObjects({
47
47
+
prefix: bucket.path.replace(/^\//, ""),
48
48
+
}),
49
49
+
);
50
50
+
51
51
+
return list
52
52
+
.filter((l) => isAudioFile(l.key))
53
53
+
.map((l) => {
54
54
+
const cachedTrack = cache[`/${l.key}`];
55
55
+
56
56
+
const id = cachedTrack?.id || crypto.randomUUID();
57
57
+
const stats = cachedTrack?.stats;
58
58
+
const tags = cachedTrack?.tags;
59
59
+
60
60
+
const track: Track = {
61
61
+
id,
62
62
+
stats,
63
63
+
tags,
64
64
+
uri: buildURI(bucket, l.key),
65
65
+
};
66
66
+
67
67
+
return track;
68
68
+
});
69
69
+
});
70
70
+
71
71
+
return (await Promise.all(promises)).flat(1);
72
72
+
}
73
73
+
74
74
+
async function resolve({ method, uri }: { method: string; uri: string }) {
75
75
+
const bucket = parseURI(uri);
76
76
+
if (!bucket) return undefined;
77
77
+
78
78
+
const client = createClient(bucket);
79
79
+
const parsedURI = URI.parse(uri);
80
80
+
const path = (
81
81
+
bucket.path.replace(/\/$/, "") + URI.unescapeComponent(parsedURI.path || "")
82
82
+
).replace(/^\//, "");
83
83
+
84
84
+
const expiresInSeconds = 60 * 60 * 24 * 7; // 7 days
85
85
+
const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds;
86
86
+
const url = await client.getPresignedUrl(method.toUpperCase() as any, path);
87
87
+
88
88
+
return { expiresAt: expiresAtSeconds, url };
89
89
+
}