CDN (Cloudflare R2)
Overview
The application uses Cloudflare R2 for file storage and Supabase for database management. Files are uploaded using a two-step process:
- Get a presigned URL from R2
- Upload the file directly to R2 using the presigned URL
Basic Implementation Pattern
1. Frontend Component
Create a file input component that handles file selection:
<template>
<div>
<input type="file" @change="handleFileSelect" />
</div>
</template>
<script setup>
const handleFileSelect = async (event) => {
const file = event.target.files[0];
if (!file) return;
await uploadFile(file);
};
</script>
2. Upload Function
Create a composable for R2 uploads (e.g., useR2Upload):
export const useR2Upload = () => {
// Single file upload
const uploadSingleFile = async (file: File, path: string, user_id: string) => {
// 1. Get presigned URL from backend
const { data } = await useFetch("/api/storage/upload/path", {
method: "POST",
body: {
user_id,
file: {
name: file.name,
type: file.type,
},
},
});
// 2. Upload to R2 using presigned URL
await fetch(data.value.uploadUrl, {
method: "PUT",
body: file,
headers: {
"Content-Type": file.type,
},
});
return data.value.publicUrl;
};
// Multiple files upload
const uploadMultipleFiles = async (files: File[], path: string, user_id: string) => {
// 1. Get presigned URLs for all files
const fileInfo = files.map((file) => ({
fileName: file.name,
contentType: file.type,
}));
const { data } = await useFetch("/api/storage/upload/path", {
method: "POST",
body: {
user_id,
files: fileInfo,
},
});
// 2. Upload all files to R2
const uploadPromises = files.map((file, index) =>
fetch(data.value[index].uploadUrl, {
method: "PUT",
body: file,
headers: {
"Content-Type": file.type,
},
})
);
await Promise.all(uploadPromises);
return data.value.map((result) => result.publicUrl);
};
return {
uploadSingleFile,
uploadMultipleFiles,
};
};
3. Backend API Endpoint
Create an API endpoint to generate presigned URLs:
// server/api/storage/upload/[path].ts
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3Client = new S3Client({
region: "auto",
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const { user_id, files } = body;
// For single file
if (!Array.isArray(files)) {
const file_key = `users/${user_id}/path/${files.name}`;
const uploadUrl = await getSignedUrl(
s3Client,
new PutObjectCommand({
Bucket: process.env.R2_BUCKET_NAME,
Key: file_key,
ContentType: files.type,
}),
{ expiresIn: 3600 }
);
return {
uploadUrl,
publicUrl: `https://cdn.example.com/${file_key}`,
};
}
// For multiple files
const results = await Promise.all(
files.map(async (file) => {
const file_key = `users/${user_id}/path/${file.fileName}`;
const uploadUrl = await getSignedUrl(
s3Client,
new PutObjectCommand({
Bucket: process.env.R2_BUCKET_NAME,
Key: file_key,
ContentType: file.contentType,
}),
{ expiresIn: 3600 }
);
return {
uploadUrl,
publicUrl: `https://cdn.example.com/${file_key}`,
};
})
);
return results;
});
Usage Examples
Single File Upload (e.g., Avatar)
// In component script
const uploadAvatar = async (file) => {
const { uploadSingleFile } = useR2Upload();
const publicUrl = await uploadSingleFile(file, "avatar", user_id);
// Update database with new URL
await supabase.from("profiles").update({ avatar_url: publicUrl }).eq("id", user_id);
};
Multiple Files Upload (e.g., Product Images)
// In component script
const uploadProductImages = async (files) => {
const { uploadMultipleFiles } = useR2Upload();
const publicUrls = await uploadMultipleFiles(files, "products", user_id);
// Update database with new URLs
await supabase
.from("products")
.update({
images: publicUrls,
})
.eq("id", product_id);
};
File Structure
├── composables/
│ └── useR2Upload.ts
├── server/
│ └── api/
│ └── storage/
│ ├── upload/
│ │ └── [path].ts
│ └── delete.ts
└── components/
└── FileUploader.vue
Important Notes
- Always validate file types and sizes before upload
- Use environment variables for R2 credentials
- Handle errors appropriately
- Consider implementing file deletion when needed
- URLs are stored in Supabase after successful upload