@marimo-team/islands 0.20.5-dev44 → 0.20.5-dev48
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/dist/main.js +1 -1
- package/package.json +1 -1
- package/src/components/storage/__tests__/storage-snippets.test.ts +253 -0
- package/src/components/storage/storage-inspector.tsx +36 -4
- package/src/components/storage/storage-snippets.ts +67 -0
- package/src/core/storage/__tests__/state.test.ts +1 -0
package/dist/main.js
CHANGED
|
@@ -70710,7 +70710,7 @@ Image URL: ${r.imageUrl}`)), contextToXml({
|
|
|
70710
70710
|
return Logger.warn("Failed to get version from mount config"), null;
|
|
70711
70711
|
}
|
|
70712
70712
|
}
|
|
70713
|
-
const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.20.5-
|
|
70713
|
+
const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.20.5-dev48"), showCodeInRunModeAtom = atom(true);
|
|
70714
70714
|
atom(null);
|
|
70715
70715
|
var import_compiler_runtime$89 = require_compiler_runtime();
|
|
70716
70716
|
function useKeydownOnElement(e, r) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
STORAGE_SNIPPETS,
|
|
5
|
+
type StorageSnippetContext,
|
|
6
|
+
} from "../storage-snippets";
|
|
7
|
+
|
|
8
|
+
const readSnippet = STORAGE_SNIPPETS.find((s) => s.id === "read-file")!;
|
|
9
|
+
const downloadSnippet = STORAGE_SNIPPETS.find((s) => s.id === "download-file")!;
|
|
10
|
+
|
|
11
|
+
function makeCtx(
|
|
12
|
+
overrides: Partial<StorageSnippetContext> = {},
|
|
13
|
+
): StorageSnippetContext {
|
|
14
|
+
return {
|
|
15
|
+
variableName: "store",
|
|
16
|
+
protocol: "s3",
|
|
17
|
+
entry: {
|
|
18
|
+
path: "data/file.csv",
|
|
19
|
+
kind: "object",
|
|
20
|
+
size: 1024,
|
|
21
|
+
lastModified: null,
|
|
22
|
+
},
|
|
23
|
+
backendType: "obstore",
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("read-file snippet", () => {
|
|
29
|
+
it("obstore backend", () => {
|
|
30
|
+
expect(readSnippet.getCode(makeCtx())).toMatchInlineSnapshot(`
|
|
31
|
+
"_data = store.get("data/file.csv").bytes()
|
|
32
|
+
_data"
|
|
33
|
+
`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("fsspec backend", () => {
|
|
37
|
+
expect(
|
|
38
|
+
readSnippet.getCode(
|
|
39
|
+
makeCtx({ backendType: "fsspec", variableName: "fs" }),
|
|
40
|
+
),
|
|
41
|
+
).toMatchInlineSnapshot(`
|
|
42
|
+
"_data = fs.cat_file("data/file.csv")
|
|
43
|
+
_data"
|
|
44
|
+
`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns null for directories", () => {
|
|
48
|
+
expect(
|
|
49
|
+
readSnippet.getCode(
|
|
50
|
+
makeCtx({
|
|
51
|
+
entry: {
|
|
52
|
+
path: "data/",
|
|
53
|
+
kind: "directory",
|
|
54
|
+
size: 0,
|
|
55
|
+
lastModified: null,
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("escapes double quotes in paths", () => {
|
|
63
|
+
expect(
|
|
64
|
+
readSnippet.getCode(
|
|
65
|
+
makeCtx({
|
|
66
|
+
entry: {
|
|
67
|
+
path: 'data/"file".csv',
|
|
68
|
+
kind: "object",
|
|
69
|
+
size: 100,
|
|
70
|
+
lastModified: null,
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
).toMatchInlineSnapshot(`
|
|
75
|
+
"_data = store.get("data/\\"file\\".csv").bytes()
|
|
76
|
+
_data"
|
|
77
|
+
`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("escapes backslashes in paths", () => {
|
|
81
|
+
expect(
|
|
82
|
+
readSnippet.getCode(
|
|
83
|
+
makeCtx({
|
|
84
|
+
entry: {
|
|
85
|
+
path: "data\\file.csv",
|
|
86
|
+
kind: "object",
|
|
87
|
+
size: 100,
|
|
88
|
+
lastModified: null,
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
).toMatchInlineSnapshot(`
|
|
93
|
+
"_data = store.get("data\\\\file.csv").bytes()
|
|
94
|
+
_data"
|
|
95
|
+
`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("escapes newlines and tabs in paths", () => {
|
|
99
|
+
expect(
|
|
100
|
+
readSnippet.getCode(
|
|
101
|
+
makeCtx({
|
|
102
|
+
entry: {
|
|
103
|
+
path: "data/file\nname\there.csv",
|
|
104
|
+
kind: "object",
|
|
105
|
+
size: 100,
|
|
106
|
+
lastModified: null,
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
).toMatchInlineSnapshot(`
|
|
111
|
+
"_data = store.get("data/file\\nname\\there.csv").bytes()
|
|
112
|
+
_data"
|
|
113
|
+
`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("escapes control characters in paths", () => {
|
|
117
|
+
expect(
|
|
118
|
+
readSnippet.getCode(
|
|
119
|
+
makeCtx({
|
|
120
|
+
entry: {
|
|
121
|
+
path: "data/\u0000\u001F.csv",
|
|
122
|
+
kind: "object",
|
|
123
|
+
size: 100,
|
|
124
|
+
lastModified: null,
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
128
|
+
).toMatchInlineSnapshot(`
|
|
129
|
+
"_data = store.get("data/\\u0000\\u001f.csv").bytes()
|
|
130
|
+
_data"
|
|
131
|
+
`);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("download-file snippet", () => {
|
|
136
|
+
it("obstore s3 backend", () => {
|
|
137
|
+
expect(downloadSnippet.getCode(makeCtx())).toMatchInlineSnapshot(`
|
|
138
|
+
"from datetime import timedelta
|
|
139
|
+
from obstore import sign
|
|
140
|
+
|
|
141
|
+
signed_url = sign(
|
|
142
|
+
store, "GET", "data/file.csv",
|
|
143
|
+
expires_in=timedelta(hours=1),
|
|
144
|
+
)
|
|
145
|
+
signed_url"
|
|
146
|
+
`);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("obstore gcs backend", () => {
|
|
150
|
+
expect(
|
|
151
|
+
downloadSnippet.getCode(makeCtx({ protocol: "gcs" })),
|
|
152
|
+
).toMatchInlineSnapshot(`
|
|
153
|
+
"from datetime import timedelta
|
|
154
|
+
from obstore import sign
|
|
155
|
+
|
|
156
|
+
signed_url = sign(
|
|
157
|
+
store, "GET", "data/file.csv",
|
|
158
|
+
expires_in=timedelta(hours=1),
|
|
159
|
+
)
|
|
160
|
+
signed_url"
|
|
161
|
+
`);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("obstore cloudflare backend", () => {
|
|
165
|
+
expect(
|
|
166
|
+
downloadSnippet.getCode(makeCtx({ protocol: "cloudflare" })),
|
|
167
|
+
).toMatchInlineSnapshot(`
|
|
168
|
+
"from datetime import timedelta
|
|
169
|
+
from obstore import sign
|
|
170
|
+
|
|
171
|
+
signed_url = sign(
|
|
172
|
+
store, "GET", "data/file.csv",
|
|
173
|
+
expires_in=timedelta(hours=1),
|
|
174
|
+
)
|
|
175
|
+
signed_url"
|
|
176
|
+
`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("returns null for http obstore (not signable)", () => {
|
|
180
|
+
expect(downloadSnippet.getCode(makeCtx({ protocol: "http" }))).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("returns null for file obstore (not signable)", () => {
|
|
184
|
+
expect(downloadSnippet.getCode(makeCtx({ protocol: "file" }))).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("returns null for in-memory obstore (not signable)", () => {
|
|
188
|
+
expect(
|
|
189
|
+
downloadSnippet.getCode(makeCtx({ protocol: "in-memory" })),
|
|
190
|
+
).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("fsspec backend", () => {
|
|
194
|
+
expect(
|
|
195
|
+
downloadSnippet.getCode(
|
|
196
|
+
makeCtx({ backendType: "fsspec", variableName: "fs" }),
|
|
197
|
+
),
|
|
198
|
+
).toMatchInlineSnapshot(`"fs.get("data/file.csv", "file.csv")"`);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("fsspec backend with nested path", () => {
|
|
202
|
+
expect(
|
|
203
|
+
downloadSnippet.getCode(
|
|
204
|
+
makeCtx({
|
|
205
|
+
backendType: "fsspec",
|
|
206
|
+
variableName: "fs",
|
|
207
|
+
entry: {
|
|
208
|
+
path: "nested/dir/report.parquet",
|
|
209
|
+
kind: "file",
|
|
210
|
+
size: 500,
|
|
211
|
+
lastModified: null,
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
),
|
|
215
|
+
).toMatchInlineSnapshot(
|
|
216
|
+
`"fs.get("nested/dir/report.parquet", "report.parquet")"`,
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("returns null for directories", () => {
|
|
221
|
+
expect(
|
|
222
|
+
downloadSnippet.getCode(
|
|
223
|
+
makeCtx({
|
|
224
|
+
entry: {
|
|
225
|
+
path: "data/",
|
|
226
|
+
kind: "directory",
|
|
227
|
+
size: 0,
|
|
228
|
+
lastModified: null,
|
|
229
|
+
},
|
|
230
|
+
}),
|
|
231
|
+
),
|
|
232
|
+
).toBeNull();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("all snippets return null for directories", () => {
|
|
237
|
+
for (const snippet of STORAGE_SNIPPETS) {
|
|
238
|
+
it(`${snippet.id} returns null for directory entries`, () => {
|
|
239
|
+
expect(
|
|
240
|
+
snippet.getCode(
|
|
241
|
+
makeCtx({
|
|
242
|
+
entry: {
|
|
243
|
+
path: "some-dir/",
|
|
244
|
+
kind: "directory",
|
|
245
|
+
size: 0,
|
|
246
|
+
lastModified: null,
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
),
|
|
250
|
+
).toBeNull();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import React, { useCallback, useState } from "react";
|
|
16
16
|
import { useLocale } from "react-aria";
|
|
17
17
|
import { EngineVariable } from "@/components/databases/engine-variable";
|
|
18
|
+
import { useAddCodeToNewCell } from "@/components/editor/cell/useAddCell";
|
|
18
19
|
import { PanelEmptyState } from "@/components/editor/chrome/panels/empty-state";
|
|
19
20
|
import { AddConnectionDialog } from "@/components/editor/connections/add-connection-dialog";
|
|
20
21
|
import {
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
DropdownMenu,
|
|
33
34
|
DropdownMenuContent,
|
|
34
35
|
DropdownMenuItem,
|
|
36
|
+
DropdownMenuSeparator,
|
|
35
37
|
DropdownMenuTrigger,
|
|
36
38
|
} from "@/components/ui/dropdown-menu";
|
|
37
39
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
@@ -47,7 +49,7 @@ import type {
|
|
|
47
49
|
StorageNamespace,
|
|
48
50
|
StoragePathKey,
|
|
49
51
|
} from "@/core/storage/types";
|
|
50
|
-
import { storagePathKey
|
|
52
|
+
import { storagePathKey } from "@/core/storage/types";
|
|
51
53
|
import type { VariableName } from "@/core/variables/types";
|
|
52
54
|
import { cn } from "@/utils/cn";
|
|
53
55
|
import { copyToClipboard } from "@/utils/copy";
|
|
@@ -58,6 +60,7 @@ import { ErrorState } from "../datasources/components";
|
|
|
58
60
|
import { Button } from "../ui/button";
|
|
59
61
|
import { ProtocolIcon } from "./components";
|
|
60
62
|
import { StorageFileViewer } from "./storage-file-viewer";
|
|
63
|
+
import { STORAGE_SNIPPETS } from "./storage-snippets";
|
|
61
64
|
|
|
62
65
|
interface OpenFileInfo {
|
|
63
66
|
entry: StorageEntry;
|
|
@@ -145,6 +148,7 @@ const StorageEntryChildren: React.FC<{
|
|
|
145
148
|
namespace: string;
|
|
146
149
|
protocol: string;
|
|
147
150
|
rootPath: string;
|
|
151
|
+
backendType: StorageNamespace["backendType"];
|
|
148
152
|
prefix: string;
|
|
149
153
|
depth: number;
|
|
150
154
|
locale: string;
|
|
@@ -154,6 +158,7 @@ const StorageEntryChildren: React.FC<{
|
|
|
154
158
|
namespace,
|
|
155
159
|
protocol,
|
|
156
160
|
rootPath,
|
|
161
|
+
backendType,
|
|
157
162
|
prefix,
|
|
158
163
|
depth,
|
|
159
164
|
locale,
|
|
@@ -214,6 +219,7 @@ const StorageEntryChildren: React.FC<{
|
|
|
214
219
|
namespace={namespace}
|
|
215
220
|
protocol={protocol}
|
|
216
221
|
rootPath={rootPath}
|
|
222
|
+
backendType={backendType}
|
|
217
223
|
depth={depth}
|
|
218
224
|
locale={locale}
|
|
219
225
|
searchValue={searchValue}
|
|
@@ -229,6 +235,7 @@ const StorageEntryRow: React.FC<{
|
|
|
229
235
|
namespace: string;
|
|
230
236
|
protocol: string;
|
|
231
237
|
rootPath: string;
|
|
238
|
+
backendType: StorageNamespace["backendType"];
|
|
232
239
|
depth: number;
|
|
233
240
|
locale: string;
|
|
234
241
|
searchValue: string;
|
|
@@ -238,6 +245,7 @@ const StorageEntryRow: React.FC<{
|
|
|
238
245
|
namespace,
|
|
239
246
|
protocol,
|
|
240
247
|
rootPath,
|
|
248
|
+
backendType,
|
|
241
249
|
depth,
|
|
242
250
|
locale,
|
|
243
251
|
searchValue,
|
|
@@ -245,6 +253,7 @@ const StorageEntryRow: React.FC<{
|
|
|
245
253
|
}) => {
|
|
246
254
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
247
255
|
const { entriesByPath } = useStorage();
|
|
256
|
+
const addCodeToNewCell = useAddCodeToNewCell();
|
|
248
257
|
const isDir = entry.kind === "directory";
|
|
249
258
|
const name = displayName(entry.path);
|
|
250
259
|
const hasSearch = !!searchValue.trim();
|
|
@@ -361,13 +370,12 @@ const StorageEntryRow: React.FC<{
|
|
|
361
370
|
)}
|
|
362
371
|
<DropdownMenuItem
|
|
363
372
|
onSelect={async () => {
|
|
364
|
-
|
|
365
|
-
await copyToClipboard(url.toString());
|
|
373
|
+
await copyToClipboard(entry.path);
|
|
366
374
|
toast({ title: "Copied to clipboard" });
|
|
367
375
|
}}
|
|
368
376
|
>
|
|
369
377
|
<CopyIcon className={MENU_ITEM_ICON_CLASS} />
|
|
370
|
-
Copy
|
|
378
|
+
Copy path
|
|
371
379
|
</DropdownMenuItem>
|
|
372
380
|
{!isDir && (
|
|
373
381
|
<DropdownMenuItem onSelect={() => handleDownload()}>
|
|
@@ -375,6 +383,28 @@ const StorageEntryRow: React.FC<{
|
|
|
375
383
|
Download
|
|
376
384
|
</DropdownMenuItem>
|
|
377
385
|
)}
|
|
386
|
+
<DropdownMenuSeparator />
|
|
387
|
+
{STORAGE_SNIPPETS.map((snippet) => {
|
|
388
|
+
const code = snippet.getCode({
|
|
389
|
+
variableName: namespace,
|
|
390
|
+
protocol,
|
|
391
|
+
entry,
|
|
392
|
+
backendType,
|
|
393
|
+
});
|
|
394
|
+
if (code === null) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
const Icon = snippet.icon;
|
|
398
|
+
return (
|
|
399
|
+
<DropdownMenuItem
|
|
400
|
+
key={snippet.id}
|
|
401
|
+
onSelect={() => addCodeToNewCell(code)}
|
|
402
|
+
>
|
|
403
|
+
<Icon className={MENU_ITEM_ICON_CLASS} />
|
|
404
|
+
{snippet.label}
|
|
405
|
+
</DropdownMenuItem>
|
|
406
|
+
);
|
|
407
|
+
})}
|
|
378
408
|
</DropdownMenuContent>
|
|
379
409
|
</DropdownMenu>
|
|
380
410
|
</div>
|
|
@@ -384,6 +414,7 @@ const StorageEntryRow: React.FC<{
|
|
|
384
414
|
namespace={namespace}
|
|
385
415
|
protocol={protocol}
|
|
386
416
|
rootPath={rootPath}
|
|
417
|
+
backendType={backendType}
|
|
387
418
|
prefix={entry.path}
|
|
388
419
|
depth={depth + 1}
|
|
389
420
|
locale={locale}
|
|
@@ -498,6 +529,7 @@ const StorageNamespaceSection: React.FC<{
|
|
|
498
529
|
namespace={namespaceName}
|
|
499
530
|
protocol={namespace.protocol}
|
|
500
531
|
rootPath={namespace.rootPath}
|
|
532
|
+
backendType={namespace.backendType}
|
|
501
533
|
depth={1}
|
|
502
534
|
locale={locale}
|
|
503
535
|
searchValue={searchValue}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { LucideIcon } from "lucide-react";
|
|
4
|
+
import { BookOpenIcon, LinkIcon } from "lucide-react";
|
|
5
|
+
import type { StorageEntry, StorageNamespace } from "@/core/storage/types";
|
|
6
|
+
|
|
7
|
+
type BackendType = StorageNamespace["backendType"];
|
|
8
|
+
|
|
9
|
+
export interface StorageSnippetContext {
|
|
10
|
+
variableName: string;
|
|
11
|
+
protocol: string;
|
|
12
|
+
entry: StorageEntry;
|
|
13
|
+
backendType: BackendType;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StorageSnippet {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
icon: LucideIcon;
|
|
20
|
+
/** Return the code string, or null to hide the snippet for this context. */
|
|
21
|
+
getCode: (ctx: StorageSnippetContext) => string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const NOT_SIGNABLE_PROTOCOLS = new Set(["http", "file", "in-memory"]);
|
|
25
|
+
|
|
26
|
+
function escapeForPythonString(value: string): string {
|
|
27
|
+
return JSON.stringify(value).slice(1, -1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const STORAGE_SNIPPETS: StorageSnippet[] = [
|
|
31
|
+
{
|
|
32
|
+
id: "read-file",
|
|
33
|
+
label: "Insert read snippet",
|
|
34
|
+
icon: BookOpenIcon,
|
|
35
|
+
getCode: (ctx) => {
|
|
36
|
+
if (ctx.entry.kind === "directory") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const path = escapeForPythonString(ctx.entry.path);
|
|
40
|
+
if (ctx.backendType === "obstore") {
|
|
41
|
+
return `_data = ${ctx.variableName}.get("${path}").bytes()\n_data`;
|
|
42
|
+
}
|
|
43
|
+
return `_data = ${ctx.variableName}.cat_file("${path}")\n_data`;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "download-file",
|
|
48
|
+
label: "Insert download snippet",
|
|
49
|
+
icon: LinkIcon,
|
|
50
|
+
getCode: (ctx) => {
|
|
51
|
+
if (ctx.entry.kind === "directory") {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const path = escapeForPythonString(ctx.entry.path);
|
|
55
|
+
if (ctx.backendType === "obstore") {
|
|
56
|
+
if (NOT_SIGNABLE_PROTOCOLS.has(ctx.protocol)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return `from datetime import timedelta\nfrom obstore import sign\n\nsigned_url = sign(\n ${ctx.variableName}, "GET", "${path}",\n expires_in=timedelta(hours=1),\n)\nsigned_url`;
|
|
60
|
+
}
|
|
61
|
+
const filename = escapeForPythonString(
|
|
62
|
+
ctx.entry.path.split("/").pop() || "download",
|
|
63
|
+
);
|
|
64
|
+
return `${ctx.variableName}.get("${path}", "${filename}")`;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
];
|
|
@@ -10,6 +10,7 @@ function makeNamespace(
|
|
|
10
10
|
overrides: Partial<StorageNamespace> & { name: string },
|
|
11
11
|
): StorageNamespace {
|
|
12
12
|
return {
|
|
13
|
+
backendType: overrides.backendType ?? "obstore",
|
|
13
14
|
displayName: overrides.displayName ?? overrides.name,
|
|
14
15
|
name: overrides.name,
|
|
15
16
|
protocol: overrides.protocol ?? "s3",
|