Skip to content

Implement POST /upload endpoint for multipart image upload with consumer validation#1

Open
Copilot wants to merge 4 commits intomainfrom
copilot/add-upload-image-endpoint
Open

Implement POST /upload endpoint for multipart image upload with consumer validation#1
Copilot wants to merge 4 commits intomainfrom
copilot/add-upload-image-endpoint

Conversation

Copy link

Copilot AI commented Dec 15, 2025

Implements FastAPI endpoint for uploading consumer meter reading images with MySQL persistence and validation.

Implementation

Core endpoint (main.py):

  • POST /upload accepts consumerId (int) and file (UploadFile) as multipart/form-data
  • Validates consumer exists in database, file format is JPEG/JPG/PNG
  • Saves to /srv/ecitko/uploads with naming: {consumerId}_{timestamp}_{filename}
  • Inserts record into images table with consumer_id, image_url, processed=0
  • Returns message, image_id, image_url on success
  • Cleans up file on database failure to prevent orphaned uploads

Database layer (database.py):

  • MySQL connection via mysql-connector-python with dependency injection
  • consumer_exists() validates consumer_id
  • insert_image() inserts record, returns generated id
  • Proper logging and error propagation

Infrastructure:

  • Docker Compose setup with MySQL 8.0 and API service
  • init.sql creates schema (consumers, images, readings) with test data
  • Dependencies: fastapi, uvicorn, python-multipart, mysql-connector-python

Example Usage

curl -X POST "http://localhost:8000/upload" \
  -F "consumerId=1" \
  -F "file=@meter_reading.jpg"

Response:

{
  "message": "Image uploaded successfully",
  "image_id": 42,
  "image_url": "/srv/ecitko/uploads/1_20231215_143022_meter_reading.jpg"
}

Interactive docs available at /docs (Swagger) and /redoc.

Original prompt

Zadatak: Implementirati endpoint POST /upload (multipart consumerId + image) za prihvat slike

Potrebno je kreirati kompletan FastAPI endpoint koji omogućava upload slika sa sledećim funkcionalnostima:

Zahtevi:

  1. Endpoint detalji:

    • Ruta: POST /upload
    • Prihvata multipart/form-data sa parametrima:
      • consumerId (int) - ID potrošača
      • file (UploadFile) - slika fajl
  2. Validacija:

    • Proveriti da li consumerId postoji u MySQL tabeli consumers
    • Proveriti da li je fajl validnog formata (JPEG, JPG, PNG)
    • Vratiti odgovarajuće error poruke ako validacija ne prođe
  3. Čuvanje slike:

    • Sačuvati sliku u direktorijum /srv/ecitko/uploads
    • Koristiti imenovanje: {consumerId}_{timestamp}_{original_filename}
  4. Upis u bazu:

    • Dodati zapis u MySQL tabelu images sa kolonama:
      • consumer_id - ID potrošača
      • image_url - putanja do slike
      • processed - default 0
      • created_at - automatski timestamp
  5. Odgovor:

    • Pri uspešnom uploadu vratiti JSON sa:
      • message: "Image uploaded successfully"
      • image_id: ID kreiranog zapisa u bazi
      • image_url: putanja do slike
    • Pri greški vratiti odgovarajući HTTP status i poruku

Tehnički zahtevi:

  • Koristiti FastAPI framework
  • MySQL konekcija preko mysql-connector-python ili pymysql
  • Kreirati ili ažurirati sledeće fajlove:
    • main.py - glavni FastAPI fajl sa endpointom
    • database.py - MySQL konekcija i helper funkcije
    • requirements.txt - dependencies (fastapi, uvicorn, python-multipart, mysql-connector-python, itd.)
    • .env.example - primer environment varijabli

MySQL konfiguracija:

DB_HOST=localhost
DB_USER=ecitko_user
DB_PASSWORD=strongpassword
DB_NAME=ecitko_db

Existing MySQL Schema:

Tabele već postoje:

  • consumers (id, name, address, created_at)
  • readings (id, consumer_id, reading_value, image_path, created_at)
  • images (id, consumer_id, image_url, processed, created_at)

Implementirati kompletno rešenje sa error handlingom i best practices.

This pull request was created as a result of the following prompt from Copilot chat.

Zadatak: Implementirati endpoint POST /upload (multipart consumerId + image) za prihvat slike

Potrebno je kreirati kompletan FastAPI endpoint koji omogućava upload slika sa sledećim funkcionalnostima:

Zahtevi:

  1. Endpoint detalji:

    • Ruta: POST /upload
    • Prihvata multipart/form-data sa parametrima:
      • consumerId (int) - ID potrošača
      • file (UploadFile) - slika fajl
  2. Validacija:

    • Proveriti da li consumerId postoji u MySQL tabeli consumers
    • Proveriti da li je fajl validnog formata (JPEG, JPG, PNG)
    • Vratiti odgovarajuće error poruke ako validacija ne prođe
  3. Čuvanje slike:

    • Sačuvati sliku u direktorijum /srv/ecitko/uploads
    • Koristiti imenovanje: {consumerId}_{timestamp}_{original_filename}
  4. Upis u bazu:

    • Dodati zapis u MySQL tabelu images sa kolonama:
      • consumer_id - ID potrošača
      • image_url - putanja do slike
      • processed - default 0
      • created_at - automatski timestamp
  5. Odgovor:

    • Pri uspešnom uploadu vratiti JSON sa:
      • message: "Image uploaded successfully"
      • image_id: ID kreiranog zapisa u bazi
      • image_url: putanja do slike
    • Pri greški vratiti odgovarajući HTTP status i poruku

Tehnički zahtevi:

  • Koristiti FastAPI framework
  • MySQL konekcija preko mysql-connector-python ili pymysql
  • Kreirati ili ažurirati sledeće fajlove:
    • main.py - glavni FastAPI fajl sa endpointom
    • database.py - MySQL konekcija i helper funkcije
    • requirements.txt - dependencies (fastapi, uvicorn, python-multipart, mysql-connector-python, itd.)
    • .env.example - primer environment varijabli

MySQL konfiguracija:

DB_HOST=localhost
DB_USER=ecitko_user
DB_PASSWORD=strongpassword
DB_NAME=ecitko_db

Existing MySQL Schema:

Tabele već postoje:

  • consumers (id, name, address, created_at)
  • readings (id, consumer_id, reading_value, image_path, created_at)
  • images (id, consumer_id, image_url, processed, created_at)

Implementirati kompletno rešenje sa error handlingom i best practices.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits December 15, 2025 22:24
…pload

Co-authored-by: drdule <204492158+drdule@users.noreply.github.com>
Co-authored-by: drdule <204492158+drdule@users.noreply.github.com>
@drdule drdule marked this pull request as ready for review December 15, 2025 22:26
Copilot AI review requested due to automatic review settings December 15, 2025 22:26
Co-authored-by: drdule <204492158+drdule@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement POST /upload endpoint for image upload Implement POST /upload endpoint for multipart image upload with consumer validation Dec 15, 2025
Copilot AI requested a review from drdule December 15, 2025 22:28
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a FastAPI-based image upload endpoint for a meter reading application. The implementation includes MySQL database integration, file validation, and proper error handling. The PR creates a complete working application from scratch with all necessary configuration files and documentation.

Key Changes

  • Added POST /upload endpoint with multipart form support for consumer ID and image file
  • Implemented MySQL database layer with connection management and helper functions for consumer validation and image record insertion
  • Added comprehensive validation for file formats (JPEG, JPG, PNG) and consumer existence

Reviewed changes

Copilot reviewed 8 out of 10 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
requirements.txt Specifies Python dependencies including FastAPI, uvicorn, mysql-connector-python, and python-multipart
main.py Implements the FastAPI application with /upload endpoint, file validation, and error handling
database.py Provides Database class for MySQL connection management and query execution
README.md Comprehensive documentation including installation, API usage, and database schema
.env.example Template for environment variables including database credentials and upload directory
.gitignore Standard Python gitignore rules to exclude environment files, caches, and IDE files

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +73 to +75
contents = await file.read()
with open(file_path, "wb") as f:
f.write(contents)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file is read entirely into memory before writing. For large image files, this could cause memory issues. Consider using chunked reading and writing to handle large files more efficiently, especially for a production upload endpoint.

Suggested change
contents = await file.read()
with open(file_path, "wb") as f:
f.write(contents)
CHUNK_SIZE = 1024 * 1024 # 1MB per chunk
with open(file_path, "wb") as f:
while True:
chunk = await file.read(CHUNK_SIZE)
if not chunk:
break
f.write(chunk)

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slika je mala može da bude do 2 mega pixela, uslovljeni smo uređajem koje imamo.


# Save file to disk
try:
contents = await file.read()
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no limit on file size, which could allow users to upload extremely large files and cause disk space exhaustion or DoS attacks. Add a file size validation check before reading the file contents.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

možemo da ograničimo kada budemo znali koliku sliku šalje uređaj

upload_path.mkdir(parents=True, exist_ok=True)

# Generate filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timestamp format lacks milliseconds or microseconds, which could lead to filename collisions if multiple uploads occur within the same second for the same consumer. Consider adding milliseconds to the timestamp format or using a UUID to ensure uniqueness.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nisu potrebne milisekunde jer uređaj ne može da napravi više do 1 slike u post 1 slanju

Comment on lines +96 to +97
status_code=500,
detail=f"Database error: {str(e)}"
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message exposes internal database error details with str(e). In production, this could leak sensitive information about database structure or queries. Consider logging the full error internally and returning a generic error message to the user.

Copilot uses AI. Check for mistakes.
self.user = os.getenv('DB_USER', 'ecitko_user')
self.password = os.getenv('DB_PASSWORD', 'strongpassword')
self.database = os.getenv('DB_NAME', 'ecitko_db')
self.connection = None
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Database class stores a single connection instance that is shared across methods. In a web application with concurrent requests, this could lead to race conditions or connection state issues. Consider using a connection pool or ensuring thread-safety, especially since FastAPI is async by default.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +79
status_code=500,
detail=f"Failed to save file: {str(e)}"
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message exposes internal exception details with str(e). In production, this could leak sensitive information about the file system structure or permissions. Consider logging the full error internally and returning a generic error message to the user.

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +90
if image_id is None:
raise HTTPException(
status_code=500,
detail="Failed to insert image record into database"
)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for image_id being None is redundant since the previous except block would catch any database errors. If insert_image returns None without raising an exception, that would indicate a logic error in the database module. Consider removing this check or documenting why insert_image might return None without raising an exception.

Suggested change
if image_id is None:
raise HTTPException(
status_code=500,
detail="Failed to insert image record into database"
)

Copilot uses AI. Check for mistakes.
password=self.password,
database=self.database
)
return self.connection
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The connect method returns the connection, but this return value is not used anywhere (the connection is already stored in self.connection). Consider removing the return statement to simplify the method signature, or document why it might be needed.

Suggested change
return self.connection

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +51
query = "SELECT id FROM consumers WHERE id = %s"
cursor.execute(query, (consumer_id,))
result = cursor.fetchone()
cursor.close()
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cursor is not closed in an exception scenario. If an error occurs after cursor creation but before cursor.close(), the cursor will leak. Use a try-finally block or context manager to ensure the cursor is always closed.

Copilot uses AI. Check for mistakes.

# Save file to disk
try:
contents = await file.read()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

možemo da ograničimo kada budemo znali koliku sliku šalje uređaj


CREATE TABLE IF NOT EXISTS consumers (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

treba mi ispred name šifra korisnika, ili kako da rešimo jer jedan korisnik može da ima više vodomera koje treba da očitamo

upload_path.mkdir(parents=True, exist_ok=True)

# Generate filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nisu potrebne milisekunde jer uređaj ne može da napravi više do 1 slike u post 1 slanju


# Generate filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
new_filename = f"{consumerId}_{timestamp}_{file.filename}"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok je ovo ali ako ne opterećuje sistem možemo da koristimo čisto radi bezbednosti jer post slanjem mi kreiramo ime datoteke

Comment on lines +73 to +75
contents = await file.read()
with open(file_path, "wb") as f:
f.write(contents)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slika je mala može da bude do 2 mega pixela, uslovljeni smo uređajem koje imamo.

Comment on lines +27 to +107
@app.post("/upload")
async def upload_image(
consumerId: int = Form(...),
file: UploadFile = File(...),
db: Database = Depends(get_database)
):
"""
Upload an image for a consumer.

Args:
consumerId: The ID of the consumer
file: The image file to upload (JPEG, JPG, or PNG)
db: Database dependency

Returns:
JSON response with message, image_id, and image_url
"""

# Validate file format
if not file.filename:
raise HTTPException(status_code=400, detail="No file provided")

if not is_allowed_file(file.filename):
raise HTTPException(
status_code=400,
detail=f"Invalid file format. Allowed formats: {', '.join(ALLOWED_EXTENSIONS).upper()}"
)

# Validate consumer exists
if not db.consumer_exists(consumerId):
raise HTTPException(
status_code=404,
detail=f"Consumer with ID {consumerId} not found"
)

# Create upload directory if it doesn't exist
upload_path = Path(UPLOAD_DIR)
upload_path.mkdir(parents=True, exist_ok=True)

# Generate filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
new_filename = f"{consumerId}_{timestamp}_{file.filename}"
file_path = upload_path / new_filename

# Save file to disk
try:
contents = await file.read()
with open(file_path, "wb") as f:
f.write(contents)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to save file: {str(e)}"
)

# Insert record into database
image_url = str(file_path)
try:
image_id = db.insert_image(consumerId, image_url)
if image_id is None:
raise HTTPException(
status_code=500,
detail="Failed to insert image record into database"
)
except Exception as e:
# Clean up file if database insert fails
if file_path.exists():
file_path.unlink()
raise HTTPException(
status_code=500,
detail=f"Database error: {str(e)}"
)

return JSONResponse(
status_code=200,
content={
"message": "Image uploaded successfully",
"image_id": image_id,
"image_url": image_url
}
)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

može samo greške sistema ili baze podataka.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants