Skip to content

Commit

Permalink
Merge pull request #4 from kscalelabs/dev
Browse files Browse the repository at this point in the history
feature:DynamoDB_S3_connection
  • Loading branch information
Serhii Ofii authored Aug 23, 2024
2 parents fd63d34 + ead618e commit 7a3eeb6
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 11 deletions.
Empty file removed .env
Empty file.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ build/
dist/
*.so
out*/
venv/
venv/


linguaphoto/.env
1 change: 1 addition & 0 deletions frontend/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REACT_APP_BACKEND_URL=http://localhost:8080
1 change: 1 addition & 0 deletions frontend/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REACT_APP_BACKEND_URL=https://api.linguaphoto.com
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -23,6 +24,7 @@ const App = () => {
<Container className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/test" element={<Test />} />
<Route path="/404" element={<NotFound />} />
<Route path="*" element={<NotFoundRedirect />} />
</Routes>
Expand Down
21 changes: 19 additions & 2 deletions frontend/src/hooks/api.tsx → frontend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export interface Bom {
}

export interface Image {
caption: string;
url: string;
filename: string;
s3_url: string;
}

export interface Robot {
Expand All @@ -33,6 +33,23 @@ export class api {
constructor(api: AxiosInstance) {
this.api = api;
}
public async test(): Promise<string> {
const response = await this.api.get(`/`);
return response.data.message;
}
public async handleUpload(formData: FormData): Promise<Image> {
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<string> {
const response = await this.api.get(`/users/${userId}`);
return response.data.email;
Expand Down
117 changes: 117 additions & 0 deletions frontend/src/pages/Test.tsx
Original file line number Diff line number Diff line change
@@ -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<string>("");
const [file, setFile] = useState<File | null>(null);
(async () => {
const text = await (async () => {
return await API.test();
})();
setMessage(text);
})();

const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
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 (
<Container
className="flex-column pt-5 gap-4 d-flex justify-content-center"
style={{ display: "flex", minHeight: "90vh" }}
>
<Row className="align-items-center">
<Col lg={4}>
<h1 className="display-4">{message}</h1>
<input
type="file"
onChange={handleFileChange}
style={customStyles.input}
/>
<button onClick={handleUpload} style={customStyles.button}>
Upload
</button>
{imageURL && (
<div>
<h2>Uploaded Image</h2>
<img
src={imageURL}
alt="Uploaded file"
style={customStyles.img}
/>
<p>
Image URL:{" "}
<a
href={imageURL}
target="_blank"
rel="noopener noreferrer"
style={customStyles.link}
>
{imageURL}
</a>
</p>
</div>
)}
</Col>
</Row>
</Container>
);
};

export default Home;
92 changes: 84 additions & 8 deletions linguaphoto/main.py
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -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("/")
Expand Down
2 changes: 2 additions & 0 deletions linguaphoto/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ numpy-stl

# Types
types-aioboto3[dynamodb, s3]

python-dotenv

0 comments on commit 7a3eeb6

Please sign in to comment.