@lobb-js/lobb-ext-storage 0.15.1 → 0.16.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/dist/index.js +2 -2
- package/dist/lib/components/FilePickerField.svelte +48 -0
- package/dist/lib/components/FilePickerField.svelte.d.ts +14 -0
- package/dist/lib/components/fileExplorer.svelte +6 -7
- package/dist/lib/components/fileIcon.svelte +1 -1
- package/extensions/storage/index.ts +2 -1
- package/extensions/storage/migrations.ts +4 -56
- package/extensions/storage/studio/index.ts +2 -2
- package/extensions/storage/studio/lib/components/FilePickerField.svelte +48 -0
- package/extensions/storage/studio/lib/components/fileExplorer.svelte +6 -7
- package/extensions/storage/studio/lib/components/fileIcon.svelte +1 -1
- package/extensions/storage/utils.ts +0 -16
- package/extensions/storage/workflows/actionController.ts +87 -0
- package/extensions/storage/workflows/{services.ts → actions.ts} +104 -134
- package/extensions/storage/workflows/index.ts +2 -4
- package/package.json +3 -3
- package/dist/lib/components/foreignKeyComponent.svelte +0 -44
- package/dist/lib/components/foreignKeyComponent.svelte.d.ts +0 -14
- package/extensions/storage/studio/lib/components/foreignKeyComponent.svelte +0 -44
- package/extensions/storage/types.ts +0 -11
- package/extensions/storage/workflows/controllers.ts +0 -103
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import FileExplorerPage from "./lib/components/pages/fileExplorerPage.svelte";
|
|
2
|
-
import
|
|
2
|
+
import FilePickerField from "./lib/components/FilePickerField.svelte";
|
|
3
3
|
import ChildrenFileExplorer from "./lib/components/childrenFileExplorer.svelte";
|
|
4
4
|
import ExplorerNotSupported from "./lib/components/explorerNotSupported.svelte";
|
|
5
5
|
export default function extension(utils) {
|
|
@@ -7,7 +7,7 @@ export default function extension(utils) {
|
|
|
7
7
|
name: "storage",
|
|
8
8
|
components: {
|
|
9
9
|
"pages.file_manager": FileExplorerPage,
|
|
10
|
-
"detailView.fields.foreignKey.storage_fs":
|
|
10
|
+
"detailView.fields.foreignKey.storage_fs": FilePickerField,
|
|
11
11
|
"detailView.create.subRecords.storage_fs": ExplorerNotSupported,
|
|
12
12
|
"detailView.update.subRecords.storage_fs": ChildrenFileExplorer,
|
|
13
13
|
"listView.entry.children.storage_fs": ChildrenFileExplorer,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "@lobb-js/studio";
|
|
3
|
+
import FileExplorer, { type Entry } from "./fileExplorer.svelte";
|
|
4
|
+
import FileIcon from "./fileIcon.svelte";
|
|
5
|
+
|
|
6
|
+
interface Props extends ExtensionProps {
|
|
7
|
+
value: Entry;
|
|
8
|
+
header?: any;
|
|
9
|
+
isDisabled?: boolean;
|
|
10
|
+
errorMessages?: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { utils, value = $bindable(), header, isDisabled = false, errorMessages = [], ...props }: Props = $props();
|
|
14
|
+
|
|
15
|
+
const FilePlus2 = $derived(utils.components.Icons.FilePlus2);
|
|
16
|
+
const Drawer = $derived(utils.components.Drawer);
|
|
17
|
+
const FieldWrapper = $derived(utils.components.FieldWrapper);
|
|
18
|
+
|
|
19
|
+
let openDrawer = $state(false);
|
|
20
|
+
|
|
21
|
+
async function onHideHandle() {
|
|
22
|
+
openDrawer = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function handleOnFileSelect(entry: Entry) {
|
|
26
|
+
value = entry;
|
|
27
|
+
openDrawer = false;
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<FieldWrapper span={2} {isDisabled} {errorMessages} {header}>
|
|
32
|
+
{#snippet children()}
|
|
33
|
+
<button onclick={() => openDrawer = true} class="flex flex-col items-center gap-2 w-full border p-4 rounded-md bg-muted">
|
|
34
|
+
{#if value}
|
|
35
|
+
<FileIcon entry={value} {utils} />
|
|
36
|
+
{:else}
|
|
37
|
+
<FilePlus2 class="text-muted-foreground" />
|
|
38
|
+
<div class="text-sm text-muted-foreground">Click to add an asset</div>
|
|
39
|
+
{/if}
|
|
40
|
+
</button>
|
|
41
|
+
|
|
42
|
+
{#if openDrawer}
|
|
43
|
+
<Drawer onHide={onHideHandle}>
|
|
44
|
+
<FileExplorer onFileSelect={handleOnFileSelect} utils={utils} {...props} />
|
|
45
|
+
</Drawer>
|
|
46
|
+
{/if}
|
|
47
|
+
{/snippet}
|
|
48
|
+
</FieldWrapper>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: Record<string, never>;
|
|
4
|
+
events: {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
};
|
|
7
|
+
slots: {};
|
|
8
|
+
};
|
|
9
|
+
export type FilePickerFieldProps = typeof __propDef.props;
|
|
10
|
+
export type FilePickerFieldEvents = typeof __propDef.events;
|
|
11
|
+
export type FilePickerFieldSlots = typeof __propDef.slots;
|
|
12
|
+
export default class FilePickerField extends SvelteComponentTyped<FilePickerFieldProps, FilePickerFieldEvents, FilePickerFieldSlots> {
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
contextMenuOpen = false;
|
|
67
67
|
const newFolderName = prompt("Please enter a name for the new folder:");
|
|
68
68
|
if (newFolderName) {
|
|
69
|
-
const response = await utils.lobb.
|
|
69
|
+
const response = await utils.lobb.runAction("storage_create", {
|
|
70
70
|
data: { name: newFolderName, path: location, type: "directory", ...foreignKeyObject },
|
|
71
71
|
});
|
|
72
72
|
const result = await response.json();
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
|
|
90
90
|
async function uploadFile(file: File) {
|
|
91
91
|
if (file) {
|
|
92
|
-
const response = await utils.lobb.
|
|
93
|
-
"
|
|
92
|
+
const response = await utils.lobb.runAction(
|
|
93
|
+
"storage_create",
|
|
94
94
|
{ data: { path: location, ...foreignKeyObject } },
|
|
95
95
|
file,
|
|
96
96
|
);
|
|
@@ -133,10 +133,9 @@
|
|
|
133
133
|
if (!entryId) {
|
|
134
134
|
throw new Error("The id of the entry is (null) for some reason");
|
|
135
135
|
}
|
|
136
|
-
const response = await utils.lobb.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
);
|
|
136
|
+
const response = await utils.lobb.runAction("storage_delete", {
|
|
137
|
+
id: String(entryId),
|
|
138
|
+
});
|
|
140
139
|
const result = await response.json();
|
|
141
140
|
if (result.status >= 400) {
|
|
142
141
|
return;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
class="pointer-events-none flex flex-col items-center gap-2 min-w-28"
|
|
17
17
|
>
|
|
18
18
|
{#if entry.file_mime_type.startsWith("image/")}
|
|
19
|
-
<div class="rounded-md w-24 h-24 bg-center bg-contain bg-no-repeat" style="background-image: url('{utils.lobb.lobbUrl}/api/
|
|
19
|
+
<div class="rounded-md w-24 h-24 bg-center bg-contain bg-no-repeat" style="background-image: url('{utils.lobb.lobbUrl}/api/actions/storage_read?id={entry.id}&action=view');"></div>
|
|
20
20
|
{:else}
|
|
21
21
|
<div
|
|
22
22
|
class="pointer-events-none relative"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Extension } from "@lobb-js/core";
|
|
2
2
|
import type { ExtensionConfig } from "./config/extensionConfigSchema.ts";
|
|
3
|
-
export type { StorageFsCreateOne, StorageFsFindOne } from "./types.ts";
|
|
4
3
|
|
|
5
4
|
import packageJson from "../../package.json" with { type: "json" };
|
|
6
5
|
import { collections } from "./collections/index.ts";
|
|
@@ -8,6 +7,7 @@ import { meta } from "./meta.ts";
|
|
|
8
7
|
import { migrations } from "./migrations.ts";
|
|
9
8
|
import { openapi } from "./openapi.ts";
|
|
10
9
|
import { getWorkflows } from "./workflows/index.ts";
|
|
10
|
+
import { getStorageActions } from "./workflows/actions.ts";
|
|
11
11
|
|
|
12
12
|
export default function storage(extensionConfig: ExtensionConfig): Extension {
|
|
13
13
|
return {
|
|
@@ -15,6 +15,7 @@ export default function storage(extensionConfig: ExtensionConfig): Extension {
|
|
|
15
15
|
name: "storage",
|
|
16
16
|
icon: "Folders",
|
|
17
17
|
collections: (_lobb: any) => collections(extensionConfig),
|
|
18
|
+
actions: getStorageActions(extensionConfig),
|
|
18
19
|
migrations: migrations,
|
|
19
20
|
meta: meta,
|
|
20
21
|
workflows: getWorkflows(extensionConfig),
|
|
@@ -1,59 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Backfill `parent_id` for any row in storage_fs that still has it null.
|
|
5
|
-
* Resolves the parent by walking the existing `path` string and matching
|
|
6
|
-
* the (parent_path, name) of every directory record. Idempotent — running
|
|
7
|
-
* it twice is safe; rows that already have parent_id are skipped.
|
|
8
|
-
*/
|
|
9
|
-
export async function backfillStorageFsParentId(lobb: Lobb): Promise<void> {
|
|
10
|
-
const rows = (await lobb.collectionService.findAll({
|
|
11
|
-
collectionName: "storage_fs",
|
|
12
|
-
params: { limit: 1000000 },
|
|
13
|
-
})).data;
|
|
14
|
-
if (rows.length === 0) return;
|
|
15
|
-
|
|
16
|
-
// Index every directory by (path, name) so we can resolve any child's
|
|
17
|
-
// parent in one lookup. This is the same key that resolveParentIdFromPath
|
|
18
|
-
// walks at runtime, just precomputed.
|
|
19
|
-
const dirByKey = new Map<string, number>();
|
|
20
|
-
for (const r of rows as any[]) {
|
|
21
|
-
if (r.type === "directory") {
|
|
22
|
-
dirByKey.set(`${r.path}::${r.name}`, r.id);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function parentIdFor(row: any): number | null {
|
|
27
|
-
const path: string = row.path ?? "/";
|
|
28
|
-
if (path === "/" || path === "") return null;
|
|
29
|
-
const segments = path.split("/").filter(Boolean);
|
|
30
|
-
if (segments.length === 0) return null;
|
|
31
|
-
const lastName = segments[segments.length - 1];
|
|
32
|
-
// No-trailing-slash convention: root is "/", deeper levels are
|
|
33
|
-
// "/foo" or "/foo/bar". Older data may have either form; this
|
|
34
|
-
// resolves only the normalized one. Loose-format rows get retried
|
|
35
|
-
// on the next pass.
|
|
36
|
-
const parentDirPath =
|
|
37
|
-
segments.length > 1 ? "/" + segments.slice(0, -1).join("/") : "/";
|
|
38
|
-
const key = `${parentDirPath}::${lastName}`;
|
|
39
|
-
return dirByKey.get(key) ?? null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for (const row of rows as any[]) {
|
|
43
|
-
if (row.parent_id != null) continue;
|
|
44
|
-
const pid = parentIdFor(row);
|
|
45
|
-
if (pid != null) {
|
|
46
|
-
await lobb.collectionStore.updateOne({
|
|
47
|
-
collectionName: "storage_fs",
|
|
48
|
-
id: row.id,
|
|
49
|
-
data: { parent_id: pid },
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
import type { Migrations } from "@lobb-js/core";
|
|
54
2
|
|
|
55
3
|
// No built-in migrations for the storage extension. The `parent_id` column
|
|
56
|
-
// is set on every new write by the service;
|
|
57
|
-
// that
|
|
58
|
-
//
|
|
4
|
+
// is set on every new write by the service; no backfill ships with the
|
|
5
|
+
// package — host apps that need to reconcile legacy rows can write their
|
|
6
|
+
// own migration against `storage_fs`.
|
|
59
7
|
export const migrations: Migrations = {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Extension, ExtensionUtils } from "@lobb-js/studio";
|
|
2
2
|
|
|
3
3
|
import FileExplorerPage from "./lib/components/pages/fileExplorerPage.svelte";
|
|
4
|
-
import
|
|
4
|
+
import FilePickerField from "./lib/components/FilePickerField.svelte";
|
|
5
5
|
import ChildrenFileExplorer from "./lib/components/childrenFileExplorer.svelte";
|
|
6
6
|
import ExplorerNotSupported from "./lib/components/explorerNotSupported.svelte";
|
|
7
7
|
|
|
@@ -10,7 +10,7 @@ export default function extension(utils: ExtensionUtils): Extension {
|
|
|
10
10
|
name: "storage",
|
|
11
11
|
components: {
|
|
12
12
|
"pages.file_manager": FileExplorerPage,
|
|
13
|
-
"detailView.fields.foreignKey.storage_fs":
|
|
13
|
+
"detailView.fields.foreignKey.storage_fs": FilePickerField,
|
|
14
14
|
"detailView.create.subRecords.storage_fs": ExplorerNotSupported,
|
|
15
15
|
"detailView.update.subRecords.storage_fs": ChildrenFileExplorer,
|
|
16
16
|
"listView.entry.children.storage_fs": ChildrenFileExplorer,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "@lobb-js/studio";
|
|
3
|
+
import FileExplorer, { type Entry } from "./fileExplorer.svelte";
|
|
4
|
+
import FileIcon from "./fileIcon.svelte";
|
|
5
|
+
|
|
6
|
+
interface Props extends ExtensionProps {
|
|
7
|
+
value: Entry;
|
|
8
|
+
header?: any;
|
|
9
|
+
isDisabled?: boolean;
|
|
10
|
+
errorMessages?: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { utils, value = $bindable(), header, isDisabled = false, errorMessages = [], ...props }: Props = $props();
|
|
14
|
+
|
|
15
|
+
const FilePlus2 = $derived(utils.components.Icons.FilePlus2);
|
|
16
|
+
const Drawer = $derived(utils.components.Drawer);
|
|
17
|
+
const FieldWrapper = $derived(utils.components.FieldWrapper);
|
|
18
|
+
|
|
19
|
+
let openDrawer = $state(false);
|
|
20
|
+
|
|
21
|
+
async function onHideHandle() {
|
|
22
|
+
openDrawer = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function handleOnFileSelect(entry: Entry) {
|
|
26
|
+
value = entry;
|
|
27
|
+
openDrawer = false;
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<FieldWrapper span={2} {isDisabled} {errorMessages} {header}>
|
|
32
|
+
{#snippet children()}
|
|
33
|
+
<button onclick={() => openDrawer = true} class="flex flex-col items-center gap-2 w-full border p-4 rounded-md bg-muted">
|
|
34
|
+
{#if value}
|
|
35
|
+
<FileIcon entry={value} {utils} />
|
|
36
|
+
{:else}
|
|
37
|
+
<FilePlus2 class="text-muted-foreground" />
|
|
38
|
+
<div class="text-sm text-muted-foreground">Click to add an asset</div>
|
|
39
|
+
{/if}
|
|
40
|
+
</button>
|
|
41
|
+
|
|
42
|
+
{#if openDrawer}
|
|
43
|
+
<Drawer onHide={onHideHandle}>
|
|
44
|
+
<FileExplorer onFileSelect={handleOnFileSelect} utils={utils} {...props} />
|
|
45
|
+
</Drawer>
|
|
46
|
+
{/if}
|
|
47
|
+
{/snippet}
|
|
48
|
+
</FieldWrapper>
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
contextMenuOpen = false;
|
|
67
67
|
const newFolderName = prompt("Please enter a name for the new folder:");
|
|
68
68
|
if (newFolderName) {
|
|
69
|
-
const response = await utils.lobb.
|
|
69
|
+
const response = await utils.lobb.runAction("storage_create", {
|
|
70
70
|
data: { name: newFolderName, path: location, type: "directory", ...foreignKeyObject },
|
|
71
71
|
});
|
|
72
72
|
const result = await response.json();
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
|
|
90
90
|
async function uploadFile(file: File) {
|
|
91
91
|
if (file) {
|
|
92
|
-
const response = await utils.lobb.
|
|
93
|
-
"
|
|
92
|
+
const response = await utils.lobb.runAction(
|
|
93
|
+
"storage_create",
|
|
94
94
|
{ data: { path: location, ...foreignKeyObject } },
|
|
95
95
|
file,
|
|
96
96
|
);
|
|
@@ -133,10 +133,9 @@
|
|
|
133
133
|
if (!entryId) {
|
|
134
134
|
throw new Error("The id of the entry is (null) for some reason");
|
|
135
135
|
}
|
|
136
|
-
const response = await utils.lobb.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
);
|
|
136
|
+
const response = await utils.lobb.runAction("storage_delete", {
|
|
137
|
+
id: String(entryId),
|
|
138
|
+
});
|
|
140
139
|
const result = await response.json();
|
|
141
140
|
if (result.status >= 400) {
|
|
142
141
|
return;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
class="pointer-events-none flex flex-col items-center gap-2 min-w-28"
|
|
17
17
|
>
|
|
18
18
|
{#if entry.file_mime_type.startsWith("image/")}
|
|
19
|
-
<div class="rounded-md w-24 h-24 bg-center bg-contain bg-no-repeat" style="background-image: url('{utils.lobb.lobbUrl}/api/
|
|
19
|
+
<div class="rounded-md w-24 h-24 bg-center bg-contain bg-no-repeat" style="background-image: url('{utils.lobb.lobbUrl}/api/actions/storage_read?id={entry.id}&action=view');"></div>
|
|
20
20
|
{:else}
|
|
21
21
|
<div
|
|
22
22
|
class="pointer-events-none relative"
|
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
import type { Context } from "hono";
|
|
2
1
|
import type { Lobb } from "@lobb-js/core";
|
|
3
2
|
import { LobbError } from "@lobb-js/core";
|
|
4
3
|
|
|
5
|
-
export async function getFileFromRequestBody(
|
|
6
|
-
c: Context,
|
|
7
|
-
): Promise<File | undefined> {
|
|
8
|
-
const contentType = c.req.header("Content-Type");
|
|
9
|
-
if (contentType?.startsWith("multipart/form-data")) {
|
|
10
|
-
const formData = await c.req.formData();
|
|
11
|
-
const entry = formData.get("file");
|
|
12
|
-
if (entry instanceof File) {
|
|
13
|
-
return entry;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
// return undefined if no file or wrong content type
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
4
|
// ── parent_id / path helpers ──────────────────────────────────────────────
|
|
21
5
|
//
|
|
22
6
|
// The storage_fs table uses parent_id as the source of truth for the
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Workflow } from "@lobb-js/core";
|
|
2
|
+
import type { Context } from "hono";
|
|
3
|
+
|
|
4
|
+
// The two storage operations that need the HTTP layer (a multipart file, or a
|
|
5
|
+
// binary response) are served by adapting the *action* endpoints rather than
|
|
6
|
+
// touching the collection CRUD routes (which stay clean and predictable).
|
|
7
|
+
//
|
|
8
|
+
// POST /api/actions/storage_create (multipart) → upload a file
|
|
9
|
+
// GET /api/actions/storage_read?action=view|download → stream file bytes
|
|
10
|
+
//
|
|
11
|
+
// Everything else flows through the default JSON action handling.
|
|
12
|
+
|
|
13
|
+
export function getActionControllerWorkflows(): Workflow[] {
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
name: "storageActionController",
|
|
17
|
+
eventName: "core.actions.controller.override",
|
|
18
|
+
handler: async (input, ctx) => {
|
|
19
|
+
const name = input.name as string;
|
|
20
|
+
const context = input.context as Context;
|
|
21
|
+
|
|
22
|
+
// Multipart upload → pull the File off the request and pass it to the action.
|
|
23
|
+
if (name === "storage_create") {
|
|
24
|
+
const contentType = context.req.header("Content-Type");
|
|
25
|
+
if (!contentType?.startsWith("multipart/form-data")) return; // JSON create → default flow
|
|
26
|
+
|
|
27
|
+
const force = context.req.query("force") === "true";
|
|
28
|
+
const body = await context.req.parseBody({ all: true });
|
|
29
|
+
const fileEntry = body["file"];
|
|
30
|
+
|
|
31
|
+
if (!fileEntry) {
|
|
32
|
+
throw new ctx.LobbError({ code: "BAD_REQUEST", message: "No files were provided." });
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(fileEntry)) {
|
|
35
|
+
throw new ctx.LobbError({ code: "BAD_REQUEST", message: "Only one file can be uploaded at a time." });
|
|
36
|
+
}
|
|
37
|
+
if (!(fileEntry instanceof File)) {
|
|
38
|
+
throw new ctx.LobbError({ code: "BAD_REQUEST", message: "The file shouldnt be a string" });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const extraData: Record<string, string> = {};
|
|
42
|
+
for (const [key, val] of Object.entries(body)) {
|
|
43
|
+
if (key !== "file" && key !== "payload" && typeof val === "string") {
|
|
44
|
+
extraData[key] = val;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await ctx.lobb.runAction("storage_create", {
|
|
49
|
+
file: fileEntry,
|
|
50
|
+
force,
|
|
51
|
+
data: extraData,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return context.json(result, 201);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// GET file view/download → stream raw bytes (browser-embeddable).
|
|
58
|
+
if (name === "storage_read") {
|
|
59
|
+
const action = context.req.query("action");
|
|
60
|
+
if (action !== "view" && action !== "download") return; // non-binary read → default flow
|
|
61
|
+
|
|
62
|
+
const id = context.req.query("id");
|
|
63
|
+
if (!id) {
|
|
64
|
+
throw new ctx.LobbError({ code: "BAD_REQUEST", message: "The (id) query param is required." });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = await ctx.lobb.runAction("storage_read", { id, readFile: true });
|
|
68
|
+
|
|
69
|
+
if (!result.data) {
|
|
70
|
+
throw new ctx.LobbError({ code: "NOT_FOUND", message: "The record is not found" });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const headers: Record<string, string> = {
|
|
74
|
+
"Content-Type": result.data.file_mime_type,
|
|
75
|
+
};
|
|
76
|
+
if (action === "download") {
|
|
77
|
+
headers["Content-Disposition"] = `attachment; filename="${result.data.name}"`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return new Response(result.file.buffer as ArrayBuffer, { status: 200, headers });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ActionsConfig } from "@lobb-js/core";
|
|
2
2
|
import { LobbError } from "@lobb-js/core";
|
|
3
3
|
import type { ExtensionConfig } from "../config/extensionConfigSchema.ts";
|
|
4
4
|
import { initStorageAdapter } from "../adapters/index.ts";
|
|
@@ -9,43 +9,20 @@ import {
|
|
|
9
9
|
resolveParentIdFromPath,
|
|
10
10
|
} from "../utils.ts";
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// The storage_fs hierarchy invariant — `path === parent.path + parent.name` —
|
|
13
|
+
// is maintained by these actions on every write. `parent_id` is the source of
|
|
14
|
+
// truth; `path` is a derived cache for fast subtree queries and breadcrumbs.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
export function getServicesWorkflows(extensionConfig: ExtensionConfig): Workflow[] {
|
|
22
|
-
return [
|
|
23
|
-
{
|
|
24
|
-
name: "storageFindOneService",
|
|
25
|
-
eventName: "core.service.findOne.override",
|
|
26
|
-
handler: async (input, ctx) => {
|
|
27
|
-
if (input.collectionName === "storage_fs") {
|
|
28
|
-
const result = await ctx.lobb.collectionStore.findOne({
|
|
29
|
-
collectionName: "storage_fs",
|
|
30
|
-
id: input.id,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
if (result.data?.type === "file" && input.readFile) {
|
|
34
|
-
const adapter = initStorageAdapter(extensionConfig);
|
|
35
|
-
const file = await adapter.readFile(result.data.id);
|
|
36
|
-
return { ...result, file: new Uint8Array(file) };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
name: "storageCreateOneServiceOverride",
|
|
45
|
-
eventName: "core.service.createOne.override",
|
|
46
|
-
handler: async (input, ctx) => {
|
|
47
|
-
if (input.collectionName !== "storage_fs") return;
|
|
16
|
+
// Complex/multi-step operations (upload, create, rename/move, cascade delete)
|
|
17
|
+
// live here as actions. Reads stay free on the collection routes; only file
|
|
18
|
+
// view/download and multipart upload are adapted from the collection route into
|
|
19
|
+
// `storage_read` / `storage_create` by the controller workflows.
|
|
48
20
|
|
|
21
|
+
export function getStorageActions(extensionConfig: ExtensionConfig): ActionsConfig {
|
|
22
|
+
return {
|
|
23
|
+
// Create a file (when `file` is present) or a directory / metadata record.
|
|
24
|
+
storage_create: {
|
|
25
|
+
handler: async (input: any, ctx) => {
|
|
49
26
|
const raw = input.data ?? {};
|
|
50
27
|
const explicitParentId = raw.parent_id ?? null;
|
|
51
28
|
const pathInput: string = raw.path ?? "/";
|
|
@@ -89,8 +66,7 @@ export function getServicesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
89
66
|
return result;
|
|
90
67
|
}
|
|
91
68
|
|
|
92
|
-
// Non-file writes (directories or pure metadata records)
|
|
93
|
-
// generic store handle it after we stamp parent_id + path.
|
|
69
|
+
// Non-file writes (directories or pure metadata records).
|
|
94
70
|
return await ctx.lobb.collectionStore.createOne({
|
|
95
71
|
collectionName: "storage_fs",
|
|
96
72
|
data: {
|
|
@@ -105,99 +81,28 @@ export function getServicesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
105
81
|
});
|
|
106
82
|
},
|
|
107
83
|
},
|
|
108
|
-
{
|
|
109
|
-
name: "storageDeleteOneService",
|
|
110
|
-
eventName: "core.service.deleteOne.override",
|
|
111
|
-
handler: async (input, ctx) => {
|
|
112
|
-
if (input.collectionName !== "storage_fs") return;
|
|
113
|
-
|
|
114
|
-
const entry = (await ctx.lobb.collectionStore.findOne({
|
|
115
|
-
collectionName: "storage_fs",
|
|
116
|
-
id: input.id,
|
|
117
|
-
})).data;
|
|
118
|
-
if (!entry) {
|
|
119
|
-
return ctx.lobb.collectionStore.deleteOne({
|
|
120
|
-
collectionName: "storage_fs",
|
|
121
|
-
id: input.id,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const adapter = initStorageAdapter(extensionConfig);
|
|
126
|
-
|
|
127
|
-
if (entry.type === "file") {
|
|
128
|
-
await adapter.removeFile(entry.id);
|
|
129
|
-
} else if (entry.type === "directory") {
|
|
130
|
-
// Cascade leaf-first so each deleted row has no remaining children
|
|
131
|
-
// referencing it — Lobb's core FK-children check would otherwise
|
|
132
|
-
// block the delete. findDescendants returns BFS (shallow → deep);
|
|
133
|
-
// reverse to walk deep → shallow.
|
|
134
|
-
const descendants = await findDescendants(entry.id, ctx.lobb);
|
|
135
|
-
for (const d of [...descendants].reverse()) {
|
|
136
|
-
if (d.type === "file") {
|
|
137
|
-
await adapter.removeFile(d.id);
|
|
138
|
-
}
|
|
139
|
-
await ctx.lobb.collectionStore.deleteOne({
|
|
140
|
-
collectionName: "storage_fs",
|
|
141
|
-
id: String(d.id),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
84
|
|
|
146
|
-
|
|
85
|
+
// Read a record, optionally with the file bytes (for view/download).
|
|
86
|
+
storage_read: {
|
|
87
|
+
handler: async (input: any, ctx) => {
|
|
88
|
+
const result = await ctx.lobb.collectionStore.findOne({
|
|
147
89
|
collectionName: "storage_fs",
|
|
148
90
|
id: input.id,
|
|
149
91
|
});
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
name: "storageDeleteManyService",
|
|
154
|
-
eventName: "core.service.deleteMany.override",
|
|
155
|
-
handler: async (input, ctx) => {
|
|
156
|
-
if (input.collectionName !== "storage_fs") return;
|
|
157
|
-
|
|
158
|
-
const matched = (await ctx.lobb.collectionStore.findAll({
|
|
159
|
-
collectionName: "storage_fs",
|
|
160
|
-
params: { filter: input.filter },
|
|
161
|
-
})).data;
|
|
162
92
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const ordered: any[] = [];
|
|
168
|
-
const seen = new Set<number>();
|
|
169
|
-
for (const entry of matched) {
|
|
170
|
-
if (seen.has(entry.id)) continue;
|
|
171
|
-
seen.add(entry.id);
|
|
172
|
-
ordered.push(entry);
|
|
173
|
-
if (entry.type === "directory") {
|
|
174
|
-
for (const d of await findDescendants(entry.id, ctx.lobb)) {
|
|
175
|
-
if (seen.has(d.id)) continue;
|
|
176
|
-
seen.add(d.id);
|
|
177
|
-
ordered.push(d);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
for (const row of [...ordered].reverse()) {
|
|
183
|
-
if (row.type === "file") await adapter.removeFile(row.id);
|
|
184
|
-
await ctx.lobb.collectionStore.deleteOne({
|
|
185
|
-
collectionName: "storage_fs",
|
|
186
|
-
id: String(row.id),
|
|
187
|
-
});
|
|
93
|
+
if (result.data?.type === "file" && input.readFile) {
|
|
94
|
+
const adapter = initStorageAdapter(extensionConfig);
|
|
95
|
+
const file = await adapter.readFile(result.data.id);
|
|
96
|
+
return { ...result, file: new Uint8Array(file) };
|
|
188
97
|
}
|
|
189
98
|
|
|
190
|
-
|
|
191
|
-
// matched rows). Cascaded descendants are an implementation detail.
|
|
192
|
-
return { affectedCount: matched.length };
|
|
99
|
+
return result;
|
|
193
100
|
},
|
|
194
101
|
},
|
|
195
|
-
{
|
|
196
|
-
name: "storageUpdateOneService",
|
|
197
|
-
eventName: "core.service.updateOne.override",
|
|
198
|
-
handler: async (input, ctx) => {
|
|
199
|
-
if (input.collectionName !== "storage_fs") return;
|
|
200
102
|
|
|
103
|
+
// Rename / move / update metadata, cascading `path` to all descendants.
|
|
104
|
+
storage_update: {
|
|
105
|
+
handler: async (input: any, ctx) => {
|
|
201
106
|
const current = (await ctx.lobb.collectionStore.findOne({
|
|
202
107
|
collectionName: "storage_fs",
|
|
203
108
|
id: input.id,
|
|
@@ -212,17 +117,13 @@ export function getServicesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
212
117
|
|
|
213
118
|
const data: any = { ...input.data };
|
|
214
119
|
|
|
215
|
-
// Translate any path-based reparenting into a parent_id update
|
|
216
|
-
// the rest of this handler has a single canonical signal to work
|
|
217
|
-
// with. Explicit parent_id wins if both are present.
|
|
120
|
+
// Translate any path-based reparenting into a parent_id update.
|
|
218
121
|
if (data.path != null && data.parent_id == null) {
|
|
219
122
|
data.parent_id = await resolveParentIdFromPath(data.path, ctx.lobb);
|
|
220
123
|
}
|
|
221
124
|
// The path field is derived — never let a caller overwrite it raw.
|
|
222
125
|
delete data.path;
|
|
223
126
|
|
|
224
|
-
// If the row's location in the tree is changing (name or parent),
|
|
225
|
-
// recompute its own path and cascade the change to every descendant.
|
|
226
127
|
const nameChanged = data.name != null && data.name !== current.name;
|
|
227
128
|
const parentChanged =
|
|
228
129
|
Object.prototype.hasOwnProperty.call(data, "parent_id") &&
|
|
@@ -232,8 +133,7 @@ export function getServicesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
232
133
|
const newParentId = parentChanged ? (data.parent_id ?? null) : current.parent_id;
|
|
233
134
|
const newName = nameChanged ? data.name : current.name;
|
|
234
135
|
|
|
235
|
-
// Cycle prevention
|
|
236
|
-
// and walking up from it must never hit the row's own id.
|
|
136
|
+
// Cycle prevention.
|
|
237
137
|
if (parentChanged && newParentId != null) {
|
|
238
138
|
if (Number(newParentId) === Number(input.id)) {
|
|
239
139
|
throw new LobbError({
|
|
@@ -266,15 +166,11 @@ export function getServicesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
266
166
|
data: { ...data, path: newSelfPath },
|
|
267
167
|
});
|
|
268
168
|
|
|
269
|
-
// Cascade
|
|
270
|
-
// to this row's name + its own path. Recompute by walking the
|
|
271
|
-
// tree once in memory — no DB round trip per descendant.
|
|
169
|
+
// Cascade descendant paths.
|
|
272
170
|
if (current.type === "directory") {
|
|
273
171
|
const descendants = await findDescendants(Number(input.id), ctx.lobb);
|
|
274
172
|
const byId = new Map<number, any>();
|
|
275
173
|
for (const d of descendants) byId.set(d.id, d);
|
|
276
|
-
// The directory's OWN full path (= its parent's path + its name)
|
|
277
|
-
// is what every direct child stores in `path`.
|
|
278
174
|
const newDirFullPath = newSelfPath === "/" ? "/" + newName : newSelfPath + "/" + newName;
|
|
279
175
|
const pathOf = (id: number | null): string => {
|
|
280
176
|
if (id == null) return "/";
|
|
@@ -303,5 +199,79 @@ export function getServicesWorkflows(extensionConfig: ExtensionConfig): Workflow
|
|
|
303
199
|
});
|
|
304
200
|
},
|
|
305
201
|
},
|
|
306
|
-
|
|
202
|
+
|
|
203
|
+
// Delete a file or directory, cascading leaf-first and removing files from disk.
|
|
204
|
+
storage_delete: {
|
|
205
|
+
handler: async (input: any, ctx) => {
|
|
206
|
+
const entry = (await ctx.lobb.collectionStore.findOne({
|
|
207
|
+
collectionName: "storage_fs",
|
|
208
|
+
id: input.id,
|
|
209
|
+
})).data;
|
|
210
|
+
if (!entry) {
|
|
211
|
+
return ctx.lobb.collectionStore.deleteOne({
|
|
212
|
+
collectionName: "storage_fs",
|
|
213
|
+
id: input.id,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const adapter = initStorageAdapter(extensionConfig);
|
|
218
|
+
|
|
219
|
+
if (entry.type === "file") {
|
|
220
|
+
await adapter.removeFile(entry.id);
|
|
221
|
+
} else if (entry.type === "directory") {
|
|
222
|
+
const descendants = await findDescendants(entry.id, ctx.lobb);
|
|
223
|
+
for (const d of [...descendants].reverse()) {
|
|
224
|
+
if (d.type === "file") {
|
|
225
|
+
await adapter.removeFile(d.id);
|
|
226
|
+
}
|
|
227
|
+
await ctx.lobb.collectionStore.deleteOne({
|
|
228
|
+
collectionName: "storage_fs",
|
|
229
|
+
id: String(d.id),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return ctx.lobb.collectionStore.deleteOne({
|
|
235
|
+
collectionName: "storage_fs",
|
|
236
|
+
id: input.id,
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// Delete every row matching `filter`, cascading descendants, removing files.
|
|
242
|
+
storage_delete_many: {
|
|
243
|
+
handler: async (input: any, ctx) => {
|
|
244
|
+
const matched = (await ctx.lobb.collectionStore.findAll({
|
|
245
|
+
collectionName: "storage_fs",
|
|
246
|
+
params: { filter: input.filter },
|
|
247
|
+
})).data;
|
|
248
|
+
|
|
249
|
+
const adapter = initStorageAdapter(extensionConfig);
|
|
250
|
+
const ordered: any[] = [];
|
|
251
|
+
const seen = new Set<number>();
|
|
252
|
+
for (const entry of matched) {
|
|
253
|
+
if (seen.has(entry.id)) continue;
|
|
254
|
+
seen.add(entry.id);
|
|
255
|
+
ordered.push(entry);
|
|
256
|
+
if (entry.type === "directory") {
|
|
257
|
+
for (const d of await findDescendants(entry.id, ctx.lobb)) {
|
|
258
|
+
if (seen.has(d.id)) continue;
|
|
259
|
+
seen.add(d.id);
|
|
260
|
+
ordered.push(d);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
for (const row of [...ordered].reverse()) {
|
|
266
|
+
if (row.type === "file") await adapter.removeFile(row.id);
|
|
267
|
+
await ctx.lobb.collectionStore.deleteOne({
|
|
268
|
+
collectionName: "storage_fs",
|
|
269
|
+
id: String(row.id),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { affectedCount: matched.length };
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
};
|
|
307
277
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ExtensionConfig } from "../config/extensionConfigSchema.ts";
|
|
2
|
-
import {
|
|
3
|
-
import { getServicesWorkflows } from "./services.ts";
|
|
2
|
+
import { getActionControllerWorkflows } from "./actionController.ts";
|
|
4
3
|
import { init } from "../init.ts";
|
|
5
4
|
|
|
6
5
|
export function getWorkflows(extensionConfig: ExtensionConfig) {
|
|
@@ -12,7 +11,6 @@ export function getWorkflows(extensionConfig: ExtensionConfig) {
|
|
|
12
11
|
await init(extensionConfig);
|
|
13
12
|
},
|
|
14
13
|
},
|
|
15
|
-
...
|
|
16
|
-
...getControllersWorkflows(),
|
|
14
|
+
...getActionControllerWorkflows(),
|
|
17
15
|
];
|
|
18
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobb-js/lobb-ext-storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"package": "svelte-package --input extensions/storage/studio"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@lobb-js/core": "^0.
|
|
35
|
+
"@lobb-js/core": "^0.41.0",
|
|
36
36
|
"browser-fs-access": "^0.35.0",
|
|
37
37
|
"hono": "^4.7.0",
|
|
38
38
|
"openapi-types": "^12.1.3",
|
|
39
39
|
"path-browserify": "^1.0.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@lobb-js/studio": "^0.
|
|
42
|
+
"@lobb-js/studio": "^0.49.0",
|
|
43
43
|
"@lucide/svelte": "^0.563.1",
|
|
44
44
|
"@sveltejs/adapter-node": "^5.5.4",
|
|
45
45
|
"@sveltejs/kit": "^2.60.1",
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { ExtensionProps } from "@lobb-js/studio";
|
|
3
|
-
import FileExplorer, { type Entry } from "./fileExplorer.svelte";
|
|
4
|
-
import FileIcon from "./fileIcon.svelte";
|
|
5
|
-
|
|
6
|
-
interface Props extends ExtensionProps {
|
|
7
|
-
value: Entry;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
let { utils, value = $bindable(), ...props }: Props = $props();
|
|
11
|
-
|
|
12
|
-
const { FilePlus2 } = utils.components.Icons;
|
|
13
|
-
const { Drawer } = utils.components;
|
|
14
|
-
|
|
15
|
-
let openDrawer = $state(false);
|
|
16
|
-
|
|
17
|
-
function handleClick() {
|
|
18
|
-
openDrawer = true;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function onHideHandle() {
|
|
22
|
-
openDrawer = false;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function handleOnFileSelect(entry: Entry) {
|
|
26
|
-
value = entry;
|
|
27
|
-
openDrawer = false;
|
|
28
|
-
}
|
|
29
|
-
</script>
|
|
30
|
-
|
|
31
|
-
<button onclick={handleClick} class="flex flex-col items-center gap-2 w-full border p-4 rounded-md bg-muted">
|
|
32
|
-
{#if value}
|
|
33
|
-
<FileIcon entry={value} {utils} />
|
|
34
|
-
{:else}
|
|
35
|
-
<FilePlus2 class="text-muted-foreground" />
|
|
36
|
-
<div class="text-sm text-muted-foreground">Click to add an asset</div>
|
|
37
|
-
{/if}
|
|
38
|
-
</button>
|
|
39
|
-
|
|
40
|
-
{#if openDrawer}
|
|
41
|
-
<Drawer onHide={onHideHandle}>
|
|
42
|
-
<FileExplorer onFileSelect={handleOnFileSelect} utils={utils} {...props} />
|
|
43
|
-
</Drawer>
|
|
44
|
-
{/if}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { SvelteComponentTyped } from "svelte";
|
|
2
|
-
declare const __propDef: {
|
|
3
|
-
props: Record<string, never>;
|
|
4
|
-
events: {
|
|
5
|
-
[evt: string]: CustomEvent<any>;
|
|
6
|
-
};
|
|
7
|
-
slots: {};
|
|
8
|
-
};
|
|
9
|
-
export type ForeignKeyComponentProps = typeof __propDef.props;
|
|
10
|
-
export type ForeignKeyComponentEvents = typeof __propDef.events;
|
|
11
|
-
export type ForeignKeyComponentSlots = typeof __propDef.slots;
|
|
12
|
-
export default class ForeignKeyComponent extends SvelteComponentTyped<ForeignKeyComponentProps, ForeignKeyComponentEvents, ForeignKeyComponentSlots> {
|
|
13
|
-
}
|
|
14
|
-
export {};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { ExtensionProps } from "@lobb-js/studio";
|
|
3
|
-
import FileExplorer, { type Entry } from "./fileExplorer.svelte";
|
|
4
|
-
import FileIcon from "./fileIcon.svelte";
|
|
5
|
-
|
|
6
|
-
interface Props extends ExtensionProps {
|
|
7
|
-
value: Entry;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
let { utils, value = $bindable(), ...props }: Props = $props();
|
|
11
|
-
|
|
12
|
-
const { FilePlus2 } = utils.components.Icons;
|
|
13
|
-
const { Drawer } = utils.components;
|
|
14
|
-
|
|
15
|
-
let openDrawer = $state(false);
|
|
16
|
-
|
|
17
|
-
function handleClick() {
|
|
18
|
-
openDrawer = true;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function onHideHandle() {
|
|
22
|
-
openDrawer = false;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function handleOnFileSelect(entry: Entry) {
|
|
26
|
-
value = entry;
|
|
27
|
-
openDrawer = false;
|
|
28
|
-
}
|
|
29
|
-
</script>
|
|
30
|
-
|
|
31
|
-
<button onclick={handleClick} class="flex flex-col items-center gap-2 w-full border p-4 rounded-md bg-muted">
|
|
32
|
-
{#if value}
|
|
33
|
-
<FileIcon entry={value} {utils} />
|
|
34
|
-
{:else}
|
|
35
|
-
<FilePlus2 class="text-muted-foreground" />
|
|
36
|
-
<div class="text-sm text-muted-foreground">Click to add an asset</div>
|
|
37
|
-
{/if}
|
|
38
|
-
</button>
|
|
39
|
-
|
|
40
|
-
{#if openDrawer}
|
|
41
|
-
<Drawer onHide={onHideHandle}>
|
|
42
|
-
<FileExplorer onFileSelect={handleOnFileSelect} utils={utils} {...props} />
|
|
43
|
-
</Drawer>
|
|
44
|
-
{/if}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { ExposedServiceOutput, ServiceOp } from "@lobb-js/core";
|
|
2
|
-
|
|
3
|
-
export type StorageFsCreateOne = ServiceOp<
|
|
4
|
-
{ file: File; data?: { path?: string; parent_id?: number | null }; force?: boolean },
|
|
5
|
-
ExposedServiceOutput
|
|
6
|
-
>;
|
|
7
|
-
|
|
8
|
-
export type StorageFsFindOne = ServiceOp<
|
|
9
|
-
{ id: string; readFile: boolean },
|
|
10
|
-
{ data: { file_mime_type: string; name: string } | null; file: Uint8Array }
|
|
11
|
-
>;
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import type { Lobb, Workflow } from "@lobb-js/core";
|
|
2
|
-
import type { Context } from "hono";
|
|
3
|
-
import type { StorageFsCreateOne, StorageFsFindOne } from "../types.ts";
|
|
4
|
-
|
|
5
|
-
export function getControllersWorkflows(): Workflow[] {
|
|
6
|
-
return [
|
|
7
|
-
{
|
|
8
|
-
name: "storagePreCreateController",
|
|
9
|
-
eventName: "core.controllers.createOne.override",
|
|
10
|
-
handler: async (input, ctx) => {
|
|
11
|
-
if (input.collectionName === "storage_fs") {
|
|
12
|
-
const context = input.context as Context;
|
|
13
|
-
const lobb = context.get("lobb") as Lobb;
|
|
14
|
-
const contentType = context.req.header("Content-Type");
|
|
15
|
-
|
|
16
|
-
if (contentType?.startsWith("multipart/form-data")) {
|
|
17
|
-
const body = await context.req.parseBody({ all: true });
|
|
18
|
-
const fileEntry = body["file"];
|
|
19
|
-
|
|
20
|
-
if (!fileEntry) {
|
|
21
|
-
throw new ctx.LobbError({
|
|
22
|
-
code: "BAD_REQUEST",
|
|
23
|
-
message: "No files were provided.",
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (Array.isArray(fileEntry)) {
|
|
28
|
-
throw new ctx.LobbError({
|
|
29
|
-
code: "BAD_REQUEST",
|
|
30
|
-
message: "Only one file can be uploaded at a time.",
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!(fileEntry instanceof File)) {
|
|
35
|
-
throw new ctx.LobbError({
|
|
36
|
-
code: "BAD_REQUEST",
|
|
37
|
-
message: "The file shouldnt be a string",
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const force = context.req.query("force") === "true";
|
|
42
|
-
|
|
43
|
-
const extraData: Record<string, string> = {};
|
|
44
|
-
for (const [key, val] of Object.entries(body)) {
|
|
45
|
-
if (key !== "file" && key !== "payload" && typeof val === "string") {
|
|
46
|
-
extraData[key] = val;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const result = await ctx.lobb.collectionService.createOne<StorageFsCreateOne>({
|
|
51
|
-
collectionName: "storage_fs",
|
|
52
|
-
file: fileEntry,
|
|
53
|
-
force,
|
|
54
|
-
data: extraData,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
return context.json(result, 201);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: "storageFindOneController",
|
|
64
|
-
eventName: "core.controllers.findOne.override",
|
|
65
|
-
handler: async (input, ctx) => {
|
|
66
|
-
if (input.collectionName === "storage_fs") {
|
|
67
|
-
const context = input.context as Context;
|
|
68
|
-
const action = context.req.query("action");
|
|
69
|
-
|
|
70
|
-
if (action === "view" || action === "download") {
|
|
71
|
-
const result = await ctx.lobb.collectionService.findOne<StorageFsFindOne>({
|
|
72
|
-
collectionName: "storage_fs",
|
|
73
|
-
id: input.id,
|
|
74
|
-
readFile: true,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (!result.data) {
|
|
78
|
-
throw new ctx.LobbError({
|
|
79
|
-
code: "NOT_FOUND",
|
|
80
|
-
message: "The record is not found",
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const headers: Record<string, string> = {
|
|
85
|
-
"Content-Type": result.data.file_mime_type,
|
|
86
|
-
};
|
|
87
|
-
if (action === "download") {
|
|
88
|
-
headers["Content-Disposition"] =
|
|
89
|
-
`attachment; filename="${result.data.name}"`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return new Response(result.file.buffer as ArrayBuffer, { status: 200, headers });
|
|
93
|
-
} else if (action) {
|
|
94
|
-
throw new ctx.LobbError({
|
|
95
|
-
code: "BAD_REQUEST",
|
|
96
|
-
message: `The passed (${action}) action query param is not supported`,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
];
|
|
103
|
-
}
|