diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..95d21bf Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d19ec7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv +.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..153a165 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..3b437df 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..fb89d90 --- /dev/null +++ b/app.py @@ -0,0 +1,32 @@ +import os +import logging +from flask import Flask +from models.db_extension import db +from config import Config + +# Initialize Flask App +app = Flask(__name__) +app.config.from_object(Config) + +# Initialize database +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) + +# 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..9e64997 --- /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 \ No newline at end of file diff --git a/controllers/__pycache__/file_controller.cpython-313.pyc b/controllers/__pycache__/file_controller.cpython-313.pyc new file mode 100644 index 0000000..45bc00a 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..8b987da --- /dev/null +++ b/controllers/file_controller.py @@ -0,0 +1,63 @@ +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__) + +# 🟢 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 = 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 + + +# 🟡 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, 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 + +# 🔴 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/logs/app.log b/logs/app.log new file mode 100644 index 0000000..883aac4 --- /dev/null +++ b/logs/app.log @@ -0,0 +1,79 @@ +2025-10-18 14:26:53,240 [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 14:26:53,240 [INFO] Press CTRL+C to quit +2025-10-18 14:26:53,241 [INFO] * Restarting with stat +2025-10-18 14:26:53,454 [WARNING] * Debugger is active! +2025-10-18 14:26:53,469 [INFO] * Debugger PIN: 138-035-899 +2025-10-18 14:27:15,095 [INFO] * Detected change in '/Users/jcatillo/Documents/School/Proglan/file-upload/config.py', reloading +2025-10-18 14:27:15,152 [INFO] * Restarting with stat +2025-10-18 14:27:15,411 [WARNING] * Debugger is active! +2025-10-18 14:27:15,419 [INFO] * Debugger PIN: 138-035-899 +2025-10-18 14:27:16,618 [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 14:27:16,619 [INFO] Press CTRL+C to quit +2025-10-18 14:27:16,619 [INFO] * Restarting with stat +2025-10-18 14:27:16,809 [WARNING] * Debugger is active! +2025-10-18 14:27:16,818 [INFO] * Debugger PIN: 138-035-899 +2025-10-18 14:27:24,063 [INFO] * Detected change in '/Users/jcatillo/Documents/School/Proglan/file-upload/config.py', reloading +2025-10-18 14:27:24,133 [INFO] * Restarting with stat +2025-10-18 14:27:24,353 [WARNING] * Debugger is active! +2025-10-18 14:27:24,361 [INFO] * Debugger PIN: 138-035-899 +2025-10-18 14:27:28,068 [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 14:27:28,068 [INFO] Press CTRL+C to quit +2025-10-18 14:27:28,069 [INFO] * Restarting with stat +2025-10-18 14:27:28,257 [WARNING] * Debugger is active! +2025-10-18 14:27:28,265 [INFO] * Debugger PIN: 138-035-899 +2025-10-18 14:29:46,546 [INFO] 127.0.0.1 - - [18/Oct/2025 14:29:46] "POST / HTTP/1.1" 404 - +2025-10-18 14:30:34,354 [INFO] 127.0.0.1 - - [18/Oct/2025 14:30:34] "POST / HTTP/1.1" 404 - +2025-10-18 14:31:08,293 [INFO] File saved: uploads/a33bb971cfdd4e1eabd0ed5ff4fabcc9/1c98826ed5274f90a05891d35d75e36b_back_e9c58ea9-72a3-4684-b596-5e2243dac9bd.png +2025-10-18 14:31:08,296 [INFO] 127.0.0.1 - - [18/Oct/2025 14:31:08] "POST /upload HTTP/1.1" 201 - +2025-10-18 14:31:57,850 [INFO] 127.0.0.1 - - [18/Oct/2025 14:31:57] "GET /file/1 HTTP/1.1" 200 - +2025-10-18 14:32:27,671 [INFO] File saved: uploads/e39b25e138fc4db7a772d18499dce12d_Screenshot_2025-10-16_at_2.37.18_PM.png +2025-10-18 14:32:27,676 [INFO] File updated: ID=1 +2025-10-18 14:32:27,677 [INFO] 127.0.0.1 - - [18/Oct/2025 14:32:27] "PUT /file/1 HTTP/1.1" 200 - +2025-10-18 14:32:37,451 [INFO] 127.0.0.1 - - [18/Oct/2025 14:32:37] "GET /file/1 HTTP/1.1" 200 - +2025-10-18 14:32:48,327 [INFO] File deleted from storage: uploads/e39b25e138fc4db7a772d18499dce12d_Screenshot_2025-10-16_at_2.37.18_PM.png +2025-10-18 14:32:48,330 [INFO] Record deleted: ID=1 +2025-10-18 14:32:48,330 [INFO] 127.0.0.1 - - [18/Oct/2025 14:32:48] "DELETE /file/1 HTTP/1.1" 200 - +2025-10-18 14:33:43,178 [INFO] * Detected change in '/Users/jcatillo/Documents/School/Proglan/file-upload/services/file_service.py', reloading +2025-10-18 14:33:43,234 [INFO] * Restarting with stat +2025-10-18 14:33:43,511 [WARNING] * Debugger is active! +2025-10-18 14:33:43,521 [INFO] * Debugger PIN: 138-035-899 +2025-10-18 14:33:45,592 [INFO] * Detected change in '/Users/jcatillo/Documents/School/Proglan/file-upload/services/file_service.py', reloading +2025-10-18 14:33:45,650 [INFO] * Restarting with stat +2025-10-18 14:33:45,856 [WARNING] * Debugger is active! +2025-10-18 14:33:45,864 [INFO] * Debugger PIN: 138-035-899 +2025-10-18 14:34:41,103 [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 14:34:41,104 [INFO] Press CTRL+C to quit +2025-10-18 14:34:41,104 [INFO] * Restarting with stat +2025-10-18 14:34:41,306 [WARNING] * Debugger is active! +2025-10-18 14:34:41,316 [INFO] * Debugger PIN: 138-035-899 +2025-10-18 14:34:52,645 [INFO] * Detected change in '/Users/jcatillo/Documents/School/Proglan/file-upload/config.py', reloading +2025-10-18 14:34:52,688 [INFO] * Restarting with stat +2025-10-18 14:34:52,882 [WARNING] * Debugger is active! +2025-10-18 14:34:52,889 [INFO] * Debugger PIN: 138-035-899 +2025-10-18 14:34:57,021 [INFO] * Detected change in '/Users/jcatillo/Documents/School/Proglan/file-upload/config.py', reloading +2025-10-18 14:34:57,085 [INFO] * Restarting with stat +2025-10-18 14:34:57,304 [WARNING] * Debugger is active! +2025-10-18 14:34:57,312 [INFO] * Debugger PIN: 138-035-899 +2025-10-18 14:35:03,207 [INFO] 127.0.0.1 - - [18/Oct/2025 14:35:03] "GET /file/1 HTTP/1.1" 404 - +2025-10-18 14:35:48,359 [INFO] 127.0.0.1 - - [18/Oct/2025 14:35:48] "POST /upload HTTP/1.1" 400 - +2025-10-18 14:35:52,681 [INFO] 127.0.0.1 - - [18/Oct/2025 14:35:52] "POST /upload HTTP/1.1" 400 - +2025-10-18 14:36:08,203 [INFO] File saved: uploads/621c73459b2e4335ad912e4965962f30/9fe79edeabe845f98d1a8149047fc99e_698dd37b-18c8-4705-9873-cc5acaf9c7e4_Fake-ID-Criminal-Charge-Indianapolis-Indiana.jpg +2025-10-18 14:36:08,204 [INFO] 127.0.0.1 - - [18/Oct/2025 14:36:08] "POST /upload HTTP/1.1" 201 - +2025-10-18 14:37:15,597 [ERROR] Upload error: File type not allowed +2025-10-18 14:37:15,599 [INFO] 127.0.0.1 - - [18/Oct/2025 14:37:15] "POST /upload HTTP/1.1" 400 - +2025-10-18 14:37:36,707 [INFO] File saved: uploads/0b0915db3e504aa1966be5b3660eb082/e330d2c083044df5a4324392c9aa169d_Screenshot_2025-06-29_at_2.44.28_PM.png +2025-10-18 14:37:36,709 [INFO] 127.0.0.1 - - [18/Oct/2025 14:37:36] "POST /upload HTTP/1.1" 201 - +2025-10-18 14:39:32,579 [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 14:39:32,579 [INFO] Press CTRL+C to quit +2025-10-18 14:39:32,580 [INFO] * Restarting with stat +2025-10-18 14:39:32,775 [WARNING] * Debugger is active! +2025-10-18 14:39:32,787 [INFO] * Debugger PIN: 138-035-899 +2025-10-18 14:39:40,500 [INFO] 127.0.0.1 - - [18/Oct/2025 14:39:40] "GET /file/4 HTTP/1.1" 200 - +2025-10-18 14:39:43,280 [INFO] 127.0.0.1 - - [18/Oct/2025 14:39:43] "GET /file/3 HTTP/1.1" 200 - +2025-10-18 14:39:46,088 [ERROR] Get error: [Errno 2] No such file or directory: '/Users/jcatillo/Documents/School/Proglan/file-upload/uploads/e39b25e138fc4db7a772d18499dce12d_Screenshot_2025-10-16_at_2.37.18_PM.png' +2025-10-18 14:39:46,093 [INFO] 127.0.0.1 - - [18/Oct/2025 14:39:46] "GET /file/2 HTTP/1.1" 500 - diff --git a/models/__pycache__/db_extension.cpython-313.pyc b/models/__pycache__/db_extension.cpython-313.pyc new file mode 100644 index 0000000..8086510 Binary files /dev/null and b/models/__pycache__/db_extension.cpython-313.pyc differ diff --git a/models/__pycache__/uploaded_file.cpython-313.pyc b/models/__pycache__/uploaded_file.cpython-313.pyc new file mode 100644 index 0000000..49d18d3 Binary files /dev/null and b/models/__pycache__/uploaded_file.cpython-313.pyc differ diff --git a/models/db_extension.py b/models/db_extension.py new file mode 100644 index 0000000..2e1eeb6 --- /dev/null +++ b/models/db_extension.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() \ No newline at end of file diff --git a/models/uploaded_file.py b/models/uploaded_file.py new file mode 100644 index 0000000..3bb80f9 --- /dev/null +++ b/models/uploaded_file.py @@ -0,0 +1,6 @@ +from .db_extension 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/requirements.txt b/requirements.txt new file mode 100644 index 0000000..156b641 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +blinker==1.9.0 +click==8.3.0 +Flask==3.1.2 +Flask-SQLAlchemy==3.1.1 +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..f43c943 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..b483918 --- /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 models.db_extension 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}") \ No newline at end of file diff --git a/uploads/0b0915db3e504aa1966be5b3660eb082/e330d2c083044df5a4324392c9aa169d_Screenshot_2025-06-29_at_2.44.28_PM.png b/uploads/0b0915db3e504aa1966be5b3660eb082/e330d2c083044df5a4324392c9aa169d_Screenshot_2025-06-29_at_2.44.28_PM.png new file mode 100644 index 0000000..573a6cf Binary files /dev/null and b/uploads/0b0915db3e504aa1966be5b3660eb082/e330d2c083044df5a4324392c9aa169d_Screenshot_2025-06-29_at_2.44.28_PM.png differ diff --git a/uploads/621c73459b2e4335ad912e4965962f30/9fe79edeabe845f98d1a8149047fc99e_698dd37b-18c8-4705-9873-cc5acaf9c7e4_Fake-ID-Criminal-Charge-Indianapolis-Indiana.jpg b/uploads/621c73459b2e4335ad912e4965962f30/9fe79edeabe845f98d1a8149047fc99e_698dd37b-18c8-4705-9873-cc5acaf9c7e4_Fake-ID-Criminal-Charge-Indianapolis-Indiana.jpg new file mode 100644 index 0000000..10b73fb Binary files /dev/null and b/uploads/621c73459b2e4335ad912e4965962f30/9fe79edeabe845f98d1a8149047fc99e_698dd37b-18c8-4705-9873-cc5acaf9c7e4_Fake-ID-Criminal-Charge-Indianapolis-Indiana.jpg differ