diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000..a04bf97 Binary files /dev/null and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..30561b1 Binary files /dev/null and b/__pycache__/config.cpython-313.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..a3e1ef1 --- /dev/null +++ b/app.py @@ -0,0 +1,31 @@ +import os +import logging +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from config import Config + +# Initialize Flask app +app = Flask(__name__) +app.config.from_object(Config) + +# Initialize database +db = SQLAlchemy(app) +# Configure logging + +os.makedirs(app.config['LOG_FOLDER'], exist_ok=True) +logging.basicConfig( +filename=f"{app.config['LOG_FOLDER']}/app.log", +level=logging.INFO, +format='%(asctime)s [%(levelname)s] %(message)s' +) + +# Register blueprints +from controllers.file_controller import file_bp +app.register_blueprint(file_bp) + +# Create tables if not exist +with app.app_context(): + db.create_all() + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..7cadb9e --- /dev/null +++ b/config.py @@ -0,0 +1,9 @@ +import os +from dotenv import load_dotenv +load_dotenv() # Load .env variables +class Config: + SQLALCHEMY_DATABASE_URI = (f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}@" f"{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}") + SQLALCHEMY_TRACK_MODIFICATIONS = False + UPLOAD_FOLDER = os.getenv('UPLOAD_FOLDER', 'uploads') + LOG_FOLDER = 'logs' + DEBUG = True diff --git a/controllers/__pycache__/file_controller.cpython-313.pyc b/controllers/__pycache__/file_controller.cpython-313.pyc new file mode 100644 index 0000000..d27794c Binary files /dev/null and b/controllers/__pycache__/file_controller.cpython-313.pyc differ diff --git a/controllers/file_controller.py b/controllers/file_controller.py new file mode 100644 index 0000000..031f43a --- /dev/null +++ b/controllers/file_controller.py @@ -0,0 +1,59 @@ +from flask import Blueprint, request, jsonify, send_file +from services.file_service import save_file, update_file, delete_file +from models.uploaded_file import UploadedFile +from app import app +import logging +import uuid +import os + +file_bp = Blueprint('file_bp', __name__) + +# POST — Upload file +@file_bp.route('/upload', methods=['POST']) +def upload_file(): + try: + if 'file' not in request.files: + return jsonify({'message': 'No file uploaded'}), 400 + file = request.files['file'] + folder = os.path.join(app.config['UPLOAD_FOLDER'], uuid.uuid4().hex) # new folder per request + uploaded = save_file(file, folder) + return jsonify({'message': 'Upload successful', 'id': uploaded.id, 'path': uploaded.file_path}), 201 + except Exception as e: + logging.error(f"Upload error: {e}") + return jsonify({'message': str(e)}), 400 + +# GET — Retrieve file info or download +@file_bp.route('/file/', methods=['GET']) +def get_file(file_id): + try: + file = UploadedFile.query.get(file_id) + if not file: + return jsonify({'message': 'File not found'}), 404 + # You can test using Postman → GET http://localhost:5000/file/ + return send_file(file.file_path, as_attachment=True) + except Exception as e: + logging.error(f"Get error: {e}") + return jsonify({'message': str(e)}), 400 + +# PUT — Update file +@file_bp.route('/file/', methods=['PUT']) +def update_existing_file(file_id): + try: + if 'file' not in request.files: + return jsonify({'message': 'No file provided'}), 400 + file = request.files['file'] + updated = update_file(file_id, file, app.config['UPLOAD_FOLDER']) + return jsonify({'message': 'File updated successfully', 'path': updated.file_path}), 200 + except Exception as e: + logging.error(f"Update error: {e}") + return jsonify({'message': str(e)}), 400 + +# DELETE — Delete file +@file_bp.route('/file/', methods=['DELETE']) +def delete_existing_file(file_id): + try: + delete_file(file_id) + return jsonify({'message': 'File deleted successfully'}), 200 + except Exception as e: + logging.error(f"Delete error: {e}") + return jsonify({'message': str(e)}), 400 \ No newline at end of file diff --git a/file-upload b/file-upload new file mode 160000 index 0000000..191fc57 --- /dev/null +++ b/file-upload @@ -0,0 +1 @@ +Subproject commit 191fc57d4264363bfb95f0e25dda13eaa1bff4c2 diff --git a/logs/app.log b/logs/app.log new file mode 100644 index 0000000..5ecdbc1 --- /dev/null +++ b/logs/app.log @@ -0,0 +1,62 @@ +2025-10-19 13:09:58,582 [INFO] WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on http://127.0.0.1:5000 +2025-10-19 13:09:58,582 [INFO] Press CTRL+C to quit +2025-10-19 13:11:56,377 [INFO] 127.0.0.1 - - [19/Oct/2025 13:11:56] "GET /file/1 HTTP/1.1" 404 - +2025-10-19 13:12:04,641 [INFO] 127.0.0.1 - - [19/Oct/2025 13:12:04] "POST /file/1 HTTP/1.1" 405 - +2025-10-19 13:12:21,921 [INFO] 127.0.0.1 - - [19/Oct/2025 13:12:21] "POST /file HTTP/1.1" 404 - +2025-10-19 13:14:00,216 [INFO] 127.0.0.1 - - [19/Oct/2025 13:14:00] "POST /file HTTP/1.1" 404 - +2025-10-19 13:14:03,698 [INFO] 127.0.0.1 - - [19/Oct/2025 13:14:03] "POST /file/1 HTTP/1.1" 405 - +2025-10-19 13:14:24,561 [INFO] 127.0.0.1 - - [19/Oct/2025 13:14:24] "POST /file/1 HTTP/1.1" 405 - +2025-10-19 13:14:33,916 [INFO] 127.0.0.1 - - [19/Oct/2025 13:14:33] "POST /file HTTP/1.1" 404 - +2025-10-19 13:15:19,857 [INFO] 127.0.0.1 - - [19/Oct/2025 13:15:19] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:15:24,869 [INFO] 127.0.0.1 - - [19/Oct/2025 13:15:24] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:15:46,337 [INFO] 127.0.0.1 - - [19/Oct/2025 13:15:46] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:15:55,368 [INFO] 127.0.0.1 - - [19/Oct/2025 13:15:55] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:16:17,766 [INFO] 127.0.0.1 - - [19/Oct/2025 13:16:17] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:17:02,675 [INFO] 127.0.0.1 - - [19/Oct/2025 13:17:02] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:18:27,851 [INFO] 127.0.0.1 - - [19/Oct/2025 13:18:27] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:18:29,633 [INFO] 127.0.0.1 - - [19/Oct/2025 13:18:29] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:19:35,417 [INFO] 127.0.0.1 - - [19/Oct/2025 13:19:35] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:20:30,892 [INFO] 127.0.0.1 - - [19/Oct/2025 13:20:30] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:22:09,038 [INFO] 127.0.0.1 - - [19/Oct/2025 13:22:09] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:22:58,789 [ERROR] Upload error: 'filename' is an invalid keyword argument for UploadedFile +2025-10-19 13:22:58,790 [INFO] 127.0.0.1 - - [19/Oct/2025 13:22:58] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:23:35,607 [ERROR] Upload error: 'filename' is an invalid keyword argument for UploadedFile +2025-10-19 13:23:35,607 [INFO] 127.0.0.1 - - [19/Oct/2025 13:23:35] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:24:48,690 [INFO] 127.0.0.1 - - [19/Oct/2025 13:24:48] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:24:50,756 [INFO] 127.0.0.1 - - [19/Oct/2025 13:24:50] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:27:39,040 [INFO] 127.0.0.1 - - [19/Oct/2025 13:27:39] "GET /file/1 HTTP/1.1" 404 - +2025-10-19 13:28:48,941 [INFO] 127.0.0.1 - - [19/Oct/2025 13:28:48] "GET /file/11 HTTP/1.1" 404 - +2025-10-19 13:28:54,539 [INFO] 127.0.0.1 - - [19/Oct/2025 13:28:54] "GET /file/2 HTTP/1.1" 404 - +2025-10-19 13:28:57,558 [INFO] 127.0.0.1 - - [19/Oct/2025 13:28:57] "GET /file/3 HTTP/1.1" 404 - +2025-10-19 13:29:00,602 [INFO] 127.0.0.1 - - [19/Oct/2025 13:29:00] "GET /file/4 HTTP/1.1" 404 - +2025-10-19 13:29:03,631 [INFO] 127.0.0.1 - - [19/Oct/2025 13:29:03] "GET /file/1 HTTP/1.1" 404 - +2025-10-19 13:31:41,221 [INFO] 127.0.0.1 - - [19/Oct/2025 13:31:41] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:31:53,694 [ERROR] Upload error: 'filename' is an invalid keyword argument for UploadedFile +2025-10-19 13:31:53,695 [INFO] 127.0.0.1 - - [19/Oct/2025 13:31:53] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:35:17,352 [ERROR] Upload error: 'filename' is an invalid keyword argument for UploadedFile +2025-10-19 13:35:17,353 [INFO] 127.0.0.1 - - [19/Oct/2025 13:35:17] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:35:23,211 [INFO] 127.0.0.1 - - [19/Oct/2025 13:35:23] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:35:54,777 [INFO] WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on http://127.0.0.1:5000 +2025-10-19 13:35:54,777 [INFO] Press CTRL+C to quit +2025-10-19 13:38:11,748 [ERROR] Upload error: (pymysql.err.OperationalError) (1054, "Unknown column 'filename' in 'field list'") +[SQL: INSERT INTO uploaded_file (filename, file_path) VALUES (%(filename)s, %(file_path)s)] +[parameters: {'filename': 'fe29cabba4eb4a8bb0c971c2cab891d3_ERD_REPORT.txt', 'file_path': 'uploads\\ae1044410425409898794fcc91b55fc6\\fe29cabba4eb4a8bb0c971c2cab891d3_ERD_REPORT.txt'}] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2025-10-19 13:38:11,749 [INFO] 127.0.0.1 - - [19/Oct/2025 13:38:11] "POST /upload HTTP/1.1" 400 - +2025-10-19 13:44:15,154 [INFO] WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on http://127.0.0.1:5000 +2025-10-19 13:44:15,154 [INFO] Press CTRL+C to quit +2025-10-19 13:45:17,213 [INFO] File saved: uploads\21f23ce5e9ca4acd984937d15d5ae5b8\ef7738c9921640a982a07020043853f3_ERD_REPORT.txt +2025-10-19 13:45:17,218 [INFO] 127.0.0.1 - - [19/Oct/2025 13:45:17] "POST /upload HTTP/1.1" 201 - +2025-10-19 13:45:38,865 [INFO] 127.0.0.1 - - [19/Oct/2025 13:45:38] "GET /file/1 HTTP/1.1" 200 - +2025-10-19 13:47:10,958 [INFO] File saved: uploads\524bb7ea762a4add914c87f3165e1c6d_Flask_File_Upload_Mvc_With_My_Sql_And_Logging_Step-by-step_Documentation.pdf +2025-10-19 13:47:10,969 [INFO] File updated: ID=1 +2025-10-19 13:47:10,971 [INFO] 127.0.0.1 - - [19/Oct/2025 13:47:10] "PUT /file/1 HTTP/1.1" 200 - +2025-10-19 13:47:17,147 [INFO] File deleted from storage: uploads\524bb7ea762a4add914c87f3165e1c6d_Flask_File_Upload_Mvc_With_My_Sql_And_Logging_Step-by-step_Documentation.pdf +2025-10-19 13:47:17,161 [INFO] Record deleted: ID=1 +2025-10-19 13:47:17,162 [INFO] 127.0.0.1 - - [19/Oct/2025 13:47:17] "DELETE /file/1 HTTP/1.1" 200 - +2025-10-19 13:48:33,260 [INFO] 127.0.0.1 - - [19/Oct/2025 13:48:33] "DELETE /upload HTTP/1.1" 405 - +2025-10-19 13:48:50,942 [INFO] File saved: uploads\30472a9d9dee45c482ed4b8f4ffd2997\a19db5f610ee425aba07da3946a7a7bf_Flask_Logging_Assignment_Documentation.pdf +2025-10-19 13:48:50,944 [INFO] 127.0.0.1 - - [19/Oct/2025 13:48:50] "POST /upload HTTP/1.1" 201 - diff --git a/models/__pycache__/uploaded_file.cpython-313.pyc b/models/__pycache__/uploaded_file.cpython-313.pyc new file mode 100644 index 0000000..a03d6be Binary files /dev/null and b/models/__pycache__/uploaded_file.cpython-313.pyc differ diff --git a/models/uploaded_file.py b/models/uploaded_file.py new file mode 100644 index 0000000..9dc8695 --- /dev/null +++ b/models/uploaded_file.py @@ -0,0 +1,7 @@ +from app import db + +# This model represents an uploaded file record in the database +class UploadedFile(db.Model): + id = db.Column(db.Integer, primary_key=True) + filename = db.Column(db.String(255), nullable=False) + file_path = db.Column(db.String(255), nullable=False) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a2fbb49 Binary files /dev/null and b/requirements.txt differ diff --git a/services/__pycache__/file_service.cpython-313.pyc b/services/__pycache__/file_service.cpython-313.pyc new file mode 100644 index 0000000..7b54f35 Binary files /dev/null and b/services/__pycache__/file_service.cpython-313.pyc differ diff --git a/services/file_service.py b/services/file_service.py new file mode 100644 index 0000000..4f80145 --- /dev/null +++ b/services/file_service.py @@ -0,0 +1,54 @@ +import os +import uuid +from werkzeug.utils import secure_filename +from models.uploaded_file import UploadedFile +from app import db +import logging + +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'pdf', 'txt'} + +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +def save_file(file, base_folder): + if not allowed_file(file.filename): + raise ValueError('File type not allowed') + + os.makedirs(base_folder, exist_ok=True) + filename = secure_filename(file.filename) + unique_name = f"{uuid.uuid4().hex}_{filename}" + save_path = os.path.join(base_folder, unique_name) + file.save(save_path) + new_file = UploadedFile(filename=unique_name, file_path=save_path) + db.session.add(new_file) + db.session.commit() + logging.info(f"File saved: {save_path}") + return new_file + +def update_file(file_id, new_file, base_folder): + existing = UploadedFile.query.get(file_id) + + if not existing: + raise ValueError('File not found') + if os.path.exists(existing.file_path): + os.remove(existing.file_path) + + new_record = save_file(new_file, base_folder) + existing.filename = new_record.filename + existing.file_path = new_record.file_path + db.session.commit() + + logging.info(f"File updated: ID={file_id}") + return existing + +def delete_file(file_id): + file_record = UploadedFile.query.get(file_id) + if not file_record: + raise ValueError('File not found') + if os.path.exists(file_record.file_path): + os.remove(file_record.file_path) + + logging.info(f"File deleted from storage: {file_record.file_path}") + db.session.delete(file_record) + db.session.commit() + logging.info(f"Record deleted: ID={file_id}") diff --git a/uploads/30472a9d9dee45c482ed4b8f4ffd2997/a19db5f610ee425aba07da3946a7a7bf_Flask_Logging_Assignment_Documentation.pdf b/uploads/30472a9d9dee45c482ed4b8f4ffd2997/a19db5f610ee425aba07da3946a7a7bf_Flask_Logging_Assignment_Documentation.pdf new file mode 100644 index 0000000..429db4e Binary files /dev/null and b/uploads/30472a9d9dee45c482ed4b8f4ffd2997/a19db5f610ee425aba07da3946a7a7bf_Flask_Logging_Assignment_Documentation.pdf differ diff --git a/uploads/ea426657e7ad462193cff9d2434c606b_Flask_File_Upload_Mvc_With_My_Sql_And_Logging_Step-by-step_Documentation.pdf b/uploads/ea426657e7ad462193cff9d2434c606b_Flask_File_Upload_Mvc_With_My_Sql_And_Logging_Step-by-step_Documentation.pdf new file mode 100644 index 0000000..113299f Binary files /dev/null and b/uploads/ea426657e7ad462193cff9d2434c606b_Flask_File_Upload_Mvc_With_My_Sql_And_Logging_Step-by-step_Documentation.pdf differ