diff --git a/frontend/src/components/DownloadKernelImage.tsx b/frontend/src/components/DownloadKernelImage.tsx index ceb21467..9846587b 100644 --- a/frontend/src/components/DownloadKernelImage.tsx +++ b/frontend/src/components/DownloadKernelImage.tsx @@ -49,12 +49,17 @@ const DownloadKernelImage = ({ kernelImage, onEdit, onDelete }: Props) => { setIsDownloading(true); try { + console.log( + `Requesting download URL for kernel image ID: ${kernelImage.id}`, + ); const response = await axios.get( `/api/kernel-images/download/${kernelImage.id}`, ); const presignedUrl = response.data; + console.log(`Received presigned URL: ${presignedUrl}`); + // Open the URL in a new tab window.open(presignedUrl, "_blank"); } catch (error) { console.error("Error downloading kernel image:", error); diff --git a/frontend/src/components/modals/EditKernelImageModal.tsx b/frontend/src/components/modals/EditKernelImageModal.tsx index f2f7f76d..b01f4be3 100644 --- a/frontend/src/components/modals/EditKernelImageModal.tsx +++ b/frontend/src/components/modals/EditKernelImageModal.tsx @@ -37,12 +37,13 @@ export const EditKernelImageModal = ({ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await onEdit({ + const updatedData: Partial = { name, description, is_public: isPublic, is_official: isOfficial, - }); + }; + await onEdit(updatedData); onOpenChange(false); }; diff --git a/frontend/src/components/modals/UploadModal.tsx b/frontend/src/components/modals/UploadKerenlImageModal.tsx similarity index 83% rename from frontend/src/components/modals/UploadModal.tsx rename to frontend/src/components/modals/UploadKerenlImageModal.tsx index 4b11e933..64ffcbd4 100644 --- a/frontend/src/components/modals/UploadModal.tsx +++ b/frontend/src/components/modals/UploadKerenlImageModal.tsx @@ -13,7 +13,7 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Upload, X } from "lucide-react"; -interface UploadModalProps { +interface UploadKernelImageModalProps { isOpen: boolean; onClose: () => void; onUpload: ( @@ -25,12 +25,25 @@ interface UploadModalProps { ) => Promise; } -export function UploadModal({ isOpen, onClose, onUpload }: UploadModalProps) { +export function UploadKernelImageModal({ + isOpen, + onClose, + onUpload, +}: UploadKernelImageModalProps) { const [file, setFile] = useState(null); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [isPublic, setIsPublic] = useState(false); const [isOfficial, setIsOfficial] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const resetModalData = useCallback(() => { + setFile(null); + setName(""); + setDescription(""); + setIsPublic(false); + setIsOfficial(false); + }, []); const onDrop = useCallback((acceptedFiles: File[]) => { if (acceptedFiles.length > 0) { @@ -45,17 +58,38 @@ export function UploadModal({ isOpen, onClose, onUpload }: UploadModalProps) { const handleUpload = useCallback(async () => { if (file) { + setIsLoading(true); try { await onUpload(file, name, description, isPublic, isOfficial); + resetModalData(); onClose(); } catch (error) { console.error("Error uploading kernel image:", error); + } finally { + setIsLoading(false); } } - }, [file, name, description, isPublic, isOfficial, onUpload, onClose]); + }, [ + file, + name, + description, + isPublic, + isOfficial, + onUpload, + onClose, + resetModalData, + ]); return ( - + { + if (!open) { + resetModalData(); + } + onClose(); + }} + > Upload Kernel Image @@ -141,10 +175,11 @@ export function UploadModal({ isOpen, onClose, onUpload }: UploadModalProps) {
diff --git a/frontend/src/components/pages/Download.tsx b/frontend/src/components/pages/Download.tsx index 049479d4..2ba4110d 100644 --- a/frontend/src/components/pages/Download.tsx +++ b/frontend/src/components/pages/Download.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import DownloadKernelImage from "@/components/DownloadKernelImage"; import LoadingArtifactCard from "@/components/listing/artifacts/LoadingArtifactCard"; -import { UploadModal } from "@/components/modals/UploadModal"; +import { UploadKernelImageModal } from "@/components/modals/UploadKerenlImageModal"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -215,7 +215,7 @@ export default function DownloadsPage() { {canUpload && ( - setIsUploadModalOpen(false)} onUpload={handleUpload} diff --git a/store/app/crud/kernel_images.py b/store/app/crud/kernel_images.py index 59bd04b6..d516e04b 100644 --- a/store/app/crud/kernel_images.py +++ b/store/app/crud/kernel_images.py @@ -2,9 +2,8 @@ import io import logging -from typing import TypedDict +from typing import IO, TypedDict -import boto3 from botocore.exceptions import ClientError from fastapi import UploadFile @@ -69,13 +68,46 @@ async def update_kernel_image(self, kernel_image_id: str, user: User, **updates: valid_updates = {k: v for k, v in updates.items() if k in valid_fields} if valid_updates: + kernel_image = await self.get_kernel_image(kernel_image_id) + if kernel_image is None: + raise ValueError("Kernel image not found") + + # If the name is being updated, we need to rename the S3 object + if ( + "name" in valid_updates + and isinstance(valid_updates["name"], str) + and valid_updates["name"] != kernel_image.name + ): + old_s3_filename = f"{kernel_image.id}/{kernel_image.name}" + new_s3_filename = f"{kernel_image.id}/{valid_updates['name']}" + await self._rename_s3_object(old_s3_filename, new_s3_filename) + await self._update_item(kernel_image_id, KernelImage, valid_updates) + async def _rename_s3_object(self, old_filename: str, new_filename: str) -> None: + try: + # Use self.s3 instead of aioboto3.client + await self.s3.meta.client.copy_object( + Bucket=settings.s3.bucket, + CopySource=f"{settings.s3.bucket}/{settings.s3.prefix}{old_filename}", + Key=f"{settings.s3.prefix}{new_filename}", + ) + await self.s3.meta.client.delete_object( + Bucket=settings.s3.bucket, Key=f"{settings.s3.prefix}{old_filename}" + ) + except ClientError as e: + logger.error(f"Error renaming object in S3: {e}") + raise + async def delete_kernel_image(self, kernel_image: KernelImage, user: User) -> None: if not user.permissions or not ({"is_mod", "is_admin"} & user.permissions): raise ValueError("Only moderators or admins can delete kernel images") - await self._delete_from_s3(f"{kernel_image.id}/{kernel_image.name}") + # Delete the file from S3 + s3_filename = f"{kernel_image.id}/{kernel_image.name}" + await self._delete_from_s3(s3_filename) + + # Delete the item from the database await self._delete_item(kernel_image) async def get_public_kernel_images(self) -> list[KernelImage]: @@ -92,17 +124,57 @@ async def increment_downloads(self, kernel_image_id: str) -> None: async def get_kernel_image_download_url(self, kernel_image: KernelImage) -> str: s3_filename = f"{kernel_image.id}/{kernel_image.name}" + logger.info(f"Generating presigned URL for S3 filename: {s3_filename}") + + # Check if the object exists in S3 + try: + await self.s3.meta.client.head_object(Bucket=settings.s3.bucket, Key=f"{settings.s3.prefix}{s3_filename}") + except ClientError as e: + if e.response["Error"]["Code"] == "404": + logger.error(f"S3 object not found: {s3_filename}") + raise ValueError(f"S3 object not found: {s3_filename}") + else: + logger.error(f"Error checking S3 object: {str(e)}") + raise + return await self._get_presigned_url(s3_filename) async def _get_presigned_url(self, s3_filename: str) -> str: - s3_client = boto3.client("s3") try: - presigned_url = s3_client.generate_presigned_url( + logger.info( + f"Generating presigned URL for S3 bucket: {settings.s3.bucket}, key: {settings.s3.prefix}{s3_filename}" + ) + presigned_url = await self.s3.meta.client.generate_presigned_url( "get_object", - Params={"Bucket": settings.s3.bucket, "Key": f"{settings.s3.prefix}{s3_filename}"}, - ExpiresIn=3600, - ) # URL expires in 1 hour + Params={ + "Bucket": settings.s3.bucket, + "Key": f"{settings.s3.prefix}{s3_filename}", + }, + ExpiresIn=3600, # URL expires in 1 hour + ) + logger.info(f"Generated presigned URL: {presigned_url}") + return presigned_url except ClientError as e: logger.error(f"Error generating presigned URL: {e}") raise - return presigned_url + + async def _delete_from_s3(self, s3_filename: str) -> None: + try: + # Use self.s3 instead of aioboto3.client + await self.s3.meta.client.delete_object(Bucket=settings.s3.bucket, Key=f"{settings.s3.prefix}{s3_filename}") + except ClientError as e: + logger.error(f"Error deleting object from S3: {e}") + raise + + async def _upload_to_s3(self, data: IO[bytes], name: str, filename: str, content_type: str) -> None: + try: + # Use self.s3 instead of aioboto3.client + await self.s3.meta.client.upload_fileobj( + data, + settings.s3.bucket, + f"{settings.s3.prefix}{filename}", + ExtraArgs={"ContentType": content_type}, + ) + except ClientError as e: + logger.error(f"Error uploading file to S3: {e}") + raise diff --git a/store/app/routers/kernel_images.py b/store/app/routers/kernel_images.py index 65b6c5da..33ff2e26 100644 --- a/store/app/routers/kernel_images.py +++ b/store/app/routers/kernel_images.py @@ -178,10 +178,15 @@ async def download_kernel_image( crud: Annotated[Crud, Depends(Crud.get)], user: Annotated[User, Depends(get_session_user_with_read_permission)], ) -> str: + logger.info(f"Received download request for kernel image ID: {kernel_image_id}") + kernel_image = await crud.get_kernel_image(kernel_image_id) if kernel_image is None: + logger.error(f"Kernel image not found: {kernel_image_id}") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kernel image not found") + if not kernel_image.is_public and user.id != kernel_image.user_id: + logger.error(f"Permission denied for user {user.id} to download kernel image {kernel_image_id}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You do not have permission to download this kernel image" ) @@ -190,5 +195,10 @@ async def download_kernel_image( await crud.increment_downloads(kernel_image_id) # Get the download URL - download_url = await crud.get_kernel_image_download_url(kernel_image) - return download_url + try: + download_url = await crud.get_kernel_image_download_url(kernel_image) + logger.info(f"Generated presigned URL for kernel image {kernel_image_id}: {download_url}") + return download_url + except Exception as e: + logger.error(f"Error generating presigned URL for kernel image {kernel_image_id}: {str(e)}") + raise HTTPException(status_code=500, detail="Error generating download URL")