| import { useMutation, useQueryClient } from '@tanstack/react-query'; |
| import { useAuth } from '@/components/AuthProvider'; |
| import { fileQueryKeys } from './use-file-queries'; |
| import { FileCache } from '@/hooks/use-cached-file'; |
| import { toast } from 'sonner'; |
| |
| function normalizePath(path: string): string { |
| if (!path) return '/'; |
| |
| |
| path = path.trim(); |
| |
| |
| if (!path.startsWith('/')) { |
| path = '/' + path; |
| } |
| |
| |
| path = path.replace(/\/+/g, '/'); |
| |
| |
| if (path.length > 1 && path.endsWith('/')) { |
| path = path.slice(0, -1); |
| } |
| |
| return path; |
| } |
|
|
| const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || ''; |
|
|
| |
| |
| |
| export function useFileUpload() { |
| const { session } = useAuth(); |
| const queryClient = useQueryClient(); |
|
|
| return useMutation({ |
| mutationFn: async ({ |
| sandboxId, |
| file, |
| targetPath, |
| }: { |
| sandboxId: string; |
| file: File; |
| targetPath: string; |
| }) => { |
| if (!session?.access_token) { |
| throw new Error('No access token available'); |
| } |
|
|
| const formData = new FormData(); |
| formData.append('file', file); |
| formData.append('path', targetPath); |
|
|
| const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, { |
| method: 'POST', |
| headers: { |
| Authorization: `Bearer ${session.access_token}`, |
| }, |
| body: formData, |
| }); |
|
|
| if (!response.ok) { |
| const error = await response.text(); |
| throw new Error(error || 'Upload failed'); |
| } |
|
|
| return await response.json(); |
| }, |
| onSuccess: (_, variables) => { |
| |
| const directoryPath = variables.targetPath.substring(0, variables.targetPath.lastIndexOf('/')); |
| queryClient.invalidateQueries({ |
| queryKey: fileQueryKeys.directory(variables.sandboxId, directoryPath), |
| }); |
|
|
| |
| queryClient.invalidateQueries({ |
| queryKey: fileQueryKeys.directories(), |
| }); |
|
|
| toast.success(`Uploaded: ${variables.file.name}`); |
| }, |
| onError: (error) => { |
| const message = error instanceof Error ? error.message : String(error); |
| toast.error(`Upload failed: ${message}`); |
| }, |
| }); |
| } |
|
|
| |
| |
| |
| export function useFileDelete() { |
| const { session } = useAuth(); |
| const queryClient = useQueryClient(); |
|
|
| return useMutation({ |
| mutationFn: async ({ |
| sandboxId, |
| filePath, |
| }: { |
| sandboxId: string; |
| filePath: string; |
| }) => { |
| if (!session?.access_token) { |
| throw new Error('No access token available'); |
| } |
|
|
| const response = await fetch( |
| `${API_URL}/sandboxes/${sandboxId}/files?path=${encodeURIComponent(filePath)}`, |
| { |
| method: 'DELETE', |
| headers: { |
| Authorization: `Bearer ${session.access_token}`, |
| }, |
| } |
| ); |
|
|
| if (!response.ok) { |
| const error = await response.text(); |
| throw new Error(error || 'Delete failed'); |
| } |
|
|
| return await response.json(); |
| }, |
| onSuccess: (_, variables) => { |
| |
| const directoryPath = variables.filePath.substring(0, variables.filePath.lastIndexOf('/')); |
| queryClient.invalidateQueries({ |
| queryKey: fileQueryKeys.directory(variables.sandboxId, directoryPath), |
| }); |
|
|
| |
| queryClient.invalidateQueries({ |
| queryKey: fileQueryKeys.directories(), |
| }); |
|
|
| |
| |
| queryClient.invalidateQueries({ |
| predicate: (query) => { |
| const queryKey = query.queryKey; |
| |
| return ( |
| queryKey.length >= 4 && |
| queryKey[0] === 'files' && |
| queryKey[1] === 'content' && |
| queryKey[2] === variables.sandboxId && |
| queryKey[3] === variables.filePath |
| ); |
| }, |
| }); |
|
|
| |
| ['text', 'blob', 'json'].forEach(contentType => { |
| const queryKey = fileQueryKeys.content(variables.sandboxId, variables.filePath, contentType); |
| queryClient.removeQueries({ queryKey }); |
| }); |
|
|
| |
| const normalizedPath = normalizePath(variables.filePath); |
| const legacyCacheKeys = [ |
| `${variables.sandboxId}:${normalizedPath}:blob`, |
| `${variables.sandboxId}:${normalizedPath}:text`, |
| `${variables.sandboxId}:${normalizedPath}:json`, |
| `${variables.sandboxId}:${normalizedPath}`, |
| |
| `${variables.sandboxId}:${normalizedPath.substring(1)}:blob`, |
| `${variables.sandboxId}:${normalizedPath.substring(1)}:text`, |
| `${variables.sandboxId}:${normalizedPath.substring(1)}:json`, |
| `${variables.sandboxId}:${normalizedPath.substring(1)}`, |
| ]; |
|
|
| legacyCacheKeys.forEach(key => { |
| const cachedEntry = (FileCache as any).cache?.get(key); |
| if (cachedEntry) { |
| |
| if (cachedEntry.type === 'url' && typeof cachedEntry.content === 'string' && cachedEntry.content.startsWith('blob:')) { |
| console.log(`[FILE DELETE] Revoking blob URL for deleted file: ${cachedEntry.content}`); |
| URL.revokeObjectURL(cachedEntry.content); |
| } |
| FileCache.delete(key); |
| } |
| }); |
| }, |
| onError: (error) => { |
| const message = error instanceof Error ? error.message : String(error); |
| toast.error(`Delete failed: ${message}`); |
| }, |
| }); |
| } |
|
|
| |
| |
| |
| export function useFileCreate() { |
| const { session } = useAuth(); |
| const queryClient = useQueryClient(); |
|
|
| return useMutation({ |
| mutationFn: async ({ |
| sandboxId, |
| filePath, |
| content, |
| }: { |
| sandboxId: string; |
| filePath: string; |
| content: string; |
| }) => { |
| if (!session?.access_token) { |
| throw new Error('No access token available'); |
| } |
|
|
| const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${session.access_token}`, |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ |
| path: filePath, |
| content, |
| }), |
| }); |
|
|
| if (!response.ok) { |
| const error = await response.text(); |
| throw new Error(error || 'Create failed'); |
| } |
|
|
| return await response.json(); |
| }, |
| onSuccess: (_, variables) => { |
| |
| const directoryPath = variables.filePath.substring(0, variables.filePath.lastIndexOf('/')); |
| queryClient.invalidateQueries({ |
| queryKey: fileQueryKeys.directory(variables.sandboxId, directoryPath), |
| }); |
|
|
| toast.success('File created successfully'); |
| }, |
| onError: (error) => { |
| const message = error instanceof Error ? error.message : String(error); |
| toast.error(`Create failed: ${message}`); |
| }, |
| }); |
| } |