diff --git a/optifit backend/__pycache__/app.cpython-310.pyc b/optifit backend/__pycache__/app.cpython-310.pyc index bc000b9..d7d5c63 100644 Binary files a/optifit backend/__pycache__/app.cpython-310.pyc and b/optifit backend/__pycache__/app.cpython-310.pyc differ diff --git a/optifit backend/__pycache__/squat_counter.cpython-310.pyc b/optifit backend/__pycache__/squat_counter.cpython-310.pyc index e9c8d96..fdc8357 100644 Binary files a/optifit backend/__pycache__/squat_counter.cpython-310.pyc and b/optifit backend/__pycache__/squat_counter.cpython-310.pyc differ diff --git a/optifit backend/app.py b/optifit backend/app.py index 78e5e9e..34bd6e4 100644 --- a/optifit backend/app.py +++ b/optifit backend/app.py @@ -5,6 +5,7 @@ pass else: ssl._create_default_https_context = _create_unverified_https_context + from flask import Flask, request, send_file, jsonify, url_for import os import threading @@ -12,14 +13,30 @@ import time from werkzeug.utils import secure_filename from squat_counter import process_squat_video # Import the actual processing logic +from validation import * +import logging app = Flask(__name__) +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + UPLOAD_FOLDER = 'uploads' PROCESSED_FOLDER = 'processed' os.makedirs(UPLOAD_FOLDER, exist_ok=True) os.makedirs(PROCESSED_FOLDER, exist_ok=True) + +# Creates standardised error response +def error_response(message, status_code): + return jsonify({ + "success": False, + "error": True, + "message": message, + "status_code": status_code, + "timestamp": int(time.time()) + }), status_code + # In-memory job store: {job_id: {"status": "processing"/"done", "result": {...}}} jobs = {} @@ -40,7 +57,7 @@ def process_video_async(job_id, input_path, output_path, video_url): jobs[job_id]["error"] = str(e) print(f"Error in background processing: {e}") - +#Route to home @app.route('/', methods=['GET']) def home(): base_info = { @@ -51,57 +68,70 @@ def home(): "/result/": "GET - Check processing status and get results" } } - - return base_info, 200 + return jsonify(base_info), 200 + +#Route to ping the server @app.route('/ping', methods=['GET']) def ping(): - return {"message": "Server is live!"}, 200 + return jsonify({"message": "Server is live!"}), 200 +#Route to get upload the video @app.route('/upload', methods=['POST']) def upload_video(): - if 'video' not in request.files: - return {'error': 'No video file part'}, 400 + try: + video = validate_upload_request(request) + validate_video_file(video) - video = request.files['video'] - filename = secure_filename(video.filename) - input_path = os.path.join(UPLOAD_FOLDER, filename) - output_path = os.path.join(PROCESSED_FOLDER, f"processed_{filename}") - - # Save uploaded video - video.save(input_path) - - # Generate video URL - video_url = url_for('get_processed_video', filename=f"processed_{filename}", _external=True) - - # Create job - job_id = str(uuid.uuid4()) - jobs[job_id] = {"status": "processing"} - - # Start background processing with pre-generated URL - threading.Thread(target=process_video_async, args=(job_id, input_path, output_path, video_url)).start() + filename = secure_filename(video.filename) + input_path = os.path.join(UPLOAD_FOLDER, filename) + output_path = os.path.join(PROCESSED_FOLDER, f"processed_{filename}") + + # Save uploaded video + video.save(input_path) + + # Generate video URL + video_url = url_for('get_processed_video', filename=f"processed_{filename}", _external=True) + + # Create job + job_id = str(uuid.uuid4()) + jobs[job_id] = {"status": "processing"} + + # Start background processing with pre-generated URL + threading.Thread(target=process_video_async, args=(job_id, input_path, output_path, video_url)).start() + + response_data = { + "status": "processing", + "job_id": job_id, + "message": "Video uploaded successfully. Processing started.", + "video_url": video_url + } + + return jsonify(response_data) - response_data = { - "status": "processing", - "job_id": job_id, - "message": "Video uploaded successfully. Processing started.", - "video_url": video_url - } + except APIError as e: + logger.error(f"API Error in {request.endpoint}: {e.message}", exc_info=True) + return error_response(e.message, e.status_code) - return jsonify(response_data) + +# Route to get the result of the job with the job id @app.route('/result/', methods=['GET']) def get_result(job_id): - job = jobs.get(job_id) - if not job: - return jsonify({"status": "not_found", "error": "Job not found"}), 404 - - if job["status"] == "processing": - return jsonify({"status": "processing", "message": "Video is being processed..."}) - elif job["status"] == "error": - return jsonify({"status": "error", "error": job.get("error", "Unknown error")}), 500 - else: - return jsonify({"status": "done", "result": job["result"]}) + try: + validate_job_request(job_id,jobs) + + job = jobs.get(job_id) + if job["status"] == "processing": + return jsonify({"status": "processing", "message": "Video is being processed..."}) + elif job["status"] == "error": + raise InternalServerError("Unknown Error") + else: + return jsonify({"status": "done", "result": job["result"]}) + + except APIError as e: + logger.error(f"API Error in {request.endpoint}: {e.message}", exc_info=True) + return error_response(e.message, e.status_code) # New endpoint to serve processed videos by filename @app.route('/processed/') diff --git a/optifit backend/validation.py b/optifit backend/validation.py new file mode 100644 index 0000000..1740dc9 --- /dev/null +++ b/optifit backend/validation.py @@ -0,0 +1,117 @@ +import os +import uuid + +class APIError(Exception): + def __init__(self, message, status_code=500): + self.message = message + self.status_code = status_code + +class BadRequestError(APIError): + def __init__(self, message): + super().__init__(message, 400) + +class NotFoundError(APIError): + def __init__(self, message): + super().__init__(message, 404) + +class MethodNotAllowedError(APIError): + def __init__(self, message): + super().__init__(message, 405) + +class UnsupportedMediaTypeError(APIError): + def __init__(self, message): + super().__init__(message, 415) + +class InternalServerError(APIError): + def __init__(self, message): + super().__init__(message, 500) + +class PayloadTooLargeError(APIError): + def __init__(self, message): + super().__init__(message, 413) + + +# Configuration constants +MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB (removed extra * 1024) +ALLOWED_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.wmv'} + + +# Validating the upload request for the file +def validate_upload_request(request): + # Checks included -> valid method, video field in request, if video is provided + + if request.method != 'POST': + raise MethodNotAllowedError("Only POST method is allowed") + + if not request.files: + raise BadRequestError("No files sent in request") + + if 'video' not in request.files: + raise BadRequestError("No 'video' field found in request") + + video = request.files['video'] + + if not video: + raise BadRequestError("No video file provided") + + if not video.filename or video.filename.strip() == '': + raise BadRequestError("No video file selected") + + return video + + +# Validates the video file iteslf in terms of file size, extensions and filename +def validate_video_file(file): + # Calculating the file size + file.seek(0, 2) # Seek to end + file_size = file.tell() + file.seek(0) + + #Checks if the file size is within limits + if file_size == 0: + raise BadRequestError("Video file is empty") + + if file_size > MAX_FILE_SIZE: + raise PayloadTooLargeError( + f"File size ({file_size / (1024*1024):.1f}MB) exceeds maximum " + f"allowed size ({MAX_FILE_SIZE / (1024*1024)}MB)" + ) + + if not file.filename: + raise BadRequestError("File has no name") + + + # Validates for given extensions + file_ext = os.path.splitext(file.filename)[1].lower() + if file_ext not in ALLOWED_EXTENSIONS: + raise UnsupportedMediaTypeError( + f"Unsupported file format '{file_ext}'. " + f"Allowed formats: {', '.join(ALLOWED_EXTENSIONS)}" + ) + + +# Validating the job request for getting the results +def validate_job_request(job_id, jobs): + if not job_id: + raise BadRequestError("Job ID is required") + + # Validate UUID format + try: + uuid.UUID(job_id) + except ValueError: + raise BadRequestError("Invalid job ID format") + + job = jobs.get(job_id) + if not job: + raise NotFoundError("Job not found") + + +def validate_file_serving(filename): + if not filename: + raise BadRequestError("Filename is required") + + if '..' in filename or '/' in filename or '\\' in filename: + raise BadRequestError("Invalid filename - path traversal not allowed") + + return True +