diff --git a/app.py b/app.py new file mode 100644 index 0000000..8d946a7 --- /dev/null +++ b/app.py @@ -0,0 +1,37 @@ +import os +import logging +from flask import Flask +from config import Config +from extensions import db +from controllers.auth_controller import auth_bp + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config) + + # Initialize db with app + db.init_app(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) + app.register_blueprint(auth_bp) + + # Create tables + with app.app_context(): + db.create_all() + + return app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True, host='127.0.0.1', port=5000) diff --git a/config.py b/config.py new file mode 100644 index 0000000..b0e8885 --- /dev/null +++ b/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/controllers/__pycache__/auth_controller.cpython-313.pyc b/controllers/__pycache__/auth_controller.cpython-313.pyc new file mode 100644 index 0000000..d26c9ae Binary files /dev/null and b/controllers/__pycache__/auth_controller.cpython-313.pyc differ diff --git a/controllers/__pycache__/file_controller.cpython-313.pyc b/controllers/__pycache__/file_controller.cpython-313.pyc new file mode 100644 index 0000000..fd3ec0c Binary files /dev/null and b/controllers/__pycache__/file_controller.cpython-313.pyc differ diff --git a/controllers/auth_controller.py b/controllers/auth_controller.py new file mode 100644 index 0000000..944fe04 --- /dev/null +++ b/controllers/auth_controller.py @@ -0,0 +1,26 @@ +import logging +from flask import Blueprint, request, jsonify + +auth_bp = Blueprint('auth_bp', __name__) + +# Temporary static credentials +VALID_USERNAME = "zander" +VALID_PASSWORD = "zander123" + +@auth_bp.route('/login', methods=['POST']) +def login(): + data = request.get_json() + + if not data or 'username' not in data or 'password' not in data: + logging.warning("Login attempt with missing fields") + return jsonify({"message": "Username and password are required"}), 400 + + username = data['username'] + password = data['password'] + + if username == VALID_USERNAME and password == VALID_PASSWORD: + logging.info(f"Successful login for user: {username}") + return jsonify({"message": "Login successful"}), 200 + else: + logging.warning(f"Failed login attempt for user: {username}") + return jsonify({"message": "Invalid username or password"}), 401 diff --git a/controllers/file_controller.py b/controllers/file_controller.py new file mode 100644 index 0000000..be84dc0 --- /dev/null +++ b/controllers/file_controller.py @@ -0,0 +1,53 @@ +import uuid +import logging +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 config import Config + +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"{Config.UPLOAD_FOLDER}/{uuid.uuid4().hex}" + 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 + 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, 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 diff --git a/extensions.py b/extensions.py new file mode 100644 index 0000000..f0b13d6 --- /dev/null +++ b/extensions.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() diff --git a/logs/app.log b/logs/app.log new file mode 100644 index 0000000..763716f --- /dev/null +++ b/logs/app.log @@ -0,0 +1,110 @@ +2025-10-18 11:28:33,106 [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 11:28:33,107 [INFO] Press CTRL+C to quit +2025-10-18 11:28:33,108 [INFO] * Restarting with stat +2025-10-18 11:28:33,731 [WARNING] * Debugger is active! +2025-10-18 11:28:33,768 [INFO] * Debugger PIN: 125-425-247 +2025-10-18 11:31:34,041 [INFO] 127.0.0.1 - - [18/Oct/2025 11:31:34] "POST /upload HTTP/1.1" 400 - +2025-10-18 11:37:48,094 [INFO] 127.0.0.1 - - [18/Oct/2025 11:37:48] "POST /upload HTTP/1.1" 400 - +2025-10-18 11:39:04,317 [INFO] 127.0.0.1 - - [18/Oct/2025 11:39:04] "POST /upload HTTP/1.1" 400 - +2025-10-18 11:40:22,064 [INFO] File saved: uploads/252487abf6e14a39bfa40b72412da69e/1aec644281434ee2a219a2f2f8e3a5b1_23790439261-2.pdf +2025-10-18 11:40:22,072 [INFO] 127.0.0.1 - - [18/Oct/2025 11:40:22] "POST /upload HTTP/1.1" 201 - +2025-10-18 11:47:02,491 [INFO] 127.0.0.1 - - [18/Oct/2025 11:47:02] "GET /file/1 HTTP/1.1" 200 - +2025-10-18 11:48:51,208 [ERROR] Update error: name 'app' is not defined +2025-10-18 11:48:51,210 [INFO] 127.0.0.1 - - [18/Oct/2025 11:48:51] "PUT /file/1 HTTP/1.1" 400 - +2025-10-18 11:49:15,582 [ERROR] Update error: name 'app' is not defined +2025-10-18 11:49:15,583 [INFO] 127.0.0.1 - - [18/Oct/2025 11:49:15] "PUT /file/1 HTTP/1.1" 400 - +2025-10-18 11:52:23,076 [INFO] * Detected change in '/Users/Zander/flask_file_upload_mvc/controllers/file_controller.py', reloading +2025-10-18 11:52:23,477 [INFO] * Restarting with stat +2025-10-18 11:52:24,544 [WARNING] * Debugger is active! +2025-10-18 11:52:24,573 [INFO] * Debugger PIN: 125-425-247 +2025-10-18 11:52:25,660 [INFO] * Detected change in '/Users/Zander/flask_file_upload_mvc/controllers/file_controller.py', reloading +2025-10-18 11:52:25,849 [INFO] * Restarting with stat +2025-10-18 11:52:26,589 [WARNING] * Debugger is active! +2025-10-18 11:52:26,606 [INFO] * Debugger PIN: 125-425-247 +2025-10-18 11:52:57,097 [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 11:52:57,098 [INFO] Press CTRL+C to quit +2025-10-18 11:52:57,099 [INFO] * Restarting with stat +2025-10-18 11:52:57,766 [WARNING] * Debugger is active! +2025-10-18 11:52:57,788 [INFO] * Debugger PIN: 125-425-247 +2025-10-18 11:53:08,816 [INFO] File saved: uploads/7d00800752d64e77bce7e5cb0a37a406_ERD_Dormitally.pdf +2025-10-18 11:53:08,831 [INFO] File updated: ID=1 +2025-10-18 11:53:08,834 [INFO] 127.0.0.1 - - [18/Oct/2025 11:53:08] "PUT /file/1 HTTP/1.1" 200 - +2025-10-18 11:53:41,896 [INFO] File deleted from storage: uploads/7d00800752d64e77bce7e5cb0a37a406_ERD_Dormitally.pdf +2025-10-18 11:53:41,901 [INFO] Record deleted: ID=1 +2025-10-18 11:53:41,902 [INFO] 127.0.0.1 - - [18/Oct/2025 11:53:41] "DELETE /file/1 HTTP/1.1" 200 - +2025-10-19 21:53:39,376 [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 21:53:39,376 [INFO] Press CTRL+C to quit +2025-10-19 21:53:39,378 [INFO] * Restarting with stat +2025-10-19 21:53:39,891 [WARNING] * Debugger is active! +2025-10-19 21:53:39,924 [INFO] * Debugger PIN: 820-901-174 +2025-10-19 22:00:48,564 [INFO] File saved: uploads/7f799bf24a634687a65c7b953243be31/90c1b158498d4b4783853be14fd6035b_ERD_Dormitally.pdf +2025-10-19 22:00:48,570 [INFO] 127.0.0.1 - - [19/Oct/2025 22:00:48] "POST /upload HTTP/1.1" 201 - +2025-10-19 22:09:16,689 [INFO] 127.0.0.1 - - [19/Oct/2025 22:09:16] "GET /3 HTTP/1.1" 404 - +2025-10-19 22:09:48,774 [INFO] 127.0.0.1 - - [19/Oct/2025 22:09:48] "GET /file/3 HTTP/1.1" 200 - +2025-10-19 22:10:14,576 [INFO] File saved: uploads/04a938cda90548a8bb5b55b271b5b2b1_23790439261.pdf +2025-10-19 22:10:14,594 [INFO] File updated: ID=3 +2025-10-19 22:10:14,597 [INFO] 127.0.0.1 - - [19/Oct/2025 22:10:14] "PUT /file/3 HTTP/1.1" 200 - +2025-10-19 22:10:28,744 [INFO] File deleted from storage: uploads/04a938cda90548a8bb5b55b271b5b2b1_23790439261.pdf +2025-10-19 22:10:28,749 [INFO] Record deleted: ID=3 +2025-10-19 22:10:28,750 [INFO] 127.0.0.1 - - [19/Oct/2025 22:10:28] "DELETE /file/3 HTTP/1.1" 200 - +2025-10-30 14:43:42,381 [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-30 14:43:42,382 [INFO] Press CTRL+C to quit +2025-10-30 14:43:42,383 [INFO] * Restarting with stat +2025-10-30 14:43:43,002 [WARNING] * Debugger is active! +2025-10-30 14:43:43,036 [INFO] * Debugger PIN: 820-901-174 +2025-10-30 14:45:10,604 [INFO] * Detected change in '/Users/Zander/flask_file_upload_mvc/app.py', reloading +2025-10-30 14:45:10,876 [INFO] * Restarting with stat +2025-10-30 14:46:02,643 [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-30 14:46:02,643 [INFO] Press CTRL+C to quit +2025-10-30 14:46:02,646 [INFO] * Restarting with stat +2025-10-30 14:46:03,177 [WARNING] * Debugger is active! +2025-10-30 14:46:03,195 [INFO] * Debugger PIN: 820-901-174 +2025-10-30 14:46:25,448 [INFO] * Detected change in '/Users/Zander/flask_file_upload_mvc/app.py', reloading +2025-10-30 14:46:25,667 [INFO] * Restarting with stat +2025-10-30 14:46:26,438 [WARNING] * Debugger is active! +2025-10-30 14:46:26,473 [INFO] * Debugger PIN: 820-901-174 +2025-10-30 14:46:27,519 [INFO] * Detected change in '/Users/Zander/flask_file_upload_mvc/app.py', reloading +2025-10-30 14:46:27,700 [INFO] * Restarting with stat +2025-10-30 14:46:28,374 [WARNING] * Debugger is active! +2025-10-30 14:46:28,394 [INFO] * Debugger PIN: 820-901-174 +2025-10-30 14:46:39,526 [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-30 14:46:39,526 [INFO] Press CTRL+C to quit +2025-10-30 14:47:39,099 [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-30 14:47:39,099 [INFO] Press CTRL+C to quit +2025-10-30 14:47:39,101 [INFO] * Restarting with stat +2025-10-30 14:47:39,755 [WARNING] * Debugger is active! +2025-10-30 14:47:39,777 [INFO] * Debugger PIN: 820-901-174 +2025-10-30 14:48:49,220 [INFO] 127.0.0.1 - - [30/Oct/2025 14:48:49] "POST /login HTTP/1.1" 415 - +2025-10-30 14:49:01,353 [INFO] 127.0.0.1 - - [30/Oct/2025 14:49:01] "POST /login HTTP/1.1" 415 - +2025-10-30 14:50:54,899 [INFO] 127.0.0.1 - - [30/Oct/2025 14:50:54] "POST /login HTTP/1.1" 415 - +2025-10-30 14:51:41,423 [INFO] Successful login for user: admin +2025-10-30 14:51:41,424 [INFO] 127.0.0.1 - - [30/Oct/2025 14:51:41] "POST /login HTTP/1.1" 200 - +2025-10-30 14:52:32,451 [INFO] * Detected change in '/Users/Zander/flask_file_upload_mvc/controllers/auth_controller.py', reloading +2025-10-30 14:52:32,663 [INFO] * Restarting with stat +2025-10-30 14:52:33,821 [WARNING] * Debugger is active! +2025-10-30 14:52:33,836 [INFO] * Debugger PIN: 820-901-174 +2025-10-30 14:52:37,912 [INFO] * Detected change in '/Users/Zander/flask_file_upload_mvc/controllers/auth_controller.py', reloading +2025-10-30 14:52:38,290 [INFO] * Restarting with stat +2025-10-30 14:52:38,940 [WARNING] * Debugger is active! +2025-10-30 14:52:38,958 [INFO] * Debugger PIN: 820-901-174 +2025-10-30 14:57:09,956 [INFO] Successful login for user: zander +2025-10-30 14:57:09,960 [INFO] 127.0.0.1 - - [30/Oct/2025 14:57:09] "POST /login HTTP/1.1" 200 - +2025-10-30 14:58:03,175 [WARNING] Failed login attempt for user: zander +2025-10-30 14:58:03,175 [INFO] 127.0.0.1 - - [30/Oct/2025 14:58:03] "POST /login HTTP/1.1" 401 - +2025-10-30 15:00:39,059 [INFO] File saved: uploads/c46cafea63d04260a1fa75b93596d34e/d31965e3bd2a4ccebe7ff24bd4ed9f95_ERD_Dormitally.pdf +2025-10-30 15:00:39,071 [INFO] 127.0.0.1 - - [30/Oct/2025 15:00:39] "POST /upload HTTP/1.1" 201 - +2025-10-30 15:01:33,801 [INFO] 127.0.0.1 - - [30/Oct/2025 15:01:33] "GET /file/5 HTTP/1.1" 200 - +2025-10-30 15:01:39,525 [INFO] 127.0.0.1 - - [30/Oct/2025 15:01:39] "GET /file/6 HTTP/1.1" 404 - +2025-10-30 15:02:39,355 [INFO] File saved: uploads/b6d734cd5425426290025e385be8f33d_Untitled_Diagram.drawio.png +2025-10-30 15:02:39,372 [INFO] File updated: ID=5 +2025-10-30 15:02:39,374 [INFO] 127.0.0.1 - - [30/Oct/2025 15:02:39] "PUT /file/5 HTTP/1.1" 200 - +2025-10-30 15:02:53,039 [INFO] File deleted from storage: uploads/b6d734cd5425426290025e385be8f33d_Untitled_Diagram.drawio.png +2025-10-30 15:02:53,047 [INFO] Record deleted: ID=5 +2025-10-30 15:02:53,048 [INFO] 127.0.0.1 - - [30/Oct/2025 15:02:53] "DELETE /file/5 HTTP/1.1" 200 - diff --git a/models/__pycache__/uploaded_file.cpython-313.pyc b/models/__pycache__/uploaded_file.cpython-313.pyc new file mode 100644 index 0000000..d8a7d79 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..70dbdcc --- /dev/null +++ b/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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1f61aab --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +blinker==1.9.0 +click==8.3.0 +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 +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/services/__pycache__/file_service.cpython-313.pyc b/services/__pycache__/file_service.cpython-313.pyc new file mode 100644 index 0000000..ce2b09e 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..663628b --- /dev/null +++ b/services/file_service.py @@ -0,0 +1,58 @@ +import os +import uuid +import logging +from werkzeug.utils import secure_filename +from models.uploaded_file import UploadedFile +from extensions import db + +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') + + # remove old file if exists + 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}")