File Uploads
Handle file uploads with storage service.
Note: This is mock/placeholder content for demonstration purposes.
Enable users to upload and manage files using the storage service.
Setup
Configure Storage Buckets
Configure storage buckets in your storage service configuration:
// config/storage.config.ts
export const storageConfig = {
buckets: {
avatars: {
public: true,
maxSize: 5 * 1024 * 1024, // 5MB
},
documents: {
public: false,
maxSize: 10 * 1024 * 1024, // 10MB
},
},
};
Set Storage Policies
Configure access policies in your storage service:
// Allow users to upload their own avatars
export const storagePolicies = {
avatars: {
upload: (userId: string, path: string) => {
return path.startsWith(`${userId}/`);
},
view: (userId: string, path: string) => {
return path.startsWith(`${userId}/`);
},
delete: (userId: string, path: string) => {
return path.startsWith(`${userId}/`);
},
},
};
Upload Component
Basic File Upload
'use client';
import { useState } from 'react';
import { uploadFileAction } from '../_lib/actions';
export function FileUpload() {
const [uploading, setUploading] = useState(false);
const [file, setFile] = useState<File | null>(null);
const handleUpload = async () => {
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append('file', file);
const result = await uploadFileAction(formData);
if (result.success) {
toast.success('File uploaded successfully');
}
setUploading(false);
};
return (
<div>
<input
type="file"
onChange={(e) => setFile(e.files?.[0] || null)}
accept="image/*"
/>
<button
onClick={handleUpload}
disabled={!file || uploading}
>
{uploading ? 'Uploading...' : 'Upload'}
</button>
</div>
);
}
Server Action
'use server';
import { enhanceAction } from '@kit/next/actions';
import { uploadFile } from '@kit/storage/server';
export const uploadFileAction = enhanceAction(
async (formData: FormData, user) => {
const file = formData.get('file') as File;
if (!file) {
throw new Error('No file provided');
}
const fileExt = file.name.split('.').pop();
const fileName = `${user.id}/${Date.now()}.${fileExt}`;
const { url, path } = await uploadFile({
bucket: 'avatars',
fileName,
file,
options: {
cacheControl: '3600',
upsert: false,
},
});
return {
success: true,
url,
path,
};
},
{ auth: true }
);
Drag and Drop Upload
Use the built-in ImageUploader component from @kit/ui/image-uploader for drag-and-drop image uploads:
'use client';
import { useState } from 'react';
import { ImageUploader } from '@kit/ui/image-uploader';
export function DragDropUpload() {
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const handleFileChange = (file: File | null) => {
if (file) {
setPreviewUrl(URL.createObjectURL(file));
// Upload file...
} else {
setPreviewUrl(null);
}
};
return (
<ImageUploader
value={previewUrl}
onValueChange={handleFileChange}
onError={(error) => {
if (error === 'size') toast.error('File too large');
if (error === 'type') toast.error('Invalid file type');
}}
>
<span>Click to upload or drag and drop</span>
</ImageUploader>
);
}
For more advanced uploads with progress tracking, use the useUppyImageUpload hook:
'use client';
import { useUppyImageUpload } from '@kit/ui/hooks/use-uppy-image-upload';
export function UppyUpload() {
const { addFile, upload, isUploading, previewUrl, error } = useUppyImageUpload(
{
type: 'user',
onUploadSuccess: (url) => console.log('Uploaded:', url),
onUploadError: (err) => console.error('Failed:', err),
},
);
return (
<div>
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) addFile(file);
}}
/>
{previewUrl && <img src={previewUrl} alt="Preview" />}
<button onClick={() => upload()} disabled={isUploading}>
{isUploading ? 'Uploading...' : 'Upload'}
</button>
{error && <p className="text-red-500">{error}</p>}
</div>
);
}
File Validation
Client-Side Validation
function validateFile(file: File) {
const maxSize = 5 * 1024 * 1024; // 5MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (file.size > maxSize) {
throw new Error('File size must be less than 5MB');
}
if (!allowedTypes.includes(file.type)) {
throw new Error('File type must be JPEG, PNG, or GIF');
}
return true;
}
Server-Side Validation
export const uploadFileAction = enhanceAction(
async (formData: FormData, user) => {
const file = formData.get('file') as File;
// Validate file size
if (file.size > 5 * 1024 * 1024) {
throw new Error('File too large');
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type');
}
// Validate dimensions for images
if (file.type.startsWith('image/')) {
const dimensions = await getImageDimensions(file);
if (dimensions.width > 4000 || dimensions.height > 4000) {
throw new Error('Image dimensions too large');
}
}
// Continue with upload...
},
{ auth: true }
);
Image Optimization
Resize on Upload
import sharp from 'sharp';
export const uploadAvatarAction = enhanceAction(
async (formData: FormData, user) => {
const file = formData.get('file') as File;
const buffer = Buffer.from(await file.arrayBuffer());
// Resize image
const resized = await sharp(buffer)
.resize(200, 200, {
fit: 'cover',
position: 'center',
})
.jpeg({ quality: 90 })
.toBuffer();
const fileName = `${user.id}/avatar.jpg`;
await uploadFile({
bucket: 'avatars',
fileName,
file: resized,
options: {
contentType: 'image/jpeg',
upsert: true,
},
});
return { success: true };
},
{ auth: true }
);
Progress Tracking
'use client';
import { useState } from 'react';
export function UploadWithProgress() {
const [progress, setProgress] = useState(0);
const handleUpload = async (file: File) => {
await uploadFile({
bucket: 'documents',
fileName: `uploads/${file.name}`,
file,
onProgress: (progressEvent) => {
const percent = (progressEvent.loaded / progressEvent.total) * 100;
setProgress(Math.round(percent));
},
});
};
return (
<div>
<input type="file" onChange={(e) => handleUpload(e.target.files![0])} />
{progress > 0 && (
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
)}
</div>
);
}
Downloading Files
Get Public URL
import { getPublicUrl } from '@kit/storage/server';
const url = getPublicUrl({
bucket: 'avatars',
fileName: 'user-id/avatar.jpg',
});
console.log(url);
Download Private File
import { downloadFile } from '@kit/storage/server';
const file = await downloadFile({
bucket: 'documents',
fileName: 'private-file.pdf',
});
if (file) {
const url = URL.createObjectURL(file);
const a = document.createElement('a');
a.href = url;
a.download = 'file.pdf';
a.click();
}
Generate Signed URL
import { getSignedUrl } from '@kit/storage/server';
const url = await getSignedUrl({
bucket: 'documents',
fileName: 'private-file.pdf',
expiresIn: 3600, // 1 hour
});
console.log(url);
Deleting Files
import { deleteFile } from '@kit/storage/server';
export const deleteFileAction = enhanceAction(
async (data, user) => {
await deleteFile({
bucket: 'avatars',
fileName: data.path,
});
return { success: true };
},
{
schema: z.object({
path: z.string(),
}),
auth: true,
}
);
Best Practices
- Validate on both sides - Client and server
- Limit file sizes - Prevent abuse
- Sanitize filenames - Remove special characters
- Use unique names - Prevent collisions
- Optimize images - Resize before upload
- Set storage policies - Control access
- Monitor usage - Track storage costs
- Clean up unused files - Regular maintenance
- Use CDN - For public files
- Implement virus scanning - For user uploads