てきとーになんか書きます

過去の記事はね,汚点.

node.jsで署名付きURLを発行し,PythonからS3バケットに画像をアップロードする

テストツールが,Pythonだった(アップロードのほうが本題)

前提

  • node.jsはインストール済み
    • node.js 18.12.1
    • aws-sdk 3.256.0
  • Pythonは3.6 >=ぐらいで
  • 有効期限は3600秒
  • AWSのcredentialsは設定している
    • 要するにC:\Users\<YOUR_USERNAME>\.aws\credentialsが存在する状態
    • AWS-CLI credentialsとかでググれば出ます

署名付きURLの発行

インストール

npm install aws-sdk

発行

実際はファイル分けして,バケット名・キー名・有効期限を引数で渡すとURLが返ってくる,といった感じにするはず.
myBucketバケットmyDirフォルダにtest.jpgという名前で置くとすると,以下のとおり*1

import { PutObjectCommand, S3Client} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

// S3クライアント
const REGION = "ap-northeast-1"
const client = new S3Client({
  region: REGION
});

// 発行
const s3BucketName = "myBucket"
const expiresIn = 3600
const s3Key = "myDir/test.jpg"
const objectParams = {
  Bucket: s3BucketName,
  Key: s3Key
}
const url = await getSignedUrl(client, new PutObjectCommand(objectParams), {
  expiresIn
});

アップロード

実際にPythonの方からアップロードする.
ファイルの場所やURLなどは適宜置き換え.

urllibパッケージでやる

import urllib.error
import urllib.request

s3_url = "your/s3/signed/url"
img_name = "my/image/file"

req_body = None
with open(img_name, 'rb') as f:
  req_body = f.read()
req = urllib.request.Request(
  s3_url,
  req_body,
  method="PUT"
  headers={"Content-Type": "application/octet-stream"}
)
try:
  with urllib.request.urlopen(req) as res:
    body = res.read()
except (urllib.error.HTTPError, urllib.error.URLError) as e:
  raise e

requestsパッケージでやる(うまくいかない)

requestsパッケージを使うとすると,以下のとおり:

import requests

s3_url = "your/s3/signed/url"
img_name = "my/image/file"

img = open(img_name, 'rb')
file = {'upload_file': img}
headers={"Content-Type": "application/octet-stream"}

res = requests.put(s3_url, headers=headers, files=file)
res.raise_for_status()

fileのキー名はなんでもいいけど,これでS3にputして,S3に上げた画像を見ると画像が開けない.
ファイルをテキストで確認すると以下のようになっている*2

--da72d2d3252f4f373c7a588dda07173c
Content-Disposition: form-data; name="upload_file"; filename="test.jpg"

~~~よくわからない文字列~~~
--da72d2d3252f4f373c7a588dda07173c--

これをバイナリエディタで途中から見ると以下のとおり:

...
6A 70 67 22 0D 0A 0D 0A
FF D8 FF E0 00 10 4A 46 49 46 00
...

1行目はjpg"と2行改行(CRLF x2).2行目は,分かる人は分かると思うけどjpgのヘッダ.
このような形で,画像ファイルが壊れたと言うよりは,requestsで送信する際のヘッダとフッタが付いたままのjpgになってしまっている.

基本的な用法として,サーバ側の処理として

file = req.files["upload_file"]

などとしてファイルを受け取ってやるらしいので,その際にヘッダとフッタが除去される(はず).
しかしS3バケットにあげようとすると,除去される前の画像をそのまま上げてしまって,だめ.

そもそもdictにしなければいいとなるけれど,requests.put()だとdictで送る必要がある.
じゃあrequests.post()ならとなるけれど,そうすると403 Forbiddenが返ってくるので結局だめ.

おわりに

これって,壊れてる.

*1:アップロードでなくダウンロードの場合はPutObjectCommandがGetObjectCommandに変わるぐらい.

*2:32文字の16進数はその度に変わる.