この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
*このエントリはシリーズ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>
);
}
}
全体のコードはこんな感じになります。
App.js
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を作成します。