ファイル交換用のウェブストレージをgoogle cloudで組み立てる時のメモ

# 目的と前提
ファイル交換を目的としたウェブストレージをGoogle CloudのストレージサービスとVMインスタンスを使って組み立てるための自分用のメモです。
VMインスタンスとバケット間のファイルのやり取りにはcgiとしてpythonを使います。
次のものはすでに準備しているものとして詳細を省略します。
* ストレージのバケット
* インターネットアクセスが可能なVMインスタンス（ubuntu server）
* Apacheのインストール
* phpのインストール
* pythonライブラリ google-cloud-storage のインストール


## 手順
1) サービスアカウントの作成
2) jsonキーの設定
3) apacheでcgiにpython追加
4) ファイルサイズに関するphpの設定変更
5) pythonコード


### 1. サービスアカウントの作成
VMインスタンスからバケットにアクセスするために、サービスアカウントを使う。認証手段としてjsonキーを使う。
* 使うコンソールメニューは、APIとサービス＞認証情報。
* ロールは、オブジェクトユーザ。
* 鍵は、jsonキーを作成。

:::note warn
VMインスタンスからバケットにアクセスするためには、VMインスタンスにIAM権限を設定する方法の方が良いと思うが、なぜかうまくいかなかったので、jsonキーを使う。
:::


### 2. jsonキーの設定
jsonキーをVMインスタンスに保存。
cgiでjsonキーを使うには、cgiコードに直接キーのパスを記入。jsonキーの権限設定に注意（www-dataがアクセスできるか）。
```python
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/path/to/jsonkey.json"
```
VMインスタンスのコンソールからコードを実行する際には、環境変数に設定してしまえばコード内にキーのパスを記入する必要がないが、今回はcgiでの実行なので、環境変数に設定してもwww-dataユーザには引き継がれない。
なお、環境変数への設定は、次のとおり。
* 永続させる
```shell
echo 'export GOOGLE_APPLICATION_CREDENTIALS="/path/to/jsonkey.json"' >> ~/.bashrc
source ~/.bashrc
```
* 永続させない
```shell
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/jsonkey.json"
```


### 3. apacheでcgiにpython追加
いつもこの設定をどのconfファイルに設定するのか忘れてしまうので、メモ。
* CGIの許可
```shell
sudo a2enmod cgi
sudo systemctl restart apache2
```
* confファイルの編集
/etc/apache2/sites-available/000-default.confの末尾に次の内容を追加
```vim
<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コード（アップロード、リスト表示、ダウンロード）
#### アップロードの枠組み
1) HTMLフォームから、保存ファイルに設定する接頭辞(name="upidx")とファイル(name="upfiles[]")が送られるので、それを処理する。
2) 処理状況をHTMLとして返す。
3) 返されたHTMLはフォーム側でalertとして表示する。
```python
#!/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('ファイルが選択されていません')
```

#### リスト表示の枠組み
1) HTMLフォームから、検索する接頭辞(name="dlidx")が送られるので、それを処理する。
2) 処理状況をHTMLとして返す。
3) 返されたHTMLはフォーム側で指定の<div>タグ内表示する。
```python
#!/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)
```

#### ダウンロードの枠組み
1) HTMLフォームから、ダウンロードするファイル名(name="filename")が送られるので、それを処理する。
2) ファイルダウンロード処理を返す。
```python
#!/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)
```