@file-goblin/upload-goblin
Version:
Alchemy-powered file uploader for Cloudflare Workers with R2 and S3 support
264 lines (226 loc) • 7.5 kB
text/typescript
// pages/api/uploads/generate-signature.ts
// or app/api/uploads/generate-signature/route.ts (App Router)
import { NextApiRequest, NextApiResponse } from 'next';
import { generateUploadSignature } from '@file-goblin/goblin-uploader';
// Configuration
const UPLOAD_SECRET_KEY = process.env.UPLOAD_SECRET_KEY!;
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const ALLOWED_FILE_TYPES = [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'text/plain'
];
interface UploadRequest {
fileName: string;
fileSize: number;
fileType: string;
userId?: string;
folder?: string;
}
interface UploadResponse {
signature: string;
fileId: string;
uploadUrl: string;
expiresAt: number;
}
// Pages Router version
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { fileName, fileSize, fileType, userId, folder }: UploadRequest = req.body;
// Validate input
if (!fileName || !fileSize || !fileType) {
return res.status(400).json({
error: 'Missing required fields: fileName, fileSize, fileType'
});
}
// Validate file size
if (fileSize > MAX_FILE_SIZE) {
return res.status(400).json({
error: `File too large. Maximum size: ${MAX_FILE_SIZE / (1024 * 1024)}MB`
});
}
// Validate file type
if (!ALLOWED_FILE_TYPES.includes(fileType)) {
return res.status(400).json({
error: `File type not allowed. Allowed types: ${ALLOWED_FILE_TYPES.join(', ')}`
});
}
// Generate unique file ID
const timestamp = Date.now();
const randomId = Math.random().toString(36).substring(2, 15);
const sanitizedFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_');
let fileId: string;
if (userId) {
fileId = folder
? `user-${userId}/${folder}/${timestamp}-${randomId}-${sanitizedFileName}`
: `user-${userId}/${timestamp}-${randomId}-${sanitizedFileName}`;
} else {
fileId = `uploads/${timestamp}-${randomId}-${sanitizedFileName}`;
}
// Set expiration (30 minutes from now)
const expiresAt = Date.now() + 30 * 60 * 1000;
// Generate signature
const signature = generateUploadSignature({
fileId,
expiresAt,
userId,
maxFileSize: fileSize,
allowedFileType: fileType,
fileName: sanitizedFileName
}, UPLOAD_SECRET_KEY);
// Your worker URL (replace with your actual deployed worker URL)
const workerUrl = process.env.UPLOADER_WORKER_URL || 'https://your-uploader.your-subdomain.workers.dev';
const uploadUrl = `${workerUrl}/uploads/${encodeURIComponent(fileId)}?fileName=${encodeURIComponent(fileName)}`;
const response: UploadResponse = {
signature,
fileId,
uploadUrl,
expiresAt
};
res.status(200).json(response);
} catch (error) {
console.error('Error generating upload signature:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
// App Router version (uncomment if using App Router)
/*
export async function POST(request: Request) {
try {
const { fileName, fileSize, fileType, userId, folder }: UploadRequest = await request.json();
// Validate input
if (!fileName || !fileSize || !fileType) {
return Response.json({
error: 'Missing required fields: fileName, fileSize, fileType'
}, { status: 400 });
}
// Validate file size
if (fileSize > MAX_FILE_SIZE) {
return Response.json({
error: `File too large. Maximum size: ${MAX_FILE_SIZE / (1024 * 1024)}MB`
}, { status: 400 });
}
// Validate file type
if (!ALLOWED_FILE_TYPES.includes(fileType)) {
return Response.json({
error: `File type not allowed. Allowed types: ${ALLOWED_FILE_TYPES.join(', ')}`
}, { status: 400 });
}
// Generate unique file ID
const timestamp = Date.now();
const randomId = Math.random().toString(36).substring(2, 15);
const sanitizedFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_');
let fileId: string;
if (userId) {
fileId = folder
? `user-${userId}/${folder}/${timestamp}-${randomId}-${sanitizedFileName}`
: `user-${userId}/${timestamp}-${randomId}-${sanitizedFileName}`;
} else {
fileId = `uploads/${timestamp}-${randomId}-${sanitizedFileName}`;
}
// Set expiration (30 minutes from now)
const expiresAt = Date.now() + 30 * 60 * 1000;
// Generate signature
const signature = generateUploadSignature({
fileId,
expiresAt,
userId,
maxFileSize: fileSize,
allowedFileType: fileType,
fileName: sanitizedFileName
}, UPLOAD_SECRET_KEY);
// Your worker URL (replace with your actual deployed worker URL)
const workerUrl = process.env.UPLOADER_WORKER_URL || 'https://your-uploader.your-subdomain.workers.dev';
const uploadUrl = `${workerUrl}/uploads/${encodeURIComponent(fileId)}?fileName=${encodeURIComponent(fileName)}`;
const response: UploadResponse = {
signature,
fileId,
uploadUrl,
expiresAt
};
return Response.json(response);
} catch (error) {
console.error('Error generating upload signature:', error);
return Response.json({ error: 'Internal server error' }, { status: 500 });
}
}
*/
// Client-side usage example:
/*
// components/FileUploader.tsx
import React, { useState } from 'react';
interface UploadResponse {
signature: string;
fileId: string;
uploadUrl: string;
expiresAt: number;
}
export function FileUploader({ userId }: { userId: string }) {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const handleFileUpload = async (file: File) => {
setUploading(true);
setProgress(0);
try {
// 1. Get upload signature from your API
const signatureResponse = await fetch('/api/uploads/generate-signature', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName: file.name,
fileSize: file.size,
fileType: file.type,
userId,
folder: 'documents'
})
});
if (!signatureResponse.ok) {
throw new Error('Failed to get upload signature');
}
const { signature, uploadUrl }: UploadResponse = await signatureResponse.json();
// 2. Upload directly to your Cloudflare Worker
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${signature}`,
'Content-Type': file.type,
'X-Metadata-User-Id': userId,
'X-Metadata-Upload-Source': 'nextjs-app'
},
body: file
});
if (!uploadResponse.ok) {
throw new Error('Upload failed');
}
const result = await uploadResponse.json();
console.log('Upload successful:', result);
setProgress(100);
} catch (error) {
console.error('Upload error:', error);
} finally {
setUploading(false);
}
};
return (
<div>
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleFileUpload(file);
}}
disabled={uploading}
/>
{uploading && <div>Progress: {progress}%</div>}
</div>
);
}
*/