はじめに
みなさん、こんにちは!「爆速でシリーズ」って聞いたことありますよね?でも、そのまま真似してみても、環境の違いやバージョンアップのせいで動かないことってありますよね。でも大丈夫!今回は、そんな壁に負けずに挑戦してみましょう!
お題概要
今回のミッションは、S3のバケットを確認して、ファイルのダウンロードとアップロードができるようにすることです。わくわくしますね!
お題詳細
ある日、企画部門から「システムで生成されるCSVデータを確認したい」という依頼が飛び込んできました。時間がない中で、どうしよう?どうしよう?と悩んでいたら、「S3を閲覧するツールを作ってみては?」というアイデアが浮かびました。他にも方法はあるかもしれませんが、今はこのアイデアで突き進むしかありません! そこで、SvelteKitを使ってS3バケットの一覧参照、ダウンロード、アップロードができるツールを作ることにしました。さあ、一緒に挑戦してみましょう!
作業概要
ここからが本番です!以下の順番で作業を進めていきます
- SvelteKitの初期設定
- .envファイルの作成
- ディレクトリ、ファイルを作る
- 追加でいろいろインストールする
- ダウンロード機能を実装する
- アップロード機能を実装する
- 一覧表示機能を作成する
- 表示用ページを作る
- 起動する
- 動かしてみた
準備はいいですか?それでは、一つずつ見ていきましょう!
1. SvelteKitの初期設定
まずは、SvelteKitのプロジェクトを立ち上げます。今回は、Node.jsのコンテナ内で作業していますが、コンテナの設定は省略させていただきます。ごめんなさいね!
公式ページを参考に、以下のコマンドを実行しました
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
$ npm create svelte@latest ./ > npx > create-svelte ./ create-svelte version 6.3.10 ┌ Welcome to SvelteKit! │ ◇ Which Svelte app template? │ SvelteKit demo app │ ◇ Add type checking with TypeScript? │ Yes, using TypeScript syntax │ ◇ Select additional options (use arrow keys/space bar) │ Add ESLint for code linting, Add Prettier for code formatting, Add Playwright for browser testing, Add Vitest for unit testing │ └ Your project is ready! Install more integrations with: npx svelte-add Next steps: 1: npm install 2: git init && git add -A && git commit -m "Initial commit" (optional) 3: npm run dev -- --open To close the dev server, hit Ctrl-C Stuck? Visit us at https://svelte.dev/chat |
選択肢はこんな感じで進めました
- SvelteKit demo appを選択
- TypeScriptを使用
- ESLint、Prettier、Playwright、Vitestを選択
最後に、npm installを実行して準備完了です!
1 |
npm install |
2. .envファイルの作成
S3にアクセスするための大切な情報を.envファイルに保存します。セキュリティ面で気になる方もいるかもしれませんが、今回は爆速重視で進めちゃいます!
1 |
touch .env |
.envファイルの中身はこんな感じです
1 2 3 4 5 6 |
# 機密 AWS_ACCESS_KEY_ID= # IAMで発行したキーを入れてください AWS_SECRET_ACCESS_KEY= # IAMで発行したシークレットキーを入れてください AWS_REGION=ap-northeast-1 # 使用するリージョンを指定してください AWS_ENDPOINT_URL=https://s3.amazonaws.com # 必要に応じて変更してください S3_BUCKET_NAME= # 使用するバケット名を入れてください |
エンドポイントは、業務用に別のものを使う可能性もあるので、変更できるようにしています。臨機応変に対応できるって素晴らしいですよね!
3. ディレクトリ、ファイルを作る
さて、ここからが本格的な作業の始まりです!各機能のコードを置くためのディレクトリとファイルを作成しましょう。
1 2 3 4 5 6 7 |
mkdir ./src/routes/api mkdir ./src/routes/api/download mkdir ./src/routes/api/upload mkdir ./src/routes/api/list-files touch ./src/routes/api/download/+server.ts touch ./src/routes/api/upload/+server.ts touch ./src/routes/api/list-files/+server.ts |
正直に言うと、私もSvelteKitをまだ完全には理解していません。でも大丈夫!
- 社内でよくSvelteKitの話を耳にする
- 最新技術に乗り遅れたくない
- GPTがあるから何とかなる!
という3つの理由でSvelteKitを選びました。ファイル構成が正攻法かどうかはわかりませんが、とにかくチャレンジです!
4. 追加でいろいろインストールする
S3の操作には@aws-sdk/client-s3を使います。これはGPTさんが教えてくれました。ありがとう、GPTさん!
さらに、@types/nodeもインストールしておくと良さそうです。準備万端ですね!
1 2 |
npm install @aws-sdk/client-s3 npm install --save-dev @types/node |
5. ダウンロード機能を実装する
GPTさんに「S3のバケットのダウンロード機能をSvelteKitで作成したい」とお願いして、こんなコードができました
src/routes/api/download/+server.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; import type { RequestHandler } from '@sveltejs/kit'; import { error } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; const s3Client = new S3Client({ region: env.AWS_REGION, credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY }, endpoint: env.AWS_ENDPOINT_URL, }); export const GET: RequestHandler = async ({ url }) => { const fileName = url.searchParams.get('file'); if (!fileName) { throw error(400, 'File name is required'); } const bucketName = env.S3_BUCKET_NAME; const s3Key = fileName; try { const command = new GetObjectCommand({ Bucket: bucketName, Key: s3Key, }); const response = await s3Client.send(command); if (!response.Body) { throw error(404, 'File not found'); } // ストリームをArrayBufferに変換 const chunks: Uint8Array[] = []; for await (const chunk of response.Body as AsyncIterable<Uint8Array>) { chunks.push(chunk); } const fileBuffer = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)); let offset = 0; for (const chunk of chunks) { fileBuffer.set(chunk, offset); offset += chunk.length; } return new Response(fileBuffer, { headers: { 'Content-Type': response.ContentType || 'application/octet-stream', 'Content-Disposition': `attachment; filename="${fileName}"`, }, }); } catch (err) { console.error("Error", err); throw error(500, 'Failed to download file from S3'); } }; |
詳細は省きますが、なんとなくバイナリデータをレスポンスに入れている感じがわかりますね。面白いです!
6. アップロード機能を実装する
アップロード機能もGPTさんにお願いしました。結果はこちら
src/routes/api/upload/+server.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import { error, json } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit'; import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; import { env } from '$env/dynamic/private'; const s3Client = new S3Client({ region: env.AWS_REGION, credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY }, endpoint: env.AWS_ENDPOINT_URL, }); export const POST: RequestHandler = async ({ request }: { request: Request }) => { try { const formData = await request.formData(); const file = formData.get('file') as File; if (!file) { throw error(400, 'No file uploaded'); } const bucketName = env.S3_BUCKET_NAME; const key = `in/${Date.now()}-${file.name}`; const fileBuffer = await file.arrayBuffer(); const putObjectParams = { Bucket: bucketName, Key: key, Body: Buffer.from(fileBuffer), ContentType: file.type, }; const command = new PutObjectCommand(putObjectParams); await s3Client.send(command); return json({ message: 'File uploaded successfully', key }); } catch (err) { console.error('Error uploading file:', err); throw error(500, 'Internal server error'); } }; |
const key = の部分に「in/」があるのは、今回はアップロード先を固定してみたかったからです。必要なければ削除してOKです!(直に書くのはよくないですね!)
7. 一覧表示機能を作成する
一覧表示機能も、もちろんGPTさんにお願いしました
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3"; import type { RequestHandler } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; const s3Client = new S3Client({ region: env.AWS_REGION, credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY }, endpoint: env.AWS_ENDPOINT_URL, }); export const GET: RequestHandler = async () => { const bucketName = env.S3_BUCKET_NAME; try { const command = new ListObjectsV2Command({ Bucket: bucketName, Delimiter: '/' }); const response = await s3Client.send(command); const allFiles = []; if (response.Contents) { const files = response.Contents .filter(item => item.Size !== 0) // サイズが0のファイルを除外 .map(item => ({ key: item.Key, size: item.Size, lastModified: item.LastModified })); allFiles.push(...files); } // バケット直下のディレクトリの処理 if (response.CommonPrefixes) { const dirs = response.CommonPrefixes.map(prefix => ({ key: prefix.Prefix, isDirectory: true })); allFiles.push(...dirs); } return json({ files: allFiles }); } catch (err) { console.error("Error listing files:", err); throw error(500, 'Failed to list files from S3'); } }; |
ListObjectsV2Commandで取得したファイル情報をallFilesに詰め込んでいるんですね。わかりやすい!
8. 表示用ページを作る
最後に表示用ページを作ります。既存のファイルを上書きしちゃいましょう!
src/routes/+page.svelte
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
<script lang="ts"> class HttpError extends Error { constructor(public status: number, message?: string) { super(message || `HTTP error! status: ${status}`); this.name = 'HttpError'; } } import { onMount } from 'svelte'; interface S3File { key: string; size: number; lastModified: string; } let files: S3File[] = []; let loading = true; let error = ''; let fileInput: HTMLInputElement; onMount(async () => { await fetchFiles(); }); async function fetchFiles() { try { loading = true; const response = await fetch('/api/list-files'); if (!response.ok) { throw new HttpError(response.status); } const data = await response.json(); files = data.files; } catch (e) { if (e instanceof HttpError) { error = `Failed to fetch files: ${e.message}`; } else { error = 'An unexpected error occurred while fetching files'; } console.error(e); } finally { loading = false; } } function formatBytes(bytes: number, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } async function downloadFile(fileName: string) { try { const response = await fetch(`/api/download?file=${encodeURIComponent(fileName)}`); if (!response.ok) throw new Error('Download failed'); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); } catch (error) { console.error('Error downloading file:', error); alert('Failed to download file'); } } async function uploadFile() { if (!fileInput.files || fileInput.files.length === 0) { alert('Please select a file to upload'); return; } const file = fileInput.files[0]; const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/upload', { method: 'POST', body: formData }); if (!response.ok) { throw new HttpError(response.status, 'Upload failed'); } alert('File uploaded successfully'); await fetchFiles(); // Refresh the file list } catch (error) { if (error instanceof HttpError) { console.error('Error uploading file:', error.message); alert(`Failed to upload file: ${error.message}`); } else { console.error('Unexpected error during upload:', error); alert('An unexpected error occurred during upload'); } } } </script> <h1>S3 Bucket Files</h1> <div class="upload-container"> <input type="file" bind:this={fileInput}> <button on:click={uploadFile}>Upload File</button> </div> {#if loading} <p>Loading...</p> {:else if error} <p class="error">{error}</p> {:else if files.length === 0} <p>No files found in the bucket.</p> {:else} <table> <thead> <tr> <th>File Name</th> <th>Size</th> <th>Last Modified</th> </tr> </thead> <tbody> {#each files as file} <tr> <td> <button on:click={() => downloadFile(file.key)} class="download-link"> {file.key} </button> </td> <td>{formatBytes(file.size)}</td> <td>{new Date(file.lastModified).toLocaleString()}</td> </tr> {/each} </tbody> </table> {/if} <style> .upload-container { margin-bottom: 20px; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } .error { color: red; } button { margin-left: 10px; padding: 5px 10px; background-color: #007bff; color: white; border: none; cursor: pointer; } button:hover { background-color: #0056b3; } .download-link { background: none; border: none; color: #007bff; text-decoration: none; cursor: pointer; padding: 0; font: inherit; } .download-link:hover { text-decoration: underline; } </style> |
styleもそれっぽく作ってくれていますね!tableタグしか使えない私には夢のようです!
9. 起動する
いよいよ起動です!以下のコマンドで立ち上げましょう
1 |
npm run dev -- --host 0.0.0.0 |
毎回、「 — –host 0.0.0.0」を入力するのが面倒な方は、vite.config.tsをこんな感じに変更しておくと便利ですよ
1 2 3 4 5 6 7 8 9 10 |
import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], server: { host: '0.0.0.0' } }); |
これで npm run dev だけで起動できます。簡単ですね!
10.動かしてみた
単に開いてみた
アップロードしてみた
OK押してみた
あれ? inとoutのSizeとLast Modifiedの表示、なんだか不思議ですね!まるで宝探しゲームみたい。これは次のアップデートで解明する楽しい謎になりそうです!
おっと、「ファイルを選択」ボタンの横のファイル名がクリアされていないみたい。でも大丈夫!これはユーザーに「あなたが最後に選んだファイルだよ〜」って親切に教えてくれているんですね。ちょっとしたサプライズ機能かも?(笑)
終わりに(後日談というか今回のオチ)
なんと驚くことに、私たちが苦労して作ったものと似たようなものを公式が作っていたんです!
https://github.com/aws-amplify/amplify-ui/issues/5731
「時代が私に追いついた!」なんて言えたら格好いいんですけどね(笑)。でも、これって逆に考えると、私たちが本当に必要とされているものを作っていたってことですよね。すごくない?
この経験を通して、私はとても大切なことを学びました。それは、「SvelteKitがよくわからなくても、GPTの力を借りればここまでできる」ということです。正直、最初は不安でしたが、やってみたらどんどん形になっていって、すごくワクワクしました!
みなさんも、「やったことないからわからない」って思っても、まずは一歩踏み出してみてください。きっと新しい発見があるはずです。失敗を恐れずに、どんどんチャレンジしていきましょう!
そうそう、最後にちょっとした宣伝です。ニフティでは、このような面白い挑戦を常に募集しています。一緒に新しいことにチャレンジしてみませんか?きっと楽しい経験になりますよ!
さあ、次はどんな冒険が待っているでしょうか。楽しみですね!それでは、また次回の「爆速シリーズ」でお会いしましょう。バイバーイ!
終わりの終わりに
いかがでしたでしょうか。何とも言えないテンションだったかと思います。今回はある程度、原稿を書いた状態で、「明るい口調で書き直して!」とGPTさんにお願いしてみたところこのような文章になりました。中の人はこんなキャラではありません。お許しください。ただ、最後の「時代が私に追いついた」の部分については、「本当に必要とされているものを作っていた」なんてポジティブな考えはまったく思っていませんでした。こういうものの考え方もあるのだなあと感心しました。コード生成だけではなく考え方の参考にもなると気づかされまして今後、活用していきたいと思います。