آپلود فایل در Flask: آموزش عملی

فهرست مطالب

آپلود فایل در Flask: آموزش عملی

در دنیای امروز که وب‌اپلیکیشن‌ها بخش جدایی‌ناپذیری از زندگی روزمره ما شده‌اند، قابلیت‌های تعاملی برای کاربران از اهمیت بالایی برخوردار است. یکی از این قابلیت‌های اساسی که تقریباً در هر نوع وب‌سایتی از شبکه‌های اجتماعی گرفته تا سیستم‌های مدیریت محتوا، از آن استفاده می‌شود، امکان آپلود فایل توسط کاربران است. این فایل‌ها می‌توانند شامل تصاویر پروفایل، اسناد متنی، فایل‌های صوتی، ویدئوها و انواع داده‌های دیگر باشند. فریم‌ورک سبک و قدرتمند Flask در پایتون، با سادگی و انعطاف‌پذیری بی‌نظیر خود، راهکارهای کارآمدی برای پیاده‌سازی این قابلیت ارائه می‌دهد.

هدف این آموزش جامع، ارائه یک راهنمای عملی و گام‌به‌گام برای توسعه‌دهندگانی است که قصد دارند قابلیت آپلود فایل را به برنامه‌های Flask خود اضافه کنند. ما از مفاهیم اولیه مانند راه‌اندازی محیط و ساختار فرم HTML آغاز کرده و سپس به جزئیات فنی و امنیتی حیاتی مانند اعتبارسنجی نوع و حجم فایل، جلوگیری از حملات امنیتی رایج و بهترین روش‌های مدیریت فایل‌های آپلود شده خواهیم پرداخت. همچنین، راهکارهای پیشرفته‌تری مانند آپلود همزمان چندین فایل، نمایش پیشرفت آپلود و استفاده از ذخیره‌سازی ابری نیز مورد بررسی قرار خواهد گرفت.

در پایان این مقاله، شما نه تنها درک عمیقی از نحوه عملکرد آپلود فایل در Flask خواهید داشت، بلکه قادر خواهید بود یک سیستم آپلود فایل امن، کارآمد و با قابلیت‌های پیشرفته را در پروژه‌های خود پیاده‌سازی کنید. این آموزش به گونه‌ای طراحی شده است که هم برای مبتدیان و هم برای توسعه‌دهندگان با تجربه که به دنبال بهترین روش‌ها و نکات امنیتی هستند، مفید واقع شود.

چرا آپلود فایل در Flask اهمیت دارد؟

قابلیت آپلود فایل، یکی از تعاملی‌ترین و کاربردی‌ترین ویژگی‌هایی است که یک وب‌سایت یا وب‌اپلیکیشن می‌تواند ارائه دهد. اهمیت این قابلیت در Flask، فراتر از یک ویژگی صرفاً فنی است و تأثیر مستقیمی بر تجربه کاربری، غنای محتوا و حتی مدل کسب‌وکار یک پلتفرم دارد. در ادامه به برخی از دلایل کلیدی اهمیت آپلود فایل در Flask می‌پردازیم:

  • افزایش تعامل و غنای محتوا: به کاربران امکان می‌دهد تا محتوای خود را (مانند تصاویر، ویدئوها، اسناد) به برنامه اضافه کنند. این امر به غنای محتوا کمک کرده و تعامل کاربران را به شکل چشمگیری افزایش می‌دهد. تصور کنید یک پلتفرم شبکه‌ اجتماعی بدون امکان آپلود عکس یا یک سیستم مدیریت اسناد بدون قابلیت آپلود فایل!
  • پشتیبانی از انواع مختلف اپلیکیشن‌ها: از وبلاگ‌ها و پورتال‌های خبری گرفته تا فروشگاه‌های آنلاین، سیستم‌های مدیریت پروژه، ابزارهای همکاری تیمی و پلتفرم‌های آموزشی، تقریباً همه به نوعی به این قابلیت نیاز دارند. Flask به عنوان یک میکروفریم‌ورک، انعطاف‌پذیری لازم برای پیاده‌سازی این ویژگی را در انواع مختلف پروژه‌ها فراهم می‌کند.
  • سفارشی‌سازی و شخصی‌سازی: کاربران می‌توانند پروفایل‌های خود را با آپلود عکس یا آواتار شخصی‌سازی کنند، یا در اپلیکیشن‌های طراحی، فایل‌های کاری خود را آپلود و ویرایش نمایند. این قابلیت حس مالکیت و ارتباط کاربر با پلتفرم را تقویت می‌کند.
  • ورودی داده‌های کاربر: بسیاری از اپلیکیشن‌ها برای دریافت ورودی‌های پیچیده از کاربران نیاز به آپلود فایل دارند. به عنوان مثال، رزومه‌ها در سایت‌های کاریابی، مقالات در پلتفرم‌های علمی، یا طرح‌های اولیه در سایت‌های معماری.
  • انعطاف‌پذیری و کنترل در Flask: Flask به دلیل طبیعت میکروفریم‌ورک خود، به توسعه‌دهندگان کنترل بسیار بالایی بر نحوه هندلینگ فایل‌ها می‌دهد. این به معنای امکان پیاده‌سازی منطق‌های سفارشی برای اعتبارسنجی، ذخیره‌سازی، و پردازش پس از آپلود است که برای نیازهای خاص یک پروژه حیاتی است.
  • ملاحظات امنیتی حیاتی: با وجود تمام مزایا، آپلود فایل بدون رعایت ملاحظات امنیتی می‌تواند به یک نقطه ضعف بزرگ تبدیل شود. Flask به توسعه‌دهندگان اجازه می‌دهد تا با استفاده از ابزارهایی مانند secure_filename و پیکربندی‌های دقیق، ریسک‌های امنیتی مرتبط با آپلود فایل (مانند آپلود کدهای مخرب، حملات directory traversal یا مصرف بی‌رویه منابع) را به حداقل برسانند. این جنبه امنیتی، از اهمیت بسیار بالایی برخوردار است.
  • تسهیل فرآیندهای کسب‌وکار: در بسیاری از کسب‌وکارها، به اشتراک‌گذاری و مدیریت اسناد، تصاویر و سایر فایل‌ها بین تیم‌ها یا با مشتریان، یک فرآیند حیاتی است. سیستم‌های آپلود فایل که با Flask پیاده‌سازی شده‌اند، می‌توانند این فرآیندها را ساده‌تر و کارآمدتر کنند.

به طور خلاصه، آپلود فایل در Flask نه تنها یک ویژگی کاربردی است، بلکه یک رکن اساسی برای ساخت وب‌اپلیکیشن‌های مدرن، تعاملی، شخصی‌سازی شده و ایمن است. درک صحیح و پیاده‌سازی امن این قابلیت، کلید موفقیت بسیاری از پروژه‌های توسعه وب خواهد بود.

پیش‌نیازها و آماده‌سازی محیط

قبل از اینکه بتوانیم وارد جزئیات کدنویسی شویم، نیاز داریم محیط توسعه خود را به درستی آماده کنیم. این گام‌ها تضمین می‌کنند که تمام ابزارهای لازم در دسترس هستند و پروژه شما به صورت سازمان‌یافته پیش می‌رود. برای شروع، شما به دانش اولیه پایتون و HTML نیاز دارید.

نصب پایتون

Flask یک فریم‌ورک پایتونی است، بنابراین بدیهی است که برای شروع به نصب پایتون نیاز داریم. توصیه می‌شود از آخرین نسخه پایدار پایتون 3 (مثلاً 3.8 به بالا) استفاده کنید. می‌توانید پایتون را از وب‌سایت رسمی python.org/downloads دانلود و نصب کنید. هنگام نصب در ویندوز، حتماً گزینه “Add Python to PATH” را تیک بزنید.

برای بررسی نصب صحیح پایتون، ترمینال یا Command Prompt خود را باز کرده و دستور زیر را اجرا کنید:

python --version
# یا
python3 --version

خروجی باید نسخه پایتون نصب شده را نمایش دهد.

استفاده از محیط مجازی (Virtualenv)

استفاده از محیط مجازی (virtual environment) یک بهترین روش (best practice) در توسعه پایتون است. محیط مجازی به شما اجازه می‌دهد تا وابستگی‌های (dependencies) پروژه‌های مختلف را ایزوله نگه دارید و از تداخل بین آن‌ها جلوگیری کنید. این کار برای جلوگیری از مشکلات “dependency hell” در پروژه‌های بزرگ‌تر بسیار مهم است.

برای ایجاد یک محیط مجازی، ابتدا وارد پوشه پروژه خود شوید (مثلاً `flask_upload_app`):

mkdir flask_upload_app
cd flask_upload_app

سپس، محیط مجازی را ایجاد کنید (ما آن را `venv` نامگذاری می‌کنیم):

python -m venv venv

پس از ایجاد، باید محیط مجازی را فعال کنید. دستور فعال‌سازی بسته به سیستم عامل شما متفاوت است:

  • ویندوز (Command Prompt):
    venv\Scripts\activate
  • ویندوز (PowerShell):
    .\venv\Scripts\Activate.ps1
  • لینوکس/macOS:
    source venv/bin/activate

پس از فعال‌سازی، نام محیط مجازی ((venv)) در ابتدای خط فرمان شما نمایش داده می‌شود که نشان‌دهنده فعال بودن آن است.

نصب Flask

حالا که محیط مجازی فعال است، می‌توانیم Flask را در آن نصب کنیم. این کار تضمین می‌کند که Flask فقط برای پروژه فعلی شما نصب می‌شود و با سایر پروژه‌ها تداخل ندارد.

pip install Flask

برای اطمینان از نصب صحیح، می‌توانید یک فایل پایتون ایجاد کرده و آن را اجرا کنید:

# test_flask.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello, Flask!"

if __name__ == '__main__':
    app.run(debug=True)

سپس با دستور python test_flask.py آن را اجرا کرده و در مرورگر به آدرس http://127.0.0.1:5000/ مراجعه کنید.

ساختار پوشه‌های پروژه

یک ساختار پوشه سازمان‌یافته به مدیریت بهتر پروژه کمک می‌کند. برای این آموزش، ساختار زیر را پیشنهاد می‌کنیم:

flask_upload_app/
├── venv/
├── app.py
├── templates/
│   ├── index.html
│   ├── upload.html
│   ├── upload_success.html
│   ├── multi_upload.html
│   ├── multi_upload_success.html
│   └── ajax_upload.html
└── uploads/  (این پوشه را بعداً خواهیم ساخت)

پوشه `templates` برای نگهداری فایل‌های HTML (قالب‌ها) است و Flask به طور خودکار به دنبال آن‌ها در این پوشه می‌گردد. پوشه `uploads` محلی است که فایل‌های آپلود شده توسط کاربران در آن ذخیره خواهند شد.

با آماده‌سازی این پیش‌نیازها، شما آماده‌اید تا وارد دنیای هیجان‌انگیز آپلود فایل در Flask شوید.

مبانی آپلود فایل در Flask

آپلود فایل در Flask بر اساس مفاهیم استاندارد وب و پروتکل HTTP بنا شده است. برای درک چگونگی عملکرد آن، لازم است با چند مفهوم کلیدی آشنا شویم:

فرم‌های HTML و enctype="multipart/form-data"

زمانی که می‌خواهید یک فایل را از طریق فرم HTML به سرور ارسال کنید، باید مطمئن شوید که فرم شما به درستی پیکربندی شده است. مهمترین قسمت، ویژگی enctype در تگ <form> است که باید روی "multipart/form-data" تنظیم شود. این نوع کدگذاری به مرورگر اجازه می‌دهد تا داده‌های باینری (مانند فایل‌ها) را به همراه سایر داده‌های فرم (مانند متن) به سرور ارسال کند.

مثال یک فرم ساده:

<form method="POST" enctype="multipart/form-data">
    <input type="file" name="file">
    <input type="submit" value="Upload">
</form>

در اینجا:

  • method="POST": برای ارسال فایل‌ها، همیشه باید از متد HTTP POST استفاده کنید.
  • enctype="multipart/form-data": این ویژگی ضروری است و بدون آن فایل شما به سرور ارسال نخواهد شد.
  • <input type="file" name="file">: این تگ مسئول انتخاب فایل از سیستم کاربر است. ویژگی name="file" بسیار مهم است زیرا Flask از این نام برای شناسایی فایل در سمت سرور استفاده می‌کند.

request.files در Flask

وقتی یک فرم با enctype="multipart/form-data" و متد POST به سرور Flask ارسال می‌شود، Flask تمام فایل‌های آپلود شده را در شیء request.files ذخیره می‌کند. این شیء یک MultiDict (یک نوع دیکشنری) است که در آن کلیدها (keys) همان ویژگی name تگ‌های ورودی فایل در HTML هستند و مقادیر (values) آن اشیاء FileStorage هستند.

یک شیء FileStorage دارای ویژگی‌ها و متدهایی است که به شما امکان دسترسی به اطلاعات فایل و ذخیره آن را می‌دهد:

  • filename: نام اصلی فایل (که توسط کاربر آپلود شده است).
  • mimetype: نوع MIME فایل (مثلاً image/jpeg، application/pdf).
  • stream: یک فایل‌لایف‌آبجکت (file-like object) که می‌توانید از آن برای خواندن محتویات فایل استفاده کنید.
  • save(destination): یک متد کلیدی که فایل را در مسیر مشخص شده در سیستم فایل سرور ذخیره می‌کند.

مثال دسترسی به فایل در Flask:

from flask import Flask, request

app = Flask(__name__)

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' in request.files:
        f = request.files['file']
        # اکنون می توانید از f.filename, f.mimetype و f.save() استفاده کنید.
        # ...
    return 'File uploaded successfully'

secure_filename برای امنیت نام فایل‌ها

یکی از مهمترین ملاحظات امنیتی هنگام آپلود فایل، نام فایل است. نام فایل‌هایی که توسط کاربران آپلود می‌شوند، ممکن است حاوی کاراکترهای مخرب یا مسیرهای نسبی (مانند ../../path/to/malicious_file.exe) باشند که می‌تواند به حملات Directory Traversal منجر شود. Flask یک تابع بسیار مفید به نام secure_filename را در ماژول werkzeug.utils ارائه می‌دهد که نام فایل را پاکسازی می‌کند تا فقط حاوی کاراکترهای امن باشد و از این نوع حملات جلوگیری کند.

همیشه از secure_filename برای نام‌گذاری فایل‌های آپلود شده استفاده کنید.

from werkzeug.utils import secure_filename

# ...

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' in request.files:
        f = request.files['file']
        if f.filename == '': # اگر فایلی انتخاب نشده باشد
            return 'No selected file'
        filename = secure_filename(f.filename)
        # حالا می توانید filename امن را برای ذخیره فایل استفاده کنید
        # f.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        # ...
    return 'File uploaded successfully'

تعیین پوشه آپلود

شما باید یک پوشه مشخص روی سرور خود برای ذخیره فایل‌های آپلود شده تعیین کنید. این مسیر باید قابل نوشتن (writable) توسط سرور باشد. Flask به شما اجازه می‌دهد تا این مسیر را در تنظیمات (app.config) خود مشخص کنید.

import os
from flask import Flask, request, redirect, url_for
from werkzeug.utils import secure_filename

UPLOAD_FOLDER = 'uploads' # نام پوشه ای که فایل ها در آن ذخیره می شوند
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'} # پسوندهای مجاز

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

با درک این مفاهیم پایه، ما آماده‌ایم تا اولین برنامه آپلود فایل خود را در Flask بسازیم. این بخش‌ها، بلوک‌های سازنده هر سیستم آپلود فایلی هستند و تسلط بر آن‌ها برای پیاده‌سازی‌های پیچیده‌تر ضروری است.

ساختار اولیه یک برنامه آپلود فایل

در این بخش، ما یک برنامه Flask کوچک و کاربردی خواهیم ساخت که قابلیت آپلود یک فایل را داراست. این مثال شامل ایجاد فایل اصلی Flask، طراحی فرم HTML و پیاده‌سازی منطق سمت سرور برای دریافت و ذخیره فایل خواهد بود.

ایجاد فایل app.py

در پوشه اصلی پروژه (flask_upload_app)، فایل app.py را ایجاد کنید و کدهای زیر را در آن قرار دهید. توجه داشته باشید که این کد شامل ساختار پایه، اعتبارسنجی اولیه، و سرویس‌دهی فایل‌های آپلود شده است.

import os
import uuid
from datetime import datetime

from flask import Flask, request, redirect, url_for, render_template, send_from_directory, flash, jsonify
from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge
from flask_sqlalchemy import SQLAlchemy

# --- پیکربندی اپلیکیشن Flask ---
app = Flask(__name__)
# مسیر پوشه ای که فایل های آپلود شده در آن ذخیره می شوند
UPLOAD_FOLDER = 'uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
# حداکثر حجم فایل 16 مگابایت (برای جلوگیری از حملات DoS و مصرف زیاد منابع)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 
# کلید مخفی برای Flask (برای فلش مسیج ها و سشن ها)
app.config['SECRET_KEY'] = 'a_very_secret_key_that_should_be_changed_in_production'

# پیکربندی پایگاه داده SQLite برای Flask-SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

# تعریف پسوندهای مجاز
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
# تعریف MIME Typeهای مجاز
ALLOWED_MIMETYPES = {'text/plain', 'application/pdf', 'image/png', 'image/jpeg', 'image/gif'}

# اطمینان از وجود پوشه آپلود
if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)

# --- مدل پایگاه داده برای فایل های آپلود شده ---
class UploadedFile(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    original_filename = db.Column(db.String(255), nullable=False)
    stored_filename = db.Column(db.String(255), nullable=False, unique=True)
    mimetype = db.Column(db.String(100))
    filesize = db.Column(db.BigInteger) # در بایت
    upload_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    # uploader_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) # می تواند برای ارتباط با کاربران استفاده شود

    def __repr__(self):
        return f"<File {self.stored_filename} - {self.original_filename}>"

# --- توابع کمکی ---
def allowed_file(filename, mimetype):
    # ابتدا پسوند را بررسی می کنیم
    if not ('.' in filename and \
            filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS):
        return False
    # سپس MIME Type را بررسی می کنیم
    if mimetype not in ALLOWED_MIMETYPES:
        return False
    return True

# --- مدیریت خطای فایل بزرگ ---
@app.errorhandler(RequestEntityTooLarge)
def handle_too_large_file(e):
    flash("فایل انتخابی بیش از حد بزرگ است. حداکثر حجم مجاز 16 مگابایت است.", "danger")
    return redirect(request.url) # به صفحه ای که از آن آمده ایم برمی گردیم

# --- مسیرهای برنامه ---

@app.route('/')
def index():
    # نمایش لیست فایل های موجود در پایگاه داده
    files_db = UploadedFile.query.all()
    return render_template('index.html', files=files_db)

@app.route('/upload', methods=['GET', 'POST'])
def upload_single_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash("درخواست شما شامل بخش فایل نبود.", "danger")
            return redirect(request.url)
        
        file = request.files['file']
        
        if file.filename == '':
            flash("فایلی انتخاب نشده است.", "danger")
            return redirect(request.url)
        
        if file and allowed_file(file.filename, file.mimetype):
            original_filename = secure_filename(file.filename)
            file_extension = original_filename.rsplit('.', 1)[1].lower()
            unique_filename = str(uuid.uuid4()) + '.' + file_extension
            
            file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
            file.save(file_path)

            # ذخیره متادیتا در پایگاه داده
            new_file_record = UploadedFile(
                original_filename=original_filename,
                stored_filename=unique_filename,
                mimetype=file.mimetype,
                filesize=os.path.getsize(file_path),
                upload_date=datetime.utcnow()
            )
            db.session.add(new_file_record)
            db.session.commit()

            flash(f"فایل '{original_filename}' با موفقیت آپلود شد!", "success")
            return redirect(url_for('upload_success', filename=unique_filename))
        else:
            flash(f"پسوند یا نوع فایل انتخابی مجاز نیست. پسوندهای مجاز: {', '.join(ALLOWED_EXTENSIONS)}", "danger")
            return redirect(request.url)
    
    return render_template('upload.html', config=app.config)

@app.route('/multi-upload', methods=['GET', 'POST'])
def multi_upload_file():
    if request.method == 'POST':
        if 'files[]' not in request.files:
            flash("درخواست شما شامل بخش فایل نبود.", "danger")
            return redirect(request.url)
        
        files = request.files.getlist('files[]')
        uploaded_filenames = []
        
        for file in files:
            if file.filename == '':
                continue
            
            if file and allowed_file(file.filename, file.mimetype):
                original_filename = secure_filename(file.filename)
                file_extension = original_filename.rsplit('.', 1)[1].lower()
                unique_filename = str(uuid.uuid4()) + '.' + file_extension
                
                file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
                
                try:
                    file.save(file_path)
                    
                    new_file_record = UploadedFile(
                        original_filename=original_filename,
                        stored_filename=unique_filename,
                        mimetype=file.mimetype,
                        filesize=os.path.getsize(file_path),
                        upload_date=datetime.utcnow()
                    )
                    db.session.add(new_file_record)
                    db.session.commit()
                    uploaded_filenames.append(unique_filename)
                except RequestEntityTooLarge:
                    flash(f"فایل '{original_filename}' بیش از حد بزرگ است و آپلود نشد.", "warning")
                    continue
                except Exception as e:
                    print(f"Error saving file {original_filename}: {e}")
                    flash(f"خطا در ذخیره فایل '{original_filename}' رخ داد.", "danger")
                    continue
            else:
                flash(f"فایل '{file.filename}' به دلیل پسوند یا نوع نامعتبر، آپلود نشد.", "warning")
                continue
        
        if uploaded_filenames:
            flash(f"{len(uploaded_filenames)} فایل با موفقیت آپلود شدند!", "success")
            return redirect(url_for('multi_upload_success', filenames=",".join(uploaded_filenames)))
        else:
            flash("هیچ فایل معتبری برای آپلود پیدا نشد.", "info")
            return redirect(request.url)
            
    return render_template('multi_upload.html')

@app.route('/ajax-upload', methods=['POST'])
def ajax_upload_file():
    if 'file' not in request.files:
        return jsonify({'error': 'No file part in the request'}), 400
    
    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': 'No selected file'}), 400
    
    if file and allowed_file(file.filename, file.mimetype):
        original_filename = secure_filename(file.filename)
        file_extension = original_filename.rsplit('.', 1)[1].lower()
        unique_filename = str(uuid.uuid4()) + '.' + file_extension
        
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
        
        try:
            file.save(file_path)
            
            new_file_record = UploadedFile(
                original_filename=original_filename,
                stored_filename=unique_filename,
                mimetype=file.mimetype,
                filesize=os.path.getsize(file_path),
                upload_date=datetime.utcnow()
            )
            db.session.add(new_file_record)
            db.session.commit()

            return jsonify({'message': 'File uploaded successfully', 'filename': unique_filename}), 200
        except RequestEntityTooLarge:
            return jsonify({'error': 'File is too large. Max size is 16MB.'}), 413
        except Exception as e:
            print(f"Error saving file: {e}")
            return jsonify({'error': 'Server error saving file'}), 500
    else:
        return jsonify({'error': 'Invalid file type or mimetype'}), 400

@app.route('/uploads/<filename>')
def uploaded_file(filename):
    # برای نمایش فایل های آپلود شده
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

@app.route('/upload_success/<filename>')
def upload_success(filename):
    file_record = UploadedFile.query.filter_by(stored_filename=filename).first()
    if file_record:
        return render_template('upload_success.html', filename=file_record.original_filename, stored_filename=filename)
    flash("فایل درخواستی یافت نشد.", "danger")
    return redirect(url_for('index'))

@app.route('/multi-upload-success/<filenames>')
def multi_upload_success(filenames):
    file_list = filenames.split(',')
    file_records = UploadedFile.query.filter(UploadedFile.stored_filename.in_(file_list)).all()
    return render_template('multi_upload_success.html', files=file_records)

@app.route('/delete/<int:file_id>', methods=['POST'])
def delete_file(file_id):
    file_record = UploadedFile.query.get_or_404(file_id)
    
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], file_record.stored_filename)
    
    if os.path.exists(file_path):
        os.remove(file_path)
        db.session.delete(file_record)
        db.session.commit()
        flash(f"فایل '{file_record.original_filename}' با موفقیت حذف شد.", "success")
    else:
        db.session.delete(file_record) # اگر فایل فیزیکی موجود نباشد، رکورد دیتابیس را حذف می کنیم
        db.session.commit()
        flash(f"فایل فیزیکی '{file_record.original_filename}' یافت نشد، اما رکورد آن حذف گردید.", "warning")
    
    return redirect(url_for('index'))

@app.route('/upload-progress')
def upload_progress_form():
    return render_template('ajax_upload.html')

if __name__ == '__main__':
    with app.app_context():
        db.create_all() # ایجاد جداول دیتابیس اگر وجود ندارند
    app.run(debug=True)

توضیحات کلی app.py:

  • پیکربندی: متغیرهایی برای UPLOAD_FOLDER، MAX_CONTENT_LENGTH (حداکثر حجم فایل)، SECRET_KEY (برای فلش مسیج‌ها) و تنظیمات Flask-SQLAlchemy تعریف شده‌اند.
  • مدل UploadedFile: یک مدل پایگاه داده برای ذخیره متادیتا (نام اصلی، نام ذخیره شده، نوع، حجم، تاریخ آپلود) هر فایل.
  • تابع allowed_file: یک تابع کمکی قوی‌تر که هم پسوند فایل و هم MIME Type آن را برای امنیت بیشتر بررسی می‌کند.
  • هندل‌کننده خطا RequestEntityTooLarge: خطای حجم زیاد فایل را به شکل کاربرپسندتری مدیریت می‌کند.
  • مسیر / (index): لیست تمام فایل‌های آپلود شده را از پایگاه داده بازیابی و نمایش می‌دهد.
  • مسیر /upload: برای آپلود یک فایل واحد (GET برای نمایش فرم، POST برای پردازش).
  • مسیر /multi-upload: برای آپلود چندین فایل به صورت همزمان.
  • مسیر /ajax-upload: نقطه پایانی برای آپلود فایل با AJAX که پاسخ JSON برمی‌گرداند.
  • مسیر /uploads/<filename>: برای سرویس‌دهی فایل‌های آپلود شده به صورت استاتیک با استفاده از send_from_directory.
  • مسیر /delete/<int:file_id>: برای حذف یک فایل از سیستم و پایگاه داده.
  • Flash Messages: از flash برای نمایش پیام‌های موفقیت، خطا یا هشدار به کاربر استفاده می‌کند.
  • UUID برای نام فایل: هر فایل با یک نام منحصر به فرد UUID ذخیره می‌شود تا از تداخل و مسائل امنیتی جلوگیری شود.

طراحی فرم HTML برای آپلود

در پوشه templates، فایل‌های HTML مورد نیاز را ایجاد کنید. این فایل‌ها شامل صفحات اصلی، فرم‌های آپلود و صفحات موفقیت‌آمیز بودن آپلود هستند.

templates/index.html (صفحه اصلی)

<!doctype html>
<html lang="fa" dir="rtl">
<head>
    <meta charset="utf-8">
    <title>فهرست فایل‌های آپلود شده</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; text-align: right; background-color: #f4f7f6; color: #333; }
        h1, h2 { color: #2c3e50; }
        ul { list-style-type: none; padding: 0; }
        li { background-color: #ffffff; margin-bottom: 10px; padding: 10px; border-radius: 5px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center; }
        a { text-decoration: none; color: #007bff; }
        a:hover { text-decoration: underline; }
        .upload-links { margin-top: 20px; display: flex; gap: 10px; }
        .upload-link, .button-delete { display: inline-block; padding: 10px 15px; border-radius: 5px; text-align: center; cursor: pointer; }
        .upload-link { background-color: #28a745; color: white; }
        .upload-link:hover { background-color: #218838; }
        .button-delete { background-color: #dc3545; color: white; border: none; font-size: 0.9em; margin-right: 10px; }
        .button-delete:hover { background-color: #c82333; }
        .flash { padding: 10px; margin-bottom: 15px; border-radius: 5px; }
        .flash.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .flash.danger { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        .flash.warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
        .flash.info { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
    </style>
</head>
<body>
    <h1>فهرست فایل‌های آپلود شده</h1>
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            <div class="flashes">
            {% for category, message in messages %}
                <div class="flash {{ category }}">{{ message }}</div>
            {% endfor %}
            </div>
        {% endif %}
    {% endwith %}

    <div class="upload-links">
        <a href="/upload" class="upload-link">آپلود یک فایل</a>
        <a href="/multi-upload" class="upload-link">آپلود چند فایل</a>
        <a href="/upload-progress" class="upload-link">آپلود با نوار پیشرفت (AJAX)</a>
    </div>

    <h2>لیست فایل‌ها:</h2>
    {% if files %}
    <ul>
        {% for file in files %}
        <li>
            <div>
                <a href="{{ url_for('uploaded_file', filename=file.stored_filename) }}" target="_blank">{{ file.original_filename }}</a>
                <br>
                <small>({{ (file.filesize / 1024 / 1024)|round(2) }} MB) - {{ file.upload_date.strftime('%Y-%m-%d %H:%M') }}</small>
            </div>
            <form action="{{ url_for('delete_file', file_id=file.id) }}" method="post">
                <button type="submit" class="button-delete">حذف</button>
            </form>
        </li>
        {% endfor %}
    </ul>
    {% else %}
    <p>هنوز فایلی آپلود نشده است.</p>
    {% endif %}
</body>
</html>

templates/upload.html (فرم آپلود تک فایل)

<!doctype html>
<html lang="fa" dir="rtl">
<head>
    <meta charset="utf-8">
    <title>آپلود یک فایل</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; text-align: right; background-color: #f4f7f6; color: #333; }
        h1 { color: #2c3e50; }
        form { background-color: #ffffff; padding: 25px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-width: 500px; margin-right: auto; margin-left: auto; }
        input[type="file"] { margin-bottom: 20px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; display: block; width: 100%; box-sizing: border-box; }
        input[type="submit"] { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s ease; }
        input[type="submit"]:hover { background-color: #0056b3; }
        .flash { padding: 10px; margin-bottom: 15px; border-radius: 5px; }
        .flash.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .flash.danger { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        .flash.warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
        .flash.info { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
        .back-link { display: inline-block; margin-top: 20px; color: #007bff; text-decoration: none; padding: 8px 15px; border: 1px solid #007bff; border-radius: 4px; transition: background-color 0.3s ease, color 0.3s ease; }
        .back-link:hover { background-color: #007bff; color: white; }
    </style>
</head>
<body>
    <h1>آپلود یک فایل به سرور</h1>
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            <div class="flashes">
            {% for category, message in messages %}
                <div class="flash {{ category }}">{{ message }}</div>
            {% endfor %}
            </div>
        {% endif %}
    {% endwith %}
    <form method="POST" enctype="multipart/form-data">
        <input type="file" name="file" required>
        <input type="submit" value="آپلود">
    </form>
    <a href="/" class="back-link">بازگشت به صفحه اصلی</a>
</body>
</html>

templates/upload_success.html (صفحه موفقیت‌آمیز بودن آپلود تک فایل)

<!doctype html>
<html lang="fa" dir="rtl">
<head>
    <meta charset="utf-8">
    <title>آپلود موفقیت‌آمیز</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; text-align: right; background-color: #f4f7f6; color: #333; }
        h1 { color: #28a745; }
        p { font-size: 1.1em; }
        a { text-decoration: none; color: #007bff; }
        a:hover { text-decoration: underline; }
        .success-message { background-color: #e6ffe6; border: 1px solid #c3e6cb; padding: 15px; border-radius: 5px; margin-bottom: 20px; color: #155724; }
        .button { display: inline-block; margin-top: 15px; padding: 10px 20px; background-color: #007bff; color: white; border-radius: 5px; text-decoration: none; margin-left: 10px; }
        .button:hover { background-color: #0056b3; }
    </style>
</head>
<body>
    <h1>آپلود فایل با موفقیت انجام شد!</h1>
    <div class="success-message">
        <p>فایل <strong>{{ filename }}</strong> با موفقیت آپلود و ذخیره شد.</p>
        <p>می‌توانید فایل آپلود شده را <a href="{{ url_for('uploaded_file', filename=stored_filename) }}" target="_blank">اینجا مشاهده کنید</a>.</p>
    </div>
    <a href="/upload" class="button">آپلود فایل دیگر</a>
    <a href="/" class="button">بازگشت به صفحه اصلی</a>
</body>
</html>

templates/multi_upload.html (فرم آپلود چند فایل)

<!doctype html>
<html lang="fa" dir="rtl">
<head>
    <meta charset="utf-8">
    <title>آپلود چند فایل</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; text-align: right; background-color: #f4f7f6; color: #333; }
        h1 { color: #2c3e50; }
        form { background-color: #ffffff; padding: 25px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-width: 500px; margin-right: auto; margin-left: auto; }
        input[type="file"] { margin-bottom: 20px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; display: block; width: 100%; box-sizing: border-box; }
        input[type="submit"] { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s ease; }
        input[type="submit"]:hover { background-color: #0056b3; }
        .flash { padding: 10px; margin-bottom: 15px; border-radius: 5px; }
        .flash.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .flash.danger { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        .flash.warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
        .flash.info { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
        .back-link { display: inline-block; margin-top: 20px; color: #007bff; text-decoration: none; padding: 8px 15px; border: 1px solid #007bff; border-radius: 4px; transition: background-color 0.3s ease, color 0.3s ease; }
        .back-link:hover { background-color: #007bff; color: white; }
    </style>
</head>
<body>
    <h1>آپلود چندین فایل به سرور</h1>
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            <div class="flashes">
            {% for category, message in messages %}
                <div class="flash {{ category }}">{{ message }}</div>
            {% endfor %}
            </div>
        {% endif %}
    {% endwith %}
    <form method="POST" enctype="multipart/form-data">
        <input type="file" name="files[]" multiple required>
        <input type="submit" value="آپلود همه">
    </form>
    <a href="/" class="back-link">بازگشت به صفحه اصلی</a>
</body>
</html>

templates/multi_upload_success.html (صفحه موفقیت‌آمیز بودن آپلود چند فایل)

<!doctype html>
<html lang="fa" dir="rtl">
<head>
    <meta charset="utf-8">
    <title>آپلود چند فایل موفقیت‌آمیز</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; text-align: right; background-color: #f4f7f6; color: #333; }
        h1 { color: #28a745; }
        p { font-size: 1.1em; }
        ul { list-style-type: disc; margin-right: 20px; padding: 0; }
        li { margin-bottom: 5px; }
        a { text-decoration: none; color: #007bff; }
        a:hover { text-decoration: underline; }
        .success-message { background-color: #e6ffe6; border: 1px solid #c3e6cb; padding: 15px; border-radius: 5px; margin-bottom: 20px; color: #155724; }
        .button { display: inline-block; margin-top: 15px; padding: 10px 20px; background-color: #007bff; color: white; border-radius: 5px; text-decoration: none; margin-left: 10px; }
        .button:hover { background-color: #0056b3; }
    </style>
</head>
<body>
    <h1>آپلود فایل‌ها با موفقیت انجام شد!</h1>
    <div class="success-message">
        <p>فایل‌های زیر با موفقیت آپلود شدند:</p>
        <ul>
            {% if files %}
            {% for file in files %}
            <li><a href="{{ url_for('uploaded_file', filename=file.stored_filename) }}" target="_blank">{{ file.original_filename }}</a></li>
            {% endfor %}
            {% else %}
            <li>هیچ فایلی برای نمایش وجود ندارد.</li>
            {% endif %}
        </ul>
    </div>
    <a href="/multi-upload" class="button">آپلود فایل‌های دیگر</a>
    <a href="/" class="button">بازگشت به صفحه اصلی</a>
</body>
</html>

templates/ajax_upload.html (فرم آپلود با نوار پیشرفت AJAX)

<!doctype html>
<html lang="fa" dir="rtl">
<head>
    <meta charset="utf-8">
    <title>آپلود فایل با نوار پیشرفت (AJAX)</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; text-align: right; background-color: #f4f7f6; color: #333; }
        h1 { color: #2c3e50; }
        form { background-color: #ffffff; padding: 25px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-width: 500px; margin-right: auto; margin-left: auto; }
        input[type="file"] { margin-bottom: 20px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; display: block; width: 100%; box-sizing: border-box; }
        input[type="submit"] { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s ease; }
        input[type="submit"]:hover { background-color: #0056b3; }
        .progress-container { width: 100%; background-color: #e0e0e0; border-radius: 5px; margin-top: 20px; display: none; overflow: hidden; }
        .progress-bar { width: 0%; height: 25px; background-color: #4CAF50; text-align: center; line-height: 25px; color: white; border-radius: 5px; transition: width 0.3s ease; }
        #status { margin-top: 10px; font-weight: bold; }
        .error { color: red; }
        .success { color: green; }
        .back-link { display: inline-block; margin-top: 20px; color: #007bff; text-decoration: none; padding: 8px 15px; border: 1px solid #007bff; border-radius: 4px; transition: background-color 0.3s ease, color 0.3s ease; }
        .back-link:hover { background-color: #007bff; color: white; }
    </style>
</head>
<body>
    <h1>آپلود فایل با نوار پیشرفت (AJAX)</h1>
    <form id="uploadForm">
        <input type="file" name="file" id="fileInput" required>
        <input type="submit" value="آپلود" id="uploadButton">
    </form>
    
    <div class="progress-container" id="progressContainer">
        <div class="progress-bar" id="progressBar">0%</div>
    </div>
    <div id="status"></div>
    <a href="/" class="back-link">بازگشت به صفحه اصلی</a>

    <script>
        document.getElementById('uploadForm').addEventListener('submit', function(e) {
            e.preventDefault(); // جلوگیری از ارسال فرم به صورت سنتی

            var fileInput = document.getElementById('fileInput');
            var file = fileInput.files[0]; // تنها یک فایل را انتخاب می کنیم

            if (!file) {
                document.getElementById('status').className = 'error';
                document.getElementById('status').textContent = 'لطفاً فایلی را انتخاب کنید.';
                return;
            }

            var formData = new FormData();
            formData.append('file', file); // 'file' باید با نام فیلد در سمت سرور یکسان باشد

            var xhr = new XMLHttpRequest();
            
            // نمایش نوار پیشرفت
            var progressBar = document.getElementById('progressBar');
            var progressContainer = document.getElementById('progressContainer');
            var statusDiv = document.getElementById('status');
            var uploadButton = document.getElementById('uploadButton');
            
            progressBar.style.width = '0%';
            progressBar.textContent = '0%';
            statusDiv.textContent = 'در حال آپلود...';
            statusDiv.className = '';
            progressContainer.style.display = 'block'; // نمایش نوار پیشرفت
            uploadButton.disabled = true; // غیرفعال کردن دکمه آپلود

            xhr.open('POST', '/ajax-upload', true); // مسیر API سمت سرور Flask
            
            // ردیابی پیشرفت آپلود
            xhr.upload.onprogress = function(event) {
                if (event.lengthComputable) {
                    var percentComplete = (event.loaded / event.total) * 100;
                    progressBar.style.width = percentComplete.toFixed(0) + '%';
                    progressBar.textContent = percentComplete.toFixed(0) + '%';
                }
            };

            // پس از اتمام آپلود یا در صورت خطا
            xhr.onload = function() {
                uploadButton.disabled = false; // فعال کردن دکمه آپلود
                if (xhr.status === 200) {
                    var response = JSON.parse(xhr.responseText);
                    statusDiv.className = 'success';
                    statusDiv.textContent = 'فایل با موفقیت آپلود شد: ' + response.filename;
                    progressBar.style.width = '100%';
                    progressBar.textContent = '100%';
                    fileInput.value = ''; // پاک کردن ورودی فایل
                    setTimeout(function() {
                        progressContainer.style.display = 'none'; // مخفی کردن نوار پیشرفت پس از چند ثانیه
                        statusDiv.textContent = ''; // پاک کردن پیام وضعیت
                    }, 3000);
                } else {
                    statusDiv.className = 'error';
                    statusDiv.textContent = 'خطا در آپلود فایل: ' + xhr.responseText;
                    progressBar.style.width = '0%'; // ریست کردن نوار پیشرفت
                    progressContainer.style.display = 'none'; // مخفی کردن نوار پیشرفت
                }
            };

            xhr.onerror = function() {
                uploadButton.disabled = false; // فعال کردن دکمه آپلود
                statusDiv.className = 'error';
                statusDiv.textContent = 'خطا در شبکه یا سرور رخ داد.';
                progressContainer.style.display = 'none';
            };

            xhr.send(formData);
        });
    </script>
</body>
</html>

نحوه اجرا:

  1. اطمینان حاصل کنید که Flask و Flask-SQLAlchemy نصب شده‌اند: pip install Flask Flask-SQLAlchemy
  2. محیط مجازی را فعال کنید.
  3. فایل app.py و تمامی فایل‌های .html را در ساختار پوشه مشخص شده (flask_upload_app/ و flask_upload_app/templates/) قرار دهید.
  4. python app.py را در ترمینال اجرا کنید.
  5. به آدرس http://127.0.0.1:5000/ در مرورگر خود مراجعه کنید.

با این ساختار کامل، شما یک سیستم قوی و چندمنظوره برای آپلود فایل در Flask دارید که شامل اعتبارسنجی‌ها، مدیریت داده‌ها، و بهبود تجربه کاربری است.

اعتبارسنجی و امنیت در آپلود فایل

یکی از حیاتی‌ترین جنبه‌های هر سیستم آپلود فایل، اعتبارسنجی دقیق و رعایت اصول امنیتی است. نادیده گرفتن این موارد می‌تواند منجر به آسیب‌پذیری‌های جدی مانند حملات DoS (Denial of Service)، اجرای کد مخرب (Malware Execution)، افشای اطلاعات (Information Disclosure) و حملات XSS (Cross-Site Scripting) شود. Flask ابزارها و رویکردهایی را برای کمک به شما در ایمن‌سازی فرآیند آپلود فایل ارائه می‌دهد.

اعتبارسنجی نوع فایل (MIME Type در برابر پسوند)

همانطور که در کد اولیه دیدیم، ما از پسوند فایل برای اعتبارسنجی نوع فایل استفاده کردیم. این یک گام ضروری است، اما کافی نیست. مهاجمان می‌توانند به راحتی پسوند فایل را تغییر دهند تا سیستم‌های اعتبارسنجی ساده را دور بزنند. برای مثال، یک فایل اجرایی با نام malicious_script.php.jpg ممکن است توسط سیستم شما به عنوان تصویر تلقی شود اما در واقع یک کد مخرب باشد.

تفاوت کلیدی:

  • پسوند فایل: بخشی از نام فایل (مثلاً .jpg) که نشان‌دهنده نوع آن است و به راحتی قابل تغییر است.
  • MIME Type (Content-Type): یک شناسه استاندارد که نوع واقعی محتوای فایل را مشخص می‌کند (مثلاً image/jpeg). این اطلاعات توسط مرورگر هنگام آپلود ارسال می‌شود و در شیء FileStorage در file.mimetype قابل دسترسی است. اگرچه MIME Type نیز می‌تواند توسط کاربر دستکاری شود، اما ترکیب آن با اعتبارسنجی پسوند، لایه امنیتی قوی‌تری را ایجاد می‌کند.

پیاده‌سازی اعتبارسنجی MIME Type:

شما می‌توانید یک لیست از MIME Typeهای مجاز نیز تعریف کنید و هنگام آپلود، هر دو را بررسی کنید. در نمونه کد کامل app.py، تابع allowed_file هر دو را بررسی می‌کند:

# app.py (بخشی از کد)

# ...
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
ALLOWED_MIMETYPES = {'text/plain', 'application/pdf', 'image/png', 'image/jpeg', 'image/gif'}

# ...

def allowed_file(filename, mimetype):
    # ابتدا پسوند را بررسی می کنیم
    if not ('.' in filename and \
            filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS):
        return False
    # سپس MIME Type را بررسی می کنیم
    if mimetype not in ALLOWED_MIMETYPES:
        return False
    return True

برای امنیت بسیار بالا، به خصوص در مواردی که فایل‌های اجرایی ممکن است آپلود شوند (که به شدت توصیه می‌شود از آن اجتناب کنید)، می‌توانید محتویات واقعی فایل را با “Magic Numbers” بررسی کنید. این روش پیچیده‌تر است و نیازمند خواندن چند بایت اول فایل برای شناسایی فرمت واقعی آن است (مثلاً JPEG با بایت‌های FF D8 FF E0 شروع می‌شود). کتابخانه‌هایی مانند python-magic می‌توانند در این زمینه کمک کنند.

محدودیت حجم فایل

همانطور که در app.py دیدیم، با تنظیم app.config['MAX_CONTENT_LENGTH'] می‌توانید حداکثر حجم مجاز برای درخواست‌های ورودی (که شامل فایل‌ها می‌شود) را تعیین کنید. این یک محافظت مهم در برابر حملات DoS است که مهاجمان با آپلود فایل‌های بسیار بزرگ می‌توانند منابع سرور شما را مصرف کنند. اگر کاربر فایلی بزرگ‌تر از این مقدار را آپلود کند، Flask به طور خودکار یک خطای 413 Request Entity Too Large ایجاد می‌کند.

برای مدیریت این خطا به شکل کاربرپسندتر، در نمونه کد کامل از یک هندل‌کننده خطا برای RequestEntityTooLarge استفاده شده است:

# app.py (بخشی از کد)

from werkzeug.exceptions import RequestEntityTooLarge

# ...

@app.errorhandler(RequestEntityTooLarge)
def handle_too_large_file(e):
    flash("فایل انتخابی بیش از حد بزرگ است. حداکثر حجم مجاز 16 مگابایت است.", "danger")
    return redirect(request.url) # به صفحه ای که از آن آمده ایم برمی گردیم

این کنترل، قبل از شروع پردازش فایل در سمت پایتون اتفاق می‌افتد و از مصرف بی‌مورد منابع جلوگیری می‌کند.

جلوگیری از حملات Directory Traversal

همانطور که قبلاً اشاره شد، تابع secure_filename از werkzeug.utils برای پاکسازی نام فایل از کاراکترهای مخرب و مسیرهای نسبی استفاده می‌کند. این تابع، نام فایل را به یک نسخه امن تبدیل می‌کند که فقط شامل حروف، اعداد، خط تیره، زیرخط و نقطه است. این یک گام حیاتی برای جلوگیری از اینکه مهاجمان بتوانند فایل‌ها را در مسیرهای غیرمجاز روی سرور شما ذخیره کنند (مثلاً خارج از پوشه آپلود یا حتی در پوشه‌های سیستم عامل).

همیشه، بدون استثنا، از secure_filename برای نام‌گذاری فایل‌های آپلود شده استفاده کنید.

ذخیره‌سازی امن فایل‌ها

حتی پس از اعتبارسنجی و امن‌سازی نام فایل، چند ملاحظه دیگر برای ذخیره‌سازی امن فایل‌ها وجود دارد:

  • تغییر نام فایل به یک UUID: حتی اگر نام فایل امن‌سازی شود، ممکن است مهاجمان یک فایل با نامی مشابه یک فایل سیستمی یا یک فایل مهم دیگر آپلود کنند تا آن را بازنویسی (overwrite) کنند. بهتر است پس از امن‌سازی نام، یک پیشوند یا پسوند منحصر به فرد (مانند یک UUID) به نام فایل اضافه کنید تا از تداخل نام‌ها و همچنین حدس زدن نام فایل‌ها جلوگیری شود. در نمونه کد کامل app.py از این روش استفاده شده است:
    import uuid
    # ...
    original_filename = secure_filename(file.filename)
    file_extension = original_filename.rsplit('.', 1)[1].lower()
    unique_filename = str(uuid.uuid4()) + '.' + file_extension
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
    file.save(file_path)
    
  • ذخیره‌سازی خارج از ریشه وب (Web Root): اگر فایل‌های آپلود شده نیاز به سرویس‌دهی مستقیم توسط وب سرور (مانند Nginx یا Apache) ندارند، بهتر است آن‌ها را در پوشه‌ای خارج از ریشه وب (یا حداقل خارج از پوشه static) ذخیره کنید. این کار از اجرای تصادفی فایل‌های مخرب توسط وب سرور جلوگیری می‌کند. در مثال ما، پوشه uploads در کنار app.py قرار دارد که امن‌تر از قرار گرفتن در پوشه static است. با استفاده از send_from_directory، Flask کنترل کاملی بر سرویس‌دهی فایل‌ها دارد و این تابع به طور خودکار از دسترسی به فایل‌های خارج از پوشه مشخص شده جلوگیری می‌کند.
  • پیکربندی وب سرور: اگر از یک وب سرور مانند Nginx یا Apache در جلوی Flask استفاده می‌کنید، مطمئن شوید که فایل‌های آپلود شده به عنوان فایل‌های استاتیک ساده سرویس‌دهی می‌شوند و نه به عنوان اسکریپت‌های قابل اجرا (مثلاً PHP، CGI). این امر به خصوص برای فایل‌هایی با پسوندهای مشکوک مانند .php، .phtml، .pl و … مهم است.
  • اسکن آنتی‌ویروس: برای بالاترین سطح امنیت، به خصوص در محیط‌های سازمانی، می‌توانید فایل‌های آپلود شده را پس از ذخیره‌سازی اولیه توسط یک سرویس اسکن آنتی‌ویروس (مانلاً ClamAV) بررسی کنید. این فرآیند معمولاً به صورت ناهمزمان (asynchronously) در پس‌زمینه انجام می‌شود.
  • مجوزهای فایل (File Permissions): اطمینان حاصل کنید که پوشه آپلود و فایل‌های داخل آن دارای مجوزهای دسترسی صحیح هستند. معمولاً، فایل‌ها باید قابلیت خواندن داشته باشند اما قابلیت اجرا یا نوشتن توسط همه کاربران را نداشته باشند. پوشه آپلود باید برای کاربر وب سرور قابل نوشتن باشد، اما نباید به سایر کاربران اجازه نوشتن یا اجرای فایل داده شود.

با پیاده‌سازی این اقدامات اعتبارسنجی و امنیتی، می‌توانید یک سیستم آپلود فایل بسیار امن‌تر و مقاوم‌تر در برابر حملات در Flask ایجاد کنید.

مدیریت فایل‌های آپلود شده

پس از اینکه کاربران فایل‌ها را با موفقیت آپلود کردند، نیاز به راهی برای مدیریت آن‌ها دارید. این مدیریت شامل نمایش، حذف، سازماندهی و حتی دستکاری فایل‌ها می‌شود. Flask ابزارهایی برای انجام این کارها فراهم می‌کند.

نمایش فایل‌های آپلود شده

در نمونه کد app.py، ما از تابع send_from_directory برای سرویس‌دهی فایل‌های آپلود شده استفاده کردیم. این تابع یک روش امن و راحت برای نمایش فایل‌های استاتیک از یک پوشه مشخص است. این تابع به طور خودکار از Directory Traversal جلوگیری می‌کند و تنها فایل‌های داخل پوشه پیکربندی شده را سرویس می‌دهد.

@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

استفاده از پایگاه داده برای مدیریت متادیتا:

برای برنامه‌های پیچیده‌تر، صرفاً ذخیره فایل‌ها در یک پوشه و نمایش آن‌ها از طریق نام، کافی نیست. شما اغلب نیاز دارید که متادیتای مربوط به هر فایل (مانند نام اصلی فایل، نام ذخیره شده، نوع MIME، حجم، تاریخ آپلود، کاربری که آن را آپلود کرده، و حتی توضیحات یا برچسب‌ها) را در یک پایگاه داده ذخیره کنید. در نمونه کد کامل app.py، ما از Flask-SQLAlchemy برای ایجاد مدل UploadedFile استفاده کرده‌ایم که این متادیتا را ذخیره می‌کند.

این کار به شما امکان می‌دهد:

  • فایل‌ها را بر اساس معیارهای مختلف جستجو و فیلتر کنید.
  • تاریخچه فایل‌ها را ردیابی کنید.
  • دسترسی به فایل‌ها را بر اساس نقش‌های کاربری کنترل کنید.
  • فایل‌ها را بدون نیاز به دانستن نام فیزیکی آن‌ها، مدیریت کنید.

در صفحه index.html نیز، ما فایل‌ها را با استفاده از اطلاعات ذخیره شده در مدل UploadedFile نمایش می‌دهیم.

حذف فایل‌ها

قابلیت حذف فایل‌ها نیز به همان اندازه آپلود آن‌ها مهم است. برای حذف فیزیکی یک فایل از سرور، می‌توانید از تابع os.remove() در پایتون استفاده کنید. همچنین، اگر از پایگاه داده برای متادیتا استفاده می‌کنید، باید رکورد مربوطه را نیز حذف کنید.

# app.py (بخشی از کد)

@app.route('/delete/<int:file_id>', methods=['POST'])
def delete_file(file_id):
    file_record = UploadedFile.query.get_or_404(file_id) # یافتن رکورد فایل از دیتابیس
    
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], file_record.stored_filename)
    
    if os.path.exists(file_path):
        os.remove(file_path) # حذف فایل فیزیکی
        db.session.delete(file_record) # حذف رکورد از دیتابیس
        db.session.commit()
        flash(f"فایل '{file_record.original_filename}' با موفقیت حذف شد.", "success")
    else:
        # اگر فایل فیزیکی موجود نباشد، فقط رکورد دیتابیس را حذف می کنیم
        db.session.delete(file_record)
        db.session.commit()
        flash(f"فایل فیزیکی '{file_record.original_filename}' یافت نشد، اما رکورد آن حذف گردید.", "warning")
    
    return redirect(url_for('index'))

نکات مهم امنیتی برای حذف فایل:

  • احراز هویت و مجوزها (Authentication & Authorization): هرگز اجازه ندهید هر کسی فایل‌ها را حذف کند. اطمینان حاصل کنید که فقط کاربران احراز هویت شده و دارای مجوزهای کافی (مثلاً مالک فایل یا مدیر سیستم) بتوانند فایل‌ها را حذف کنند.
  • اعتبارسنجی مسیر فایل: قبل از فراخوانی os.remove()، همیشه مطمئن شوید که file_path به یک فایل در پوشه آپلود شما اشاره دارد و نه به یک فایل سیستمی حساس. os.path.join و secure_filename در جلوگیری از Directory Traversal در این زمینه نیز کمک می‌کنند.
  • استفاده از POST: همیشه از متد POST برای عملیات حذف استفاده کنید و از GET خودداری کنید تا از حملات CSRF و حذف تصادفی جلوگیری شود.

سازماندهی فایل‌ها

با افزایش تعداد فایل‌های آپلود شده، مدیریت آن‌ها در یک پوشه واحد دشوار می‌شود. راه‌حل این است که فایل‌ها را در زیرپوشه‌ها سازماندهی کنید. روش‌های متداول شامل:

  • بر اساس کاربر: ایجاد یک پوشه برای هر کاربر (مثلاً uploads/user_id/).
    # ...
    # فرض کنید user_id از سشن کاربر احراز هویت شده گرفته می شود
    user_id = 123 
    user_upload_folder = os.path.join(app.config['UPLOAD_FOLDER'], str(user_id))
    if not os.path.exists(user_upload_folder):
        os.makedirs(user_upload_folder)
    # file.save(os.path.join(user_upload_folder, unique_filename))
    
  • بر اساس تاریخ: سازماندهی بر اساس سال، ماه یا روز (مثلاً uploads/2023/10/ یا uploads/2023/10/26/).
    # ...
    from datetime import datetime
    today = datetime.now()
    date_folder = os.path.join(app.config['UPLOAD_FOLDER'], today.strftime('%Y'), today.strftime('%m'))
    if not os.path.exists(date_folder):
        os.makedirs(date_folder)
    # file.save(os.path.join(date_folder, unique_filename))
    
  • بر اساس نوع فایل: ایجاد پوشه‌های جداگانه برای تصاویر، اسناد، ویدئوها و غیره (مثلاً uploads/images/، uploads/documents/).

ترکیبی از این روش‌ها نیز می‌تواند استفاده شود، مثلاً uploads/user_id/images/2023/. این سازماندهی به شما کمک می‌کند تا فایل‌ها را راحت‌تر پیدا کنید و مدیریت کنید، به خصوص زمانی که پایگاه داده‌ای برای نگهداری متادیتا استفاده می‌کنید و مسیر ذخیره شده را در دیتابیس ذخیره می‌کنید.

مدیریت صحیح فایل‌های آپلود شده، بخش مهمی از نگهداری یک وب‌اپلیکیشن پایدار و امن است. با رویکردهای توضیح داده شده، می‌توانید اطمینان حاصل کنید که فایل‌های کاربران به صورت کارآمد و ایمن اداره می‌شوند.

آپلود چندین فایل به صورت همزمان

در بسیاری از سناریوها، کاربران نیاز دارند که بیش از یک فایل را به صورت همزمان آپلود کنند، مانند آپلود چندین عکس برای یک گالری یا چندین سند برای یک پروژه. Flask و HTML این قابلیت را به سادگی فراهم می‌کنند.

تغییر فرم HTML برای پشتیبانی از چند فایل

برای اینکه یک ورودی فایل بتواند چندین فایل را انتخاب کند، کافی است ویژگی multiple را به تگ <input type="file"> اضافه کنید. همچنین، نام ورودی را به یک آرایه (با []) تغییر می‌دهیم که به سرور نشان می‌دهد چندین مقدار برای این فیلد انتظار می‌رود.

<!-- templates/multi_upload.html (بخشی از کد) -->
<form method="POST" enctype="multipart/form-data">
    <input type="file" name="files[]" multiple required> <!-- توجه به 'files[]' و 'multiple' -->
    <input type="submit" value="آپلود همه">
</form>

تغییرات مهم:

  • name="files[]": نام ورودی را به یک آرایه (با []) تغییر می‌دهیم. Flask به طور خودکار این را به یک لیست از اشیاء FileStorage تبدیل می‌کند.
  • multiple: این ویژگی به مرورگر اجازه می‌دهد تا چندین فایل را انتخاب کند.

پردازش چند فایل در سمت سرور Flask

در Flask، برای دسترسی به چندین فایل که از یک ورودی <input type="file" name="files[]" multiple> ارسال شده‌اند، می‌توانید از request.files.getlist('files[]') استفاده کنید. این متد یک لیست از اشیاء FileStorage را برمی‌گرداند که می‌توانید روی آن‌ها حلقه بزید و هر فایل را به صورت جداگانه پردازش کنید.

# app.py (بخشی از کد)

@app.route('/multi-upload', methods=['GET', 'POST'])
def multi_upload_file():
    if request.method == 'POST':
        if 'files[]' not in request.files:
            flash("درخواست شما شامل بخش فایل نبود.", "danger")
            return redirect(request.url)
        
        files = request.files.getlist('files[]') # گرفتن لیست فایل ها
        uploaded_filenames = []
        
        for file in files:
            if file.filename == '':
                continue # از فایل های خالی رد می شویم
            
            if file and allowed_file(file.filename, file.mimetype): # اعتبارسنجی هر فایل
                original_filename = secure_filename(file.filename)
                file_extension = original_filename.rsplit('.', 1)[1].lower()
                unique_filename = str(uuid.uuid4()) + '.' + file_extension
                
                file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
                
                try:
                    file.save(file_path)
                    
                    # ذخیره متادیتا در پایگاه داده
                    new_file_record = UploadedFile(
                        original_filename=original_filename,
                        stored_filename=unique_filename,
                        mimetype=file.mimetype,
                        filesize=os.path.getsize(file_path),
                        upload_date=datetime.utcnow()
                    )
                    db.session.add(new_file_record)
                    db.session.commit()
                    uploaded_filenames.append(unique_filename)
                except RequestEntityTooLarge:
                    flash(f"فایل '{original_filename}' بیش از حد بزرگ است و آپلود نشد.", "warning")
                    continue
                except Exception as e:
                    print(f"Error saving file {original_filename}: {e}")
                    flash(f"خطا در ذخیره فایل '{original_filename}' رخ داد.", "danger")
                    continue
            else:
                flash(f"فایل '{file.filename}' به دلیل پسوند یا نوع نامعتبر، آپلود نشد.", "warning")
                continue
        
        if uploaded_filenames:
            flash(f"{len(uploaded_filenames)} فایل با موفقیت آپلود شدند!", "success")
            return redirect(url_for('multi_upload_success', filenames=",".join(uploaded_filenames)))
        else:
            flash("هیچ فایل معتبری برای آپلود پیدا نشد.", "info")
            return redirect(request.url)
            
    return render_template('multi_upload.html')

@app.route('/multi-upload-success/<filenames>')
def multi_upload_success(filenames):
    file_list = filenames.split(',')
    file_records = UploadedFile.query.filter(UploadedFile.stored_filename.in_(file_list)).all()
    return render_template('multi_upload_success.html', files=file_records)

توضیحات:

  • request.files.getlist('files[]'): تمام اشیاء FileStorage مرتبط با نام files[] را به صورت یک لیست برمی‌گرداند.
  • حلقه بر روی فایل‌ها: کد بر روی هر فایل در لیست حلقه می‌زند.
  • اعتبارسنجی فردی: هر فایل به صورت جداگانه اعتبارسنجی می‌شود (بررسی نام خالی، پسوند و MIME Type). اگر هر فایلی نامعتبر باشد، می‌توان آن را رد کرد یا کل عملیات را متوقف کرد.
  • ذخیره‌سازی با نام منحصر به فرد: هر فایل با استفاده از secure_filename و یک UUID منحصر به فرد نامگذاری و ذخیره می‌شود تا از تداخل و مسائل امنیتی جلوگیری شود.
  • مدیریت خطا: خطاها به صورت مناسب مدیریت می‌شوند و پیام‌های فلش به کاربر نمایش داده می‌شوند.

با این تغییرات، برنامه شما اکنون قادر به پذیرش و پردازش چندین فایل به صورت همزمان خواهد بود. این قابلیت تجربه کاربری را به طور قابل توجهی بهبود می‌بخشد، به خصوص در اپلیکیشن‌هایی که محتوای رسانه‌ای یا اسناد زیادی را مدیریت می‌کنند.

نمایش پیشرفت آپلود با AJAX

هنگام آپلود فایل‌های بزرگ، ممکن است فرآیند آپلود زمان‌بر باشد. در این حالت، نمایش یک نوار پیشرفت (progress bar) به کاربر می‌تواند تجربه کاربری را به شدت بهبود بخشد، زیرا کاربر متوجه می‌شود که فرآیند در حال انجام است و نیازی به رفرش صفحه نیست. این قابلیت معمولاً با استفاده از AJAX (Asynchronous JavaScript and XML) یا Fetch API در سمت کلاینت (مرورگر) پیاده‌سازی می‌شود.

چرا AJAX؟

روش استاندارد ارسال فرم‌ها، صفحه را پس از ارسال رفرش می‌کند. برای نمایش پیشرفت آپلود در لحظه، نیاز داریم که آپلود به صورت ناهمزمان (asynchronous) در پس‌زمینه انجام شود، بدون اینکه صفحه کامل بارگذاری مجدد شود.

مقدمه‌ای بر AJAX و Fetch API برای آپلود

برای آپلود فایل با AJAX، از شیء FormData در جاوااسکریپت استفاده می‌کنیم. این شیء به ما اجازه می‌دهد تا یک شیء شبیه فرم HTML را در جاوااسکریپت بسازیم و فایل‌ها و سایر داده‌ها را به آن اضافه کنیم. سپس می‌توانیم این FormData را با XMLHttpRequest (که AJAX بر آن بنا شده) یا Fetch API به سرور ارسال کنیم.

XMLHttpRequest دارای یک شیء upload است که رویداد onprogress را برای ردیابی پیشرفت آپلود فراهم می‌کند. Fetch API نیز از Streams استفاده می‌کند، اما پیاده‌سازی پیشرفت آپلود با آن کمی پیچیده‌تر است و XMLHttpRequest همچنان برای این منظور متداول‌تر است.

پیاده‌سازی سمت کلاینت با JavaScript

ما یک فرم HTML ساده خواهیم داشت و یک اسکریپت جاوااسکریپت برای مدیریت آپلود ناهمزمان آن اضافه می‌کنیم. فایل templates/ajax_upload.html شامل کدهای مربوطه است که قبلاً در بخش نمونه کد کامل HTML آورده شد.

پیاده‌سازی سمت سرور برای آپلود AJAX

از آنجایی که AJAX نیز از متد POST با multipart/form-data استفاده می‌کند، کد سمت سرور Flask شما تقریباً مشابه کد قبلی برای آپلود یک فایل خواهد بود. فقط باید به جای ریدایرکت، یک پاسخ JSON برگردانید. مسیر /ajax-upload در نمونه کد کامل app.py این کار را انجام می‌دهد:

# app.py (بخشی از کد)

@app.route('/ajax-upload', methods=['POST'])
def ajax_upload_file():
    if 'file' not in request.files:
        return jsonify({'error': 'No file part in the request'}), 400
    
    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': 'No selected file'}), 400
    
    if file and allowed_file(file.filename, file.mimetype):
        original_filename = secure_filename(file.filename)
        file_extension = original_filename.rsplit('.', 1)[1].lower()
        unique_filename = str(uuid.uuid4()) + '.' + file_extension
        
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
        
        try:
            file.save(file_path)
            
            new_file_record = UploadedFile(
                original_filename=original_filename,
                stored_filename=unique_filename,
                mimetype=file.mimetype,
                filesize=os.path.getsize(file_path),
                upload_date=datetime.utcnow()
            )
            db.session.add(new_file_record)
            db.session.commit()

            return jsonify({'message': 'File uploaded successfully', 'filename': unique_filename}), 200
        except RequestEntityTooLarge:
            return jsonify({'error': 'File is too large. Max size is 16MB.'}), 413
        except Exception as e:
            print(f"Error saving file: {e}")
            return jsonify({'error': 'Server error saving file'}), 500
    else:
        return jsonify({'error': 'Invalid file type or mimetype'}), 400

توضیحات:

  • سمت کلاینت (ajax_upload.html):
    • فرم دارای id="uploadForm" است و از e.preventDefault() برای جلوگیری از ارسال سنتی فرم استفاده می‌کند.
    • یک شیء FormData ساخته شده و فایل انتخابی به آن اضافه می‌شود.
    • یک XMLHttpRequest جدید ایجاد شده و به مسیر /ajax-upload با متد POST ارسال می‌شود.
    • xhr.upload.onprogress برای به‌روزرسانی نوار پیشرفت و نمایش درصد تکمیل استفاده می‌شود.
    • xhr.onload و xhr.onerror برای مدیریت پاسخ‌های سرور (موفقیت یا خطا) استفاده می‌شوند.
  • سمت سرور (app.py):
    • مسیر /ajax-upload فقط متد POST را قبول می‌کند.
    • به جای ریدایرکت، پاسخ‌ها به صورت JSON (با استفاده از jsonify) برگردانده می‌شوند. این پاسخ‌ها توسط جاوااسکریپت سمت کلاینت خوانده و پردازش می‌شوند.
    • کدهای وضعیت HTTP (مثلاً 200 برای موفقیت، 400 برای درخواست بد، 413 برای فایل بزرگ) به درستی تنظیم شده‌اند تا کلاینت بتواند وضعیت را به درستی تشخیص دهد.

با پیاده‌سازی این رویکرد، کاربران شما تجربه کاربری بسیار بهتری خواهند داشت، به خصوص هنگام آپلود فایل‌های بزرگ‌تر، زیرا می‌توانند پیشرفت عملیات را در زمان واقعی مشاهده کنند و منتظر بارگذاری کامل صفحه نمانند.

بهترین روش‌ها و ملاحظات پیشرفته

پس از تسلط بر مفاهیم پایه و امنیتی، زمان آن رسیده که به بهترین روش‌ها و ملاحظات پیشرفته‌تر بپردازیم تا سیستم آپلود فایل شما نه تنها کارآمد، بلکه مقیاس‌پذیر، قابل نگهداری و حرفه‌ای باشد.

استفاده از ORM برای مدیریت Metadata (مانند SQLAlchemy)

همانطور که قبلاً اشاره شد، ذخیره متادیتا (اطلاعات مربوط به فایل) در پایگاه داده، یک بهترین روش است. با استفاده از یک ORM قدرتمند مانند SQLAlchemy (که Flask-SQLAlchemy آن را ساده‌تر می‌کند)، می‌توانید یک مدل داده برای فایل‌های آپلود شده ایجاد کنید. در نمونه کد کامل app.py از Flask-SQLAlchemy برای ایجاد مدل UploadedFile استفاده شده است.

مزایای استفاده از ORM برای متادیتا:

  • جستجو و فیلتر: به راحتی می‌توانید فایل‌ها را بر اساس نام اصلی، نوع، حجم، تاریخ آپلود یا کاربری که آپلود کرده، جستجو و فیلتر کنید.
  • کنترل دسترسی: پیاده‌سازی مکانیزم‌های مجوز و احراز هویت برای هر فایل، بسیار ساده‌تر می‌شود.
  • یکپارچگی داده‌ها: متادیتای فایل با اطلاعات سایر موجودیت‌های برنامه شما (مانند کاربران، پروژه‌ها) یکپارچه می‌شود.
  • انعطاف‌پذیری: اضافه کردن فیلدهای جدید (مانند برچسب‌ها، توضیحات، وضعیت پردازش) به مدل داده آسان است.
  • مقیاس‌پذیری: مدیریت میلیون‌ها فایل در یک پوشه واحد، بسیار کند و ناکارآمد است. پایگاه داده این اطلاعات را به صورت ساختاریافته مدیریت می‌کند.

آپلود به فضای ابری (Cloud Storage)

برای برنامه‌هایی که نیاز به مقیاس‌پذیری بالا، دسترسی جهانی و قابلیت اطمینان دارند، ذخیره‌سازی فایل‌ها به صورت محلی روی سرور Flask ایده‌آل نیست. در این موارد، استفاده از سرویس‌های ذخیره‌سازی ابری مانند AWS S3، Google Cloud Storage یا Azure Blob Storage بهترین گزینه است.

مزایای ذخیره‌سازی ابری:

  • مقیاس‌پذیری نامحدود: بدون نگرانی در مورد فضای دیسک سرور، هر حجمی از فایل‌ها را ذخیره کنید.
  • قابلیت اطمینان و پایداری: این سرویس‌ها با افزونگی و پشتیبان‌گیری خودکار، تضمین می‌کنند که فایل‌های شما از بین نمی‌روند.
  • کاهش بار سرور: وظیفه سرویس‌دهی فایل‌ها از سرور Flask شما به سرویس ابری منتقل می‌شود.
  • CDN (Content Delivery Network) Integration: به راحتی می‌توانید از CDN برای سرویس‌دهی سریع‌تر فایل‌ها به کاربران در سراسر جهان استفاده کنید.
  • امنیت پیشرفته: این سرویس‌ها قابلیت‌های امنیتی داخلی مانند رمزگذاری و کنترل دسترسی دقیق را ارائه می‌دهند.

نحوه پیاده‌سازی (مثلاً با AWS S3):

شما از کتابخانه‌هایی مانند boto3 (برای AWS) در پایتون استفاده می‌کنید. پس از آپلود فایل توسط کاربر به سرور Flask، به جای ذخیره آن در پوشه محلی، آن را به S3 منتقل می‌کنید:

# app.py (بخشی از کد - نیاز به نصب boto3)
# import boto3
# s3 = boto3.client('s3', aws_access_key_id='YOUR_ACCESS_KEY', aws_secret_access_key='YOUR_SECRET_KEY')
# S3_BUCKET = 'your-flask-upload-bucket'

# ...
# @app.route('/s3-upload', methods=['POST'])
# def s3_upload_file():
#     # ... (اعتبارسنجی فایل مانند قبل)
#     file = request.files['file']
#     # ... (نام امن و منحصر به فرد)
#     original_filename = secure_filename(file.filename)
#     file_extension = original_filename.rsplit('.', 1)[1].lower()
#     unique_filename = str(uuid.uuid4()) + '.' + file_extension
#     
#     try:
#         # فایل را مستقیماً از FileStorage به S3 آپلود می کنیم
#         s3.upload_fileobj(file, S3_BUCKET, unique_filename)
#         file_url = f"https://{S3_BUCKET}.s3.amazonaws.com/{unique_filename}"
#         
#         # ذخیره متادیتا در پایگاه داده با آدرس S3
#         new_file_record = UploadedFile(
#             original_filename=original_filename,
#             stored_filename=unique_filename, # یا می توانید file_url را ذخیره کنید
#             mimetype=file.mimetype,
#             filesize=file.content_length, # باید از قبل محاسبه شود
#             upload_date=datetime.utcnow()
#         )
#         db.session.add(new_file_record)
#         db.session.commit()
#         return jsonify({'message': 'File uploaded to S3 successfully', 'url': file_url}), 200
#     except Exception as e:
#         print(f"Error uploading to S3: {e}")
#         return jsonify({'error': 'Failed to upload to cloud storage'}), 500

برای مدیریت اعتبارنامه‌ها (credentials) AWS، از متغیرهای محیطی یا فایل پیکربندی ~/.aws/credentials استفاده کنید تا آن‌ها را مستقیماً در کد قرار ندهید.

پردازش پس از آپلود (Post-Upload Processing)

اغلب، پس از آپلود فایل، نیاز به انجام عملیات‌های دیگری روی آن دارید:

  • تغییر اندازه تصاویر (Image Resizing): برای نمایش سریع‌تر در وب، ایجاد نسخه‌های کوچک‌تر (thumbnails) از تصاویر. (استفاده از کتابخانه‌هایی مانند Pillow).
  • واترمارک (Watermarking): افزودن واترمارک به تصاویر.
  • تبدیل فرمت (Format Conversion): تبدیل ویدئوها یا اسناد به فرمت‌های مختلف.
  • اسکن ویروس: اجرای اسکن آنتی‌ویروس بر روی فایل‌های آپلود شده (مانند ClamAV).
  • استخراج متادیتا: خواندن اطلاعات Exif از تصاویر یا متادیتای اسناد.

انجام این عملیات‌ها به صورت همزمان با درخواست آپلود کاربر می‌تواند زمان پاسخ‌دهی (response time) سرور را افزایش دهد. بهترین روش این است که این کارها را به صورت پس‌زمینه (background tasks) و ناهمزمان (asynchronously) انجام دهید. ابزارهایی مانند Celery (همراه با یک Message Broker مانند RabbitMQ یا Redis) برای این منظور بسیار مناسب هستند.

جریان کار:

  1. کاربر فایل را آپلود می‌کند.
  2. Flask فایل را ذخیره می‌کند (به صورت محلی یا در فضای ابری) و متادیتای آن را در پایگاه داده ثبت می‌کند.
  3. Flask یک “وظیفه (task)” را به Celery (یا سایر سیستم‌های صف) ارسال می‌کند، حاوی اطلاعات فایل (مسیر، ID فایل در DB).
  4. Flask به کاربر پیام موفقیت را برمی‌گرداند.
  5. کارگر Celery (Celery Worker) وظیفه را دریافت کرده و پردازش پس از آپلود (مانند تغییر اندازه تصویر) را انجام می‌دهد.
  6. پس از اتمام پردازش، وضعیت در پایگاه داده به‌روزرسانی می‌شود (مثلاً status='processed').

این رویکرد، رابط کاربری را پاسخگو نگه می‌دارد و از بلاک شدن سرور برای وظایف زمان‌بر جلوگیری می‌کند.

تست و عیب‌یابی

مانند هر بخش مهم دیگری از یک وب‌اپلیکیشن، قابلیت آپلود فایل نیز باید به دقت تست شود. این شامل:

  • تست واحد (Unit Tests): برای توابع کمکی مانند allowed_file، و همچنین منطق اصلی مسیر آپلود (مثلاً با استفاده از Flask’s Test Client).
  • تست یکپارچه‌سازی (Integration Tests): برای اطمینان از اینکه فرم HTML با endpoint Flask به درستی کار می‌کند، و فایل‌ها به درستی در سیستم فایل یا فضای ابری ذخیره می‌شوند.
  • تست امنیتی:
    • تست آپلود فایل‌های با پسوند و MIME Type غیرمجاز.
    • تست آپلود فایل‌های بسیار بزرگ برای بررسی مدیریت MAX_CONTENT_LENGTH.
    • تست نام‌های فایل مخرب برای Directory Traversal.
    • تست انواع فایل‌های اجرایی (مانند .php) حتی اگر پسوند تغییر کرده باشد (با ابزارهایی مانند Burp Suite).
  • لاگ‌برداری (Logging): پیاده‌سازی لاگ‌های مناسب برای ثبت رویدادهای آپلود فایل (موفقیت‌آمیز، ناموفق، خطاها) برای عیب‌یابی و مانیتورینگ.

با رعایت این بهترین روش‌ها و ملاحظات پیشرفته، می‌توانید یک سیستم آپلود فایل قوی، ایمن و مقیاس‌پذیر در Flask ایجاد کنید که آماده برای محیط‌های تولید (production environments) باشد.

نتیجه‌گیری

در این آموزش جامع، ما به طور عمیق به بررسی فرآیند آپلود فایل در فریم‌ورک Flask پرداختیم. از پیکربندی اولیه محیط توسعه و ساختاردهی پروژه گرفته تا پیاده‌سازی فرم‌های HTML و منطق سمت سرور برای دریافت و ذخیره فایل‌ها، تمام جنبه‌های ضروری را پوشش دادیم. هدف ما ارائه یک راهنمای عملی و کامل بود که شما را قادر سازد تا قابلیت آپلود فایل را به برنامه‌های خود اضافه کنید.

ما آموختیم که چگونه از request.files برای دسترسی به فایل‌های آپلود شده استفاده کنیم و اهمیت حیاتی تابع secure_filename را برای جلوگیری از حملات Directory Traversal درک کردیم. همچنین، با جزئیات وارد مباحث امنیتی پیشرفته‌تر شدیم، از جمله اعتبارسنجی دقیق نوع فایل (با بررسی همزمان پسوند و MIME Type)، مدیریت محدودیت حجم فایل با MAX_CONTENT_LENGTH و بهترین روش‌ها برای ذخیره‌سازی امن فایل‌ها، مانند تغییر نام فایل با UUID و تنظیم مجوزهای صحیح.

فراتر از مبانی، به قابلیت‌های پیشرفته‌ای نظیر آپلود همزمان چندین فایل با استفاده از ویژگی multiple در HTML و request.files.getlist() در Flask، و همچنین بهبود تجربه کاربری با پیاده‌سازی نوار پیشرفت آپلود از طریق AJAX و JavaScript، پرداختیم. این تکنیک‌ها برای ساخت برنامه‌های کارآمد و کاربرپسند در دنیای واقعی بسیار مهم هستند.

در نهایت، بهترین روش‌ها و ملاحظات پیشرفته، شامل استفاده از ORM (مانند Flask-SQLAlchemy) برای مدیریت متادیتای فایل‌ها، مزایای استفاده از فضای ذخیره‌سازی ابری (مانند AWS S3) برای مقیاس‌پذیری و پایداری، و اهمیت پردازش پس از آپلود (Post-Upload Processing) به صورت ناهمزمان با استفاده از ابزارهایی مانند Celery را بررسی کردیم. همچنین بر اهمیت تست دقیق و لاگ‌برداری برای اطمینان از پایداری و امنیت سیستم تأکید شد.

با پیاده‌سازی اصول و تکنیک‌های مطرح شده در این مقاله، شما می‌توانید یک سیستم آپلود فایل قدرتمند، امن و مقیاس‌پذیر در برنامه‌های Flask خود بسازید. به یاد داشته باشید که امنیت در اولویت قرار دارد؛ هرگز به داده‌های ورودی کاربر اعتماد نکنید و همیشه اعتبارسنجی‌های سخت‌گیرانه را در سمت سرور اعمال کنید. با تمرین و پیاده‌سازی، به زودی در مدیریت فایل‌ها در Flask متخصص خواهید شد.

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان