AI解析を使った画像シェアWEBアプリを超簡単に作る【S3編】#reinvent
*このエントリはシリーズ6番目の記事です。
S3ストレージの追加
アルバムに収納したい写真データを保存するストレージを追加しましょう。 保存先にはS3を使います。
amplify add storage
- Contentを選択します
- リソースカテゴリとS3バケット名をつけます
- アクセス権は”Auth Users Only”と”read/write”を選択します
*バケット名はグローバルユニークである必要があるのですが、同じ名前のバケットがすでに存在する場合リソースがうまく作成されない場合があるので注意です
$ amplify add storage ? Please select from one of the below mentioned services Content (Images, audio, video, etc.) ? Please provide a friendly name for your resource that will be used to label this category in the project: alb umphotos ? Please provide bucket name: albumphotos ? Who should have access: Auth users only ? What kind of access do you want for Authenticated users read/write Successfully added resource albumphotos locally Some next steps: "amplify push" builds all of your local backend resources and provisions them in the cloud "amplify publish" builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud
設定が完了したらpushコマンドでリソースを作成します。
amplify push
写真アップロード機能を作る
作成したS3バケットに写真を保存できるようUIを変更しましょう。 src/App.jsのコードを更新します。
モジュールの追加
- uuidモジュールの追加
- aws-amplifyのStorageモジュールの追加
- semantic-ui-reactのDividerとFormモジュールの追加
- aws-amplify-reactのS3Imageモジュールの追加
uuidモジュールはインストールします。
npm install --save uuid
import React, { Component } from 'react'; import {BrowserRouter as Router, Route, NavLink} from 'react-router-dom'; import { Divider, Form, Grid, Header, Input, List, Segment } from 'semantic-ui-react'; import {v4 as uuid} from 'uuid'; import { Connect, S3Image, withAuthenticator } from 'aws-amplify-react'; import Amplify, { API, graphqlOperation, Storage } from 'aws-amplify'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports);
GetAlbumクエリを更新
GetAlbum変数に写真データを取得するクエリを加えます。
const GetAlbum = `query GetAlbum($id: ID!, $nextTokenForPhotos: String) { getAlbum(id: $id) { id name photos(sortDirection: DESC, nextToken: $nextTokenForPhotos) { nextToken items { thumbnail { width height key } } } } } `;
写真アップロード機能の追加
S3ImageUploadコンポーネントを追加します。
class S3ImageUpload extends React.Component { constructor(props) { super(props); this.state = { uploading: false } } uploadFile = async (file) => { const fileName = uuid(); const result = await Storage.put( fileName, file, { customPrefix: { public: 'uploads/' }, metadata: { albumid: this.props.albumId } } ); console.log('Uploaded file: ', result); } onChange = async (e) => { this.setState({uploading: true}); let files = []; for (var i=0; i<e.target.files.length; i++) { files.push(e.target.files.item(i)); } await Promise.all(files.map(f => this.uploadFile(f))); this.setState({uploading: false}); } render() { return ( <div> <Form.Button onClick={() => document.getElementById('add-image-file-input').click()} disabled={this.state.uploading} icon='file image outline' content={ this.state.uploading ? 'Uploading...' : 'Add Images' } /> <input id='add-image-file-input' type="file" accept='image/*' multiple onChange={this.onChange} style={{ display: 'none' }} /> </div> ); } }
AlbumDetailsLoaderを更新
class AlbumDetailsLoader extends React.Component { constructor(props) { super(props); this.state = { nextTokenForPhotos: null, hasMorePhotos: true, album: null, loading: true } } async loadMorePhotos() { if (!this.state.hasMorePhotos) return; this.setState({ loading: true }); const { data } = await API.graphql(graphqlOperation(GetAlbum, {id: this.props.id, nextTokenForPhotos: this.state.nextTokenForPhotos})); let album; if (this.state.album === null) { album = data.getAlbum; } else { album = this.state.album; album.photos.items = album.photos.items.concat(data.getAlbum.photos.items); } this.setState({ album: album, loading: false, nextTokenForPhotos: data.getAlbum.photos.nextToken, hasMorePhotos: data.getAlbum.photos.nextToken !== null }); } componentDidMount() { this.loadMorePhotos(); } render() { return ( <AlbumDetails loadingPhotos={this.state.loading} album={this.state.album} loadMorePhotos={this.loadMorePhotos.bind(this)} hasMorePhotos={this.state.hasMorePhotos} /> ); } }
PhotosListコンポーネントを新規追加
次章で写真がアップロードされた際にサムネイルを自動生成するLambdaを書くのですが、 作成されたサムネイルを表示させるコンポーネントを先にここで作成しておきます。
写真データはAlbumDetailsの
class PhotosList extends React.Component { photoItems() { return this.props.photos.map(photo => <S3Image key={photo.thumbnail.key} imgKey={photo.thumbnail.key.replace('public/', '')} style={{display: 'inline-block', 'paddingRight': '5px'}} /> ); } render() { return ( <div> <Divider hidden /> {this.photoItems()} </div> ); } }
全体のコードはこんな感じになります。
import React, { Component } from 'react'; import {BrowserRouter as Router, Route, NavLink} from 'react-router-dom'; import { Divider, Form, Grid, Header, Input, List, Segment } from 'semantic-ui-react'; import {v4 as uuid} from 'uuid'; import { Connect, S3Image, withAuthenticator } from 'aws-amplify-react'; import Amplify, { API, graphqlOperation, Storage } from 'aws-amplify'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports); function makeComparator(key, order='asc') { return (a, b) => { if(!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) return 0; const aVal = (typeof a[key] === 'string') ? a[key].toUpperCase() : a[key]; const bVal = (typeof b[key] === 'string') ? b[key].toUpperCase() : b[key]; let comparison = 0; if (aVal > bVal) comparison = 1; if (aVal < bVal) comparison = -1; return order === 'desc' ? (comparison * -1) : comparison }; } const ListAlbums = `query ListAlbums { listAlbums(limit: 9999) { items { id name } } }`; const SubscribeToNewAlbums = ` subscription OnCreateAlbum { onCreateAlbum { id name } } `; const GetAlbum = `query GetAlbum($id: ID!, $nextTokenForPhotos: String) { getAlbum(id: $id) { id name photos(sortDirection: DESC, nextToken: $nextTokenForPhotos) { nextToken items { thumbnail { width height key } } } } } `; class S3ImageUpload extends React.Component { constructor(props) { super(props); this.state = { uploading: false } } uploadFile = async (file) => { const fileName = uuid(); const result = await Storage.put( fileName, file, { customPrefix: { public: 'uploads/' }, metadata: { albumid: this.props.albumId } } ); console.log('Uploaded file: ', result); } onChange = async (e) => { this.setState({uploading: true}); let files = []; for (var i=0; i<e.target.files.length; i++) { files.push(e.target.files.item(i)); } await Promise.all(files.map(f => this.uploadFile(f))); this.setState({uploading: false}); } render() { return ( <div> <Form.Button onClick={() => document.getElementById('add-image-file-input').click()} disabled={this.state.uploading} icon='file image outline' content={ this.state.uploading ? 'Uploading...' : 'Add Images' } /> <input id='add-image-file-input' type="file" accept='image/*' multiple onChange={this.onChange} style={{ display: 'none' }} /> </div> ); } } class PhotosList extends React.Component { photoItems() { return this.props.photos.map(photo => <S3Image key={photo.thumbnail.key} imgKey={photo.thumbnail.key.replace('public/', '')} style={{display: 'inline-block', 'paddingRight': '5px'}} /> ); } render() { return ( <div> <Divider hidden /> {this.photoItems()} </div> ); } } class NewAlbum extends Component { constructor(props) { super(props); this.state = { albumName: '' }; } handleChange = (event) => { let change = {}; change[event.target.name] = event.target.value; this.setState(change); } handleSubmit = async (event) => { event.preventDefault(); const NewAlbum = `mutation NewAlbum($name: String!) { createAlbum(input: {name: $name}) { id name } }`; const result = await API.graphql(graphqlOperation(NewAlbum, { name: this.state.albumName })); console.info(`Created album with id ${result.data.createAlbum.id}`); this.setState({ albumName: '' }) } render() { return ( <Segment> <Header as='h3'>Add a new album</Header> <Input type='text' placeholder='New Album Name' icon='plus' iconPosition='left' action={{ content: 'Create', onClick: this.handleSubmit }} name='albumName' value={this.state.albumName} onChange={this.handleChange} /> </Segment> ) } } class AlbumsList extends React.Component { albumItems() { return this.props.albums.sort(makeComparator('name')).map(album => <List.Item key={album.id}> <NavLink to={`/albums/${album.id}`}>{album.name}</NavLink> </List.Item> ); } render() { return ( <Segment> <Header as='h3'>My Albums</Header> <List divided relaxed> {this.albumItems()} </List> </Segment> ); } } class AlbumDetailsLoader extends React.Component { constructor(props) { super(props); this.state = { nextTokenForPhotos: null, hasMorePhotos: true, album: null, loading: true } } async loadMorePhotos() { if (!this.state.hasMorePhotos) return; this.setState({ loading: true }); const { data } = await API.graphql(graphqlOperation(GetAlbum, {id: this.props.id, nextTokenForPhotos: this.state.nextTokenForPhotos})); let album; if (this.state.album === null) { album = data.getAlbum; } else { album = this.state.album; album.photos.items = album.photos.items.concat(data.getAlbum.photos.items); } this.setState({ album: album, loading: false, nextTokenForPhotos: data.getAlbum.photos.nextToken, hasMorePhotos: data.getAlbum.photos.nextToken !== null }); } componentDidMount() { this.loadMorePhotos(); } render() { return ( <AlbumDetails loadingPhotos={this.state.loading} album={this.state.album} loadMorePhotos={this.loadMorePhotos.bind(this)} hasMorePhotos={this.state.hasMorePhotos} /> ); } } class AlbumDetails extends Component { render() { if (!this.props.album) return 'Loading album...'; return ( <Segment> <Header as='h3'>{this.props.album.name}</Header> <S3ImageUpload albumId={this.props.album.id}/> <PhotosList photos={this.props.album.photos.items} /> { this.props.hasMorePhotos && <Form.Button onClick={this.props.loadMorePhotos} icon='refresh' disabled={this.props.loadingPhotos} content={this.props.loadingPhotos ? 'Loading...' : 'Load more photos'} /> } </Segment> ) } } class AlbumsListLoader extends React.Component { onNewAlbum = (prevQuery, newData) => { // When we get data about a new album, we need to put in into an object // with the same shape as the original query results, but with the new data added as well let updatedQuery = Object.assign({}, prevQuery); updatedQuery.listAlbums.items = prevQuery.listAlbums.items.concat([newData.onCreateAlbum]); return updatedQuery; } render() { return ( <Connect query={graphqlOperation(ListAlbums)} subscription={graphqlOperation(SubscribeToNewAlbums)} onSubscriptionMsg={this.onNewAlbum} > {({ data, loading }) => { if (loading) { return <div>Loading...</div>; } if (!data.listAlbums) return; return <AlbumsList albums={data.listAlbums.items} />; }} </Connect> ); } } class App extends Component { render() { return ( <Router> <Grid padded> <Grid.Column> <Route path="/" exact component={NewAlbum}/> <Route path="/" exact component={AlbumsListLoader}/> <Route path="/albums/:albumId" render={ () => <div><NavLink to='/'>Back to Albums list</NavLink></div> } /> <Route path="/albums/:albumId" render={ props => <AlbumDetailsLoader id={props.match.params.albumId}/> } /> </Grid.Column> </Grid> </Router> ); } } export default withAuthenticator(App, {includeGreetings: true});
写真をアップロードしてみる
アルバムを選択してAdd Imagesボタンから写真をアップロードしてみましょう。
まだ写真を表示させる機能は実装できていないので、マネジメントコンソールからS3バケットを確認してみます。 バケット名はsrc/aws-exports.jsのaws_user_files_s3_bucketに保存されています。
次は、アップデートした写真の一覧表示ができるように、写真のサムネイルを自動生成するLambdaを作成します。