Flask & Vue. Завантаження файлів. Приклад № 2.10


16.11.2021

У цій статті: Завантаження файлів і робота з файлами у додатках Flask + Vue

Робота web-додатка з файлами - це задача, яка має декілька шляхів для рішення. Ми розглянемо лише один з можливих.

У попередніх прикладах у моделі Countries заздалегідь було зроблено поле flag_img. Та воно було лише текстовим полем. Настав час зробити так, щоб можна було завантажити картинку :)

Як зберігати файли на сервері?

Існує декілька варіантів де зберігати файли:

  • у базі даних у Blob полях
  • зберігати файли в директорії саме у вигляді файлів
  • інші варіанти зі спеціалізованими сервісами (у цьому прикладі розглядати не будемо)

Переваги зберігання файлів в базі даних:

  • данні бази даних і вміст файлів - є одним цілим тому їх реплікація, копіювання завжди буде виконуватись разом
  • у деяких випадках можна застосувати SQL для пошуку по вмісту файлів

Недоліки зберігання файлів в базі даних:

  • розмір бази даних буде значно більшим, що впливає на вимоги до ресурсів SQL сервера
  • складніше організувати відстеження версій файлів
  • ускладнюється пошук по файлам зі специфічним вмістом
  • ускладнюється процедура оптимізації файлів (наприклад фото, тощо)
  • подальша модифікація структури бази даних буде вимагати значно більшого часу.

Переваги зберігання файлів в директорії:

  • база даних залишається легшою, продуктивність не зменшується
  • файлами простіше маніпулювати, а при необхідності оптимізувати їх
  • у подальшому можна додати функціонал для додання файлів у репозиторій GIT для збереження версій файлів
  • можна окремо створити пошук по вмісту файлів зі специфічним вмістом
  • файли можна зберігати окремо від бази даних (наприклад на окремому пристрою або сервері)

Недоліки зберігання файлів в директорії:

  • при копіюванні даних або реплікації, доведеться окремо копіювати базу даних, окремо файли. Існує шанс отримати не консистентні дані
  • файли легко підмінити. Ускладнюється процедура розподілу прав доступу до файлів.
  • у розгалужених системах можуть виникнути проблеми з синхронізацією файлів

Як саме зберігати файли має вивершувати архітектор з урахуванням всіх ризиків, масштабів та призначення ПЗ. У цьому прикладі я буду зберігати файли у директоріях.

Як буде працювати з файлами цей приклад

Будемо вважати, що кожна модель (таблиця в базі) може містити поле (або декілька полів) з файлом (або декількома файлами). Відповідно, кожен запис у таблиці може містити декілька полів, кожне з яких може містити декілька файлів.

Але вміст файлів ми не будемо зберігати у базі даних. У відповідних полях будуть лише імена файлів. Самі файли будуть зберігатися у директорії.

Як Ми будемо іменувати файли?

У окремій директорії uploads створено директорію для кожної з моделей, які мають містити файли. Наприклад, countries. У цій директорії зберігаються файли, які мають формат імен:

<id>-<field name>-<filename>

Де:

  • id - id запису
  • field name - ім'я поля
  • filename - ім'я файлу який був завантажений (тобто, ім'я файлу яке він мав на компі з якого його завантажили)

У самому полі БД будуть зберігатися лише ім'я файлу який був завантажений (<filename>).

Наприклад: Ми завантажили файл з ім'ям UA.png у поле flag_img, модель countries, id запису 1. У поле flag_img буде записано UA.png, а сам файл буде збережено як:

uploads/countries/1-flag_img-UA.png

Якщо потрібно завантажити декілька файлів (наприклад, скан декількох сторінок документа у різних файлах) і прив'язати їх до одного поля - не проблема.Одне обмеження файли мають бути названі по-різному. Тоді у полі будуть зберігатися імена файлів через кому (наприклад, 1-flag_img-page1.pdf,1-flag_img-page2.pdf,1-flag_img-page3.pdf), а файли будуть записані у директорію так:

uploads/countries/1-flag_img-page1.pdf
uploads/countries/1-flag_img-page2.pdf
uploads/countries/1-flag_img-page3.pdf

Таким чином організоване сховище файлів дозволяє легко знайти файл, який відноситься до конкретної моделі, з конкретним ID, і стосується конкретного поля. Також бачити яке ім'я мав файл під час завантаження на сервер.

Крім цього, за ім’ям файла можна легко визначити до якого поля, якого запису якої моделі належить конкретний файл.

Фронтенд. Як відправляє файли Vue?

crud.js

Для завантаження файлів методами POST (create), PUT (update) використовується Content-Type "multipart/form-data". Коли завантаження файлів не потрібне, Content-Type залишається як раніше application/json. Для цього у файлі crud.js були доопрацьовані методи: create_back, update_back.

dataset.js

У файлі dataset.js змінено опис поля flag_img

Було:

  'countries': {
    'instance': 'countries',
    'title': 'Countries',
    'perpage': 5,
    'url': 'http://localhost:5000/countries/',
    'fields': {
      'table': [
        {name:'name', 'title': 'Name', type:'string', sort: true},
        {name:'flag_img', 'title': 'Flag', type:'string', sort: false},
      ],
      'form': [
        {name:'name', 'title': 'Name', type:'string', required:true},
        {name:'code', 'title': 'Code', type:'string', required:true},
        {name:'flag_img', 'title': 'Flag', type:'string', required:false},
      ]
    }
  },

Стало:

  'countries': {
    'instance': 'countries',
    'title': 'Countries',
    'perpage': 5,
    'url': 'http://localhost:5000/countries/',
    'fields': {
      'table': [
        {name:'name', 'title': 'Name', type:'string', sort: true},
        {name:'flag_img', 'title': 'Flag', type:'file', sort: false},
      ],
      'form': [
        {name:'name', 'title': 'Name', type:'string', required:true},
        {name:'code', 'title': 'Code', type:'string', required:true},
        {name:'flag_img', 'title': 'Flag', type:'file', required:false},
      ]
    }
  },

Таким чином фронт буде працювати з цим полем як з полем, що містить файли, а не просто як з текстовим полем. Хоча по факту - це звичайне текстове поле.

Додано опис:

 'uploads': {
    'instance': 'uploads',
    'url': 'http://localhost:5000/uploads/',
 }

Це базове посилання для формування прямого посилання на завантажені файли типу http://localhost:5000/uploads/countries/1-flag_img-UA.png

FormField.vue

Поле для роботи з файлами описане у файлі FormField.vue:


  <md-field v-if="field.type=='file'">
    <label>{{field.title}}</label>
    <md-file v-model="value" @md-change="$emit('input', $event)" multiple />
  </md-field>

Якщо Вам потрібно буде змінити його вигляд чи доповнити функціонал - це робиться саме тут.

StandardTable.vue

Компонент StandardTable був доповнений таким чином, щоб для стовпчиків з типом file виводився список файлів. Вигляд кожного файлу зі списку визначає компонент UploadsItem.

UploadsItem.vue

Саме компонент UploadsItem визначає як відображати файл. У цьому прикладі зроблено так: якщо це графічний файл ('png', 'jpeg', 'jpg', 'svg', 'gif'), тоді буде вставлено його зображення з посиланням на файл. Інакше буде сформовано лише посилання на файл. Як буде бажання, можна додати іконки для різних типів файлів, тощо.

Примітка: Пряме посилання на файл вимагає зробити на бекенді спеціальний роут. Там будуть нюанси з токеном.

common/cookiefun.js

Саме для вирішення питань з токеном було додано файл common/cookiefun.js Цей модуль використовується для роботи з cookies. Нам доведеться записувати access token у cookie, інакше ми не зможемо отримати файл.

Деталі дивись тут: "Трабли з токеном. Як захистити файли?"

Бекенд. Робота з файлами

config.py

У конфіг додано директорію для файлів (UPLOADS_FOLDER), та змінено JWT_TOKEN_LOCATION. Навіщо це зроблено - дивись тут: "Трабли з токеном. Як захистити файли?"

...
UPLOADS_FOLDER = 'uploads'

#JWT_TOKEN_LOCATION = 'headers'

JWT_TOKEN_LOCATION = ['headers','cookies']
...

uploads.py

Окремий клас Uploads містить функціонал (а саме два методи: save_uploads, delete_uploads), який працює з завантаженими файлами. Моделі, які мають працювати з файлами, мають успадковуватися від двох класів.

models/countries.py

Приклад як це зроблено для Countries:

...
from app.uploads import Uploads
...
class Countries(db.Model, Uploads):
...

Звісно, при потребі, для окремих моделей можна визначити свої методи save_uploads, delete_uploads.

routes.py


Оновлено методи:
##############################
# Create
##############################
@app.route('//', methods=['POST'])

...

##############################
# Update any Model
##############################
@app.route('///', methods=['PUT'])

Тільки у цих двох роутах виконується завантаження файлів.

Додано новий роут для отримання завантажених файлів:


##############################
# Get Uploads from any Model
##############################
@app.route('/uploads//', methods=['GET'])
@jwt_required()
@access_check
def get_uploads(model, file):
	return  send_from_directory('../' + os.path.join(Config.UPLOADS_FOLDER, model), file)

Трабли з токеном. Як захистити файли?

У попередніх прикладах всі запити до бекенду, що вимагають аутентифікації та авторизації виконувались лише JS скриптом, який у заголовок запиту додавав access token. Але пряме посилання на файл, яке можна відкрити просто у браузері, не додає у заголовок запиту access token. Залишати посилання на файли відкритими для будь-кого - то є діра у безпеці. Тому access token доведеться дублювати у cookies. Саме таке рішення дозволить забезпечити доступ о файлів лише тим, в кого є права. Звісно, можна було відмовитись від збереження токенів у localstorage і цілком перенести їх у cookies. Але, я дуже дуже дуже НЕ люблю cookies. Тому зробив таке дублювання у cookies лише access token.

Як зібрати і запустити приклад?

Зкачати приклад за цим посиланням, встановити все, чого не вистачає власноруч. Запустити myblog_app.py

Дивись також:

Web-dev склерозник
Коментарі:
Додати коментар
Code
* - обов'язкові поля

Архіви