Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:29 +08:00
commit 571bc8c17c
12 changed files with 2689 additions and 0 deletions

View File

@@ -0,0 +1,226 @@
/**
* Simple R2 Upload/Download Worker
*
* Features:
* - Upload files with PUT requests
* - Download files with GET requests
* - Delete files with DELETE requests
* - List all files
* - Proper content-type handling
* - Error handling
*/
import { Hono } from 'hono';
type Bindings = {
MY_BUCKET: R2Bucket;
};
const app = new Hono<{ Bindings: Bindings }>();
// Upload a file
app.put('/files/:filename', async (c) => {
const filename = c.req.param('filename');
const body = await c.req.arrayBuffer();
const contentType = c.req.header('content-type') || 'application/octet-stream';
try {
const object = await c.env.MY_BUCKET.put(filename, body, {
httpMetadata: {
contentType: contentType,
cacheControl: 'public, max-age=3600',
},
customMetadata: {
uploadedAt: new Date().toISOString(),
uploadedBy: 'api',
},
});
return c.json({
success: true,
key: object.key,
size: object.size,
etag: object.etag,
uploaded: object.uploaded,
});
} catch (error: any) {
console.error('Upload error:', error.message);
return c.json({
success: false,
error: 'Failed to upload file',
}, 500);
}
});
// Download a file
app.get('/files/:filename', async (c) => {
const filename = c.req.param('filename');
try {
const object = await c.env.MY_BUCKET.get(filename);
if (!object) {
return c.json({
success: false,
error: 'File not found',
}, 404);
}
// Apply http metadata from R2
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
return new Response(object.body, { headers });
} catch (error: any) {
console.error('Download error:', error.message);
return c.json({
success: false,
error: 'Failed to download file',
}, 500);
}
});
// Get file metadata (without downloading body)
app.head('/files/:filename', async (c) => {
const filename = c.req.param('filename');
try {
const object = await c.env.MY_BUCKET.head(filename);
if (!object) {
return c.json({
success: false,
error: 'File not found',
}, 404);
}
return c.json({
success: true,
key: object.key,
size: object.size,
etag: object.etag,
uploaded: object.uploaded,
contentType: object.httpMetadata?.contentType,
customMetadata: object.customMetadata,
});
} catch (error: any) {
console.error('Head error:', error.message);
return c.json({
success: false,
error: 'Failed to get file metadata',
}, 500);
}
});
// Delete a file
app.delete('/files/:filename', async (c) => {
const filename = c.req.param('filename');
try {
// Check if file exists first
const exists = await c.env.MY_BUCKET.head(filename);
if (!exists) {
return c.json({
success: false,
error: 'File not found',
}, 404);
}
await c.env.MY_BUCKET.delete(filename);
return c.json({
success: true,
message: 'File deleted successfully',
key: filename,
});
} catch (error: any) {
console.error('Delete error:', error.message);
return c.json({
success: false,
error: 'Failed to delete file',
}, 500);
}
});
// List all files (with pagination)
app.get('/files', async (c) => {
const cursor = c.req.query('cursor');
const limit = parseInt(c.req.query('limit') || '100');
const prefix = c.req.query('prefix') || '';
try {
const listed = await c.env.MY_BUCKET.list({
limit: Math.min(limit, 1000), // Max 1000
cursor: cursor || undefined,
prefix: prefix || undefined,
});
return c.json({
success: true,
files: listed.objects.map(obj => ({
key: obj.key,
size: obj.size,
etag: obj.etag,
uploaded: obj.uploaded,
contentType: obj.httpMetadata?.contentType,
})),
truncated: listed.truncated,
cursor: listed.cursor,
count: listed.objects.length,
});
} catch (error: any) {
console.error('List error:', error.message);
return c.json({
success: false,
error: 'Failed to list files',
}, 500);
}
});
// Bulk delete (up to 1000 files)
app.post('/files/bulk-delete', async (c) => {
const { keys } = await c.req.json<{ keys: string[] }>();
if (!keys || !Array.isArray(keys)) {
return c.json({
success: false,
error: 'Invalid request: keys must be an array',
}, 400);
}
if (keys.length > 1000) {
return c.json({
success: false,
error: 'Cannot delete more than 1000 keys at once',
}, 400);
}
try {
await c.env.MY_BUCKET.delete(keys);
return c.json({
success: true,
message: `Deleted ${keys.length} files`,
count: keys.length,
});
} catch (error: any) {
console.error('Bulk delete error:', error.message);
return c.json({
success: false,
error: 'Failed to delete files',
}, 500);
}
});
// Health check
app.get('/health', (c) => {
return c.json({
status: 'healthy',
service: 'r2-worker',
timestamp: new Date().toISOString(),
});
});
export default app;