vfs acl
This commit is contained in:
parent
e47430369c
commit
7383b40b27
@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { FileBrowserWidget } from "@/modules/storage";
|
||||
import { AclEditor } from "@/modules/storage/AclEditor";
|
||||
import { vfsIndex, vfsClearIndex, subscribeToVfsIndexStream } from "@/modules/storage/client-vfs";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { T, translate } from "@/i18n";
|
||||
@ -8,11 +9,10 @@ import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw, Database, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { useEffect } from "react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export default function StorageManager() {
|
||||
// Default to 'root' mount, but we could allow selecting mounts if there are multiple.
|
||||
@ -20,15 +20,20 @@ export default function StorageManager() {
|
||||
const [mount, setMount] = useState("home");
|
||||
const [currentPath, setCurrentPath] = useState("/");
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||
const [manualPath, setManualPath] = useState("/");
|
||||
const [indexing, setIndexing] = useState(false);
|
||||
const [indexProgress, setIndexProgress] = useState(0);
|
||||
const [indexCountMsg, setIndexCountMsg] = useState("");
|
||||
const [fullText, setFullText] = useState(false);
|
||||
const { session } = useAuth();
|
||||
|
||||
// The ACL editor should show permissions for the SELECTED file if any,
|
||||
// otherwise for the current folder.
|
||||
const targetPath = selectedPath || currentPath;
|
||||
// The ACL editor shows permissions for the manual path.
|
||||
// Sync external file browser selections into the manual input.
|
||||
useEffect(() => {
|
||||
setManualPath(selectedPath || currentPath);
|
||||
}, [selectedPath, currentPath]);
|
||||
|
||||
const targetPath = manualPath;
|
||||
|
||||
useEffect(() => {
|
||||
if (!indexing) return;
|
||||
@ -36,32 +41,17 @@ export default function StorageManager() {
|
||||
let eventSource: EventSource | undefined;
|
||||
let isMounted = true;
|
||||
|
||||
const setupStream = async () => {
|
||||
const { data: sessionData } = await supabase.auth.getSession();
|
||||
const token = sessionData.session?.access_token || session?.access_token;
|
||||
const streamUrl = token
|
||||
? `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/stream?token=${encodeURIComponent(token)}`
|
||||
: `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/stream`;
|
||||
|
||||
eventSource = new EventSource(streamUrl);
|
||||
|
||||
eventSource.addEventListener('system', (e: any) => {
|
||||
if (!isMounted) return;
|
||||
try {
|
||||
const payload = JSON.parse(e.data);
|
||||
if (payload.type === 'vfs-index' && payload.data) {
|
||||
const matchPath = targetPath.replace(/^\//, '');
|
||||
const payloadTarget = (payload.data.targetPath || '').replace(/^\//, '');
|
||||
if (payload.data.mount === mount && payloadTarget === matchPath) {
|
||||
setIndexProgress(payload.data.progress);
|
||||
setIndexCountMsg(`Indexing: ${payload.data.indexedCount} / ${payload.data.total}`);
|
||||
}
|
||||
}
|
||||
} catch (err) { }
|
||||
});
|
||||
};
|
||||
|
||||
setupStream();
|
||||
subscribeToVfsIndexStream(mount, targetPath, (progress, msg) => {
|
||||
if (!isMounted) return;
|
||||
setIndexProgress(progress);
|
||||
setIndexCountMsg(msg);
|
||||
}, session?.access_token).then(es => {
|
||||
if (!isMounted) {
|
||||
es.close();
|
||||
} else {
|
||||
eventSource = es;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
@ -74,24 +64,7 @@ export default function StorageManager() {
|
||||
|
||||
try {
|
||||
setIndexing(true);
|
||||
const cleanPath = targetPath.replace(/^\//, ''); // Remove leading slash
|
||||
const endpoint = mount === 'home'
|
||||
? `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/vfs/admin/index/${cleanPath}`
|
||||
: `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/vfs/admin/index/${mount}/${cleanPath}`;
|
||||
|
||||
const res = await fetch(endpoint.replace(/\/$/, ''), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session?.access_token || ''}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const data = await vfsClearIndex(mount, targetPath);
|
||||
toast.success(translate("Index cleared"), {
|
||||
description: data.message
|
||||
});
|
||||
@ -109,24 +82,7 @@ export default function StorageManager() {
|
||||
setIndexing(true);
|
||||
setIndexProgress(0);
|
||||
setIndexCountMsg("Preparing indexing batch...");
|
||||
const cleanPath = targetPath.replace(/^\//, ''); // Remove leading slash
|
||||
const endpoint = mount === 'home'
|
||||
? `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/vfs/admin/index/${cleanPath}?fullText=${fullText}`
|
||||
: `${import.meta.env.VITE_SERVER_IMAGE_API_URL}/api/vfs/admin/index/${mount}/${cleanPath}?fullText=${fullText}`;
|
||||
|
||||
const res = await fetch(endpoint.replace(/\/$/, ''), { // ensure no trailing slash if path was empty
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session?.access_token || ''}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Failed to index mount');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const data = await vfsIndex(mount, targetPath, fullText);
|
||||
toast.success(translate("Indexing completed"), {
|
||||
description: data.message
|
||||
});
|
||||
@ -195,8 +151,15 @@ export default function StorageManager() {
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<div className="mb-4">
|
||||
<div className="text-sm font-medium text-muted-foreground"><T>Selected Path</T></div>
|
||||
<div className="font-mono text-sm break-all bg-muted p-2 rounded mt-1">
|
||||
{mount}:{targetPath}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="bg-muted px-3 py-2 rounded-md border text-sm font-mono text-muted-foreground select-none">
|
||||
{mount}:
|
||||
</div>
|
||||
<Input
|
||||
value={targetPath}
|
||||
onChange={(e) => setManualPath(e.target.value)}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
|
||||
@ -71,10 +71,14 @@ export const fetchWithDeduplication = async <T>(
|
||||
|
||||
export const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin;
|
||||
|
||||
export const getAuthToken = async (): Promise<string | undefined> => {
|
||||
const { data: sessionData } = await defaultSupabase.auth.getSession();
|
||||
return sessionData?.session?.access_token;
|
||||
};
|
||||
|
||||
/** Helper function to get authorization headers */
|
||||
export const getAuthHeaders = async (): Promise<HeadersInit> => {
|
||||
const { data: sessionData } = await defaultSupabase.auth.getSession();
|
||||
const token = sessionData?.session?.access_token;
|
||||
const token = await getAuthToken();
|
||||
const headers: HeadersInit = { 'Content-Type': 'application/json' };
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
return headers;
|
||||
|
||||
78
packages/ui/src/modules/storage/client-vfs.ts
Normal file
78
packages/ui/src/modules/storage/client-vfs.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { apiClient, getAuthToken, serverUrl } from '@/lib/db';
|
||||
import { vfsUrl } from '@/modules/storage/helpers';
|
||||
import { INode } from '@/modules/storage/types';
|
||||
|
||||
export interface VfsOptions {
|
||||
accessToken?: string;
|
||||
includeSize?: boolean;
|
||||
}
|
||||
|
||||
export const fetchVfsDirectory = async (mount: string, path: string, options: VfsOptions = {}): Promise<INode[]> => {
|
||||
let url = vfsUrl('ls', mount, path);
|
||||
if (options.includeSize) {
|
||||
url += '?includeSize=true';
|
||||
}
|
||||
const headers = options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : undefined;
|
||||
return apiClient<INode[]>(url, { headers, cache: 'no-cache' });
|
||||
};
|
||||
|
||||
export const fetchVfsSearch = async (mount: string, path: string, query: string, options: VfsOptions = {}): Promise<{ results: INode[] }> => {
|
||||
const base = path ? vfsUrl('search', mount, path) : vfsUrl('search', mount);
|
||||
const url = `${base}?q=${encodeURIComponent(query.trim())}&maxResults=200`;
|
||||
const headers = options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : undefined;
|
||||
return apiClient<{ results: INode[] }>(url, { headers, cache: 'no-cache' });
|
||||
};
|
||||
|
||||
export const vfsIndex = async (mount: string, path: string, fullText: boolean): Promise<{ message: string }> => {
|
||||
const cleanPath = path.replace(/^\//, '');
|
||||
let endpoint = mount === 'home'
|
||||
? `/api/vfs/admin/index/${cleanPath}?fullText=${fullText}`
|
||||
: `/api/vfs/admin/index/${mount}/${cleanPath}?fullText=${fullText}`;
|
||||
|
||||
// remove trailing slash before query params if path was empty
|
||||
endpoint = endpoint.replace(/\/(\?|$)/, '$1');
|
||||
return apiClient<{ message: string }>(endpoint, { method: 'POST' });
|
||||
};
|
||||
|
||||
export const vfsClearIndex = async (mount: string, path: string): Promise<{ message: string }> => {
|
||||
const cleanPath = path.replace(/^\//, '');
|
||||
let endpoint = mount === 'home'
|
||||
? `/api/vfs/admin/index/${cleanPath}`
|
||||
: `/api/vfs/admin/index/${mount}/${cleanPath}`;
|
||||
|
||||
endpoint = endpoint.replace(/\/$/, '');
|
||||
return apiClient<{ message: string }>(endpoint, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
export const subscribeToVfsIndexStream = async (
|
||||
mount: string,
|
||||
targetPath: string,
|
||||
onProgress: (progress: number, msg: string) => void,
|
||||
fallbackToken?: string
|
||||
): Promise<EventSource> => {
|
||||
const dbToken = await getAuthToken();
|
||||
const token = dbToken || fallbackToken;
|
||||
const streamUrl = token
|
||||
? `${serverUrl}/api/stream?token=${encodeURIComponent(token)}`
|
||||
: `${serverUrl}/api/stream`;
|
||||
|
||||
const eventSource = new EventSource(streamUrl);
|
||||
|
||||
eventSource.addEventListener('system', (e: any) => {
|
||||
try {
|
||||
const payload = JSON.parse(e.data);
|
||||
if (payload.type === 'vfs-index' && payload.data) {
|
||||
const matchPath = targetPath.replace(/^\//, '');
|
||||
const payloadTarget = (payload.data.targetPath || '').replace(/^\//, '');
|
||||
if (payload.data.mount === mount && payloadTarget === matchPath) {
|
||||
onProgress(
|
||||
payload.data.progress,
|
||||
`Indexing: ${payload.data.indexedCount} / ${payload.data.total}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) { }
|
||||
});
|
||||
|
||||
return eventSource;
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { INode, SortKey } from '@/modules/storage/types';
|
||||
import { getMimeCategory, globToRegex, sortNodes, vfsUrl } from '@/modules/storage/helpers';
|
||||
import { fetchVfsDirectory, fetchVfsSearch } from '@/modules/storage/client-vfs';
|
||||
|
||||
export interface UseVfsAdapterProps {
|
||||
mount: string;
|
||||
@ -66,35 +67,12 @@ export function useVfsAdapter({
|
||||
|
||||
try {
|
||||
const clean = dirPath.replace(/^\/+/, '');
|
||||
|
||||
let url: string;
|
||||
if (isSearchMode) {
|
||||
// Point to /api/vfs/search
|
||||
const base = clean ? vfsUrl('search', mount, clean) : vfsUrl('search', mount);
|
||||
url = `${base}?q=${encodeURIComponent(searchQuery.trim())}&maxResults=200`;
|
||||
} else {
|
||||
// Point to /api/vfs/ls
|
||||
url = vfsUrl('ls', mount, clean);
|
||||
if (includeSize) {
|
||||
url += '?includeSize=true';
|
||||
}
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (accessToken) headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
|
||||
const res = await fetch(url, { headers, cache: 'no-cache' });
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
if (isSearchMode) {
|
||||
const data = await res.json();
|
||||
const data = await fetchVfsSearch(mount, clean, searchQuery, { accessToken });
|
||||
setNodes(data.results || []);
|
||||
if (onFetchedRef.current) onFetchedRef.current(data.results || [], true);
|
||||
} else {
|
||||
const data: INode[] = await res.json();
|
||||
const data = await fetchVfsDirectory(mount, clean, { accessToken, includeSize });
|
||||
setNodes(data);
|
||||
if (onFetchedRef.current) onFetchedRef.current(data, false);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user