1. 概要
前回はCloud Firestoreのリアルタイムアップデート(onSnapshot)を使い、シンプルなチャットアプリを作る内容についてでした。今回はAWS S3に画像をアップロードしたり画面に表示する内容になります。
対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。
2. nodeのインストール
こちらを参考
3. プロジェクトを作成
3-1-1. こちらを参考
3-2-1. S3 Bucketを作成
3-3-1. AWS IAM ユーザーを作成
- ポリシー
- アクセス許可を指定
- s3:DeleteObject
- s3:GetObject
- s3:ListBucket
- s3:PutObject
- s3:PutObjectAcl
- アクセス許可を指定
3-4-1. CORSの設定
※Cross-Origin Resource Sharing(CORS)
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET"
],
"AllowedOrigins": [
"http://localhost:3000"
],
"ExposeHeaders": [
"Access-Control-Allow-Origin"
]
}
]
- 「localhost」からアクセスできるように設定
4. 必要なライブラリをインストール
4-1-1. こちらを参考
npm i @aws-sdk/client-s3
npm i @aws-sdk/s3-presigned-post
npm i @aws-sdk/s3-request-presigner
5. ソースコード
※前回より差分のみを記載
5-1-1. .env.local
NEXT_PUBLIC_AWS_REGION=******
NEXT_PUBLIC_AWS_ACCESS_KEY_ID=******
NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY=******
NEXT_PUBLIC_AWS_S3_BUCKET_NAME=******
5-1-2. next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.dog.ceo",
port: "",
pathname: "/breeds/**",
},
{
protocol: "https",
hostname: "s3.ap-northeast-1.amazonaws.com",
port: "",
pathname: "/YOUR-BUCKET-NAME/**",
},
],
},
};
module.exports = nextConfig;
5-1-3. src/app/nextjs/nextjs10/s3-client.ts
import { S3Client } from "@aws-sdk/client-s3";
const s3Client = new S3Client({
region: process.env.NEXT_PUBLIC_AWS_REGION,
credentials: {
accessKeyId: process.env.NEXT_PUBLIC_AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY!,
},
});
export default s3Client;
5-1-4. src/app/nextjs/nextjs10/handle-s3.ts
"use server";
import {
_Object,
GetObjectCommand,
paginateListObjectsV2,
ListObjectsV2CommandOutput,
} from "@aws-sdk/client-s3";
import s3Client from "./s3-client";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
import { NextResponse } from "next/server";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { Paginator } from "@smithy/types";
export type Props = {
keyName: string;
image?: FormDataEntryValue;
};
const bucketName: string = process.env.NEXT_PUBLIC_AWS_S3_BUCKET_NAME!;
const uploadFile = async (props: Props) => {
const contentType: string = "image/";
try {
const { url, fields } = await createPresignedPost(s3Client, {
Bucket: bucketName,
Key: props.keyName,
Conditions: [
["content-length-range", 0, 1000000], // content length restrictions: 0-1MB
["starts-with", "$Content-Type", contentType], // content type restriction
],
Fields: {
"Content-Type": contentType,
acl: "bucket-owner-full-control",
},
Expires: 600, //Seconds before the presigned post expires. 3600 by default.
});
const fd = new FormData();
Object.entries(fields).forEach(([key, value]) =>
fd.append(key, value as string)
);
fd.append("file", props.image!);
const res: Response = await fetch(url, { method: "POST", body: fd });
if (!res.ok) {
return NextResponse.json({ status: res.status });
}
return NextResponse.json({ status: res.ok, url, fields });
} catch (e) {
throw e;
}
};
const createPresignedUrlWithClient = async (command: GetObjectCommand) => {
return await getSignedUrl(s3Client, command, { expiresIn: 3600 });
};
const getFile = async (props: Props) => {
const key: string = props.keyName;
const command = new GetObjectCommand({
Bucket: bucketName,
Key: key,
});
const presigned: string = await createPresignedUrlWithClient(command);
return presigned;
};
const listFilesInBucket = async () => {
const pageSize: string = "10";
const objects: string[] = [];
try {
const paginator: Paginator<ListObjectsV2CommandOutput> =
paginateListObjectsV2(
{ client: s3Client, pageSize: Number.parseInt(pageSize) },
{ Bucket: bucketName }
);
for await (const page of paginator) {
if (page.Contents !== undefined) {
for (const content of page.Contents) {
if (content.Key !== undefined) {
objects.push(content.Key);
}
}
}
}
return objects;
} catch (e) {
throw e;
}
};
export { uploadFile, getFile, listFilesInBucket };
5-1-5. src/app/nextjs/nextjs10/actions.ts
"use server";
import { NextResponse } from "next/server";
import { revalidatePath } from "next/cache";
import { getFile, listFilesInBucket, uploadFile } from "./handle-s3";
import { v4 as uuidv4 } from "uuid";
import path from "path";
const uploadAction = async (prevState: any, formData: FormData) => {
const subDir: string = "test/";
const image: FormDataEntryValue | null = formData.get("image") as File;
if (image && image.size > 0) {
const extname: string = path.extname(image.name);
const uploaded: NextResponse = await uploadFile({
keyName: subDir + uuidv4() + extname,
image,
});
const resData = await uploaded.json();
prevState.success = resData.status;
}
revalidatePath("/");
return prevState;
};
const selectAction = async () => {
const files: string[] = await listFilesInBucket();
const urls: string[] = [];
for (const file of files) {
const url: string = await getFile({
keyName: file,
});
urls.push(url);
}
return urls;
};
export { uploadAction, selectAction };
5-1-6. src/app/nextjs/nextjs10/image-provider.tsx
import { useState, createContext, Context, useContext } from "react";
export type ImageProps = {
refresh: boolean;
isSeleted: boolean;
};
const initial: ImageProps = {
refresh: false,
isSeleted: false,
};
const ImageWatchContext: Context<ImageProps> = createContext(initial);
const ImageUpdateContext: Context<any> = createContext(false);
export const ImageProvider = ({ children }: any) => {
const [imageProps, setImageProps] = useState<ImageProps>(initial);
return (
<ImageWatchContext.Provider value={imageProps}>
<ImageUpdateContext.Provider value={setImageProps}>
{children}
</ImageUpdateContext.Provider>
</ImageWatchContext.Provider>
);
};
export const useWatchImage = () => useContext(ImageWatchContext);
export const useUpdateImage = () => useContext(ImageUpdateContext);
5-1-7. src/app/nextjs/nextjs10/image-list.tsx
import { useEffect, useState } from "react";
import Image from "next/image";
import { selectAction } from "./actions";
import { ImageProps, useUpdateImage, useWatchImage } from "./image-provider";
const ImageList = () => {
const [imageUrls, setImageUrls] = useState<string[]>();
const imageProps: ImageProps = useWatchImage();
const setImageProps = useUpdateImage();
useEffect(() => {
const getImages = async () => {
const urls: string[] = await selectAction();
setImageUrls(urls);
};
getImages();
if (imageProps.refresh) {
setImageProps({ ...imageProps, refresh: false });
}
}, [imageProps, setImageProps]);
return (
<>
{imageUrls ? (
imageUrls.map((imgUrl: string, idx: number) => (
<Image
key={idx}
src={imgUrl}
alt={`url${idx}`}
width={100}
height={100}
/>
))
) : (
<div>Loading image...</div>
)}
</>
);
};
export default ImageList;
5-1-8. src/app/nextjs/nextjs10/file-button.tsx
import { ChangeEvent, useRef, useState } from "react";
import { Button, Typography } from "@mui/material";
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
import { ImageProps, useUpdateImage, useWatchImage } from "./image-provider";
const FileButton = () => {
const [file, setFile] = useState<File | null>(null);
const filePickerRef = useRef<HTMLInputElement>(null);
const imageProps: ImageProps = useWatchImage();
const setImageProps = useUpdateImage();
const showFolder = () => {
if (filePickerRef.current) {
filePickerRef.current.click();
}
};
const chooseFile = (e: ChangeEvent<HTMLInputElement>) => {
const files: FileList | null = e.target.files;
if (files && files.length > 0) {
setFile(files[0]);
setImageProps({ ...imageProps, isSeleted: true });
}
};
return (
<>
<Button
variant="contained"
onClick={showFolder}
endIcon={<AddPhotoAlternateIcon />}
>
Choose
<input
type="file"
name="image"
accept={"image/*"}
ref={filePickerRef}
onChange={(e) => chooseFile(e)}
hidden
/>
</Button>
<Typography>{file?.name}</Typography>
</>
);
};
export default FileButton;
5-1-9. src/app/nextjs/nextjs10/upload-form.tsx
"use client";
import { useEffect, useRef } from "react";
import { useFormState } from "react-dom";
import { Button } from "@mui/material";
import SendIcon from "@mui/icons-material/Send";
import { uploadAction } from "./actions";
import FileButton from "./file-button";
import { ImageProps, useUpdateImage, useWatchImage } from "./image-provider";
const UploadForm = () => {
const initialFormState = {
success: false,
};
const [formState, formAction] = useFormState(uploadAction, {
initialFormState,
});
const formRef = useRef<HTMLFormElement>(null);
const imageProps: ImageProps = useWatchImage();
const setImageProps = useUpdateImage();
useEffect(() => {
if (imageProps.refresh === false && formState.success) {
setImageProps({ ...imageProps, refresh: true });
}
}, [setImageProps, formState]);
return (
<form
ref={formRef}
action={async (formData: FormData) => {
if (imageProps.isSeleted) {
await formAction(formData);
setImageProps({ ...imageProps, isSeleted: false });
formRef.current!.reset();
} else {
alert(`Please choose.`);
}
}}
>
<FileButton />
<br />
<Button type="submit" variant="contained" endIcon={<SendIcon />}>
Upload
</Button>
</form>
);
};
export default UploadForm;
5-1-10. src/app/nextjs/nextjs10/page.module.scss
.component {
color: blue;
& ul {
margin-left: 20px;
& li {
list-style: disc;
}
}
}
5-1-11. src/app/nextjs/nextjs10/child.tsx
"use client";
import { Stack } from "@mui/material";
import scss from "./page.module.scss";
import UploadForm from "./upload-form";
import ImageList from "./image-list";
import { ImageProvider } from "./image-provider";
const Child = () => {
return (
<div className={scss.component}>
<ImageProvider>
<Stack spacing={1} sx={{ width: "50%" }}>
<UploadForm />
<ImageList />
</Stack>
</ImageProvider>
</div>
);
};
export default Child;
5-1-12. src/app/nextjs/nextjs10/page.tsx
import scss from "./page.module.scss";
import GoBack from "@/lib/components/go-back";
import Child from "./child";
import { Divider } from "@mui/material";
const Nextjs10 = () => {
return (
<div className={scss.component}>
<GoBack />
<br />
<br />
<ul>
<li>AWS</li>
<ul>
<li>S3</li>
<ul>
<li>Uploading a image</li>
<li>Fetching images</li>
<li>Rendering images</li>
</ul>
</ul>
</ul>
<br />
<Divider />
<br />
<Child />
</div>
);
};
export default Nextjs10;
5-1-13. src/app/nextjs/page.tsx
"use client";
import React from "react";
import { Link } from "@mui/material";
import scss from "./page.module.scss";
const Nextjs = () => {
return (
<div className={scss.components}>
<ul>
<li>
<Link href="/nextjs/nextjs01" underline="hover">
Nextjs01
</Link>
</li>
<li>
<Link href="/nextjs/nextjs02" underline="hover">
Nextjs02
</Link>
</li>
<li>
<Link href="/nextjs/nextjs03" underline="hover">
Nextjs03
</Link>
</li>
<li>
<Link href="/nextjs/nextjs04" underline="hover">
Nextjs04
</Link>
</li>
<li>
<Link href="/nextjs/nextjs05" underline="hover">
Nextjs05
</Link>
</li>
<li>
<Link href="/nextjs/nextjs06" underline="hover">
Nextjs06
</Link>
</li>
<li>
<Link href="/nextjs/nextjs07" underline="hover">
Nextjs07
</Link>
</li>
<li>
<Link href="/nextjs/nextjs08" underline="hover">
Nextjs08
</Link>
</li>
<li>
<Link href="/nextjs/nextjs09" underline="hover">
Nextjs09
</Link>
</li>
<li>
<Link href="/nextjs/nextjs10" underline="hover">
Nextjs10
</Link>
</li>
</ul>
</div>
);
};
export default Nextjs;
6. サーバーを起動
npm run dev
7. ブラウザで確認
- http://localhost:3000
8. ディレクトリの構造
省略
9. 備考
今回はAWS S3に画像をアップロードしたり画面に表示する内容についてでした。
10. 参考
- Docs | Next.js (nextjs.org)
- Quick Start – React
- Getting Started with Redux | Redux
- Getting Started with React Redux | React Redux (react-redux.js.org)
- Material UI: React components based on Material Design (mui.com)
- Amazon S3(拡張性と耐久性を兼ね揃えたクラウドストレージ)|AWS
- AWS S3 Image Upload
- How to use S3 POST signed URLs – Advanced Web Machinery
- Use ListObjectsV2 with an AWS SDK or CLI – Amazon Simple Storage Service
- Optimizing: Images | Next.js
投稿者プロフィール
-
開発好きなシステムエンジニアです。
卓球にハマってます。