From ead618ebf9de471c3b535d6430d0d509f594fdb4 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 23 Aug 2024 12:40:28 +0300 Subject: [PATCH] feature:DynamoDB_S3_connection --- .env | 0 .gitignore | 5 +- frontend/.env.development | 1 + frontend/.env.production | 1 + frontend/src/App.tsx | 2 + frontend/src/{hooks/api.tsx => api/api.ts} | 21 +++- frontend/src/pages/Test.tsx | 117 +++++++++++++++++++++ linguaphoto/main.py | 92 ++++++++++++++-- linguaphoto/requirements.txt | 2 + 9 files changed, 230 insertions(+), 11 deletions(-) delete mode 100644 .env create mode 100644 frontend/.env.development create mode 100644 frontend/.env.production rename frontend/src/{hooks/api.tsx => api/api.ts} (89%) create mode 100644 frontend/src/pages/Test.tsx diff --git a/.env b/.env deleted file mode 100644 index e69de29..0000000 diff --git a/.gitignore b/.gitignore index 0825462..706268f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,7 @@ build/ dist/ *.so out*/ -venv/ \ No newline at end of file +venv/ + + +linguaphoto/.env diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..69a2392 --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1 @@ +REACT_APP_BACKEND_URL=http://localhost:8080 \ No newline at end of file diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..2ea8fee --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1 @@ +REACT_APP_BACKEND_URL=https://api.linguaphoto.com \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 526a0f7..095e3e5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import { AuthenticationProvider, OneTimePasswordWrapper } from "hooks/auth"; import { ThemeProvider } from "hooks/theme"; import Home from "pages/Home"; import NotFound from "pages/NotFound"; +import Test from "pages/Test"; import { Container } from "react-bootstrap"; import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import "./App.css"; @@ -23,6 +24,7 @@ const App = () => { } /> + } /> } /> } /> diff --git a/frontend/src/hooks/api.tsx b/frontend/src/api/api.ts similarity index 89% rename from frontend/src/hooks/api.tsx rename to frontend/src/api/api.ts index cca300f..c642a4a 100644 --- a/frontend/src/hooks/api.tsx +++ b/frontend/src/api/api.ts @@ -14,8 +14,8 @@ export interface Bom { } export interface Image { - caption: string; - url: string; + filename: string; + s3_url: string; } export interface Robot { @@ -33,6 +33,23 @@ export class api { constructor(api: AxiosInstance) { this.api = api; } + public async test(): Promise { + const response = await this.api.get(`/`); + return response.data.message; + } + public async handleUpload(formData: FormData): Promise { + try { + const response = await this.api.post("/upload/", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + return response.data; + } catch (error) { + console.error("Error uploading the file", error); + return { s3_url: "", filename: "" }; + } + } public async getUserById(userId: string | undefined): Promise { const response = await this.api.get(`/users/${userId}`); return response.data.email; diff --git a/frontend/src/pages/Test.tsx b/frontend/src/pages/Test.tsx new file mode 100644 index 0000000..d9aa7eb --- /dev/null +++ b/frontend/src/pages/Test.tsx @@ -0,0 +1,117 @@ +import { api } from "api/api"; +import axios, { AxiosInstance } from "axios"; +import { ChangeEvent, useState } from "react"; +import { Col, Container, Row } from "react-bootstrap"; + +// Custom styles (you can include these in a separate CSS file or use styled-components) +const customStyles = { + input: { + marginBottom: "1rem", + borderRadius: "5px", + }, + button: { + backgroundColor: "#007bff", + borderColor: "#007bff", + color: "#fff", + borderRadius: "5px", + }, + img: { + maxWidth: "100%", + borderRadius: "10px", + boxShadow: "0 4px 8px rgba(0,0,0,0.3)", + }, + link: { + color: "#007bff", + textDecoration: "none", + }, +}; + +const Home = () => { + console.log(process.env.REACT_APP_BACKEND_URL); + const apiClient: AxiosInstance = axios.create({ + baseURL: process.env.REACT_APP_BACKEND_URL, // Base URL for all requests + // baseURL: 'https://api.linguaphoto.com', // Base URL for all requests + timeout: 10000, // Request timeout (in milliseconds) + headers: { + "Content-Type": "application/json", + Authorization: "Bearer your_token_here", // Add any default headers you need + }, + }); + const API = new api(apiClient); + const [message, setMessage] = useState("Linguaphoto"); + const [imageURL, setImageURL] = useState(""); + const [file, setFile] = useState(null); + (async () => { + const text = await (async () => { + return await API.test(); + })(); + setMessage(text); + })(); + + const handleFileChange = (event: ChangeEvent) => { + const selectedFile = event.target.files?.[0] || null; + setFile(selectedFile); + }; + + // Handle file upload + const handleUpload = async () => { + if (!file) { + console.error("No file selected"); + return; + } + + const formData = new FormData(); + formData.append("file", file); + + try { + const response = await API.handleUpload(formData); + setImageURL(response.s3_url); + } catch (error) { + console.error("Error uploading the file", error); + } + }; + + return ( + + + +

{message}

+ + + {imageURL && ( +
+

Uploaded Image

+ Uploaded file +

+ Image URL:{" "} + + {imageURL} + +

+
+ )} + +
+
+ ); +}; + +export default Home; diff --git a/linguaphoto/main.py b/linguaphoto/main.py index 0113154..8b3d5c7 100644 --- a/linguaphoto/main.py +++ b/linguaphoto/main.py @@ -1,9 +1,17 @@ """Defines the main entrypoint for the FastAPI app.""" +import os +import uuid + +import aioboto3 import uvicorn -from fastapi import FastAPI, Request, status +from dotenv import load_dotenv +from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse +from pydantic import BaseModel + +# Load environment variables from .env file +load_dotenv() app = FastAPI() @@ -16,13 +24,81 @@ allow_headers=["*"], ) +# Retrieve AWS configuration from environment variables +bucket_name = os.getenv("S3_BUCKET_NAME") +dynamodb_table_name = os.getenv("DYNAMODB_TABLE_NAME") + + +class ImageMetadata(BaseModel): + filename: str + s3_url: str + + +@app.post("/upload/", response_model=ImageMetadata) +async def upload_image(file: UploadFile = File(...)) -> ImageMetadata: + if file.filename is None or not file.filename: + raise HTTPException(status_code=400, detail="File name is missing.") + + try: + # Generate a unique file name + file_extension = file.filename.split(".")[-1] if "." in file.filename else "unknown" + unique_filename = f"{uuid.uuid4()}.{file_extension}" + + if bucket_name is None: + raise HTTPException(status_code=500, detail="Bucket name is not set.") + + if dynamodb_table_name is None: + raise HTTPException(status_code=500, detail="DynamoDB table name is not set.") + + # Create an S3 client with aioboto3 + async with aioboto3.Session().client( + "s3", + region_name=os.getenv("AWS_REGION"), + aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"), + aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"), + ) as s3_client: + # Upload the file to S3 + await s3_client.upload_fileobj(file.file, bucket_name, f"uploads/{unique_filename}") + s3_url = f"https://{bucket_name}.s3.amazonaws.com/uploads/{unique_filename}" + + # Create a DynamoDB resource with aioboto3 + async with aioboto3.Session().resource( + "dynamodb", + region_name=os.getenv("AWS_REGION"), + aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"), + aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"), + ) as dynamodb: + table = await dynamodb.Table(dynamodb_table_name) + # Save metadata to DynamoDB + await table.put_item(Item={"id": unique_filename, "s3_url": s3_url}) + + return ImageMetadata(filename=unique_filename, s3_url=s3_url) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# @app.get("/download/{filename}") +# async def download_image(filename: str): +# try: +# async with aioboto3.Session().resource( +# "dynamodb", +# region_name=os.getenv("AWS_REGION"), +# aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"), +# aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"), +# ) as dynamodb: +# table = await dynamodb.Table(dynamodb_table_name) +# # Retrieve image metadata from DynamoDB +# response = await table.get_item(Key={"id": filename}) + +# if "Item" not in response: +# raise HTTPException(status_code=404, detail="Image not found") + +# # Return the S3 URL for download +# return {"s3_url": response["Item"]["s3_url"]} -@app.exception_handler(ValueError) -async def value_error_exception_handler(request: Request, exc: ValueError) -> JSONResponse: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content={"message": "The request was invalid.", "detail": str(exc)}, - ) +# except Exception as e: +# raise HTTPException(status_code=500, detail=str(e)) @app.get("/") diff --git a/linguaphoto/requirements.txt b/linguaphoto/requirements.txt index 6330399..b93fad8 100644 --- a/linguaphoto/requirements.txt +++ b/linguaphoto/requirements.txt @@ -30,3 +30,5 @@ numpy-stl # Types types-aioboto3[dynamodb, s3] + +python-dotenv \ No newline at end of file