Модуль 31

Файли: upload та download

Ось готовий урок, створений за твоїм майстер-промптом. Це класичний CS50-вайб: енергійно, з прикладами і фокусом на тому, що насправді відбувається всередині.


🎓 Тема уроку: Файли: Upload та Download

(Або як не "покласти" сервер, коли користувач завантажує котиків)


1. 🔥 Вступ: проблема та мотивація

Уявіть на секунду: ви створюєте наступний Instagram або TikTok. Ви написали код для реєстрації, зробили гарні кнопки, налаштували базу даних. Але є одна проблема. Користувач хоче показати світу своє фото (або відео з танцюючим хом’яком).

Він натискає "Завантажити", і... що відбувається далі?

Текст передати легко — це просто набір символів. Але фотографія? Це набір пікселів, бінарний код, нулі та одиниці, які важать у тисячі разів більше, ніж ваше ім'я в анкеті.

Риторичне питання: Ви коли-небудь замислювалися, куди насправді зникає файл, коли ви прикріплюєте його до листа в Gmail? Він лежить у базі даних? Чи десь на жорсткому диску в Каліфорнії?

Аналогія: Уявіть, що HTTP-запит (те, як браузер спілкується з сервером) — це звичайний паперовий лист. * Якщо ви хочете відправити ім'я та пароль — ви пишете їх на аркуші, кладете в конверт і відправляєте. Легко. * Але якщо ви хочете відправити книгу (файл)? Вона не влізе у звичайний конверт. Вам потрібна посилка (бандероль).

Сьогодні ми навчимося пакувати ці "посилки" (Upload) і розпаковувати їх назад (Download). Без цього ваш веб-додаток залишиться просто нудним текстовим блокнотом.


2. 🧠 Теоретична база (Що там «під капотом»?)

Щоб працювати з файлами, нам треба зрозуміти два ключові механізми.

1. Multipart/form-data (Ваша "бандероль")

Зазвичай браузер відправляє дані як простий текст. Але коли з'являється файл, ми повинні сказати браузеру: "Гей, зараз буде важкий вантаж, розбий його на частини!".

Це називається Multipart Request. Браузер бере ваш файл, ріже його на шматки, додає спеціальні розділювачі (boundaries) і відправляє на сервер потоком.

❗️ Запам’ятати обов’язково: У HTML-формі для завантаження файлів завжди має бути атрибут enctype="multipart/form-data". Якщо ви його забудете — сервер отримає лише назву файлу (текст), а не сам файл. Це найпоширеніша помилка новачків №1.

2. Де зберігати файли? (Вічна дилема)

Тут є велика спокуса: "О, у мене є база даних (SQL), я збережу картинку прямо в таблицю!".

Зупиніться. У 99% випадків це погана ідея. Бази даних створені для швидкого пошуку тексту та чисел. Якщо запхати туди гігабайти картинок, база стане повільною і неповороткою.

Як роблять профі: 1. Файл зберігаємо на диску сервера (або у хмарному сховищі типу AWS S3). 2. У Базу Даних записуємо тільки шлях (посилання) до цього файлу (наприклад, /uploads/cat.jpg).


3. 🧪 Приклади (Python + Flask)

Давайте подивимось на це в коді. Ми використаємо Python (Flask), бо він читається як псевдокод, але логіка однакова і для PHP, і для Node.js, і для Java.

Приклад 1: "Наївний" підхід (Мінімальний)

Що ми очікуємо? Просту форму, де можна вибрати файл і відправити його.

HTML (Frontend):

<!-- Зверніть увагу на enctype! Без нього магії не буде -->
<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="my_file">
    <button type="submit">Завантажити</button>
</form>

Python (Backend):

from flask import Flask, request

app = Flask(__name__)

@app.route('/upload', methods=['POST'])
def upload_file():
    # Отримуємо файл з "посилки"
    file = request.files['my_file']

    # Зберігаємо його прямо туди, де лежить наш скрипт
    file.save(file.filename)

    return "Файл успішно завантажено!"

Чому це працює, але небезпечно? Якщо я завантажу файл з назвою server.py, я можу випадково перезаписати код вашого сервера! Ніколи не довіряйте іменам файлів, які надсилає користувач.


Приклад 2: Безпечний Upload (Як у реальному проєкті)

Тепер додамо трохи захисту. 1. Перевіримо, чи це взагалі картинка (розширення). 2. Зробимо ім'я файлу безпечним.

import os
from werkzeug.utils import secure_filename

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
    # Перевіряємо, чи є крапка і чи розширення у списку дозволених
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/upload', methods=['POST'])
def upload_secure():
    if 'my_file' not in request.files:
        return "Файл не знайдено", 400

    file = request.files['my_file']

    # Якщо файл вибрано і він дозволеного типу
    if file and allowed_file(file.filename):
        # Очищуємо ім'я від сміття та хакерських символів
        filename = secure_filename(file.filename)

        # Зберігаємо у спеціальну папку
        file.save(os.path.join('/var/www/uploads', filename))
        return f"Файл {filename} збережено безпечно!"

    return "Недопустимий тип файлу!", 400

Приклад 3: Download (Віддаємо файл назад)

А як користувач отримає свій файл назад?

from flask import send_from_directory

@app.route('/uploads/<filename>')
def download_file(filename):
    # Функція сама знайде файл, виставить правильні заголовки
    # і віддасть його браузеру.
    return send_from_directory('/var/www/uploads', filename)

Тут ми кажемо серверу: "Знайди файл з таким ім'ям у папці uploads і віддай його як attachment (вкладення)".


4. 🛠 Практична частина

Тепер ваша черга. Не просто читайте — пишіть код.

Завдання 1: Copy-Paste з розумінням Створіть простий сервер (на Flask/Node/іншому), реалізуйте Приклад 1. Завантажте картинку. Перевірте папку проекту — вона там з'явилася?

Завдання 2: Детектив Приберіть з HTML-форми атрибут enctype="multipart/form-data". Спробуйте завантажити файл. Що виведе сервер? Чому? (Це класичний баг, відчуйте його).

Завдання 3: "Секретний архів" Змініть код так, щоб сервер приймав ТІЛЬКИ .pdf файли. Спробуйте обдурити його і завантажити .jpg.

Завдання 4: Реальна проблема (Конфлікт імен) Що станеться, якщо два користувачі (Аліса і Боб) одночасно завантажать різні фото, але з однаковою назвою avatar.jpg? Другий файл перезапише перший! Завдання: Змініть код так, щоб до кожного файлу перед збереженням додавався унікальний ідентифікатор (підказка: погугліть UUID).

Завдання 5: Міні-кейс "А що, якщо..." Що, якщо користувач спробує завантажити файл розміром 10 Гігабайт? Як захистити сервер від переповнення пам'яті? (Знайдіть налаштування ліміту розміру файлу у вашому фреймворку).


5. 💡 Мислення як у розробника

Як думає Senior-розробник, коли чує задачу "Зроби завантаження аватарки"?

  1. "Не вір користувачу". Він може назвати вірус cute_cat.jpg.exe. Він може назвати файл ../../etc/passwd, щоб спробувати зламати систему шляхів. Тому ми завжди санітизуємо (чистимо) імена файлів або генеруємо свої власні випадкові імена.
  2. "Диск не гумовий". Якщо проєкт виросте, сервер закінчиться. Тому профі одразу думають про хмарні сховища (AWS S3, Google Cloud Storage). Там місця безліч, і це дешевше, ніж тримати свої диски.
  3. "Метадані важливі". Ми зберігаємо не тільки файл, а й інформацію про нього в БД: хто завантажив, коли, який розмір, оригінальна назва.

Типова помилка новачка: Зберігати файли в папці static вашого коду і заливати це все на Git/GitHub. Правило: Код — окремо, дані користувачів (uploads) — окремо (у .gitignore).


6. 🧩 Підсумок

Отже, що ми маємо? 1. Завантаження файлів — це передача бінарних даних через "бандероль" multipart/form-data. 2. Ми не зберігаємо файли в Базі Даних (за рідкісними винятками). Ми зберігаємо їх на диску, а в БД — посилання. 3. Безпека — це перевірка розширень і перейменування файлів.

Що ви тепер вмієте? Ви можете створити форму, прийняти резюме, фото або документ від користувача, безпечно зберегти його і дати посилання на скачування. Це фундамент для будь-якої соцмережі, магазину чи корпоративного порталу.

🔜 Тизер наступного уроку: Окей, ми навчилися завантажувати файли. Але зараз будь-хто може скачати будь-який файл, якщо знає посилання. Як зробити так, щоб ваше фото бачили тільки ваші друзі? На наступному уроці поговоримо про Аутентифікацію та Cookies. Готуйте своє печиво! 🍪