diff --git a/flask_file_upload_mvc/.gitignore b/flask_file_upload_mvc/.gitignore new file mode 100644 index 0000000..1e1d699 --- /dev/null +++ b/flask_file_upload_mvc/.gitignore @@ -0,0 +1,4 @@ +.env/ +.venv/ +env/ +.env \ No newline at end of file diff --git a/flask_file_upload_mvc/__pycache__/app.cpython-314.pyc b/flask_file_upload_mvc/__pycache__/app.cpython-314.pyc new file mode 100644 index 0000000..ebc209e Binary files /dev/null and b/flask_file_upload_mvc/__pycache__/app.cpython-314.pyc differ diff --git a/flask_file_upload_mvc/__pycache__/config.cpython-314.pyc b/flask_file_upload_mvc/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000..866b994 Binary files /dev/null and b/flask_file_upload_mvc/__pycache__/config.cpython-314.pyc differ diff --git a/flask_file_upload_mvc/__pycache__/extensions.cpython-314.pyc b/flask_file_upload_mvc/__pycache__/extensions.cpython-314.pyc new file mode 100644 index 0000000..1f5ad73 Binary files /dev/null and b/flask_file_upload_mvc/__pycache__/extensions.cpython-314.pyc differ diff --git a/flask_file_upload_mvc/app.py b/flask_file_upload_mvc/app.py new file mode 100644 index 0000000..3b9982a --- /dev/null +++ b/flask_file_upload_mvc/app.py @@ -0,0 +1,27 @@ +import os +import logging +from flask import Flask +from extensions import db +from config import Config + +app = Flask(__name__) +app.config.from_object(Config) + +db.init_app(app) + +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' +) + +from controllers.file_controller import file_bp + +app.register_blueprint(file_bp) + +with app.app_context(): + db.create_all() + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/flask_file_upload_mvc/config.py b/flask_file_upload_mvc/config.py new file mode 100644 index 0000000..b0e8885 --- /dev/null +++ b/flask_file_upload_mvc/config.py @@ -0,0 +1,14 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +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/flask_file_upload_mvc/controllers/__pycache__/file_controller.cpython-314.pyc b/flask_file_upload_mvc/controllers/__pycache__/file_controller.cpython-314.pyc new file mode 100644 index 0000000..7d19830 Binary files /dev/null and b/flask_file_upload_mvc/controllers/__pycache__/file_controller.cpython-314.pyc differ diff --git a/flask_file_upload_mvc/controllers/file_controller.py b/flask_file_upload_mvc/controllers/file_controller.py new file mode 100644 index 0000000..d818984 --- /dev/null +++ b/flask_file_upload_mvc/controllers/file_controller.py @@ -0,0 +1,59 @@ +import uuid +from flask import Blueprint, request, jsonify, send_file, current_app +from services.file_service import save_file, update_file, delete_file +from models.uploaded_file import UploadedFile +import logging + +file_bp = Blueprint('file_bp', __name__) + +@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 = f"{current_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 + + +@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 + +@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, current_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 + +@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/flask_file_upload_mvc/extensions.py b/flask_file_upload_mvc/extensions.py new file mode 100644 index 0000000..2e1eeb6 --- /dev/null +++ b/flask_file_upload_mvc/extensions.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() \ No newline at end of file diff --git a/flask_file_upload_mvc/logs/app.log b/flask_file_upload_mvc/logs/app.log new file mode 100644 index 0000000..1497dd4 --- /dev/null +++ b/flask_file_upload_mvc/logs/app.log @@ -0,0 +1,21 @@ +2025-10-18 22:38:56,221 [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-18 22:38:56,222 [INFO] Press CTRL+C to quit +2025-10-18 22:38:56,223 [INFO] * Restarting with stat +2025-10-18 22:38:56,879 [WARNING] * Debugger is active! +2025-10-18 22:38:56,881 [INFO] * Debugger PIN: 681-091-574 +2025-10-18 22:39:11,250 [INFO] File saved: uploads/b18fc54fb8274d10a6f05da3d689dfbf\ccdf1d981d1a481ebe1e10010c00f68f_va.png +2025-10-18 22:39:11,255 [INFO] 127.0.0.1 - - [18/Oct/2025 22:39:11] "POST /upload HTTP/1.1" 201 - +2025-10-18 22:39:17,622 [INFO] 127.0.0.1 - - [18/Oct/2025 22:39:17] "GET /upload/1 HTTP/1.1" 404 - +2025-10-18 22:39:38,104 [INFO] 127.0.0.1 - - [18/Oct/2025 22:39:38] "GET /file/1 HTTP/1.1" 200 - +2025-10-18 22:40:31,093 [INFO] File updated: ID=1 +2025-10-18 22:40:31,096 [INFO] 127.0.0.1 - - [18/Oct/2025 22:40:31] "PUT /file/1 HTTP/1.1" 200 - +2025-10-18 22:40:46,270 [INFO] 127.0.0.1 - - [18/Oct/2025 22:40:46] "GET /file/1 HTTP/1.1" 200 - +2025-10-18 22:40:52,996 [INFO] 127.0.0.1 - - [18/Oct/2025 22:40:52] "GET /file/2 HTTP/1.1" 404 - +2025-10-18 22:40:57,066 [ERROR] Update error: File not found +2025-10-18 22:40:57,067 [INFO] 127.0.0.1 - - [18/Oct/2025 22:40:57] "PUT /file/2 HTTP/1.1" 400 - +2025-10-18 22:41:00,979 [ERROR] Delete error: File not found +2025-10-18 22:41:00,980 [INFO] 127.0.0.1 - - [18/Oct/2025 22:41:00] "DELETE /file/2 HTTP/1.1" 400 - +2025-10-18 22:41:03,949 [INFO] File deleted from storage: uploads/b18fc54fb8274d10a6f05da3d689dfbf\f022d7b6212947b7b05f6c5603bbbcb1_731-7312556_download-png.png +2025-10-18 22:41:03,953 [INFO] Record deleted: ID=1 +2025-10-18 22:41:03,953 [INFO] 127.0.0.1 - - [18/Oct/2025 22:41:03] "DELETE /file/1 HTTP/1.1" 200 - diff --git a/flask_file_upload_mvc/models/__pycache__/uploaded_file.cpython-314.pyc b/flask_file_upload_mvc/models/__pycache__/uploaded_file.cpython-314.pyc new file mode 100644 index 0000000..3e9cdf2 Binary files /dev/null and b/flask_file_upload_mvc/models/__pycache__/uploaded_file.cpython-314.pyc differ diff --git a/flask_file_upload_mvc/models/uploaded_file.py b/flask_file_upload_mvc/models/uploaded_file.py new file mode 100644 index 0000000..73ac5be --- /dev/null +++ b/flask_file_upload_mvc/models/uploaded_file.py @@ -0,0 +1,6 @@ +from extensions import db + +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/flask_file_upload_mvc/requirements.txt b/flask_file_upload_mvc/requirements.txt new file mode 100644 index 0000000..a2fbb49 Binary files /dev/null and b/flask_file_upload_mvc/requirements.txt differ diff --git a/flask_file_upload_mvc/services/__pycache__/file_service.cpython-314.pyc b/flask_file_upload_mvc/services/__pycache__/file_service.cpython-314.pyc new file mode 100644 index 0000000..3c98fa5 Binary files /dev/null and b/flask_file_upload_mvc/services/__pycache__/file_service.cpython-314.pyc differ diff --git a/flask_file_upload_mvc/services/file_service.py b/flask_file_upload_mvc/services/file_service.py new file mode 100644 index 0000000..8a70ae8 --- /dev/null +++ b/flask_file_upload_mvc/services/file_service.py @@ -0,0 +1,65 @@ +import os +import uuid +from werkzeug.utils import secure_filename +from models.uploaded_file import UploadedFile +from models.uploaded_file 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 + +# Update service reuse the existing folder and remove the bug where after updating it gets added again in the database +def update_file(file_id, new_file, _): + 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) + + existing_folder = os.path.dirname(existing.file_path) + os.makedirs(existing_folder, exist_ok=True) + filename = secure_filename(new_file.filename) + unique_name = f"{uuid.uuid4().hex}_{filename}" + save_path = os.path.join(existing_folder, unique_name) + + new_file.save(save_path) + + existing.filename = unique_name + existing.file_path = save_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}") \ No newline at end of file