npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@digibuffer/file-system

v1.0.1

Published

File system layer for managing files and folders — registration, status tracking, folder hierarchy, DB-agnostic via lifecycle callbacks.

Downloads

168

Readme

@digibuffer/file-system

High-level file and folder management layer — file registration, status tracking, folder hierarchy, rename strategies, and DB-agnostic lifecycle hooks. Pairs with @digibuffer/file-manager-core for storage operations.

Features

  • File registrationregisterFile() server-side before upload, confirmUpload() after
  • Pending → active flow — files start as pending, become active after confirmed upload
  • Folder hierarchy — create, rename, move, delete folders; breadcrumb paths
  • Two rename strategiesrenameDisplay() (DB only) and renameKey() (storage + DB)
  • DB-agnostic — implement FileSystemAdapter once for any database
  • HTTP router — drop-in API route handler, Zod-validated
  • React hooksuseFiles, useFolders, useFolderActions, useFileActions
  • Lifecycle callbacksonFileDeleted, onKeyRename, onFileConfirmed

Installation

npm install @digibuffer/file-system

Architecture

Upload library  →  registerFile()  →  status: pending
                →  presigned URL   →  client uploads directly to R2/S3
                →  confirmUpload() →  status: active

@digibuffer/file-system handles the DB side. Pair it with @digibuffer/file-manager-core for the storage side.


Setup

1. Implement the DB adapter

// lib/file-system-adapter.ts
import type { FileSystemAdapter, FileRecord, FolderRecord, ... } from '@digibuffer/file-system';

export class DrizzleAdapter implements FileSystemAdapter {
  async createFile(input) {
    return db.insert(files).values({ ...input, status: 'pending' }).returning().get();
  }
  async updateFileStatus(fileId, status) {
    return db.update(files).set({ status }).where(eq(files.id, fileId)).returning().get();
  }
  async getFile(fileId) {
    return db.select().from(files).where(eq(files.id, fileId)).get() ?? null;
  }
  async getFileByKey(key) {
    return db.select().from(files).where(eq(files.key, key)).get() ?? null;
  }
  async listFiles(options) {
    // Apply filters, sort, pagination — return { data, hasMore, total }
  }
  async deleteFile(fileId) {
    await db.delete(files).where(eq(files.id, fileId));
    return { success: true };
  }
  async deleteFiles(fileIds) {
    await db.delete(files).where(inArray(files.id, fileIds));
    return { succeeded: fileIds, failed: [] };
  }
  async updateFile(fileId, data) {
    return db.update(files).set(data).where(eq(files.id, fileId)).returning().get();
  }
  // Folders (optional)
  async createFolder(input) { ... }
  async listFolders(options) { ... }
  // etc.
}

2. Create the FileSystem instance

// lib/file-system.ts
import { FileSystem } from '@digibuffer/file-system';
import { fileManager } from './file-manager'; // your @digibuffer/file-manager-core instance
import { DrizzleAdapter } from './file-system-adapter';

export const fileSystem = new FileSystem({
  adapter: new DrizzleAdapter(),

  // Called after confirmUpload() — trigger image processing, webhooks, etc.
  onFileConfirmed: async (file) => {
    await imageQueue.add({ fileId: file.id, key: file.key });
  },

  // Called after file(s) are deleted from DB — delete from storage too
  onFileDeleted: async (keys) => {
    await fileManager.deleteObjects(keys);
  },

  // Called during renameKey() — move object in storage before DB update
  onKeyRename: async (oldKey, newKey) => {
    await fileManager.moveObject(oldKey, newKey);
  },
});

Upload flow

Server-side (in your upload route)

import { fileSystem } from '@/lib/file-system';
import { fileManager } from '@/lib/file-manager';

export async function POST(req: Request) {
  const { filename, contentType, size, folderId } = await req.json();
  const session = await getSession(req);

  // 1. Generate a storage key
  const key = `users/${session.userId}/${crypto.randomUUID()}-${filename}`;

  // 2. Register in DB as pending
  const file = await fileSystem.registerFile({
    key,
    name: filename,
    size,
    contentType,
    folderId: folderId ?? null,
    userId: session.userId,
  });

  // 3. Generate presigned PUT URL
  const uploadUrl = await fileManager.presignPutObject(key, { contentType, expiresIn: 900 });

  return Response.json({ fileId: file.id, uploadUrl });
}

After upload completes (webhook or client callback)

// app/api/upload/confirm/route.ts
export async function POST(req: Request) {
  const { fileId } = await req.json();
  const file = await fileSystem.confirmUpload(fileId);
  // → file.status is now 'active'
  // → onFileConfirmed callback is called
  return Response.json({ file });
}

File operations

// List files
const { data, hasMore, total } = await fileSystem.listFiles({
  folderId: 'folder-id',
  userId: 'user-123',
  status: 'active',
  search: 'report',
  sort: 'newest',
  limit: 20,
  page: 1,
});

// Get a file
const file = await fileSystem.getFile('file-id');
const file = await fileSystem.getFileByKey('users/123/photo.jpg');

// Delete (also calls onFileDeleted → removes from storage)
await fileSystem.deleteFile('file-id');
await fileSystem.deleteFiles(['id1', 'id2', 'id3']);

// Move to a different folder
await fileSystem.moveFile('file-id', 'new-folder-id');
await fileSystem.moveFile('file-id', null); // move to root

// Rename display name only (DB update, no storage change)
// Use when files are served by ID-based URLs
await fileSystem.renameDisplay({ fileId: 'file-id', name: 'New Name.jpg' });

// Rename storage key (moves in storage, then updates DB)
// Requires onKeyRename callback. Use when file URLs are key-based.
await fileSystem.renameKey({ fileId: 'file-id', newKey: 'users/123/new-name.jpg' });

Folder operations

// Create
const folder = await fileSystem.createFolder({ name: 'Photos', parentId: null });
const sub    = await fileSystem.createFolder({ name: '2024', parentId: folder.id });

// List
const folders = await fileSystem.listFolders({ parentId: null });

// Rename
await fileSystem.renameFolder(folder.id, 'My Photos');

// Move
await fileSystem.moveFolder({ folderId: sub.id, newParentId: null }); // move to root

// Breadcrumbs (root → current folder)
const breadcrumbs = await fileSystem.getBreadcrumbs(sub.id);
// → [{ name: 'Photos', ... }, { name: '2024', ... }]

// Delete
await fileSystem.deleteFolder(folder.id, true); // recursive

HTTP router (Next.js / any runtime)

// app/api/file-system/route.ts
import { FileSystem, createFileSystemRouter } from '@digibuffer/file-system';
import { toRouteHandler } from '@digibuffer/file-system/adapters/next';

const router = createFileSystemRouter({
  fileSystem,
  getAuthContext: async (req) => {
    const session = await getSession(req);
    return { userId: session?.user?.id };
  },
  authorize: async (context, action) => {
    return !!context.userId; // require login for all actions
  },
});

export const { POST } = toRouteHandler(router);

Supported actions

| Action | Body | Description | |---|---|---| | listFiles | { options? } | List files with filtering/pagination | | listFolders | { options? } | List folders | | getFile | { fileId } | Get a single file | | confirmUpload | { fileId } | Mark file as active | | confirmUploads | { fileIds } | Batch confirm | | deleteFile | { fileId } | Delete a file | | deleteFiles | { fileIds } | Batch delete | | renameDisplay | { fileId, name } | Rename (DB only) | | renameKey | { fileId, newKey } | Rename (storage + DB) | | moveFile | { fileId, folderId } | Move to folder | | createFolder | { name, parentId? } | Create a folder | | getFolder | { folderId } | Get a folder | | getBreadcrumbs | { folderId } | Breadcrumb path | | renameFolder | { folderId, name } | Rename a folder | | moveFolder | { folderId, newParentId } | Move a folder | | deleteFolder | { folderId, recursive? } | Delete a folder |


React client hooks

import { FileSystemProvider } from '@digibuffer/file-system/client';

<FileSystemProvider config={{ endpoint: '/api/file-system' }}>
  <App />
</FileSystemProvider>

useFiles

const { data, isLoading, hasMore, list, loadMore, reset } = useFiles();

useEffect(() => {
  list({ folderId: null, status: 'active', limit: 20 });
}, []);

const files = data?.data ?? [];

useFolders

const { data: folders, isLoading, list } = useFolders();

useEffect(() => { list({ parentId: null }); }, []);

useFolderActions

const { createFolder, renameFolder, moveFolder, deleteFolder, getBreadcrumbs } = useFolderActions();

const folder = await createFolder('Photos', null);
await renameFolder(folder.id, 'My Photos');
await moveFolder(folder.id, parentFolderId);
const crumbs = await getBreadcrumbs(folder.id);
await deleteFolder(folder.id, true); // recursive

useFileActions

const { confirmUpload, deleteFile, deleteFiles, renameDisplay, renameKey, moveFile } = useFileActions();

await confirmUpload(fileId);
await deleteFile(fileId);
await deleteFiles([id1, id2, id3]);
await renameDisplay(fileId, 'new-name.jpg');
await renameKey(fileId, 'users/123/new-name.jpg');
await moveFile(fileId, folderId);
await moveFile(fileId, null); // to root

FileSystemAdapter interface

All methods must be implemented. Folder methods are optional (only required if you use folder operations):

interface FileSystemAdapter {
  // Required
  createFile(input: RegisterFileInput): Promise<FileRecord>;
  updateFileStatus(fileId: string, status: FileStatus): Promise<FileRecord>;
  getFile(fileId: string): Promise<FileRecord | null>;
  getFileByKey(key: string): Promise<FileRecord | null>;
  listFiles(options: ListFilesOptions): Promise<PaginatedResponse<FileRecord>>;
  deleteFile(fileId: string): Promise<OperationResult>;
  deleteFiles(fileIds: string[]): Promise<{ succeeded: string[]; failed: string[] }>;
  updateFile(fileId: string, data: Partial<Pick<FileRecord, 'name' | 'key' | 'folderId' | 'metadata'>>): Promise<FileRecord>;

  // Optional (folder operations)
  createFolder?(input: CreateFolderInput): Promise<FolderRecord>;
  getFolder?(folderId: string): Promise<FolderRecord | null>;
  listFolders?(options: ListFoldersOptions): Promise<FolderRecord[]>;
  moveFolder?(input: MoveFolderInput): Promise<FolderRecord>;
  renameFolder?(folderId: string, name: string): Promise<FolderRecord>;
  deleteFolder?(folderId: string, recursive?: boolean): Promise<OperationResult>;
  buildFolderPath?(folderId: string | null): Promise<FolderRecord[]>;
}

FileRecord shape

interface FileRecord {
  id: string;
  key: string;           // storage object key
  name: string;          // display name
  size: number;
  contentType: string;
  status: 'pending' | 'active' | 'deleted';
  folderId: string | null;
  userId?: string;
  url?: string;
  metadata?: Record<string, unknown>;
  createdAt: Date;
  updatedAt: Date;
}

Rename strategies

| Strategy | When to use | What changes | |---|---|---| | renameDisplay(fileId, name) | Files served by ID-based URLs | DB name column only | | renameKey(fileId, newKey) | Files served by storage key (direct R2/S3 URLs) | Storage object moved + DB key + name updated |

renameKey requires the onKeyRename callback to be configured. It will move the object in storage before updating the DB, and roll back if storage fails.


License

Proprietary — All rights reserved.