Skip to content

Commit

Permalink
#55 Download and Delete files are now full circle
Browse files Browse the repository at this point in the history
  • Loading branch information
santthosh committed May 11, 2024
1 parent b34346d commit c3e9249
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 14 deletions.
78 changes: 76 additions & 2 deletions src/app/api/assistants/[id]/files/[fileId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
import { getToken } from 'next-auth/jwt';
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { DeleteObjectCommand, GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import path from 'path';
import OpenAI from 'openai';

const prisma = new PrismaClient();

Expand Down Expand Up @@ -35,7 +36,6 @@ const validateIncomingToken = async (req: NextRequest, assistant: any) => {
};

const createPresignedGet = async (file: string, expires: number = 3600): Promise<string> => {
// Generate pre-signed ulr for the file in S3
let configuration = { region: process.env.AWS_REGION };
// @ts-ignore
const s3Client = new S3Client(configuration);
Expand All @@ -53,6 +53,24 @@ const createPresignedGet = async (file: string, expires: number = 3600): Promise
}
}

async function deleteFileFromS3(file: string): Promise<any> {
let configuration = { region: process.env.AWS_REGION };
// @ts-ignore
const s3Client = new S3Client(configuration);

const deleteCommand = new DeleteObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: file
});

try {
return await s3Client.send(deleteCommand as any);
} catch (error) {
console.error('Failed to delete file from S3:', error);
throw error; // Rethrowing the error is useful if you need to handle it further up the chain.
}
}

export async function GET(req: NextRequest, res: NextResponse) {
let assistantId = getId(req);

Expand Down Expand Up @@ -101,7 +119,63 @@ export async function DELETE(req: NextRequest, res: NextResponse) {
);
}

let fileId = getFileId(req);

if (!(await validateIncomingToken(req, assistant))) {
return Response.json({ message: 'Unauthorized' }, { status: 401 });
}

try {
let file = await prisma.file.findFirst({
where: {
id: fileId,
},
select: {
id: true,
originalFileName: true,
object: true,
folder: true
}
});

if (!file) {
return Response.json({ message: 'File not found' }, { status: 404 });
}

if (assistant?.modelProviderId === 'openai') {
let openai = new OpenAI({
apiKey: assistant?.organization?.openAIApiKey,
});
try {
// 1. Remove file from Vector Store
// @ts-ignore
let vectorStoreFileResponse = await openai.beta.vectorStores.files.del(file.folder.object.id, file.object.i);
} catch (err) {
console.error('Error removing file from Vector Store:', err);
}

try {
// 2. Delete file from Open AI
// @ts-ignore
let filesResponse = await openai.files.del(file.object.i);
} catch (err) {
console.error('Error removing file from OpenAI:', err);
}
}
// 3. Delete file from S3
let fileName = file.id + path.extname(file.originalFileName);
let s3Response = await deleteFileFromS3(fileName.trim());

// 4. Remove file from DB
await prisma.file.delete({
where: {
id: fileId,
},
});

return Response.json(file, { status: 200 });
} catch (err) {
console.log(err);
return Response.json({ message: 'File not found' }, { status: 404 });
}
}
6 changes: 6 additions & 0 deletions src/app/assistants/[id]/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,9 @@ export async function getFile(assistantId: string | undefined, fileId: string) {

return await response.json();
}

export async function deleteFile(assistantId: string, fileId: string) {
return await fetch('/api/assistants/' + assistantId + '/files/' + fileId, {
method: 'DELETE',
});
}
74 changes: 67 additions & 7 deletions src/app/assistants/[id]/documents/DocumentsList.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import Image from 'next/image';
import * as path from 'path';
import { formatRelativeDate } from '@/app/utils/date';
import { Dropdown } from 'flowbite-react';
import { HiDotsHorizontal, HiDownload, HiOutlineTrash } from 'react-icons/hi';
import { getFile } from '@/app/assistants/[id]/client';
import { Button, Dropdown, Modal } from 'flowbite-react';
import { HiDotsHorizontal, HiDownload, HiOutlineExclamationCircle, HiOutlineTrash } from 'react-icons/hi';
import { deleteFile, getFile, uploadFile } from '@/app/assistants/[id]/client';
import AssistantContext from '@/app/assistants/[id]/AssistantContext';
import { toast } from 'react-hot-toast';

export interface DocumentsListProps {
files: any[];
refresh: () => void;
}

const getImageForFile = (file: any) => {
Expand All @@ -29,8 +31,38 @@ const getImageForFile = (file: any) => {
};

export default function DocumentsList(props: DocumentsListProps) {
const [files, setFiles] = useState<any[] | null>(props.files);
const { assistant } = useContext(AssistantContext);
const [iframeSrc, setIframeSrc] = useState('');
const [openModal, setOpenModal] = useState(false);
const [selectedFile, setSelectedFile] = useState<any>(null);
const [deletingFile, setDeletingFile] = useState(false);

const handleDeleteFile = async () => {
if (!assistant.id) {
throw new Error('Assistant ID is missing');
}
setDeletingFile(true);
const response: Response = await deleteFile(assistant.id, selectedFile.id);
if (response.ok) {
if (files) {
setFiles(files.filter((file) => file.id !== selectedFile.id));
props.refresh();
}

toast.success('Document ' + selectedFile.originalFileName + ' deleted successfully.', {
duration: 4000,
});
} else {
console.error('Delete Error:', response);
toast.error('Document ' + selectedFile.originalFileName + ' deletion failed.' + response, {
duration: 4000,
});
}

setDeletingFile(false);
setOpenModal(false)
}

const handleDownload = (file: any) => {
getFile(assistant.id, file.id).then((response) => {
Expand All @@ -43,8 +75,8 @@ export default function DocumentsList(props: DocumentsListProps) {
return (
<div className='flex grid flex-col gap-12 sm:grid-cols-4 lg:grid-cols-6 2xl:grid-cols-10'>
{iframeSrc && <iframe src={iframeSrc} style={{ display: 'none' }} onLoad={() => setIframeSrc('')}></iframe>}
{props.files &&
props.files.map((file, index) => {
{files &&
files.map((file, index) => {
return (
<div className='max-h-xs col-span-2 max-w-xs' key={index}>
<div className='mt-3 rounded-lg bg-gray-200'>
Expand All @@ -57,7 +89,10 @@ export default function DocumentsList(props: DocumentsListProps) {
>
<Dropdown.Item icon={HiDownload} onClick={() => { handleDownload(file) }}>Download</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item icon={HiOutlineTrash}>Delete</Dropdown.Item>
<Dropdown.Item icon={HiOutlineTrash} onClick={() => {
setSelectedFile(file);
setOpenModal(true);
}}>Delete</Dropdown.Item>
</Dropdown>
</div>
<div className='pb-12 pl-12 pr-12 dark:border-gray-800'>
Expand All @@ -83,6 +118,31 @@ export default function DocumentsList(props: DocumentsListProps) {
</div>
);
})}
<Modal
show={openModal}
size='xl'
onClose={() => setOpenModal(false)}
popup
>
<Modal.Header />
<Modal.Body>
<div className='text-center'>
<HiOutlineExclamationCircle className='mx-auto mb-4 h-14 w-14 text-gray-400 dark:text-gray-200' />
<h3 className='mb-5 text-lg font-normal text-gray-500 dark:text-gray-400'>
Are you sure you want to delete this document{' '}
<a className='font-bold'>{selectedFile ? selectedFile.originalFileName : ''}</a>?
</h3>
<div className='flex justify-center gap-4'>
<Button color='failure' isProcessing={deletingFile} onClick={handleDeleteFile}>
{"Yes, I'm sure"}
</Button>
<Button color='gray' disabled={deletingFile} onClick={() => setOpenModal(false)}>
Cancel
</Button>
</div>
</div>
</Modal.Body>
</Modal>
</div>
);
}
14 changes: 9 additions & 5 deletions src/app/assistants/[id]/documents/DocumentsManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ export default function DocumentsManager() {
const [file, setFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);

const handleGetFiles = async () => {
let response = await getFiles(assistant.id);
setFiles(response);
}

useEffect(() => {
getFiles(assistant.id).then((response) => {
setFiles(response);
});
handleGetFiles().then(() => {});
}, []);

useEffect(() => {
Expand Down Expand Up @@ -50,12 +53,13 @@ export default function DocumentsManager() {

const response: Response = await uploadFile(assistant.id, file);
if (response.ok) {
handleGetFiles().then(() => {});
toast.success('Document ' + file.name + ' uploaded successfully.', {
duration: 4000,
});
} else {
console.error('Upload Error:', response);
toast.success('Document ' + file.name + ' uploaded failed.' + response, {
toast.error('Document ' + file.name + ' uploaded failed.' + response, {
duration: 4000,
});
}
Expand Down Expand Up @@ -94,7 +98,7 @@ export default function DocumentsManager() {
<Spinner />
</div>
) : files && files.length ? (
<DocumentsList files={files} />
<DocumentsList files={files} refresh={handleGetFiles}/>
) : (
<DocumentsEmpty onFiles={setFile} />
)}
Expand Down

0 comments on commit c3e9249

Please sign in to comment.