diff --git a/flask_file_upload_mvc/.gitignore b/flask_file_upload_mvc/.gitignore new file mode 100644 index 0000000..ce11874 --- /dev/null +++ b/flask_file_upload_mvc/.gitignore @@ -0,0 +1,7 @@ +.venv/ +venv/ +env/ +ENV/ + +.env +.env.* 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-312.pyc b/flask_file_upload_mvc/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..5ace0fd Binary files /dev/null and b/flask_file_upload_mvc/__pycache__/config.cpython-312.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-312.pyc b/flask_file_upload_mvc/__pycache__/extensions.cpython-312.pyc new file mode 100644 index 0000000..94ed85a Binary files /dev/null and b/flask_file_upload_mvc/__pycache__/extensions.cpython-312.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..b994408 --- /dev/null +++ b/flask_file_upload_mvc/app.py @@ -0,0 +1,29 @@ +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 +from controllers.auth_controller import auth_bp + +app.register_blueprint(file_bp) +app.register_blueprint(auth_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..7da8d0f --- /dev/null +++ b/flask_file_upload_mvc/config.py @@ -0,0 +1,18 @@ +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') + + + SECRET_KEY = os.getenv('SECRET_KEY') + + LOG_FOLDER = 'logs' + DEBUG = True diff --git a/flask_file_upload_mvc/controllers/__pycache__/auth_controller.cpython-312.pyc b/flask_file_upload_mvc/controllers/__pycache__/auth_controller.cpython-312.pyc new file mode 100644 index 0000000..5f42240 Binary files /dev/null and b/flask_file_upload_mvc/controllers/__pycache__/auth_controller.cpython-312.pyc differ diff --git a/flask_file_upload_mvc/controllers/__pycache__/file_controller.cpython-312.pyc b/flask_file_upload_mvc/controllers/__pycache__/file_controller.cpython-312.pyc new file mode 100644 index 0000000..6f142fb Binary files /dev/null and b/flask_file_upload_mvc/controllers/__pycache__/file_controller.cpython-312.pyc differ 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/auth_controller.py b/flask_file_upload_mvc/controllers/auth_controller.py new file mode 100644 index 0000000..7ad6768 --- /dev/null +++ b/flask_file_upload_mvc/controllers/auth_controller.py @@ -0,0 +1,39 @@ +from flask import Blueprint, request, jsonify +import logging +import jwt +from datetime import datetime, timedelta +from config import Config + +auth_bp = Blueprint('auth_bp', __name__) + +USERNAME = "test123" +PASSWORD = "password123" + +@auth_bp.route('/login', methods=['POST']) +def login(): + try: + data = request.get_json() + username = data.get('username') + password = data.get('password') + + + if not username or not password: + logging.warning(f"{datetime.now()} Empty fields") + return jsonify({'message': 'Username and password'}), 400 + + if username == USERNAME and password == PASSWORD: + token = jwt.encode({ + 'username': username, + 'exp': datetime.now() + timedelta(minutes=20) + }, Config.SECRET_KEY, algorithm='HS256') + logging.info(f"[LOGIN SUCCESS!] username {username}") + return jsonify({'message': 'Login Successful', + 'username': username, + 'token': token + }), 200 + logging.warning(f"{datetime.now()} Invalid credentials for {username}") + return jsonify({'message': 'Invalid credentials'}), 401 + + except Exception as e: + logging.error(f"Login error : {e}") + return jsonify({'message': str(e)}), 500 \ No newline at end of file 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..9222b1b --- /dev/null +++ b/flask_file_upload_mvc/logs/app.log @@ -0,0 +1,41 @@ +2025-11-08 02:57:34,668 [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-11-08 02:57:34,668 [INFO] Press CTRL+C to quit +2025-11-08 02:57:34,669 [INFO] * Restarting with stat +2025-11-08 02:57:35,177 [WARNING] * Debugger is active! +2025-11-08 02:57:35,179 [INFO] * Debugger PIN: 670-920-050 +2025-11-08 02:58:38,552 [INFO] File saved: uploads/9b21cfac5cad4d5fa28dd898ff41805f/4d5be3ce45384824b2a52625975a98d0_Screenshot_from_2025-08-27_00-01-34.png +2025-11-08 02:58:38,562 [INFO] 127.0.0.1 - - [08/Nov/2025 02:58:38] "POST /upload HTTP/1.1" 201 - +2025-11-08 02:58:48,702 [INFO] 127.0.0.1 - - [08/Nov/2025 02:58:48] "GET /upload/1 HTTP/1.1" 404 - +2025-11-08 02:59:00,770 [INFO] 127.0.0.1 - - [08/Nov/2025 02:59:00] "GET /file/1 HTTP/1.1" 200 - +2025-11-08 02:59:30,151 [INFO] File updated: ID=1 +2025-11-08 02:59:30,154 [INFO] 127.0.0.1 - - [08/Nov/2025 02:59:30] "PUT /file/1 HTTP/1.1" 200 - +2025-11-08 02:59:36,080 [INFO] 127.0.0.1 - - [08/Nov/2025 02:59:36] "GET /file/1 HTTP/1.1" 200 - +2025-11-08 02:59:51,076 [INFO] File deleted from storage: uploads/9b21cfac5cad4d5fa28dd898ff41805f/b8ed57cb4a2a4149ac376120ccce28d5_Screenshot_from_2025-08-29_22-03-40.png +2025-11-08 02:59:51,109 [INFO] Record deleted: ID=1 +2025-11-08 02:59:51,111 [INFO] 127.0.0.1 - - [08/Nov/2025 02:59:51] "DELETE /file/1 HTTP/1.1" 200 - +2025-11-08 02:59:54,698 [INFO] 127.0.0.1 - - [08/Nov/2025 02:59:54] "GET /file/1 HTTP/1.1" 404 - +2025-11-08 03:00:06,283 [WARNING] 2025-11-08 03:00:06.283345 Empty fields +2025-11-08 03:00:06,283 [INFO] 127.0.0.1 - - [08/Nov/2025 03:00:06] "POST /login HTTP/1.1" 400 - +2025-11-08 03:00:09,134 [WARNING] 2025-11-08 03:00:09.134666 Invalid credentials for test123 +2025-11-08 03:00:09,135 [INFO] 127.0.0.1 - - [08/Nov/2025 03:00:09] "POST /login HTTP/1.1" 401 - +2025-11-08 03:00:20,003 [INFO] [LOGIN SUCCESS!] username test123 +2025-11-08 03:00:20,003 [INFO] 127.0.0.1 - - [08/Nov/2025 03:00:20] "POST /login HTTP/1.1" 200 - +2025-11-08 03:00:25,614 [WARNING] 2025-11-08 03:00:25.614643 Invalid credentials for test123 +2025-11-08 03:00:25,615 [INFO] 127.0.0.1 - - [08/Nov/2025 03:00:25] "POST /login HTTP/1.1" 401 - +2025-11-08 03:00:35,021 [INFO] [LOGIN SUCCESS!] username test123 +2025-11-08 03:00:35,021 [INFO] 127.0.0.1 - - [08/Nov/2025 03:00:35] "POST /login HTTP/1.1" 200 - +2025-11-08 03:00:42,317 [WARNING] 2025-11-08 03:00:42.317665 Invalid credentials for test2123 +2025-11-08 03:00:42,318 [INFO] 127.0.0.1 - - [08/Nov/2025 03:00:42] "POST /login HTTP/1.1" 401 - +2025-11-08 03:00:48,099 [INFO] [LOGIN SUCCESS!] username test123 +2025-11-08 03:00:48,100 [INFO] 127.0.0.1 - - [08/Nov/2025 03:00:48] "POST /login HTTP/1.1" 200 - +2025-11-08 03:01:42,924 [INFO] [LOGIN SUCCESS!] username test123 +2025-11-08 03:01:42,925 [INFO] 127.0.0.1 - - [08/Nov/2025 03:01:42] "POST /login HTTP/1.1" 200 - +2025-11-08 03:01:45,978 [WARNING] 2025-11-08 03:01:45.978766 Invalid credentials for test123s +2025-11-08 03:01:45,979 [INFO] 127.0.0.1 - - [08/Nov/2025 03:01:45] "POST /login HTTP/1.1" 401 - +2025-11-08 03:01:48,768 [INFO] [LOGIN SUCCESS!] username test123 +2025-11-08 03:01:48,769 [INFO] 127.0.0.1 - - [08/Nov/2025 03:01:48] "POST /login HTTP/1.1" 200 - +2025-11-08 03:01:51,521 [WARNING] 2025-11-08 03:01:51.521314 Invalid credentials for test123 +2025-11-08 03:01:51,522 [INFO] 127.0.0.1 - - [08/Nov/2025 03:01:51] "POST /login HTTP/1.1" 401 - +2025-11-08 03:01:55,454 [INFO] [LOGIN SUCCESS!] username test123 +2025-11-08 03:01:55,454 [INFO] 127.0.0.1 - - [08/Nov/2025 03:01:55] "POST /login HTTP/1.1" 200 - diff --git a/flask_file_upload_mvc/models/__pycache__/uploaded_file.cpython-312.pyc b/flask_file_upload_mvc/models/__pycache__/uploaded_file.cpython-312.pyc new file mode 100644 index 0000000..9dea82c Binary files /dev/null and b/flask_file_upload_mvc/models/__pycache__/uploaded_file.cpython-312.pyc differ 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..8f6a0f2 --- /dev/null +++ b/flask_file_upload_mvc/requirements.txt @@ -0,0 +1,18 @@ +blinker==1.9.0 +cffi==2.0.0 +click==8.3.0 +colorama==0.4.6 +cryptography==46.0.3 +Flask==3.1.2 +Flask-SQLAlchemy==3.1.1 +greenlet==3.2.4 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +pycparser==2.23 +PyJWT==2.10.1 +PyMySQL==1.1.2 +python-dotenv==1.1.1 +SQLAlchemy==2.0.44 +typing_extensions==4.15.0 +Werkzeug==3.1.3 diff --git a/flask_file_upload_mvc/services/__pycache__/file_service.cpython-312.pyc b/flask_file_upload_mvc/services/__pycache__/file_service.cpython-312.pyc new file mode 100644 index 0000000..ddfb4f4 Binary files /dev/null and b/flask_file_upload_mvc/services/__pycache__/file_service.cpython-312.pyc 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