From 258f853a6dce70a868608de249a9a8b0d3981e4f Mon Sep 17 00:00:00 2001 From: jcatillo Date: Sat, 18 Oct 2025 13:57:46 +0800 Subject: [PATCH] feat: file upload. TODO: fix circular error --- .gitignore | 2 + __pycache__/app.cpython-313.pyc | Bin 0 -> 1237 bytes __pycache__/config.cpython-313.pyc | Bin 0 -> 1046 bytes app.py | 30 ++++++++ config.py | 14 ++++ .../file_controller.cpython-313.pyc | Bin 0 -> 3990 bytes controllers/file_controller.py | 64 ++++++++++++++++++ logs/app.log | 0 .../__pycache__/uploaded_file.cpython-313.pyc | Bin 0 -> 816 bytes models/uploaded_file.py | 6 ++ requirements.txt | 12 ++++ .../__pycache__/file_service.cpython-313.pyc | Bin 0 -> 3563 bytes services/file_service.py | 49 ++++++++++++++ 13 files changed, 177 insertions(+) create mode 100644 .gitignore create mode 100644 __pycache__/app.cpython-313.pyc create mode 100644 __pycache__/config.cpython-313.pyc create mode 100644 app.py create mode 100644 config.py create mode 100644 controllers/__pycache__/file_controller.cpython-313.pyc create mode 100644 controllers/file_controller.py create mode 100644 logs/app.log create mode 100644 models/__pycache__/uploaded_file.cpython-313.pyc create mode 100644 models/uploaded_file.py create mode 100644 requirements.txt create mode 100644 services/__pycache__/file_service.cpython-313.pyc create mode 100644 services/file_service.py 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 0000000000000000000000000000000000000000..62c06d6b78f5a033a9bfb672138526576096ef82 GIT binary patch literal 1237 zcmb7E&u<$=6n?w2zr1UwhR~!fh?r8#wgfc-r6^F4P)Y)}n}C-}l`vYZ?eS)twb#t9 zOB-=WD3a4t!vVpc05@*j=|7N!qFPf$io}UqAVQUT>Koe;kpc&1rP=r6`@Z+)&1w%* zDHCu_B@b7AmH~bj&e77w#^Hek@GaP2OJ!iAx5Om$<+5BNjATTNl$Dao)S<1EwUW;C z4AjVZs2Q`!7mFooiE46WAK$B|k9#A#rXR6TMMagwkdPmQwe-;mhKTG*#OydE10t+y z?!OT@Wl21_@bC3pq$<75+Wr-hT5L6rZQn*un-4oJ-{WRwdC^%|u3UM)`U!>& zddrVuCtO`Kif*@iF$lf2TsH4Ujo5F~LX`P5n-Azs8gyLYewNAR+cb(?51j(xM#~SV zNO5(D;#w$Kz^Cw9E*efTxGH z6MY`Iqu!Y!!dhe4J`*2@P!u&ChZF3XTP+s0ov?m`HexO}>)dF&tF-B}h+B0x@*Cr$ zD<3W`Ec544DQ001P!?Sr_V`bLSwj9jt6Q z9FDzvn;VzgVY3&|ciDN=!dr%D8@B@@Nzx-oJc5ZmnBIfj9-RGC13kI9u_z;}TyyfSG;O=sO$E zZ|c-Q(fU_6u717zn1D9DPpr+gU&+~lqW2d!7Pl^}FYYSS2LzOJk5$Yg;YlR^D8Ib< N%zTdA&zbYH`cn)38kYb7 literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf79cfd66c8383a7a7915c4ee17ac47c8ac0bb0d GIT binary patch literal 1046 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', 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, 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..e69de29 diff --git a/models/__pycache__/uploaded_file.cpython-313.pyc b/models/__pycache__/uploaded_file.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72f139b7f54b96b9c0e965be18522bd58ba88d21 GIT binary patch literal 816 zcmbtSy>HV%6u+~b4=1S_A^}YYgt~d4mWnEsP%%)5C?6H-HCv>U6JHa<#WrVq$W{r7 zp-U-J)v6uZ5w`v%NZF!8OibC30yDge8wx^V;UvHNy`SI5?>&1wGgClt!tA*F3GOe- znV!rA7}o$CAc6>ff*xamZ=yN0fQUAah~C7T#x&vbUe;4A^p0U=M#{0U)ZAA4G6tbv z4L$;UfSO2Pf`mq}(3{5QMA+})2$}+4`jb;0(TILW6S;q6mH*Cy5sYP=@+3pDP{I7~ z3I&oA#ZKNbWvLPJz;T&d_c$A=g>1nNilOj&j_A{utUvkzWF5%7pK$K9IQxJ$tdh)A z>N`C~sVq?13*3Y&T%z<<;_!<%vy^UoB8s`^Ge4kIs0K2-7VxC!%iOvjvkntd^IVxF zvGDwk%x{Abl`c(%Xz0XUd6m*Ac4DthofyJeNz5Xsrj|d32g#>)BVr=5U$h~L^T2)( zw38n5_J#W76Qkrtbue$x&@Ap% zcdKug4r}|h_ge#VY3KfM_R3y=w|`VQtsYm;ZlByZe>j+}?K~QmD~IMg^JwAJI=0T% zP8QD_gYw*Ct9@P=hb-3h~VA(>}NNZMA-l*U~LB=f83YVV18aMgbh&4Kxy$DzFiO`rOIkM*%|D< zUF^PnczD0Jd(hq8g`nK+K3IF{LFilBaf+)c9Q+=HuaSf#E{=%Za)g7vEACq02_Kk*hiJQqd?pg2>FO#{rK!j-|xz{=3lRT#p>5{xqi;@7fU-CiSo#mok-_z|Dqns7g zl}w2!E33J@qNX<#81g3zc`Yp~@?98%zDxe#T{ld(_y>io#TVpNs8o=O4~rIlP*AcN zj;d=G3@s1OfeICT3714~f%FbH0W~e@iwH$f5+yoU*{@)>1-o5)vvv*czm*1ORl;-a*O7L-l z^Wi#O6|sIDOX>>IV-GUvVlJQ8Vt2GmX+u$qdQ8f!X<9z^8=__NX*EU{UM?|L#bC)t zxs0N-!Ifrjs_@A2r$NW2A~XMzUf4o_N52A98SRUK-HAWlGR396hfhC!^64}E`R&^9 z_2(ZN!#B*~TgLFM+VJiA?2<9ORK-gNum)wEIH4Wt9_HyT82uWhTK)vzcFv=7pC1T=qj)B9kzv>_w6PYQ&1WBGG2Cul8$OxcYT9~G2{sud&6D146wyz(%o7gw}(i(84hNDt_$mBANtq=?BbH0kEny?QuE;r}6B>gSE|qe!<)-x;7Qd!Ev@lf&2|~-M z7N!~r#g(vB;?)&h&uOZ~r)AmlWVDS9Ak~}Kve_J@7MxR8wW!C!)G$l@Agx1&((*e`3!rP4<1#^_MZGf%K|Xe(Nad6Kp&a3LW*y2At;~>MrH2V;mqAU*sUnkUVI2)A>4Tz*HFr*lkgytJU zQwf2&It{B1=0w{<`|P_8@2ExkU!Vf& zf@jR&c_Vn<42~JWu}ZcUoGQqP4)qs&KL4_3dnKZ|;1u{mJf{**{_QPnh1Q z;f*$gt}lF_`*tVH(5Mj_tqWt{^_<>&v(__RUf3TTF$X7&!O8Mm9f$XaerXO}Him%M zI_`fBU)A2x%2H*%+A~$hu@1nR2PPPdPz$ghe?DMbvUM3^hj1&rekNetapC(Pfc@%! z0PI9f09fnjJq{@r%ExLr9NzywOv%P;{dBF)z}oI(5Ki@|C_uYO0+B-3<^a!F=r)ncy= zrRA{|GC1Mbz6$d!k#X5%k6!p95&N(c?a4NHH{rzvc=W$Q1>l9w+0dAw2_rP|Y@imJ zF2}zS`(Fp%y4gQz^iP`JONRH71Ky+=x?qGZ)P;$L5d6abxxYFv^ZYII>YQV!c$%GU(SV(~Xv(w#aD89jQcBsW65Pp~ z_l>>{4Zc#JvTxwd4VzL5cZx#6b{+F1Jwohz49j*j5fSQ?zah4j?K0tQ&{xVU?~+^a zih{4xfAIs)aoh{!dx6B4=;BLs`6UXyKmjX!ql!YWTpYK|y~5pG->Y+euJ7P%7dOLn F{u`Thc>MqX literal 0 HcmV?d00001 diff --git a/services/file_service.py b/services/file_service.py new file mode 100644 index 0000000..094921a --- /dev/null +++ b/services/file_service.py @@ -0,0 +1,49 @@ +import os +import uuid +from werkzeug.utils import secure_filename +from models.uploaded_file import UploadedFile +from app 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