Python
AWS
S3
CloudFront
aws-sdk

AWS × Pythonで、簡単、格安、専用のファイルアップローダを作ってみた。

ファイル共有ツールといえば、無償ツールがたくさんあるので、普通はそれを使えばいいとは思います。

でも、こんなこと考えてる人いませんか?

・セキュリティが心配(ブルートフォースアタック対策がされているのかとか)
・そのツールの信頼性が心配(アップしたファイル紛失したりしないの?悪意持ってたりしない?)
・あとはダウンロード画面に広告が出てて汚い、見栄えが悪い(※あくまで個人の感想です)
・メール添付の時に、いちいちファイル容量気にするのがストレス
・アップローダの画面を開くのが面倒くさい
・自分はいいけど、お客さんがその不安を持ってしまわないか、とか

何より私はブラウザでアップローダのサイトのアップロード画面を開くまでのステップがメンドくさいと思ってしまうのです。(何かの病気でしょうか。。。そのクセ、こういうコードを書くのはメンドくさく感じないという矛盾。)

しかし、専用のシステムを開発するまでもコストも時間もかけたくないし、でもそれなりにちゃんとしているものを作りたい、AWSにログインする権限なくその仕組みを共有したいという時、この記事の通りに作れば、小一時間で完成します。ただ、ターミナルを開くのにアレルギー反応がある人は、今回は対象外です。aws-cliの設定とかもありますし。

ちなみに、この方法はアクセス時点でパスワードをかける方式ではありません。パスワードをかける場合は、今回はアップファイルをパスワード付zip化して対応します。(つまり、 中身は簡単に見れないものの、ファイル自体は取得できますので、セキュリティ的にはMaxではありません。)
*またMacユーザーならご存知かと思いますが、Macのzipは要注意です。windowsと共有する場合文字コードが異なるので、2バイト文字は使わないこと。(ファイル名は自動でユニークidに変換しますが、解凍後に化けます。)

完成の動作イメージ

1__bash_と_S3_Management_Console.png

このように、

upfile {ファイルパス}

と指定すると、

1__bash_と_S3_Management_Console.png

こんな感じでパスを作成するだけ(ファイルをS3にアップして、一時公開し、送信URLを取得)。

・ 乱数を間に挟んでいるので、公開しても特に誰かに見つけられる心配もかなり低いと思います。(3600兆分の1以下。さらにファイル名も知っている必要もある)

パスワード付きzipオプションも!

1__bash.png

第2引数に"zip" , 第3引数に"1day"とすると・・・

1__bash.png

パスワード付きzip化完了! 1dayディレクトリに保存。

*補足:Macなら以下で簡単パス入力が可能なので、ものの3秒くらいでアップロード&URL取得完了。
 ・ファイルをターミナルにドロップする
 ・FinderでCmmand+Option+"c"でコピー(ただし、空白などエスケープが必要な時は""でくくる)

便利ですね!

ダウンロードは、URLを知って入れば、誰でも可能(45日後(任意で設定)に失効)
新しいタブ_と_新しいタブ.png

どうでしょう?ファイル共有がサクサクできそうですよね。メール添付より速いですし、仮に間違えたらS3のファイルを消せばOKです。

zipパスワードはパスワード解析に備え、アルファベット+数字の10桁ランダムで作ってますので、36の10乗で、、、

3,656,158,440,062,980 通り。(3656兆)

これに加え、パスにも同じ乱数を設定してますので、この2乗の見つかりにくさです。

数値にすると、

image.png

↑Excelだとオーバーフローしてますが、京(ケイ)とか垓(ガイ)とか通り越して、10の32乗の1つ下、0.13溝(コウ)です。。。

とはいえ、解析不可ではありませんので超機密情報(個人情報、カード情報を含むなど)などの共有はお控えください。営業資料とか、ちょっとしたメモファイルとかの共有なら十分でしょう。

環境&準備するもの

<任意>
独自ドメインの取得とRoute 53への設定(独自ドメインにしたい場合)
ACMで独自ドメインのSSL証明書を発行

作成手順

  1. S3にバケットを作成します S3_Management_Console.png

S3_Management_Console.png
ログは任意で残すと良いでしょう。
暗号化くらいは設定しておくと良いでしょう。

S3_Management_Console.png
そのまま次へ

S3_Management_Console.png
「作成」を押しましょう

  • ファイルを一時有効にしたい場合は、ライフサイクルを設定します。(任意) S3_Management_Console.png 管理 > ライフサイクル からルールを追加します。

S3_Management_Console.png
名称を設定し、プレフィックス(ディレクトリ)を入力してください。

S3_Management_Console.png
何もせず次へ

S3_Management_Console.png
45日間で失効させます

S3_Management_Console.png
保存してバケットの準備は完了です。

2. CloudFrontを設定します(任意)
S3直接でもファイルダウンロードはできるので、不要な方は飛ばしてください。

CloudFrontを通すメリットは、以下のようなところでしょうか
- キャッシングによる高速化
- 同一ファイルへの多数ユーザーのアクセスがある場合、効率的
- SSL設定ができる
- 独自ドメインが設定できる
- 不正なサーバー攻撃に強い(特定地域を除外したり)

では手順です。
AWS_CloudFront_Management_Console.png
Create Distributionで作成開始します。

AWS_CloudFront_Management_Console.png

Webの方を、「Get Started」

AWS_CloudFront_Management_Console.png

先ほど作成したバケットを指定します。

AWS_CloudFront_Management_Console.png

次。HTTP to HTTPSとかにしておきましょう

AWS_CloudFront_Management_Console.png

独自ドメインにする場合は、ここにドメインを入力し、
ACMで設定した証明書をセットしてください。

AWS_CloudFront_Management_Console.png

お金に余裕があれば下を(でも、それならもっとちゃんとしたものを作っても良いかも)、それ以外の人は上で我慢しましょう(通常ファイル共有用途では支障ないと思います)。

以上で、最下部の「Create Distribution」を押して作成完了です。

3. Route53から、CloudFrontに向けます(独自ドメインを使用する場合のみ)
   Route_53_Management_Console.png

これでAWSサイドは、準備完了です。

4. Pythonコードをコピーして、任意の場所に保存して、カスタマイズし、エイリアスを作成します

# -*- coding: utf-8 -*-
import sys
import requests
import boto3
import os
import string
import random
import urllib.parse
import unicodedata
import uuid

### こちらがカスタム設定です ###
filedomain = '独自ドメイン'
# 例
#    独自ドメインの場合 : 'https://mydomain.com/' 注意!最後のスラッシュは必要
#    S3直接の場合      : 'https://{s3のregion}.amazonaws.com/{バケット名}/'
bucket = 'バケット名'
s3prefix = 'temporary/' #任意のプレフィックス(ディレクトリ)
############################
zip_flg = False   # デフォルトではzip化なしに設定

try:
    filepath = sys.argv[1]
    if (len(sys.argv) >=3 and sys.argv[2] == 'zip'):
        zip_flg = True
    if (len(sys.argv) >=4 and sys.argv[3]):
        s3prefix += sys.argv[3] + '/'
    else:
        s3prefix += '45days/'
except:
    print("*** エラー: ファイルパスを入力してください")
    sys.exit()

def is_japanese(string):
    for ch in string:
        name = unicodedata.name(ch)
        if "CJK UNIFIED" in name \
        or "HIRAGANA" in name \
        or "KATAKANA" in name:
            return True
    return False

def rand_str(n):
    return ''.join([random.choice(string.ascii_letters + string.digits) for i in range(n)])

def upload_to_s3(filepath):
    if os.path.exists(filepath):
        random_str = rand_str(10)
        if (zip_flg):
            password = rand_str(10)
            unique_code=uuid.uuid4()
            to_path =  s3prefix + random_str + '/'+ str(unique_code) + '.zip'
            fp = filepath
            if (filepath[-1:]=='/'):    fp = filepath[:-1]  # 末尾が'/'なら削除
            dir = os.path.dirname(fp)
            zip_path = dir + '/'+ str(unique_code)  + '.zip'
            if os.path.isdir(filepath):
                print("エラー:現在、ディレクトリzip圧縮はできません。ファイルを指定してください。")
                return
                # cmd = ['zip','-p',password, '-r', zip_path,filepath,'-x','*.DS_Store'] # ディレクトリzip化やるとしたらこんな感じだけどうまく行かなかったので、中止!
            else:
                cmd = ['zip', '-jP', password,zip_path,filepath]
            output = subprocess.run(cmd, stdout=subprocess.PIPE)
            output = output.stdout
            print(output)
            print ('*PasssWord : '+password)
            filepath = zip_path
        elif is_japanese(os.path.basename(filepath)):
            extension = os.path.splitext(filepath)[1]
            unique_code=uuid.uuid4()
            to_path =  s3prefix + random_str + '/'+ str(unique_code) + extension
        else:
            to_path =  s3prefix + random_str + '/'+ urllib.parse.quote(os.path.basename(filepath))
        boto3.resource('s3').Bucket(bucket).upload_file(filepath,to_path)
        boto3.resource('s3').ObjectAcl(bucket,to_path).put(ACL='public-read')
        print("**ファイルをアップロードしました。パス =  " + filedomain + to_path)
    else:
        print("**ファイルが存在しません。パス =  " + filepath)

upload_to_s3(filepath)

GitHub : https://github.com/esikm/s3fileSender/blob/master/file_upload.py
* MITライセンス

  • Python コードをカスタム設定してください

Python コードの10 ~ 14行目を修正します。後はお好きにカスタマイズするといいでしょう。
### こちらがカスタム部分です ###
filedomain = '独自ドメイン'
# 例)
# 独自ドメインの場合 : 'https://mydomain.com/' 注意!最後のスラッシュは必要
# S3直接の場合 : 'https://{s3のregion}.amazonaws.com/{バケット名}/'
# 'https://ap-northeast-1.amazonaws.com/fileshare'
bucket = 'バケット名'
s3prefix = 'temporary/'
############################

  • (任意)Python実行のエイリアスを./登録しておくと便利です 例えば、 /Applicastions/MAMP/htdocs/tool/uploader/file_upload.py にプログラムコードがある場合、
alias upfile='python /Applications/MAMP/htdocs/tool/uploader/file_upload.py'

このように設定すると、ターミナルで

upfile ファイル名(ターミナルにドラッグ)

でアップ実行が可能で便利です。
エイリアス登録は、どこにソースを置いたか忘れてしまっても"alias"コマンドですぐ発見できますので、その意味でもおすすめです。

パスはFinder上で Command + Option + c でも取得可能ですが、パスにスペースが含まれる場合、スペースの前に"\"(エスケープ)を設定するか、ダブルクォテーションで囲むようにしなければならない手間があります。
例: path/Google ドライブ/file.ext -> path/Google\ ドライブ/file.ext

ファイルをzip化したり、有効期間別にアップしたり、自由にカスタマイズしてお使いください。
エンジニア以外に共有する場合は、同じような仕組みをイントラサーバーに立てて、webページ用意してドラッグ&ドロップであげるようにするとか、いいかもですね。

今回悩ましかった(時間切れで中止した)のが、ディレクトリzip化でした。少し悔いが残ります。
まぁ、今回は時間をかけないことが1つのテーマでもあるので、こんなところで十分、日頃使う用途では十分ではないでしょうか。