Cookbook
File Upload & Storage
File upload, serving, listing, and deletion patterns
Patterns for handling file uploads, serving files, and managing stored assets.
Base64 Upload
Accept a file encoded as base64 in the request body:
// api/upload.js
import { vault } from 'arcway';
export const POST = {
handler: async (ctx) => {
const { files } = ctx;
const { filename, data } = ctx.req.body;
if (!filename || !data) {
return { status: 400, error: { code: 'MISSING_FIELDS', message: 'filename and data required' } };
}
const ext = filename.split('.').pop();
const storedName = `${vault.generateNanoId()}.${ext}`;
const path = `uploads/${storedName}`;
await files.write(path, Buffer.from(data, 'base64'));
return { status: 201, data: { path, filename: storedName } };
},
};Raw Binary Upload
Accept a file as raw binary by disabling JSON body parsing:
// api/upload/avatar.js
import { vault } from 'arcway';
export const POST = {
parseBody: false, // ctx.req.rawBody contains the raw binary buffer
handler: async (ctx) => {
const { files } = ctx;
const contentType = ctx.req.headers['content-type'] || 'application/octet-stream';
const ext = contentType.split('/')[1] || 'bin';
const path = `avatars/${vault.generateNanoId()}.${ext}`;
await files.write(path, Buffer.from(ctx.req.rawBody, 'binary'));
return { status: 201, data: { path } };
},
};Serve a File
Return file contents via a catch-all route:
// api/files/[...path].js
export const GET = {
handler: async (ctx) => {
const { files } = ctx;
const filePath = ctx.req.query.path; // array for catch-all params
const buffer = await files.read(filePath);
if (!buffer) {
return { status: 404, error: { code: 'NOT_FOUND', message: 'File not found' } };
}
return { data: { file: buffer.toString('base64'), path: filePath } };
},
};List and Delete Files
// api/uploads/index.js
// List all files in a directory
export const GET = {
handler: async (ctx) => {
const { files } = ctx;
const userId = ctx.req.session.userId;
const entries = await files.list(`uploads/${userId}/`);
return { data: entries };
},
};
// api/uploads/[filename].js
// Delete a file
export const DELETE = {
handler: async (ctx) => {
const { files } = ctx;
const userId = ctx.req.session.userId;
const path = `uploads/${userId}/${ctx.req.query.filename}`;
const exists = await files.exists(path);
if (!exists) {
return { status: 404, error: { code: 'NOT_FOUND', message: 'File not found' } };
}
await files.delete(path);
return { data: { ok: true } };
},
};User-Scoped Storage
Namespace files per user to avoid collisions and simplify access control:
// api/media/index.js
import { vault } from 'arcway';
export const POST = {
handler: async (ctx) => {
const { files, db } = ctx;
const { filename, data } = ctx.req.body;
const userId = ctx.req.session.userId;
const ext = filename.split('.').pop().toLowerCase();
const storedName = `${vault.generateNanoId()}.${ext}`;
const storagePath = `media/${userId}/${storedName}`;
await files.write(storagePath, Buffer.from(data, 'base64'));
// Track in database
const [id] = await db('media').insert({
user_id: userId,
filename,
storage_path: storagePath,
});
return { status: 201, data: { id, path: storagePath } };
},
};