Файл проекта: https://github.com/selfedu-rus/flasksite-17
На этом занятии
рассмотрим порядок загрузки файлов на сервер. Когда это бывает необходимо?
Например, пользователь в своем профайле хочет сменить аватарку. Для этого он
должен выбрать изображение на своем устройстве и загрузить его на сервер.
Далее, на стороне сервера происходит обработка принятого файла и связывается с
аккаунтом текущего пользователя.
Здесь может
возникнуть вопрос: зачем нам создавать отдельный обработчик upload для загрузки
файла? Почему бы его не реализовать сразу в profile? Дело в том,
что если не разнести эти обработчики, то при обновлении страницы profile автоматически
произойдет повторная загрузка этого же изображения, т.к. браузеры при refresh страницы
дублируют всю информацию, в том числе и отправку данных формы. Чтобы этого не
происходило, как раз и создается отдельный загрузчик, данные из формы
направляются к нему, а затем, осуществляется автоматическое перенаправление в
профайл. В этом случае обновление profile приведет просто
к обновлению страницы без отправки данных формы.
Добавим этот
функционал на наш сайт. Загруженную аватарку пользователя будем хранить
непосредственно в БД. Это обычная практика в веб-программировании. Поэтому в
таблице users создадим
специальное поле avatar с типом BLOB:
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
name text NOT NULL,
email text NOT NULL,
psw text NOT NULL,
avatar BLOB DEFAULT NULL,
time integer NOT NULL
);
И, затем, с
помощью известной уже вам функции create_db создадим эту
таблицу в БД. Но, предварительно удалим ее (иначе она не будет создана).
Затем, в
основном программном модуле добавим специальную константу:
MAX_CONTENT_LENGTH = 1024 * 1024
которая будет
ограничивать максимальный размер загружаемого файла, в данном случае 1 Мб.
После этого нам
нужно поправить метод addUser в классе FDataBase, чтобы
пользователь создавался с аватаром по умолчанию (значение NULL):
def addUser(self, name, email, hpsw):
…
self.__cur.execute("INSERT INTO users VALUES(NULL, ?, ?, ?, NULL, ?)", (name, email, hpsw, tm))
…
Далее, создадим
шаблон отображения профайла (profile.html). Он будет
следующий:
{% extends 'base.html' %}
{% block content %}
{{ super() }}
{% for cat, msg in get_flashed_messages(True) %}
<div class="flash {{cat}}">{{msg}}</div>
{% endfor %}
<table border="0" class="profile-table">
<tr><td valign="top">
<div class="profile-ava"><img src="{{ url_for('userava') }}"></div>
<div class="profile-load">
<form action="{{url_for('upload')}}" method="POST" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="Загрузить">
</form>
</div>
</td>
<td valign="top" class="profile-panel">
<a href="{{url_for('logout')}}">Выйти из профиля</a>
<ul class="profile-info">
<li>Имя: {{ current_user.getName() }}</li>
<li>Email: {{ current_user.getEmail() }}</li>
</ul>
</td></tr>
</table>
{% endblock %}
Мы здесь
используем прокси-переменную current_user для обращения к
авторизованному пользователю. И через нее вызываем методы класса UserLogin. Добавим этим
методы:
def getName(self):
return self.__user['name'] if self.__user else "Без имени"
def getEmail(self):
return self.__user['email'] if self.__user else "Без email"
Далее, чтобы
профиль выглядел более-менее прилично, пропишем следующие стили оформления
шаблона profile.html:
.profile-ava {
width: 130px;
text-align: center;
overflow: hidden;
background: #eee;
padding: 10px;
}
.profile-ava img {max-width: 150px; height: auto;}
.profile-load {
margin-top: 10px;
overflow: hidden;
max-width: 150px;
}
.profile-load input[type=submit], input[type=file] {
width: 100%;
font-size: 18px;
}
.profile-load p {
padding: 0;
margin: 5px 0 0 0;
}
.profile-panel {padding: 0 0 0 10px;}
ul.profile-info {
list-style: none;
margin: 10px 0 0 0;
padding: 0;
color: #7E652F;
}
ul.profile-info li {margin-top: 10px;}
Отображение аватара
Для отображения
аватара создадим отдельный обработчик userava:
@app.route('/userava')
@login_required
def userava():
img = current_user.getAvatar(app)
if not img:
return ""
h = make_response(img)
h.headers['Content-Type'] = 'image/png'
return h
Смотрите, мы
здесь загружаем аватар пользователя с помощью метода getAvatar, который
пропишем в классе UserLogin. Если данные не были получены, то
возвращается пустая строка, а иначе, создается объект ответа с параметром 'Content-Type' равным 'image/png', т.к. мы для
простоты будем полагать, что все загруженные аватары будут представлены в
формате PNG. В результате,
при отображении изображения через тег img, в профиле будем видеть
соответствующий аватар текущего пользователя.
Теперь перейдем
в класс UserLogin и там добавим
метод получения аватара:
def getAvatar(self, app):
img = None
if not self.__user['avatar']:
try:
with app.open_resource(app.root_path + url_for('static', filename='images/default.png'), "rb") as f:
img = f.read()
except FileNotFoundError as e:
print("Не найден аватар по умолчанию: "+str(e))
else:
img = self.__user['avatar']
return img
Мы здесь вначале
проверяем: если в БД аватар не был загружен, то берется изображение по
умолчанию:
static/images/default.png
А, иначе, берем
аватар из БД. В конце метод возвращает прочитанные данные изображения.
Загрузка аватара
Следующим шагом нам
нужно реализовать загрузку изображения пользователя в профайле. Как мы говорили
вначале, для этого необходим отдельный обработчик, который можно представить в
следующем виде:
@app.route('/upload', methods=["POST", "GET"])
@login_required
def upload():
if request.method == 'POST':
file = request.files['file']
if file andcurrent_user.verifyExt(file.filename):
try:
img = file.read()
res = dbase.updateUserAvatar(img, current_user.get_id())
if not res:
flash("Ошибка обновления аватара", "error")
return redirect(url_for('profile'))
flash("Аватар обновлен", "success")
except FileNotFoundError as e:
flash("Ошибка чтения файла", "error")
else:
flash("Ошибка обновления аватара", "error")
return redirect(url_for('profile'))
Здесь вначале
проверяется, что пришли данные по POST-запросу и
берется поле file из объекта request. Это поле
ассоциировано с загруженным на сервер файлом, который был выбран через форму в
профайле. Далее, идет проверка, что файл был успешно загружен и что его
расширение PNG. Затем,
происходит чтение данных из файла и обновляется аватар пользователя в БД.
Чтобы этот
обработчик работал, нужно добавить два метода. Первый verifyExt в классе UserLogin:
def verifyExt(self, filename):
ext = filename.rsplit('.', 1)[1]
if ext == "png" or ext == "PNG":
return True
return False
И второй – updateUserAvatar в классе FDataBase:
def updateUserAvatar(self, avatar, user_id):
if not avatar:
return False
try:
binary = sqlite3.Binary(avatar)
self.__cur.execute(f"UPDATE users SET avatar = ? WHERE id = ?", (binary, user_id))
self.__db.commit()
except sqlite3.Error as e:
print("Ошибка обновления аватара в БД: "+str(e))
return False
return True
Все, теперь наш
функционал полностью готов и пользователи могут загружать свои аватарки в
профайл.