@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
Maintainers
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 registration —
registerFile()server-side before upload,confirmUpload()after - Pending → active flow — files start as
pending, becomeactiveafter confirmed upload - Folder hierarchy — create, rename, move, delete folders; breadcrumb paths
- Two rename strategies —
renameDisplay()(DB only) andrenameKey()(storage + DB) - DB-agnostic — implement
FileSystemAdapteronce for any database - HTTP router — drop-in API route handler, Zod-validated
- React hooks —
useFiles,useFolders,useFolderActions,useFileActions - Lifecycle callbacks —
onFileDeleted,onKeyRename,onFileConfirmed
Installation
npm install @digibuffer/file-systemArchitecture
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); // recursiveHTTP 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); // recursiveuseFileActions
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 rootFileSystemAdapter 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.
