Next.jsとSupabaseを用いた画像管理
概要
-
画像管理システムの概要
- Next.jsとSupabaseの組み合わせによる効率的な画像管理
- Supabase Storageを使用した画像のアップロードと保存
- 画像のPublic URLの取得と表示
-
主要コンポーネントの解説
- ImageUploaderコンポーネント:画像アップロード用インターフェース
- ImageDisplayコンポーネント:Supabase Storageから取得した画像の表示
- ImageManagerコンポーネント:画像のアップロードと表示の統合
-
署名付きURL(Signed URL)の使用
- セキュリティ向上のための署名付きURL生成
- 署名付きURLを使用した画像表示コンポーネント
-
Supabaseストレージポリシーの詳細解説
- 5つの代表的なポリシー例とその使用方法
- ポリシーの比較表
-
画像ファイル管理のベストプラクティス
- 最適なファイル形式の選択(WebP形式の採用)
- 画像の最適化(サーバーサイドでの画像リサイズ)
- 効率的なストレージ構造
- CDNの活用
- セキュリティとアクセス制御
- バックアップと冗長性
-
日本語ファイル名アップロード問題の解決策
- ファイル名のエンコーディング
- ユニークIDを使用したファイル名の生成
- ファイル名の正規化
-
ユニークID使用によるファイル名生成の実装
- 実装例とコード
- 元のファイル名の保持方法
-
元のファイル名の保存と利用
- データモデルの設計
- ファイルメタデータの保存と取得
- 元のファイル名での検索機能
Next.jsとSupabaseを用いた画像管理
Webアプリケーションにおいて、画像の管理と表示は重要な機能の一つです。本記事では、Next.jsとSupabaseを組み合わせて、効率的な画像管理システムを構築する方法を解説します。
Supabaseとは
Supabaseは、オープンソースのFirebase代替サービスです。PostgreSQLデータベース、認証、ストレージなどの機能を提供し、Webアプリケーション開発を効率化します。
実装の概要
- Supabase Storageを使用した画像のアップロードと保存
- 保存された画像のPublic URLの取得と表示
- 画像のアップロードと表示を行うReactコンポーネントの実装
主要コンポーネントの解説
1. ImageUploader コンポーネント
このコンポーネントは、ユーザーが画像をアップロードするためのインターフェースを提供します。
const ImageUploader = () => {
const [file, setFile] = useState<File | null>(null)
const handleUpload = async () => {
if (!file) return
const { data, error } = await supabase.storage
.from('images')
.upload(`public/${file.name}`, file)
if (error) {
console.error('Error uploading file:', error)
} else {
console.log('File uploaded successfully:', data)
}
}
return (
<div>
<input type="file" onChange={(e) => setFile(e.target.files?.[0] || null)} />
<button onClick={handleUpload}>Upload</button>
</div>
)
}
2. ImageDisplay コンポーネント
このコンポーネントは、Supabase Storageから取得した画像を表示します。
const ImageDisplay = ({ imagePath }: { imagePath: string }) => {
const [imageUrl, setImageUrl] = useState<string | null>(null)
useEffect(() => {
const fetchImageUrl = async () => {
const { data } = await supabase.storage.from('images').getPublicUrl(imagePath)
setImageUrl(data.publicUrl)
}
fetchImageUrl()
}, [imagePath])
if (!imageUrl) return <div>Loading...</div>
return <Image src={imageUrl} alt="Uploaded image" width={300} height={200} />
}
3. ImageManager コンポーネント
このメインコンポーネントは、画像のアップロードと表示を統合します。
const ImageManager = () => {
const [images, setImages] = useState<string[]>([])
useEffect(() => {
const fetchImages = async () => {
const { data, error } = await supabase.storage.from('images').list('public')
if (data) {
setImages(data.map(file => `public/${file.name}`))
}
}
fetchImages()
}, [])
return (
<div>
<h1>Image Manager</h1>
<ImageUploader />
<div>
{images.map(imagePath => (
<ImageDisplay key={imagePath} imagePath={imagePath} />
))}
</div>
</div>
)
}
署名付きURL(Signed URL)の使用
セキュリティが重要な場合や、一時的なアクセスを提供したい場合は、署名付きURLを使用することができます。Supabaseは署名付きURLの生成をサポートしています。
署名付きURLの生成
const getSignedUrl = async (filePath: string) => {
const { data, error } = await supabase
.storage
.from('images')
.createSignedUrl(filePath, 60) // 60秒間有効なURLを生成
if (error) {
console.error('Error creating signed URL:', error)
return null
}
return data.signedUrl
}
署名付きURLを使用した画像表示コンポーネント
const SecureImageDisplay = ({ imagePath }: { imagePath: string }) => {
const [imageUrl, setImageUrl] = useState<string | null>(null)
useEffect(() => {
const fetchSignedUrl = async () => {
const signedUrl = await getSignedUrl(imagePath)
setImageUrl(signedUrl)
}
fetchSignedUrl()
}, [imagePath])
if (!imageUrl) return <div>Loading...</div>
return <Image src={imageUrl} alt="Secure image" width={300} height={200} />
}
署名付きURLの利点
- セキュリティ: 一時的なアクセス権を付与するため、セキュリティが向上します。
- アクセス制御: URLの有効期限を設定できるため、アクセスを時間で制限できます。
- 非公開コンテンツの共有: 公開せずに特定のユーザーとコンテンツを共有できます。
使用上の注意点
- パフォーマンス: 署名付きURLは動的に生成されるため、キャッシュが効きにくい場合があります。
- 有効期限の管理: URLの有効期限を適切に設定し、必要に応じて更新する仕組みが必要です。
実装時の選択: Public URL vs 署名付きURL
プロジェクトの要件に応じて、Public URLと署名付きURLを適切に使い分けることが重要です。
- Public URL: 公開コンテンツや、アクセス制御が不要な画像に適しています。
- 署名付きURL: セキュリティが重要な非公開コンテンツや、一時的なアクセスを提供したい場合に適しています。
両方の方法を組み合わせることで、柔軟で安全な画像管理システムを構築することができます。
実装のポイント
-
Supabaseクライアントの初期化: アプリケーションの開始時にSupabaseクライアントを初期化します。
-
画像のアップロード:
supabase.storage.from('images').upload()
メソッドを使用して、画像をSupabase Storageにアップロードします。 -
Public URLの取得:
supabase.storage.from('images').getPublicUrl()
メソッドで、アップロードした画像のPublic URLを取得します。 -
画像の表示: Next.jsの
Image
コンポーネントを使用して、最適化された画像を表示します。
Supabaseを使用する利点
- 簡単な設定: 認証やストレージなどの機能をすぐに利用できます。
- リアルタイム機能: データベースの変更をリアルタイムで監視できます。
- スケーラビリティ: アプリケーションの成長に合わせて自動的にスケールします。
実装時の注意点
- セキュリティ: Public URLを使用する場合、アクセス制御に注意が必要です。
- パフォーマンス: 大量の画像を扱う場合は、ページネーションや遅延ロードの実装を検討しましょう。
- エラーハンドリング: アップロードや取得時のエラーを適切に処理し、ユーザーにフィードバックを提供することが重要です。
-
画像の最適化: Next.jsの
Image
コンポーネントを活用して、自動的に画像を最適化しましょう。
まとめ
Next.jsとSupabaseを組み合わせることで、効率的で拡張性の高い画像管理システムを構築できます。この基本的な実装をベースに、プロジェクトの要件に合わせてカスタマイズすることで、より高度な機能を持つシステムを開発することができます。
Supabaseを使用した署名付き画像URLの実装と活用
Webアプリケーションにおいて、セキュアな画像管理は重要な課題です。Supabaseの署名付きURL機能を利用することで、安全かつ柔軟な画像アクセス制御を実現できます。本記事では、Next.jsとSupabaseを使用して署名付き画像URLを実装し、効果的に活用する方法を詳しく解説します。
署名付きURLとは
署名付きURLは、一時的なアクセス権を付与する特別なURLです。以下の特徴があります:
- 有効期限付きのアクセス
- 特定のユーザーやセッションに限定したアクセス
- URLに埋め込まれた暗号署名によるセキュリティ
Supabaseでの署名付きURL実装
1. 基本的な署名付きURL生成
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_ANON_KEY')
const getSignedUrl = async (filePath: string, expiresIn: number = 60) => {
const { data, error } = await supabase
.storage
.from('images')
.createSignedUrl(filePath, expiresIn)
if (error) {
console.error('Error creating signed URL:', error)
return null
}
return data.signedUrl
}
2. 署名付きURLを使用した画像表示コンポーネント
import { useState, useEffect } from 'react'
import Image from 'next/image'
const SecureImageDisplay = ({ imagePath }: { imagePath: string }) => {
const [imageUrl, setImageUrl] = useState<string | null>(null)
useEffect(() => {
const fetchSignedUrl = async () => {
const signedUrl = await getSignedUrl(imagePath)
setImageUrl(signedUrl)
}
fetchSignedUrl()
}, [imagePath])
if (!imageUrl) return <div>Loading...</div>
return <Image src={imageUrl} alt="Secure image" width={300} height={200} />
}
高度な実装テクニック
1. 動的な有効期限設定
ユースケースに応じて有効期限を動的に設定できます。
const getSignedUrlWithCustomExpiration = async (filePath: string, userRole: string) => {
let expiresIn = 60 // デフォルト: 60秒
switch (userRole) {
case 'admin':
expiresIn = 3600 // 管理者には1時間の有効期限
break
case 'premium':
expiresIn = 600 // プレミアムユーザーには10分の有効期限
break
}
return getSignedUrl(filePath, expiresIn)
}
2. バッチ処理での署名付きURL生成
複数の画像に対して効率的に署名付きURLを生成します。
const getMultipleSignedUrls = async (filePaths: string[]) => {
const signedUrls = await Promise.all(
filePaths.map(path => getSignedUrl(path))
)
return signedUrls.filter(url => url !== null)
}
3. エラーハンドリングと再試行ロジック
ネットワークエラーなどに対応するための再試行ロジックを実装します。
const getSignedUrlWithRetry = async (filePath: string, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
const url = await getSignedUrl(filePath)
if (url) return url
} catch (error) {
console.error(`Attempt ${i + 1} failed:`, error)
if (i === maxRetries - 1) throw error
}
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
}
}
セキュリティ考慮事項
-
適切な有効期限設定: 長すぎる有効期限は安全性を低下させます。必要最小限の期間を設定しましょう。
-
アクセス制御の組み合わせ: 署名付きURLと、Supabaseの認証・認可機能を組み合わせることで、多層的なセキュリティを実現できます。
const getSecureSignedUrl = async (filePath: string, userId: string) => {
const { data: permissions, error } = await supabase
.from('user_permissions')
.select('can_access')
.eq('user_id', userId)
.eq('file_path', filePath)
.single()
if (error || !permissions?.can_access) {
console.error('User does not have permission to access this file')
return null
}
return getSignedUrl(filePath)
}
- URL漏洩対策: クライアントサイドでURLを安全に扱うための対策を実装します。
// URLを暗号化してクライアントに送信
const encryptUrl = (url: string) => {
// 実際の暗号化ロジックをここに実装
return btoa(url) // 簡単な例としてBase64エンコーディングを使用
}
// クライアントサイドでURLを復号
const decryptUrl = (encodedUrl: string) => {
return atob(encodedUrl)
}
パフォーマンス最適化
- キャッシュの活用: 短期間有効な署名付きURLをクライアントサイドでキャッシュし、不必要なサーバーリクエストを減らします。
const cachedSignedUrls = new Map<string, { url: string, expiry: number }>()
const getCachedSignedUrl = async (filePath: string) => {
const now = Date.now()
const cached = cachedSignedUrls.get(filePath)
if (cached && cached.expiry > now) {
return cached.url
}
const newSignedUrl = await getSignedUrl(filePath)
if (newSignedUrl) {
cachedSignedUrls.set(filePath, { url: newSignedUrl, expiry: now + 55000 }) // 55秒間キャッシュ
}
return newSignedUrl
}
- プリフェッチ: ユーザーの行動を予測し、事前に署名付きURLを生成します。
const prefetchSignedUrls = async (likelyFilePaths: string[]) => {
likelyFilePaths.forEach(async (path) => {
if (!cachedSignedUrls.has(path)) {
await getCachedSignedUrl(path)
}
})
}
まとめ
Supabaseの署名付きURL機能を活用することで、セキュアで柔軟な画像アクセス制御を実現できます。適切な実装と最適化により、セキュリティとパフォーマンスの両立が可能です。
supabaseの画像ストレージに関するポリシーに関して
Supabaseストレージポリシーの詳細解説
はじめに
Supabaseのストレージポリシーは、アプリケーションのセキュリティと機能性を両立させるための強力なツールです。本記事では、5つの代表的なポリシーについて、より詳細に解説し、その使用方法と適用例を紹介します。
ポリシー比較表
ポリシー | 対象ユーザー | ファイル種類 | アクセス場所 | 操作 | 主な用途 |
---|---|---|---|---|---|
1. 匿名JPGアクセス | 匿名 | JPG画像のみ | 'public'フォルダ | 読み取り | 公開ギャラリー |
2. ユーザーフォルダ | 認証済み | 全種類 | ユーザーUID名フォルダ | 全操作 | 個人データ管理 |
3. 認証済み特定フォルダ | 認証済み | 全種類 | 'private'フォルダ | 全操作 | 会員限定コンテンツ |
4. 特定ユーザーネストフォルダ | 特定UID | 全種類 | 'admin/assets'フォルダ | 全操作 | 管理者リソース |
5. 特定ユーザー特定ファイル | 特定UID | 特定ファイルのみ | 特定ファイルパス | 読み取り | 機密文書アクセス |
ポリシーの詳細解説
1. 匿名ユーザーへのJPG画像アクセス許可
CREATE POLICY "allow_public_jpg_access" ON storage.objects
FOR SELECT
USING (
bucket_id = 'public_images'
AND storage.extension(name) = 'jpg'
AND LOWER((storage.foldername(name))[1]) = 'public'
AND auth.role() = 'anon'
);
特徴:
- 対象:匿名ユーザー
- ファイル種類:JPG画像のみ
- 場所:'public'フォルダ内
- 操作:SELECT(読み取り)のみ
使用例:
- 公開ウェブサイトの画像ギャラリー
- 製品カタログの表示
2. ユーザー固有フォルダへのアクセス許可
CREATE POLICY "allow_user_folder_access" ON storage.objects
FOR ALL
USING (
bucket_id = 'user_data'
AND (auth.uid()::text) = (storage.foldername(name))[1]
);
特徴:
- 対象:認証済みユーザー
- ファイル種類:全種類
- 場所:ユーザーのUID名と一致するトップレベルフォルダ
- 操作:全操作(SELECT, INSERT, UPDATE, DELETE)
使用例:
- ユーザープロファイル画像の管理
- 個人文書のストレージ
3. 認証済みユーザーへの特定フォルダアクセス許可
CREATE POLICY "allow_authenticated_private_access" ON storage.objects
FOR ALL
USING (
bucket_id = 'app_data'
AND (storage.foldername(name))[1] = 'private'
AND auth.role() = 'authenticated'
);
特徴:
- 対象:認証済みユーザー
- ファイル種類:全種類
- 場所:'private'フォルダ内
- 操作:全操作
使用例:
- 会員限定コンテンツの提供
- 共有ドキュメントリポジトリ
4. 特定ユーザーへのネストフォルダアクセス許可
CREATE POLICY "allow_admin_assets_access" ON storage.objects
FOR ALL
USING (
bucket_id = 'admin_data'
AND (storage.foldername(name))[1] = 'admin'
AND (storage.foldername(name))[2] = 'assets'
AND auth.uid()::text = 'd7bed83c-44a0-4a4f-925f-efc384ea1e50'
);
特徴:
- 対象:特定のUID('d7bed83c-44a0-4a4f-925f-efc384ea1e50')を持つユーザー
- ファイル種類:全種類
- 場所:'admin/assets'フォルダ内
- 操作:全操作
使用例:
- 管理者専用リソースへのアクセス
- 特定プロジェクトのアセット管理
5. 特定ユーザーへの特定ファイルアクセス許可
CREATE POLICY "allow_specific_file_access" ON storage.objects
FOR SELECT
USING (
bucket_id = 'restricted_data'
AND name = 'admin/assets/Costa Rican Frog.jpg'
AND auth.uid()::text = 'd7bed83c-44a0-4a4f-925f-efc384ea1e50'
);
特徴:
- 対象:特定のUID('d7bed83c-44a0-4a4f-925f-efc384ea1e50')を持つユーザー
- ファイル:'admin/assets/Costa Rican Frog.jpg'という特定のファイルのみ
- 操作:SELECT(読み取り)のみ
使用例:
- 機密文書へのアクセス制御
- ライセンス管理されたコンテンツの配布
まとめ
Supabaseのストレージポリシーは、細かな粒度でアクセス制御を行うことができる強力なツールです。適切なポリシーを組み合わせることで、セキュリティと利便性を両立したストレージシステムを構築できます。
Supabaseにおける画像ファイル管理のベストプラクティス
Supabaseを使用してウェブアプリケーションの画像ファイルを管理する際、効率性、パフォーマンス、そしてコスト最適化を考慮することが重要です。本記事では、Supabaseのストレージを活用した画像ファイル管理のベストプラクティスについて詳しく解説します。
1. 最適なファイル形式の選択
WebP形式の採用
WebP形式は、画質を維持しながらファイルサイズを大幅に削減できる現代的な画像フォーマットです。
実装例:
async function convertToWebP(file) {
const image = await loadImage(file);
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext('2d').drawImage(image, 0, 0);
return new Promise(resolve => {
canvas.toBlob(blob => resolve(blob), 'image/webp', 0.8);
});
}
async function uploadImage(file) {
const webpBlob = await convertToWebP(file);
const { data, error } = await supabase.storage
.from('images')
.upload(`webp/${file.name.replace(/\.[^/.]+$/, ".webp")}`, webpBlob);
// エラー処理とレスポンス処理
}
適応的な画像形式の提供
ブラウザのサポート状況に応じて、WebPとJPEG/PNGの両方を提供します。
HTML実装例:
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="説明">
</picture>
2. 画像の最適化
サーバーサイドでの画像リサイズ
アップロード時に複数のサイズにリサイズし、用途に応じて適切なサイズを提供します。
実装例 (Node.js + Sharp):
const sharp = require('sharp');
async function resizeAndUpload(file) {
const sizes = [200, 400, 800];
const resizedImages = await Promise.all(sizes.map(size =>
sharp(file.buffer)
.resize(size)
.webp()
.toBuffer()
));
// Supabaseにアップロード
const uploadPromises = resizedImages.map((buffer, index) =>
supabase.storage
.from('images')
.upload(`resized/${file.name}_${sizes[index]}.webp`, buffer)
);
return Promise.all(uploadPromises);
}
メタデータの最適化
不要なメタデータを削除し、ファイルサイズを削減します。
3. 効率的なストレージ構造
命名規則の統一
一貫性のある命名規則を採用し、管理を容易にします。
例:
images/
├─ original/
│ └─ {uuid}.{ext}
├─ webp/
│ └─ {uuid}.webp
└─ thumbnails/
├─ small/
│ └─ {uuid}_200.webp
├─ medium/
│ └─ {uuid}_400.webp
└─ large/
└─ {uuid}_800.webp
バケットの適切な分割
用途や公開範囲に応じてバケットを分割します。
4. CDNの活用
Supabaseのストレージは自動的にCDNを利用しますが、キャッシュ戦略を最適化することで、さらなるパフォーマンス向上が可能です。
キャッシュ制御の例:
const { data, error } = await supabase.storage
.from('images')
.upload('image.webp', file, {
cacheControl: '3600',
upsert: false
});
5. セキュリティとアクセス制御
適切なポリシーの設定
公開画像と非公開画像を適切に分離し、アクセス制御を行います。
ポリシー例:
CREATE POLICY "public_images_access"
ON storage.objects FOR SELECT
USING (bucket_id = 'public_images' AND auth.role() = 'anon');
署名付きURLの活用
一時的なアクセス権を付与する場合は、署名付きURLを使用します。
実装例:
const { signedURL, error } = await supabase.storage
.from('private_images')
.createSignedUrl('image.jpg', 60);
6. バックアップと冗長性
定期的なバックアップ
重要な画像データは定期的にバックアップを作成します。
クロスリージョンレプリケーション
可用性を高めるため、複数のリージョンにデータをレプリケートすることを検討します。
まとめ
Supabaseを使用した画像ファイル管理では、最適なファイル形式の選択、効率的なストレージ構造の設計、CDNの活用、適切なセキュリティ対策が重要です。これらのベストプラクティスを組み合わせることで、高性能で費用対効果の高い画像管理システムを構築できます。
Supabaseでの日本語ファイル名アップロード問題の解決策
問題の概要
Supabaseのストレージに日本語のファイル名でファイルをアップロードしようとすると、エラーが発生する場合があります。これは、非ASCII文字を含むファイル名がURLエンコーディングされていないことが原因です。
解決策
以下に、この問題を解決するためのいくつかの方法を示します。
1. ファイル名のエンコーディング
ファイル名をURLエンコードすることで、非ASCII文字を安全に扱うことができます。
import { encode } from 'js-base64';
export const uploadAvatar = async (formData: FormData) => {
const image = formData.get("avatar") as File;
const encodedFileName = encode(image.name);
const supabase = createClient();
const { data, error } = await supabase.storage
.from("avatars")
.upload(encodedFileName, image, {
cacheControl: "3600",
upsert: false,
});
if (error) {
console.error('Upload error:', error);
throw error;
}
const {
data: { publicUrl },
} = supabase.storage.from("avatars").getPublicUrl(encodedFileName);
return publicUrl;
};
2. ユニークIDを使用したファイル名の生成
ファイル名を完全に制御するために、ユニークなIDを生成し、それをファイル名として使用する方法があります。
import { v4 as uuidv4 } from 'uuid';
export const uploadAvatar = async (formData: FormData) => {
const image = formData.get("avatar") as File;
const fileExt = image.name.split('.').pop();
const fileName = `${uuidv4()}.${fileExt}`;
const supabase = createClient();
const { data, error } = await supabase.storage
.from("avatars")
.upload(fileName, image, {
cacheControl: "3600",
upsert: false,
});
if (error) {
console.error('Upload error:', error);
throw error;
}
const {
data: { publicUrl },
} = supabase.storage.from("avatars").getPublicUrl(fileName);
return publicUrl;
};
3. ファイル名の正規化
ファイル名から非ASCII文字を除去し、安全な文字のみを使用する方法もあります。
export const uploadAvatar = async (formData: FormData) => {
const image = formData.get("avatar") as File;
const safeFileName = image.name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
const supabase = createClient();
const { data, error } = await supabase.storage
.from("avatars")
.upload(safeFileName, image, {
cacheControl: "3600",
upsert: false,
});
if (error) {
console.error('Upload error:', error);
throw error;
}
const {
data: { publicUrl },
} = supabase.storage.from("avatars").getPublicUrl(safeFileName);
return publicUrl;
};
注意点
- エンコーディングを使用する場合、ファイルの取得時にもデコードが必要になる場合があります。
- ユニークIDを使用する場合、元のファイル名との対応を別途管理する必要があるかもしれません。
- ファイル名の正規化を行う場合、元のファイル名の情報が失われる可能性があります。
まとめ
日本語のファイル名でアップロードする際の問題は、主にURLエンコーディングやファイル名の処理で解決できます。アプリケーションの要件に応じて、最適な方法を選択してください。また、選択した方法に合わせて、ファイルの取得や表示のロジックも調整する必要があるかもしれません。
セキュリティの観点から、ユーザーが提供するファイル名をそのまま使用するのではなく、サーバーサイドで安全なファイル名を生成することをおすすめします。
SupabaseでのユニークID使用によるファイル名生成の実装
なぜユニークIDを使用するのか
- セキュリティ: ファイル名に含まれる可能性のある機密情報や特殊文字を完全に排除できます。
- 一意性: ファイル名の衝突を避けられるため、既存のファイルを誤って上書きするリスクがありません。
- 国際化対応: 日本語に限らず、どんな言語の文字が含まれていても問題なく動作します。
- パフォーマンス: 単純な文字列操作で済むため、処理が高速です。
実装例
以下に、TypeScriptを使用したNext.jsのサーバーアクション内での実装例を示します:
"use server";
import { createClient } from "@/lib/supabase/server";
import { v4 as uuidv4 } from 'uuid';
export const uploadAvatar = async (formData: FormData) => {
const image = formData.get("avatar") as File;
// オリジナルのファイル名から拡張子を取得
const fileExt = image.name.split('.').pop();
// ユニークIDと拡張子を組み合わせて新しいファイル名を生成
const fileName = `${uuidv4()}.${fileExt}`;
const supabase = createClient();
try {
const { data, error } = await supabase.storage
.from("avatars")
.upload(fileName, image, {
cacheControl: "3600",
upsert: false,
});
if (error) throw error;
// publicURLの取得
const { data: { publicUrl } } = supabase.storage
.from("avatars")
.getPublicUrl(fileName);
// オリジナルのファイル名とのマッピングをデータベースに保存(オプション)
await saveFileMapping(fileName, image.name);
return { publicUrl, fileName };
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
// ファイル名のマッピングを保存する関数(必要に応じて実装)
async function saveFileMapping(newFileName: string, originalFileName: string) {
const supabase = createClient();
const { data, error } = await supabase
.from('file_mappings')
.insert({ new_file_name: newFileName, original_file_name: originalFileName });
if (error) {
console.error('Error saving file mapping:', error);
}
}
追加の考慮点
-
元のファイル名の保持: ユーザーが元のファイル名を参照したい場合に備えて、新しいファイル名と元のファイル名のマッピングをデータベースに保存することを検討してください。
-
ファイルタイプの検証: セキュリティをさらに強化するために、アップロードされたファイルの実際の種類を確認することをおすすめします。
-
エラーハンドリング: アップロードプロセス中に発生する可能性のあるエラーを適切に処理し、ユーザーにフィードバックを提供してください。
-
ファイルサイズの制限: 必要に応じて、アップロードされるファイルのサイズに制限を設けることを検討してください。
まとめ
ユニークIDを使用したファイル名の生成方法は、セキュリティ、一意性、国際化対応、パフォーマンスのバランスが取れた解決策です。この方法を使用することで、日本語ファイル名の問題を効果的に解決しつつ、堅牢なファイル管理システムを構築することができます。
実際の実装時には、アプリケーションの具体的な要件に合わせて、この基本的なアプローチをカスタマイズしてください。
Supabaseでのファイル管理:元のファイル名の保存と利用
はじめに
ユニークIDを使用してファイルをアップロードする際、元のファイル名を保持することが重要な場合があります。ここでは、元のファイル名を保存し、必要に応じて利用する方法を説明します。
データモデル
まず、ファイル情報を保存するためのテーブルを作成します。
CREATE TABLE public.file_metadata (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
storage_path TEXT NOT NULL,
original_name TEXT NOT NULL,
mime_type TEXT,
size INTEGER,
uploaded_by UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
実装例
以下に、TypeScriptを使用したNext.jsのサーバーアクション内での実装例を示します:
"use server";
import { createClient } from "@/lib/supabase/server";
import { v4 as uuidv4 } from 'uuid';
export const uploadAvatar = async (formData: FormData) => {
const image = formData.get("avatar") as File;
const originalName = image.name;
const fileExt = originalName.split('.').pop();
const fileName = `${uuidv4()}.${fileExt}`;
const bucketName = "avatars";
const supabase = createClient();
try {
// ファイルアップロード
const { data, error } = await supabase.storage
.from(bucketName)
.upload(fileName, image, {
cacheControl: "3600",
upsert: false,
});
if (error) throw error;
// Public URLの取得
const { data: { publicUrl } } = supabase.storage
.from(bucketName)
.getPublicUrl(fileName);
// メタデータの保存
const { data: metaData, error: metaError } = await supabase
.from('file_metadata')
.insert({
storage_path: `${bucketName}/${fileName}`,
original_name: originalName,
mime_type: image.type,
size: image.size,
uploaded_by: supabase.auth.user()?.id // 現在のユーザーID
})
.select()
.single();
if (metaError) throw metaError;
return { publicUrl, fileId: metaData.id };
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
// ファイルメタデータを取得する関数
export const getFileMetadata = async (fileId: string) => {
const supabase = createClient();
const { data, error } = await supabase
.from('file_metadata')
.select('*')
.eq('id', fileId)
.single();
if (error) throw error;
return data;
};
// 元のファイル名でファイルを検索する関数
export const searchFilesByOriginalName = async (searchTerm: string) => {
const supabase = createClient();
const { data, error } = await supabase
.from('file_metadata')
.select('*')
.ilike('original_name', `%${searchTerm}%`);
if (error) throw error;
return data;
};
使用例
- ファイルアップロード時:
const { publicUrl, fileId } = await uploadAvatar(formData);
console.log(`File uploaded. Public URL: ${publicUrl}, File ID: ${fileId}`);
- ファイルメタデータの取得:
const fileMetadata = await getFileMetadata(fileId);
console.log(`Original file name: ${fileMetadata.original_name}`);
- 元のファイル名での検索:
const searchResults = await searchFilesByOriginalName("profile");
console.log(`Found ${searchResults.length} files`);
注意点
-
セキュリティ: ファイルメタデータへのアクセス権限を適切に設定し、認証されたユーザーのみがアクセスできるようにしてください。
-
パフォーマンス: 大量のファイルを扱う場合は、インデックスの作成や検索の最適化を検討してください。
-
ストレージの整合性: ファイルの削除時には、ストレージとメタデータの両方を更新するようにしてください。
まとめ
この方法を使用することで、ユニークIDを用いたセキュアなファイル保存と、元のファイル名の保持・利用の両立が可能になります。ユーザーは元のファイル名でファイルを検索したり、ダウンロード時に元のファイル名を使用したりすることができます。
まとめ SupabaseでのWebP変換と画像最適化を含むファイルアップロード
概要
ファイルアップロード時にWebP形式への変換と画像最適化を行うことで、以下の利点が得られます:
- ストレージ容量の削減
- 画像読み込み時間の短縮
- 高画質イメージの維持
実装例
以下に、Next.jsのサーバーアクションでWebP変換と画像最適化を行う実装例を示します。この例では、sharp
ライブラリを使用しています。
"use server"; // Next.jsのサーバーアクションであることを示す
import { createClient } from "@/lib/supabase/server"; // Supabaseクライアントを作成するための関数をインポート
import { v4 as uuidv4 } from 'uuid'; // ユニークIDを生成するためのuuidv4関数をインポート
import sharp from 'sharp'; // 画像処理ライブラリsharpをインポート
export const uploadOptimizedAvatar = async (formData: FormData) => {
// FormDataからアバター画像を取得
const image = formData.get("avatar") as File;
const originalName = image.name; // オリジナルのファイル名を保存
const fileId = uuidv4(); // ユニークなファイルIDを生成
const bucketName = "avatars"; // Supabaseストレージのバケット名
const supabase = createClient(); // Supabaseクライアントを作成
try {
// File オブジェクトをArrayBufferに変換し、さらにNodejsのBufferに変換
const buffer = Buffer.from(await image.arrayBuffer());
// sharp を使用して画像を WebP 形式に変換し最適化
const optimizedBuffer = await sharp(buffer)
.webp({ quality: 80 }) // WebP形式に変換し、品質を80%に設定
.resize(1000, 1000, { fit: 'inside', withoutEnlargement: true }) // 最大サイズを1000x1000に制限
.toBuffer(); // 処理結果をBufferとして取得
const fileName = `${fileId}.webp`; // WebP形式のファイル名を生成
// 最適化された画像をSupabaseストレージにアップロード
const { data, error } = await supabase.storage
.from(bucketName)
.upload(fileName, optimizedBuffer, {
contentType: 'image/webp',
cacheControl: "3600",
upsert: false,
});
if (error) throw error; // アップロードエラーがあれば例外を投げる
// アップロードされた画像のPublic URLを取得
const { data: { publicUrl } } = supabase.storage
.from(bucketName)
.getPublicUrl(fileName);
// オリジナルの高画質版もWebP形式で保存(オプション)
const highQualityFileName = `${fileId}_original.webp`;
await supabase.storage
.from(bucketName)
.upload(highQualityFileName, await sharp(buffer).webp({ quality: 100 }).toBuffer(), {
contentType: 'image/webp',
cacheControl: "3600",
upsert: false,
});
// ファイルのメタデータをデータベースに保存
const { data: metaData, error: metaError } = await supabase
.from('file_metadata')
.insert({
id: fileId,
storage_path: `${bucketName}/${fileName}`,
original_name: originalName,
mime_type: 'image/webp',
size: optimizedBuffer.length,
uploaded_by: supabase.auth.user()?.id, // 現在のユーザーID
high_quality_path: `${bucketName}/${highQualityFileName}`
})
.select()
.single();
if (metaError) throw metaError; // メタデータ保存エラーがあれば例外を投げる
return { publicUrl, fileId: metaData.id }; // 公開URLとファイルIDを返す
} catch (error) {
console.error('Upload error:', error); // エラーをコンソールに出力
throw error; // エラーを呼び出し元に伝播
}
};
// 高画質版の画像URLを取得する関数
export const getHighQualityImageUrl = async (fileId: string) => {
const supabase = createClient(); // Supabaseクライアントを作成
// file_metadataテーブルから高画質版のパスを取得
const { data, error } = await supabase
.from('file_metadata')
.select('high_quality_path')
.eq('id', fileId)
.single();
if (error) throw error; // エラーがあれば例外を投げる
// 高画質版画像のPublic URLを取得
const { data: { publicUrl } } = supabase.storage
.from(data.high_quality_path.split('/')[0]) // バケット名を取得
.getPublicUrl(data.high_quality_path.split('/')[1]); // ファイル名を取得
return publicUrl; // 高画質版のPublic URLを返す
};
主な特徴
-
WebP変換: すべての画像をWebP形式に変換し、ファイルサイズを削減します。
-
画質最適化: 通常表示用の画像は品質を80%に設定し、ファイルサイズをさらに削減します。
-
サイズ制限: 画像の最大サイズを1000x1000ピクセルに制限し、過度に大きな画像のアップロードを防ぎます。
-
高画質版の保存: オリジナルの高画質版も別途保存し、必要に応じて利用できるようにします。
-
メタデータ管理: 最適化された画像と高画質版の両方のパスをメタデータとして保存します。
使用例
- 画像のアップロード:
const { publicUrl, fileId } = await uploadOptimizedAvatar(formData);
console.log(`Optimized image uploaded. Public URL: ${publicUrl}, File ID: ${fileId}`);
- 高画質版の画像URLの取得:
const highQualityUrl = await getHighQualityImageUrl(fileId);
console.log(`High quality image URL: ${highQualityUrl}`);
注意点
-
サーバーリソース: 画像処理はサーバーリソースを消費するため、大量のアップロードがある場合はスケーリングを考慮する必要があります。
-
ブラウザ互換性: WebPはほとんどのモダンブラウザでサポートされていますが、古いブラウザでは表示できない場合があります。必要に応じてフォールバック画像を提供することを検討してください。
-
画質設定: 画質設定(80%)は一般的な値ですが、アプリケーションの要件に応じて調整してください。
まとめ
この実装により、ストレージ効率と画像表示パフォーマンスを大幅に向上させつつ、必要に応じて高画質版にアクセスすることができます。アプリケーションの具体的な要件に合わせて、この基本的なアプローチをカスタマイズしてください。