S3とフロントエンド(React+TypeScript)だけでアップローダーを作る

React(TypeScript)

bingのチャットに聞いたらフロントエンドだけでは作れないとか抜かしたので意地でも作ってみましたー。

準備

yarnとかnpmとか使うのでNodeが入っていること。

S3の設定

IAMにS3の権限が入っていて、アクセスキーとシークレットキーを取得しておく。

S3バケット作成する。アクセス許可は以下の通り。

みんなに使って貰うため、

パブリックアクセスをすべて ブロック→オフ

バケットポリシーは以下の通り

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicRead",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": "arn:aws:s3:::バケット名/*"
        }
    ]
}

CORSは以下の通り

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "PUT",
            "POST"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]

コード類

npx create-react-app test-app --template typescript

で、React(TypeScript)のひな形作成。

test-appプロジェクトの中に移動する。.envファイルはここに置き、yarn startやbuildはこの中で行うので注意。

認証情報の.envファイルは以下の感じで作成して、test-appに置く(開発環境のみ。レンタルサーバーに上げるときには認証情報は環境変数に書き込むのでアップロードしない※後述)

REACT_APP_AWS_ACCESS_KEY=AKIA*************
REACT_APP_AWS_SECRET_KEY=+5Z2**************************
BUCKET=mshiihara-test2
AWS_REGION=ap-northeast-1

コードは8割方サンプルとbingのチャットとBardで作りました。正直プロンプトエンジニアリングです。アップロード部分は、[React (typescript) ] AWS S3へのFile Uploadのパクリです。感謝! 神に祈りを!

src/index.tsxは以下の通り

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './image';

import reportWebVitals from './reportWebVitals';
import UFileUpload from './test';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <UFileUpload />
    <App />
  </React.StrictMode>
);

reportWebVitals();

src/image.tsxは

import { useState, useEffect } from 'react';
import { S3 } from 'aws-sdk';

const BUCKET_NAME = 'mshiihara-test';
const REGION = 'ap-northeast-1';

const s3 = new S3({
  region: REGION,
  accessKeyId: process.env.REACT_APP_AWS_ACCESS_KEY || '',
  secretAccessKey: process.env.REACT_APP_AWS_SECRET_KEY || '',
});
function App() {
  const [files, setFiles] = useState<S3.ObjectList>([]);

  useEffect(() => {
    s3.listObjectsV2({ Bucket: BUCKET_NAME }, (err, data) => {
      if (err) {
        console.error(err);
      } else {
        if (data.Contents) {
          // Sort files by last modified date in descending order
          const sortedFiles = data.Contents.sort((a, b) => (b.LastModified?.getTime() || 0) - (a.LastModified?.getTime() || 0));
          setFiles(sortedFiles);
        }
      }
    });
  }, [files]);

  let imgs;
  if (files && files.length > 0) {
    imgs = files.map((file, i) => (
      <img
        src={`https://${BUCKET_NAME}.s3-${REGION}.amazonaws.com/${file.Key}`}
        alt={file.Key}
        key={i}
        width="150"
      />
    ));
  }

  return (
    <div>
      {files && files.length > 0 ? (
        <div>
          <ul>{imgs}</ul>
        </div>
      ) : null}
    </div>
  );
}

export default App;

src/test.tsxは

import * as React from "react";
import Dropzone from 'react-dropzone';
import axios from 'axios';
import CSS from 'csstype';
import AWS from 'aws-sdk'

const AWS_ACCESS_KEY = process.env.REACT_APP_AWS_ACCESS_KEY;
const AWS_SECRET_KEY = process.env.REACT_APP_AWS_SECRET_KEY;

AWS.config.update({
  accessKeyId: AWS_ACCESS_KEY,
  secretAccessKey: AWS_SECRET_KEY,
  region: 'ap-northeast-1'
});

const s3 = new AWS.S3();

interface INbdProps {
}

interface INbdState {
  isUploading: boolean;
  images: any[];
}

// SDKによるアップロード
const upload_image = (file: any, bucket: string) => {

  try {
    const params = {
      Bucket: bucket,
      Key: file.name,
      ContentType: file.type,
      Body: file,
    };
 
    const res = s3.putObject(params).promise();
 
  } catch (error) {

    console.log(error);
    return;
  }
}

const baseStyle: CSS.Properties = {
  flex: 1,
  display: "flex",
  flexDirection: "column",
  alignItems: "center",
  padding: "20px",
  // borderWidth: 2,
  // borderRadius: 2,
  borderColor: "#eeeeee",
  borderStyle: "dashed",
  backgroundColor: "#fafafa",
  color: "#bdbdbd",
  outline: "none",
  transition: "border .24s ease-in-out"
};


class UFileUpload extends React.Component<INbdProps, INbdState> {

  constructor(props: INbdProps) {
    super(props);
    this.state = {
      isUploading: false,
      images: []
    };
    this.handleOnDrop = this.handleOnDrop.bind(this);
  }


  handleOnDrop(files: any[]) {

    Promise.all(files.map(file => this.uploadImage(file)))
      .then(images => {
        this.setState({
          isUploading: false,
          images: this.state.images.concat(images)
        });
      }).catch(e => console.log(e));
  }

  uploadImage = (file: any) => {

    // public access s3 へのアップロード
    upload_image(file, 'mshiihara-test')

    const params = {
      file_type: file.type,
      file_name: file.name
    }

    const options = {
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    }

    return axios.post(
      'https://mike2mike.xyz/up/', params, options)
      .then(res => {
        const img_options = {
          headers: {
            'Content-Type': file.type
          }
        };
        return axios.put(res.data.file_upload_url, file, img_options);
      })
  }

  render() {
    
    return (
      <div>
        <h1>ファイルアップローダー</h1>
        このアプリは実験のためであって、実用的なものではありません。なので、削除機能はありません。管理人が随時チェックしてファイル削除するので念のため。ただし、右クリックで保存したり、URLを収得することができます。
        <Dropzone onDrop={this.handleOnDrop} multiple maxSize={8000000} >
          {({ getRootProps, getInputProps }) => (
            <div 
              {...getRootProps({ className: "dropzone" })}
              style={baseStyle}
            >
              <input {...getInputProps()} />

              {this.state.isUploading ?
                <div>ファイルをアップロードしています</div> :
                <div>ファイルをドラックするかクリックしてファイルを選択してください</div>}
        
   
            </div>
             
          )}
        </Dropzone>
        
      </div>
    );
  }
}


export default UFileUpload;

以上。バケット名とかリージョン名は随時変更してね。https://mike2mike.xyz/up/を、http://localhost:3000にして、yarn startすればローカルで動きます。

---
ここ以下、Node.jsが動いてない環境について書いたので注意。
npm start
で実行出来て、なぜフツーのレンタルサーバーでそのまま実行出来ないかというとNode.jsが動かないから。herokuやGitHub Pagesなどにアップロードすれば、別に修正しなくても動きます。
---

そして、test-app内で、

npm run build

実行するとbuildフォルダが作られます。

buildフォルダ内に入って、index.htmlをエディタで開けて修正します。リンクが「/」になっているのを全部、「./」と相対パスにします。でないと真っ白の画面になります。

レンタルサーバーにアップロード

私が使ってるバリューサーバーの例を挙げます。

コントロールパネルにログインして左ペインに「お役立ちツール」というのがあります。この中に、自分のSSHのIPを登録する画面がありますので登録してください。通常はブロックされているので繋がりません。

次にterratermなどでSSHでサーバーに接続し、認証情報を環境変数を設定します。方法は「Linux 環境変数 export」などでググってね。認証情報が漏れると大変なことになるので注意。

terratermを閉じて、FTPでbuildフォルダをアップロードします。Public_htmlフォルダの下です。そして、フォルダの名前を変えます。わたしは「up」にしました。

これで、OK。あ、localhostにしていたら、自分のドメイン名に変更しておいてくださいね。

では、サンプル→http://mike2mike.xyz/up/

ふう、2週間掛かったわ。しかも、React(TypeScript)のことちょっとしか分かってないけど大丈夫なの?

コメント

タイトルとURLをコピーしました