目的と前提
ファイル交換を目的としたウェブストレージをGoogle CloudのストレージサービスとVMインスタンスを使って組み立てるための自分用のメモです。
VMインスタンスとバケット間のファイルのやり取りにはcgiとしてpythonを使います。
次のものはすでに準備しているものとして詳細を省略します。
- ストレージのバケット
- インターネットアクセスが可能なVMインスタンス(ubuntu server)
- Apacheのインストール
- phpのインストール
- pythonライブラリ google-cloud-storage のインストール
手順
- サービスアカウントの作成
- jsonキーの設定
- apacheでcgiにpython追加
- ファイルサイズに関するphpの設定変更
- pythonコード
1. サービスアカウントの作成
VMインスタンスからバケットにアクセスするために、サービスアカウントを使う。認証手段としてjsonキーを使う。
- 使うコンソールメニューは、APIとサービス>認証情報。
- ロールは、オブジェクトユーザ。
- 鍵は、jsonキーを作成。
VMインスタンスからバケットにアクセスするためには、VMインスタンスにIAM権限を設定する方法の方が良いと思うが、なぜかうまくいかなかったので、jsonキーを使う。
2. jsonキーの設定
jsonキーをVMインスタンスに保存。
cgiでjsonキーを使うには、cgiコードに直接キーのパスを記入。jsonキーの権限設定に注意(www-dataがアクセスできるか)。
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/path/to/jsonkey.json"
VMインスタンスのコンソールからコードを実行する際には、環境変数に設定してしまえばコード内にキーのパスを記入する必要がないが、今回はcgiでの実行なので、環境変数に設定してもwww-dataユーザには引き継がれない。
なお、環境変数への設定は、次のとおり。
- 永続させる
echo 'export GOOGLE_APPLICATION_CREDENTIALS="/path/to/jsonkey.json"' >> ~/.bashrc
source ~/.bashrc
- 永続させない
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/jsonkey.json"
3. apacheでcgiにpython追加
いつもこの設定をどのconfファイルに設定するのか忘れてしまうので、メモ。
- CGIの許可
sudo a2enmod cgi
sudo systemctl restart apache2
- confファイルの編集
/etc/apache2/sites-available/000-default.confの末尾に次の内容を追加
<Directory "/usr/lib/cgi-bin">
AllowOverride None
Options +ExecCGI
AddHandler cgi-script .py
Require all granted
</Directory>
4. ファイルサイズに関するphpの設定変更
これもいつもどこをいじったのか忘れるので、メモ。
変更する設定ファイルは、/etc/php/8.1/apache2/php.ini。設定変更するのは次の項目。確認はphpinfo()で。
- memory_limit
- post_max_size
- upload_max_filesize
5. pythonコード(アップロード、リスト表示、ダウンロード)
アップロードの枠組み
- HTMLフォームから、保存ファイルに設定する接頭辞(name="upidx")とファイル(name="upfiles[]")が送られるので、それを処理する。
- 処理状況をHTMLとして返す。
- 返されたHTMLはフォーム側でalertとして表示する。
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import cgi,sys,os,io
from google.cloud import storage as gcs
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
bucket_name = 'bucketname'
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/path/to/jsonkey.json"
client = gcs.Client()
bucket = client.bucket(bucket_name)
tmpdir = '/file/tmpsave/dir/path'
#ファイルをVMに一時保存
def single_upload_file(tmpdir, idx, item):
filename = idx+'__'+item.filename
try:
path = os.path.join(tmpdir, os.path.basename(filename))
chunk = item.file.read()
if chunk:
fout = open(path,mode='wb')
fout.write(chunk)
fout.close()
except Exception as ee:
print(ee)
#ストレージにアップロード
def upload_gcs(tmpdir, idx, filename):
try:
upfilename = idx+'__'+filename
tmppath = os.path.join(tmpdir, upfilename)
blob = bucket.blob(upfilename)
blob.upload_from_filename(tmppath)
except Exception as ee:
print(ee)
#VMの一時保存ファイルを削除
def deletefile(tmpdir, idx, filename):
try:
upfilename = idx+'__'+filename
deletepath = os.path.join(tmpdir, upfilename)
os.remove(deletepath)
except Exception as ee:
print(ee)
form = cgi.FieldStorage()
print("Content-Type: text/html")
print()
if 'upfiles[]' in form:
idx = form['upidx'].value
#ファイルが複数送られたときと1つの時に場合分け
if (isinstance(form['upfiles[]'],list)):
for f in form['upfiles[]']:
single_upload_file(tmpdir, idx, f)
print(f"{f.filename}を一時保存しました")
upload_gcs(tmpdir, idx, f.filename)
print(f"{f.filename}をストレージに保存しました")
deletefile(tmpdir, idx, f.filename)
print(f"{f.filename}のアップロードが完了しました\n")
else:
if form['upfiles[]'].filename:
single_upload_file(tmpdir, idx, form['upfiles[]'])
print(f"{form['upfiles[]'].filename}を一時保存しました")
upload_gcs(tmpdir, idx, form['upfiles[]'].filename)
print(f"{form['upfiles[]'].filename}をストレージに保存しました")
deletefile(tmpdir, idx, form['upfiles[]'].filename)
print(f"{form['upfiles[]'].filename}のアップロードが完了しました\n")
else:
print('ファイルが選択されていません')
リスト表示の枠組み
- HTMLフォームから、検索する接頭辞(name="dlidx")が送られるので、それを処理する。
- 処理状況をHTMLとして返す。
- 返されたHTMLはフォーム側で指定のタグ内表示する。
#!/usr/bin/python3 # -*- coding: utf-8 -*- import cgi,sys,os,io from google.cloud import storage as gcs sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') bucket_name = 'bucketname' os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/path/to/jsonkey.json" client = gcs.Client() bucket = client.bucket(bucket_name) tmpdir = '/file/tmpsave/dir/path' form = cgi.FieldStorage() #DLボタンクリック時の動作(jquery使用) #jspostの定義はHTML側(第2引数をpostdataとして第1引数に送る関数) jsscript = ''' <script type="text/javascript"> $('button[name="dlbtn"]').on('click', function(){ var senddata = {'filename':$(this).val()}; jspost('/cgi-bin/cgi_pythonfile.py', senddata); }); </script> ''' print("Content-Type: text/html") print() if form['dlidx'].value: prefix = form['dlidx'].value + '__' blobs = client.list_blobs(bucket_name, prefix=prefix) print(f"インデックス{prefix}ファイルリスト",'<br>') for blob in blobs: print(blob.name) print(f'<button type="button" name="dlbtn" value="{blob.name}">ダウンロード</button><br>') print(jsscript)
ダウンロードの枠組み
- HTMLフォームから、ダウンロードするファイル名(name="filename")が送られるので、それを処理する。
- ファイルダウンロード処理を返す。
#!/usr/bin/python3 # -*- coding: utf-8 -*- import cgi,sys,os,io from google.cloud import storage as gcs sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') bucket_name = 'bucketname' os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/path/to/jsonkey.json" client = gcs.Client() bucket = client.bucket(bucket_name) tmpdir = '/file/tmpsave/dir/path' form = cgi.FieldStorage() #gcsからDL gcsfilename = form['filename'].value dlpath = os.path.join(tmpdir, gcsfilename) blob = bucket.blob(gcsfilename) blob.download_to_filename(dlpath) #ファイル名から接頭辞を分離 dlfilename = form['filename'].value.split('__')[1] #DLさせるためのヘッダ print(f'Content-Disposition: attachment; filename={dlfilename}') print('', flush=True) #DL with open(dlpath, 'rb') as f: sys.stdout.buffer.write(f.read()) #一時ファイル削除 os.remove(dlpath)