見出し画像

レンタルサーバでWebアプリ作り: ファイル保存 / ファイルサーバ導入

ロリポップレンタルサーバではできることが限られていたので、ホスティングサービスの DigitalOcean を使ってWebアプリを公開するまでの道のりを記録します。

以下は DigitalOcean の紹介リンクですが、ここから手続きを進めてもらえると200ドル分の無料チケット(有効期間2ヶ月)がもらえるようですので、ぜひご活用ください。(2024/1/2時点)


背景

前回までで、Webアプリ作成までが行えました。いたってシンプルなものなので、そこにいろいろと機能追加しようと、まずは「Pythonで作成したテキストファイルを自分のパソコン(ローカル環境)に持ってくる」をしようとしたところ、なんと!!それができないとのこと。。。

DigitalOceanのApp Platformで構築した場合、SSL通信などは受け付けてないとのことでした。

App platform does not allow SSH, FTP or SFTP access to the container or from the App platform to external service.

DigitalOceanのサポートからいただいたコメント

同時に解決策を教えてくれたので、その導入記録をNoteにまとめます。

ゴール= Spaces なるサービスを導入する

サポートの人曰く、「Spacesというファイルサーバを入れたら、そこを介してファイルの授受ができるよ」とのこと。

It is recommended to store data outside in a database or DO spaces. Please refer to: https://docs.digitalocean.com/products/app-platform/how-to/store-data/

サポートからのメール

ということで、Spacesを導入していきます。
※ 「他の外部サービスも使えるよ」ということなので、Google Drive APIを使うなども可能そうです。


利用料金&容量

まずもって気になる利用料金についてです。
250GBで1ヶ月5ドル。それ以上は、1GBあたり0.02ドル。
どこに書かれているのかわからなかったので、サポートに聞いてみたら、以下のような回答を得ました。

The base rate of a Spaces subscription is $5.00 per month and gives you the ability to create multiple Spaces buckets.
There is no specific capacity limit per bucket. The Spaces subscription includes 250 GiB of data storage (cumulative across all of your buckets). Additional storage beyond this allotment is $0.02 per GiB.

サポートからの回答

私の使用目的はWebアプリの利用ログを残す程度なので、250GBで十分。ということで、こいつを使い続けようと思います。


Spacesの登録作業

まずはSpacesをCreateします。どうやらプロジェクト単位で作っていく模様。ということで、プロジェクトの中に入り、Createボタンをクリック。

すると、以下のような設定画面が出てくるので、それぞれ入力。

料金

その設定画面の下のほうに、料金が出ていました。$5ドルとのこと。Google Platformのほうが安い気もしましたが、Google Platformを使ったことがないのでよくわからず、またDigitalOceanの無料期間でもあるので躊躇なく登録♪

作成はこれだけ。
次に、PythonからAccessしてみます。

Pythonからアクセス

今回サポートにいろいろと助けてもらいましたが、その際に「以下のアドレスにサンプルあるよ」と教えてもらいました。


各種キーの取得

必要な情報は、「Endpoint URL」「Region」「Access Key」「Secret Key」「Bucket」の5つ。

ここで注意点!
Endpoint URLは、作成し終わった画面に出ている「Origin Endpoint」とは違います!!

これはEndpoint URLではありません!

まず、この [Origin Endpoint] から、[Bucket]と[Region]を見分けます。以下のようなアドレスになっていて、「abcde」がBucket、「sgp1」がRegionになります。これはシンガポール。

https://[Bucket].[Region].digitaloceanspaces.com
https://abcde.sgp1.digitaloceanspaces.com


エンドポイントURLは、「https://[Region].digitaloceanspaces.com」です。上のアドレスから Bucket を除いたものです。

https://sgp1.digitaloceanspaces.com

正しい Endpoint URL


Access Key / SecretKey は、API設定画面で取得します。左のサイドバーの下のほうに「API」というのがあり、それを押すと以下のような画面が出てきますので、Space Key を Generate します。

名前を入れて、Generate。

すると、Access KeyとSecret Keyが入手できます。
※ Secret Keyは二度と確認できないと思うので、以下の画面からしっかりコピーして保存しておきましょう。

Pythonプログラム

以下の*****の箇所に、5つのキーを貼り付ければOK。
このプログラムを回し、WebでSpacesに「hello-world.txt」というファイルができていれば成功です。

import os
import boto3
import botocore

session = boto3.session.Session()
client = session.client('s3',
                        endpoint_url='***********',
                        config=botocore.config.Config(s3={'addressing_style': 'virtual'}),
                        region_name='***',
                        aws_access_key_id='***********', 
                        aws_secret_access_key='***********'
)

client.put_object(Bucket='***********',
                  Key='hello-world.txt',
                  Body=b'Hello, World!',
                  ACL='private', 
                  Metadata={ 
                      'x-amz-meta-my-key': 'your-value'
                  }
)


関数化したもの

ファイル送信機能をapp.pyなどで使いまわしできるよう、関数化しました。
.env ファイルに以下のように各種キーを書き込んで、 access_spaces.py を app.py などでインポートして使ってみてください。

.env

SPACES_ACCESS_KEY  = '*****'
SPACES_SECRET_KEY  = '*****'
SPACES_REGION      = 'sgp1' #シンガポールの例 。適宜修正してください。
SPACES_BUCKET_NAME ='myspace' #適宜修正してください


access_spaces.py

ついでに「Spacesからファイルをダウンロード」する関数と、特定フォルダのファイル一覧を取得する関数も入れました。
※ ついでにクラス化しました。

import os
import boto3
import botocore

class digitalocean_spaces :

    session = None
    client  = None
    SPACES_BUCKET_NAME = None

    def __init__(self):
        # 環境変数を取得
        SPACES_ACCESS_KEY  = os.getenv('SPACES_ACCESS_KEY')
        SPACES_SECRET_KEY  = os.getenv('SPACES_SECRET_KEY')
        SPACES_REGION      = os.getenv('SPACES_REGION')
        SPACES_BUCKET_NAME = os.getenv('SPACES_BUCKET_NAME')

        # Boto3セッションの作成
        self.session = boto3.session.Session()
        self.client = self.session.client(
            's3',
            endpoint_url            = f'https://{SPACES_REGION}.digitaloceanspaces.com',
            config                  = botocore.config.Config(s3={'addressing_style': 'virtual'}),
            region_name             = SPACES_REGION,
            aws_access_key_id       = SPACES_ACCESS_KEY,
            aws_secret_access_key   = SPACES_SECRET_KEY
        )
        self.SPACES_BUCKET_NAME = SPACES_BUCKET_NAME
    

    def sendfile_to_spaces(self, targetfile_path:str, targetfld_to:str):
        """
        指定されたファイルをDigitalOcean Spacesにアップロードする関数です。

        Args:
            targetfile_path (str): アップロードするファイルのパス。このファイルがSpacesにアップロードされます。
            targetfld_to (str): アップロード先のSpaces内のフォルダパス。このパスにファイルが保存されます。
                                ルートフォルダに保存する場合は、空白にしてください。("."ではなく)

        """

        # ファイルをS3バケットにアップロード
        targetfile_to = os.path.join(targetfld_to, os.path.basename(targetfile_path))
        targetfile_to = targetfile_to.replace('\\', '/')
        with open(targetfile_path, 'rb') as data:
            self.client.put_object(
                Bucket   = self.SPACES_BUCKET_NAME,
                Key      = targetfile_to,
                Body     = data,
                ACL      = 'private',
                Metadata = {'x-amz-meta-my-key': 'your-value'}
            )


    def download_from_spaces(self, targetfile_fullpath:str, targetfld_to:str):
        """
        DigitalOcean Spacesから指定されたファイルをダウンロードする関数です。

        Args:
            targetfile_fullpath (str): ダウンロードするファイルのSpaces内での完全なパス。区切りは「/」で書いてください。
            targetfld_to (str): ダウンロードしたファイルを保存するローカルフォルダ名。
                                このパスにファイルが保存されます。
        """
        # ファイルをS3バケットからダウンロード
        fn = os.path.basename(targetfile_fullpath)
        self.client.download_file(self.SPACES_BUCKET_NAME, targetfile_fullpath, os.path.join(targetfld_to, fn))



    def list_files_and_folders(self, folder_name:str) -> dict:
        """
        指定されたフォルダ内のファイルとサブフォルダの一覧を取得する関数です。

        Args:
            folder_name (str): 一覧を取得したいフォルダの名前。このフォルダ内のファイルとサブフォルダがリストされます。
                               フォルダ名の末尾にスラッシュがない場合は自動的に追加されます。

        Returns:
            dict: 'file' キーにはフォルダ内のファイルのリストが、'folder' キーにはサブフォルダのリストが含まれます。
        """

        prefix = folder_name + ('/' if folder_name[-1:] != '/' else  '')
        contents = {'file': [], 'folder': []}
        paginator = self.client.get_paginator('list_objects_v2')
        for page in paginator.paginate(Bucket=self.SPACES_BUCKET_NAME, Prefix=prefix, Delimiter='/'):
            # ファイルを追加
            for obj in page.get('Contents', []):
                key = obj['Key']
                if key != prefix:  # プレフィックス自体は除外
                    contents['file'].append(key[len(prefix):])

            # フォルダを追加
            for prefix_info in page.get('CommonPrefixes', []):
                folder_name = prefix_info['Prefix'][len(prefix):-1]  # 末尾のスラッシュを除去
                contents['folder'].append(folder_name)

        return contents



# サンプルコード
if __name__ == '__main__':

    spaces = digitalocean_spaces()

    # fn = 'requirements.txt'
    # fld = 'test/test2'

    # #ファイルのアップロード 
    # spaces.sendfile_to_spaces(fn, fld)
    # #ファイルのダウンロード 
    # spaces.download_from_spaces('test/requirements.txt', 'test')


    # フォルダ内のファイル・フォルダ一覧を取得
    folder_name = 'test'
    contents = spaces.list_files_and_folders(folder_name)

    print('---ファイル一覧---')
    for file_key in contents['file']:
        print(file_key)
    print('---フォルダ一覧---')
    for folder_key in contents['folder']:
        print(folder_key)

最後までご覧いただきありがとうございました!

今回もかなりサポートセンターにお世話になったんですが、その回答内容やクイックレスポンスにとっても満足。こういうサービスって、大体はダメダメサポートですよね。
料金よりもこっちの話(ナイスなサポートセンター)のほうがDigitalOceanの推しポイントかもです。





この記事が気に入ったらサポートをしてみませんか?