diff --git a/src/app/actions/ftp/deleteFile.ts b/src/app/actions/ftp/deleteFile.ts index 81415a6..bb3a18d 100644 --- a/src/app/actions/ftp/deleteFile.ts +++ b/src/app/actions/ftp/deleteFile.ts @@ -11,12 +11,14 @@ export async function deleteFile({ }) { const client = await connectToFTP(loc); try { - await client.remove(filename); - console.log(`File ${filename} deleted successfully.`); + await client.ensureDir('/trash'); + await client.rename(`/${filename}`, `/trash/${filename}`); + + console.log(`File ${filename} moved to /trash successfully.`); } catch (e) { const errorMsg = (e as { message: string })?.message || ''; - console.error('Error deleting file:', e); - throw new Error(`Failed to delete file: ${errorMsg}`); + console.error('Error moving file to trash:', e); + throw new Error(`Failed to move file to trash: ${errorMsg}`); } finally { client.close(); } diff --git a/src/app/actions/ftp/getRecentlyDeleted.ts b/src/app/actions/ftp/getRecentlyDeleted.ts new file mode 100644 index 0000000..a448dd4 --- /dev/null +++ b/src/app/actions/ftp/getRecentlyDeleted.ts @@ -0,0 +1,23 @@ +'use server'; + +import { connectToFTP } from './connect'; + +export async function getRecentlyDeletedFiles(loc: string) { + const client = await connectToFTP(loc); + try { + await client.cd('trash'); + const files = await client.list(); + const filesData = + files?.map((file) => ({ + name: file.name, + size: file.size, + modifiedAt: file.modifiedAt, + })) ?? []; + return filesData; + } catch (e) { + console.error(e); + throw e; + } finally { + client.close(); + } +} diff --git a/src/app/actions/ftp/restoreFile.ts b/src/app/actions/ftp/restoreFile.ts new file mode 100644 index 0000000..7c6cff1 --- /dev/null +++ b/src/app/actions/ftp/restoreFile.ts @@ -0,0 +1,23 @@ +'use server'; + +import { connectToFTP } from './connect'; + +export async function restoreFile({ + filename, + loc, +}: { + filename: string; + loc: string; +}) { + const client = await connectToFTP(loc); + try { + await client.rename(`/trash/${filename}`, `/${filename}`); + console.log(`File ${filename} restored successfully.`); + } catch (e) { + const errorMsg = (e as { message: string })?.message || ''; + console.error('Error restoring file:', e); + throw new Error(`Failed to restore file: ${errorMsg}`); + } finally { + client.close(); + } +} diff --git a/src/components/FTPComponent.tsx b/src/components/FTPComponent.tsx index 2db36a1..04d46cf 100644 --- a/src/components/FTPComponent.tsx +++ b/src/components/FTPComponent.tsx @@ -2,13 +2,14 @@ 'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Download, Edit3Icon, Trash2 } from 'lucide-react'; +import { Download, Edit3Icon, Trash2, ArchiveRestore } from 'lucide-react'; import { deleteFile } from '@/app/actions/ftp/deleteFile'; import { downloadFile } from '@/app/actions/ftp/downloadFile'; import { GetFiles } from '@/app/actions/ftp/getFiles'; import { renameFile } from '@/app/actions/ftp/renameFile'; import { UploadFile } from '@/app/actions/ftp/uploadFile'; +import { getRecentlyDeletedFiles } from '@/app/actions/ftp/getRecentlyDeleted'; import { ReactTable } from '@/components/ReactTable'; import { Card, Spinner, useToast } from '@sanity/ui'; import { Button } from '@/components/ui/button'; @@ -21,6 +22,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { restoreFile } from '@/app/actions/ftp/restoreFile'; export default function FTPComponent() { const [data, setData] = useState([]); @@ -31,6 +33,7 @@ export default function FTPComponent() { const [updatedFilename, setUpdatedFilename] = useState(''); const [loc, setLoc] = useState<'images' | 'docs'>('images'); const [editingFilename, setEditingFilename] = useState(null); + const [isViewingDeleted, setIsViewingDeleted] = useState(false); const toast = useToast(); @@ -38,7 +41,10 @@ export default function FTPComponent() { try { setIsLoading(true); const result = await GetFiles(loc); - setData(result); + const filteredResult = result.filter( + (file: any) => !file.name.includes('trash') + ); + setData(filteredResult); setAllFiles(result); } catch (err) { console.log(`FTP Error: ${err}`); @@ -72,10 +78,10 @@ export default function FTPComponent() { const handleUpload = useCallback( async (event: React.FormEvent) => { - event.preventDefault(); // Prevent the default form submission + event.preventDefault(); setIsUploading(true); - const formData = new FormData(event.currentTarget); // Get form data + const formData = new FormData(event.currentTarget); const file = formData.get('file') as File; if (!file?.name) { @@ -87,6 +93,21 @@ export default function FTPComponent() { setIsUploading(false); return; } + const fileExists = allFiles.some( + (existingFile: any) => + existingFile.name.toLowerCase() === file.name.toLowerCase() + ); + + if (fileExists) { + toast.push({ + status: 'error', + title: 'File already exists', + description: + 'A file with this name already exists. Please rename the file and try again.', + }); + setIsUploading(false); + return; + } try { await UploadFile({ formData, loc }); @@ -94,7 +115,6 @@ export default function FTPComponent() { status: 'success', title: 'File uploaded successfully.', }); - // Refresh the file list after upload await fetchData(); } catch (err: unknown) { toast.push({ @@ -147,6 +167,7 @@ export default function FTPComponent() { const handleDelete = useCallback( async (filename: string) => { + setIsLoading(true); try { await deleteFile({ filename, loc }); toast.push({ @@ -161,6 +182,32 @@ export default function FTPComponent() { title: 'Failed to delete file', description: errorMsg, }); + } finally { + setIsLoading(false); + } + }, + [loc, toast, fetchData] + ); + + const handleRestore = useCallback( + async (filename: string) => { + setIsLoading(true); + try { + await restoreFile({ filename, loc }); + toast.push({ + status: 'success', + title: `File ${filename} restored successfully.`, + }); + await handleRecentlyDeleted(); + } catch (e) { + const errorMsg = (e as { message: string })?.message || ''; + toast.push({ + status: 'error', + title: 'Failed to restore file', + description: errorMsg, + }); + } finally { + setIsLoading(false); } }, [loc, toast, fetchData] @@ -206,6 +253,32 @@ export default function FTPComponent() { [loc, toast, updatedFilename, fetchData] ); + const handleRecentlyDeleted = useCallback(async () => { + try { + setIsLoading(true); + if (!isViewingDeleted) { + const deletedFiles = await getRecentlyDeletedFiles(loc); + setData(deletedFiles); + setIsViewingDeleted(true); + toast.push({ + status: 'success', + title: 'Showing recently deleted files', + }); + } else { + await fetchData(); // Return to normal view + setIsViewingDeleted(false); + } + } catch (err) { + toast.push({ + status: 'error', + title: 'Failed to fetch deleted files', + description: (err as Error).message, + }); + } finally { + setIsLoading(false); + } + }, [loc, toast, isViewingDeleted, fetchData, handleRestore]); + const columns = useMemo( () => [ { @@ -261,12 +334,21 @@ export default function FTPComponent() { > - + {isViewingDeleted ? ( + + ) : ( + + )} {editingFilename == row.original.name && ( + {isLoading ? ( diff --git a/src/sanity/components/CustomNavbar.tsx b/src/sanity/components/CustomNavbar.tsx index 7561572..eb4da1d 100644 --- a/src/sanity/components/CustomNavbar.tsx +++ b/src/sanity/components/CustomNavbar.tsx @@ -119,12 +119,12 @@ function FTPDialogTrigger() { - Upload Files via FTP + File management via FTP