diff --git a/flask_file_upload_mvc/.gitignore b/flask_file_upload_mvc/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/flask_file_upload_mvc/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/flask_file_upload_mvc/__pycache__/app.cpython-313.pyc b/flask_file_upload_mvc/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000..0af6da0 Binary files /dev/null and b/flask_file_upload_mvc/__pycache__/app.cpython-313.pyc differ diff --git a/flask_file_upload_mvc/__pycache__/config.cpython-313.pyc b/flask_file_upload_mvc/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..3d6211c Binary files /dev/null and b/flask_file_upload_mvc/__pycache__/config.cpython-313.pyc differ diff --git a/flask_file_upload_mvc/__pycache__/extensions.cpython-313.pyc b/flask_file_upload_mvc/__pycache__/extensions.cpython-313.pyc new file mode 100644 index 0000000..4ca0849 Binary files /dev/null and b/flask_file_upload_mvc/__pycache__/extensions.cpython-313.pyc differ diff --git a/flask_file_upload_mvc/app.py b/flask_file_upload_mvc/app.py new file mode 100644 index 0000000..8e417c3 --- /dev/null +++ b/flask_file_upload_mvc/app.py @@ -0,0 +1,28 @@ +import os +import logging +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from config import Config +from extensions import db + +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_controllers 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..9e64997 --- /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 \ No newline at end of file diff --git a/flask_file_upload_mvc/controllers/__pycache__/file_controllers.cpython-313.pyc b/flask_file_upload_mvc/controllers/__pycache__/file_controllers.cpython-313.pyc new file mode 100644 index 0000000..2f662ad Binary files /dev/null and b/flask_file_upload_mvc/controllers/__pycache__/file_controllers.cpython-313.pyc differ diff --git a/flask_file_upload_mvc/controllers/file_controllers.py b/flask_file_upload_mvc/controllers/file_controllers.py new file mode 100644 index 0000000..9541aa1 --- /dev/null +++ b/flask_file_upload_mvc/controllers/file_controllers.py @@ -0,0 +1,62 @@ +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 uuid +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}" + upload_file = save_file(file, folder) + + return jsonify({'message': 'Upload successfull', 'id': upload_file.id, 'path': upload_file.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, 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..5170964 --- /dev/null +++ b/flask_file_upload_mvc/extensions.py @@ -0,0 +1,4 @@ +# flask_file_upload_mvc/extensions.py +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() diff --git a/flask_file_upload_mvc/logs/app.log b/flask_file_upload_mvc/logs/app.log new file mode 100644 index 0000000..fa40a07 --- /dev/null +++ b/flask_file_upload_mvc/logs/app.log @@ -0,0 +1,55 @@ +2025-10-19 18:06:07,123 [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 18:06:07,124 [INFO] Press CTRL+C to quit +2025-10-19 18:06:07,132 [INFO] * Restarting with stat +2025-10-19 18:06:07,639 [WARNING] * Debugger is active! +2025-10-19 18:06:07,644 [INFO] * Debugger PIN: 888-738-973 +2025-10-19 18:07:56,688 [INFO] File saved: uploads/5aa65aafe1e34922ba95fc4deb14a631\ec41709c9895478299d4787de632235c_atsu-ghost-of-yotei-5120x2880-22239.jpg +2025-10-19 18:07:56,693 [INFO] 127.0.0.1 - - [19/Oct/2025 18:07:56] "POST /upload HTTP/1.1" 201 - +2025-10-19 18:08:12,420 [INFO] 127.0.0.1 - - [19/Oct/2025 18:08:12] "GET /file/1 HTTP/1.1" 200 - +2025-10-19 18:09:09,243 [INFO] 127.0.0.1 - - [19/Oct/2025 18:09:09] "POST /upload HTTP/1.1" 400 - +2025-10-19 18:10:10,627 [INFO] File saved: uploads/9eed0c129d094b9aa4d9692bc52b4e0e\e8bf7256c3a24b5f93677e6a220ee138_nternet-meme-Hitler-vs-Stalin.png +2025-10-19 18:10:10,629 [INFO] 127.0.0.1 - - [19/Oct/2025 18:10:10] "POST /upload HTTP/1.1" 201 - +2025-10-19 18:10:29,341 [INFO] 127.0.0.1 - - [19/Oct/2025 18:10:29] "GET /file/2 HTTP/1.1" 200 - +2025-10-19 18:11:01,754 [INFO] File saved: uploads/724002efaa6d45bba7b09c7d5c3d85df\a35c071a2a8b48a8a7b484f66bb4f471_ao0dY93_460s.jpg +2025-10-19 18:11:01,756 [INFO] 127.0.0.1 - - [19/Oct/2025 18:11:01] "POST /upload HTTP/1.1" 201 - +2025-10-19 18:12:06,299 [INFO] File saved: uploads/75243f10e0e4460883a8638bce349982\f7bb11f564fb40c2a9b14005e5f781ae_images.jpg +2025-10-19 18:12:06,300 [INFO] 127.0.0.1 - - [19/Oct/2025 18:12:06] "POST /upload HTTP/1.1" 201 - +2025-10-19 18:12:18,064 [INFO] File saved: uploads/3ab79e69eb944ea9a28938d8e76fa8cb\30c3f0015a8e44928ad16401e5b62ab9_8lm9ekuwf8c71.jpg +2025-10-19 18:12:18,065 [INFO] 127.0.0.1 - - [19/Oct/2025 18:12:18] "POST /upload HTTP/1.1" 201 - +2025-10-19 18:12:23,526 [INFO] 127.0.0.1 - - [19/Oct/2025 18:12:23] "GET /file/4 HTTP/1.1" 200 - +2025-10-19 18:28:13,677 [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 18:28:13,677 [INFO] Press CTRL+C to quit +2025-10-19 18:28:13,678 [INFO] * Restarting with stat +2025-10-19 18:28:14,309 [WARNING] * Debugger is active! +2025-10-19 18:28:14,312 [INFO] * Debugger PIN: 888-738-973 +2025-10-19 18:28:45,292 [INFO] File deleted from storage: uploads/5aa65aafe1e34922ba95fc4deb14a631\ec41709c9895478299d4787de632235c_atsu-ghost-of-yotei-5120x2880-22239.jpg +2025-10-19 18:28:45,301 [INFO] Record deleted: ID=1 +2025-10-19 18:28:45,301 [INFO] 127.0.0.1 - - [19/Oct/2025 18:28:45] "DELETE /file/1 HTTP/1.1" 200 - +2025-10-19 18:28:48,420 [INFO] File deleted from storage: uploads/9eed0c129d094b9aa4d9692bc52b4e0e\e8bf7256c3a24b5f93677e6a220ee138_nternet-meme-Hitler-vs-Stalin.png +2025-10-19 18:28:48,428 [INFO] Record deleted: ID=2 +2025-10-19 18:28:48,428 [INFO] 127.0.0.1 - - [19/Oct/2025 18:28:48] "DELETE /file/2 HTTP/1.1" 200 - +2025-10-19 18:28:51,280 [INFO] File deleted from storage: uploads/724002efaa6d45bba7b09c7d5c3d85df\a35c071a2a8b48a8a7b484f66bb4f471_ao0dY93_460s.jpg +2025-10-19 18:28:51,287 [INFO] Record deleted: ID=3 +2025-10-19 18:28:51,288 [INFO] 127.0.0.1 - - [19/Oct/2025 18:28:51] "DELETE /file/3 HTTP/1.1" 200 - +2025-10-19 18:28:53,341 [INFO] File deleted from storage: uploads/75243f10e0e4460883a8638bce349982\f7bb11f564fb40c2a9b14005e5f781ae_images.jpg +2025-10-19 18:28:53,348 [INFO] Record deleted: ID=4 +2025-10-19 18:28:53,349 [INFO] 127.0.0.1 - - [19/Oct/2025 18:28:53] "DELETE /file/4 HTTP/1.1" 200 - +2025-10-19 18:28:55,730 [INFO] File deleted from storage: uploads/3ab79e69eb944ea9a28938d8e76fa8cb\30c3f0015a8e44928ad16401e5b62ab9_8lm9ekuwf8c71.jpg +2025-10-19 18:28:55,737 [INFO] Record deleted: ID=5 +2025-10-19 18:28:55,738 [INFO] 127.0.0.1 - - [19/Oct/2025 18:28:55] "DELETE /file/5 HTTP/1.1" 200 - +2025-10-19 18:28:57,826 [ERROR] Delete error: File not found +2025-10-19 18:28:57,827 [INFO] 127.0.0.1 - - [19/Oct/2025 18:28:57] "DELETE /file/6 HTTP/1.1" 400 - +2025-10-19 18:29:00,971 [ERROR] Delete error: File not found +2025-10-19 18:29:00,972 [INFO] 127.0.0.1 - - [19/Oct/2025 18:29:00] "DELETE /file/7 HTTP/1.1" 400 - +2025-10-19 18:30:39,670 [INFO] File saved: uploads/32b74f9d95bc4d54aa675b28078818bf\a401aead323044169893c7724240297f_e.jpg +2025-10-19 18:30:39,673 [INFO] 127.0.0.1 - - [19/Oct/2025 18:30:39] "POST /upload HTTP/1.1" 201 - +2025-10-19 18:30:52,786 [INFO] File saved: uploads/ae2f237da33544b2b4d64597664c9da9\e2b8473a87bd40a18fc89808d1f2c438_e.jpg +2025-10-19 18:30:52,788 [INFO] 127.0.0.1 - - [19/Oct/2025 18:30:52] "POST /upload HTTP/1.1" 201 - +2025-10-19 18:31:56,520 [INFO] File saved: uploads/a7d1ce6af9ed4143927016f84b346ce4\50ae5bec1039417b9cd366e4d6075352_e.jpg +2025-10-19 18:31:56,522 [INFO] 127.0.0.1 - - [19/Oct/2025 18:31:56] "POST /upload?file= HTTP/1.1" 201 - +2025-10-19 18:32:04,368 [INFO] File saved: uploads/8bbce6f4ec0940d89b1b9276a8b9859d\9dbb5e27094945e1b7e4c7846a5bede3_w.jpg +2025-10-19 18:32:04,370 [INFO] 127.0.0.1 - - [19/Oct/2025 18:32:04] "POST /upload?file= HTTP/1.1" 201 - +2025-10-19 18:32:10,503 [INFO] File saved: uploads/a2211803c64244c49411bbeabb2dd686\88798004dec04ebb82f8165754bc9bf8_q.jpg +2025-10-19 18:32:10,504 [INFO] 127.0.0.1 - - [19/Oct/2025 18:32:10] "POST /upload?file= HTTP/1.1" 201 - diff --git a/flask_file_upload_mvc/models/__pycache__/uploaded_file.cpython-313.pyc b/flask_file_upload_mvc/models/__pycache__/uploaded_file.cpython-313.pyc new file mode 100644 index 0000000..bda052c Binary files /dev/null and b/flask_file_upload_mvc/models/__pycache__/uploaded_file.cpython-313.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..8431248 --- /dev/null +++ b/flask_file_upload_mvc/requirements.txt @@ -0,0 +1,14 @@ +blinker==1.9.0 +click==8.3.0 +colorama==0.4.6 +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/flask_file_upload_mvc/services/__pycache__/file_service.cpython-313.pyc b/flask_file_upload_mvc/services/__pycache__/file_service.cpython-313.pyc new file mode 100644 index 0000000..ae194ff Binary files /dev/null and b/flask_file_upload_mvc/services/__pycache__/file_service.cpython-313.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..dc2d300 --- /dev/null +++ b/flask_file_upload_mvc/services/file_service.py @@ -0,0 +1,57 @@ +import os +import uuid +from werkzeug.utils import secure_filename +from extensions import db +from models.uploaded_file import UploadedFile +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}") \ No newline at end of file diff --git a/flask_file_upload_mvc/uploads/32b74f9d95bc4d54aa675b28078818bf/a401aead323044169893c7724240297f_e.jpg b/flask_file_upload_mvc/uploads/32b74f9d95bc4d54aa675b28078818bf/a401aead323044169893c7724240297f_e.jpg new file mode 100644 index 0000000..16bc275 Binary files /dev/null and b/flask_file_upload_mvc/uploads/32b74f9d95bc4d54aa675b28078818bf/a401aead323044169893c7724240297f_e.jpg differ diff --git a/flask_file_upload_mvc/uploads/8bbce6f4ec0940d89b1b9276a8b9859d/9dbb5e27094945e1b7e4c7846a5bede3_w.jpg b/flask_file_upload_mvc/uploads/8bbce6f4ec0940d89b1b9276a8b9859d/9dbb5e27094945e1b7e4c7846a5bede3_w.jpg new file mode 100644 index 0000000..97c7313 Binary files /dev/null and b/flask_file_upload_mvc/uploads/8bbce6f4ec0940d89b1b9276a8b9859d/9dbb5e27094945e1b7e4c7846a5bede3_w.jpg differ diff --git a/flask_file_upload_mvc/uploads/a2211803c64244c49411bbeabb2dd686/88798004dec04ebb82f8165754bc9bf8_q.jpg b/flask_file_upload_mvc/uploads/a2211803c64244c49411bbeabb2dd686/88798004dec04ebb82f8165754bc9bf8_q.jpg new file mode 100644 index 0000000..3e69197 Binary files /dev/null and b/flask_file_upload_mvc/uploads/a2211803c64244c49411bbeabb2dd686/88798004dec04ebb82f8165754bc9bf8_q.jpg differ diff --git a/flask_file_upload_mvc/uploads/a7d1ce6af9ed4143927016f84b346ce4/50ae5bec1039417b9cd366e4d6075352_e.jpg b/flask_file_upload_mvc/uploads/a7d1ce6af9ed4143927016f84b346ce4/50ae5bec1039417b9cd366e4d6075352_e.jpg new file mode 100644 index 0000000..16bc275 Binary files /dev/null and b/flask_file_upload_mvc/uploads/a7d1ce6af9ed4143927016f84b346ce4/50ae5bec1039417b9cd366e4d6075352_e.jpg differ diff --git a/flask_file_upload_mvc/uploads/ae2f237da33544b2b4d64597664c9da9/e2b8473a87bd40a18fc89808d1f2c438_e.jpg b/flask_file_upload_mvc/uploads/ae2f237da33544b2b4d64597664c9da9/e2b8473a87bd40a18fc89808d1f2c438_e.jpg new file mode 100644 index 0000000..16bc275 Binary files /dev/null and b/flask_file_upload_mvc/uploads/ae2f237da33544b2b4d64597664c9da9/e2b8473a87bd40a18fc89808d1f2c438_e.jpg differ