Arcwayv0.3.0
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 } };
  },
};

On this page