想ひ出20: GCP/Flask1.0/ローカルからCloudStorageにアップロードしてみる
おはようございます、moqrinです。
前々回に、GCEにデプロイしたFlaskのチュートリアルをCloudSQLと接続しました。
今度はCloudStorageを使ってローカルからUploadしてみようぜ、というお話です。
なお、基本的に過去の記事からの延長なのでご留意下さい。
やること
1. モデルを修正してマイグレーション
2. CloudStorageを作成してallUsersで閲覧OKにする
3. サービスアカウントキーを作成して手元に保存
4. 環境変数にサービスアカウントの情報を設定する
5. configにプロジェクトIDを設定
6. storage.pyを作成
7. Blueprintを修正
8. 画面フォームを修正
9. 確認
1. モデルを修正してマイグレーション
テーブルに画像保存するURLと画像名のColumnを追加します。
# app/models.py from app import db, login_manager from flask_login import UserMixin class User(UserMixin, db.Model): __tablename__ = 'user' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(60), nullable=False, unique=True) password = db.Column(db.String(128), nullable=False) @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) class Post(db.Model): __tablename__ = 'post' id = db.Column(db.Integer, primary_key=True) author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) created = db.Column(db.TIMESTAMP, nullable=False) title = db.Column(db.String(200), nullable=False) body = db.Column(db.String(200), nullable=False) """ 追記 """ file_url = db.Column(db.TEXT(), nullable=False) filename = db.Column(db.String(200), nullable=False)
下記コマンドでマイグレーション対応完了です。
すでにテーブルがある場合はテーブル削除してから実行します。
flask db init flask db migrate flask db upgrade
2. CloudStorageを作成してallUsersで閲覧OKにする
Storageを作成して、allUsersで閲覧OKのメンバー追加します。
設定参考
3. サービスアカウントキーを作成して手元に保存
GCP外のアプリケーションからGoogle Cloud Storage API を有効化してアクセスするためには、サービスアカウントキーが必要となります。
Storageの読み書きできる権限で作成して、お手元にダウンロードします。
Cloud Storage Client Libraries
4. 環境変数にサービスアカウントキーを設定する
アクセス許可してもらうためにサービスアカウントキーをOSに設定しておきます。
export GOOGLE_APPLICATION_CREDENTIALS=path/to/ServiceAccountJsonKey
5. configにStorageが作成されたプロジェクトIDを設定
手っ取り早くこんな感じで設定しています。
# instance/config.py PROJECT_ID = 'moqrin-storage'
6. storage.pyを作成
ほぼチュートリアルそのままで利用しています。
# app/storage.py from __future__ import absolute_import import os os.getenv("GOOGLE_APPLICATION_CREDENTIALS") import datetime from google.cloud import storage import six from werkzeug import secure_filename from werkzeug.exceptions import BadRequest from app import ALLOWED_EXTENSIONS, PROJECT_ID, CLOUD_STORAGE_BUCKET def _get_storage_client(): return storage.Client( project=PROJECT_ID) def _check_extension(filename, allowed_extensions): if ('.' not in filename or filename.split('.').pop().lower() not in allowed_extensions): raise BadRequest( "{0} has an invalid name or extension".format(filename)) def _safe_filename(filename): filename = secure_filename(filename) date = datetime.datetime.utcnow().strftime("%Y-%m-%d-%H%M%S") basename, extension = filename.rsplit('.', 1) return "{0}-{1}.{2}".format(basename, date, extension) def upload_file(file_stream, filename, content_type): _check_extension(filename, ALLOWED_EXTENSIONS) filename = _safe_filename(filename) client = _get_storage_client() bucket = client.bucket(CLOUD_STORAGE_BUCKET) blob = bucket.blob(filename) blob.upload_from_string( file_stream, content_type=content_type) url = blob.public_url if isinstance(url, six.binary_type): url = url.decode('utf-8') print(url) return url
7. Blueprintを修正
下記の公式ドキュメントを参考にアップロード機能を実装していきます。
追記の部分を記載していきます。元はこちらです。
# app/__init__.py ... ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) PROJECT_ID = PROJECT_ID CLOUD_STORAGE_BUCKET = 'YOUR-BUCKET-NAME' ... def create_app(test_config=None): ... return app
# app/blog.py from flask import ( Blueprint, flash, g, redirect, render_template, request, url_for ) from app import storage from . import db, ALLOWED_EXTENSIONS ... bp = Blueprint('blog', __name__) def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def upload_image_file(file): if not file: return None public_url = storage.upload_file( file.read(), file.filename, file.content_type ) return public_url ... @bp.route('/create', methods=('GET', 'POST')) @login_required def create(): if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if 'img_file' not in request.files: flash('No file part') return redirect(request.url) file = request.files['img_file'] image_url = upload_image_file(request.files.get('img_file')) if error is not None: flash(error) else: post = Post(title=title, body=body, author_id=g.user.id, file_url=image_url, filename=file.filename ) ... @bp.route('/<int:id>/update', methods=('GET', 'POST')) @login_required def update(id): post = Post.query.get_or_404(id) if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if error is not None: flash(error) if 'img_file' in request.files: file = request.files['img_file'] if file and allowed_file(file.filename): image_url = upload_image_file(request.files.get('img_file')) post.id = id post.title = title post.body = body post.file_url = image_url post.filename = file.filename db.session.commit() flash('Successfully edited the post.') else: post.id = id post.title = title post.body = body db.session.commit() ...
8. 画面フォームを修正
画像表示およびアップロードができるように修正します。
# app/templates/blog/index.html {% extends 'base.html' %} {% block header %} <h1>{% block title %}Posts{% endblock %}</h1> {% if current_user.is_authenticated %} <a class="action" href="{{ url_for('blog.create') }}">New</a> {% endif %} {% endblock %} {% block content %} {% for post in posts %} <article class="post"> <header> <div> <h1>{{ post.title }}</h1> <div class="about">by {{ post.username }} on {{ post.created.strftime('%Y-%m-%d') }}</div> </div> <a class="action" href="{{ url_for('blog.update', id=post.id) }}">Edit</a> </header> <p class="body">{{ post.body }}</p> <img src="{{ post.file_url }}" width="400" height="200"> </article> {% if not loop.last %} <hr> {% endif %} {% endfor %} {% endblock %}
# app/templates/blog/create.html {% extends 'base.html' %} {% block header %} <h1>{% block title %}New Post{% endblock %}</h1> {% endblock %} {% block content %} <form method="POST" enctype="multipart/form-data"> <label for="title">Title</label> <input name="title" id="title" value="{{ request.form['title'] }}" required> <label for="body">Body</label> <textarea name="body" id="body">{{ request.form['body'] }}</textarea> <input type="file" id="img_file" name="img_file"> <input type="submit" value="Save"> </form> {% endblock %}
# app/templates/blog/update.html {% extends 'base.html' %} {% block header %} <h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1> {% endblock %} {% block content %} <form method="post" enctype="multipart/form-data"> <label for="title">Title</label> <input name="title" id="title" value="{{ request.form['title'] or post['title'] }}" required> <label for="body">Body</label> <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea> <img src="{{ post.file_url }}" width="400" height="200"> <input type="file" id="img_file" name="img_file"> <input type="submit" value="Save"> </form> <hr> <form action="{{ url_for('blog.delete', id=post['id']) }}" method="post"> <input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');"> </form> {% endblock %}
9. 確認
Google CloudのAPI Client libraryをインストール
pip install gcloud
起動。
export FLASK_APP=run export FLASK_ENV=development flask run
表示を確認して喜びます。
わーい。 でも...やっぱり何だか動作おせーー。。。w
参考:
Python での Cloud Storage の使用
Python Client for Google Cloud Storage