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

@muhgholy/next-drive

v4.23.32

Published

File storage and management for Next.js applications

Readme

@muhgholy/next-drive

File storage and management for Next.js and Express apps. Includes a responsive UI, search, trash system, and secure file handling.

Features

  • 📁 File Management – Upload, rename, move, organize files and folders
  • 🔍 Search – Search active files or trash with real-time filtering
  • 🗑️ Trash System – Soft delete, restore, and empty trash
  • Anonymous Uploads – One-time uploads with auto-expiry, confirmation, and abuse prevention
  • �📱 Responsive UI – Optimized for desktop and mobile
  • 🎬 Video Thumbnails – Auto-generated thumbnails (requires FFmpeg)
  • 🔐 Security – Signed URLs and configurable upload limits
  • 📊 View Modes – Grid/List views with sorting and grouping

Installation

npm install @muhgholy/next-drive

Requirements

| Dependency | Version | | ---------- | ------- | | Next.js | >= 14 | | React | >= 18 | | Mongoose | >= 7 | | TypeScript | >= 5 |

TypeScript Configuration:

This package uses subpath exports. Configure your tsconfig.json based on your project type:

For Next.js (App Router or Pages Router):

{
	"compilerOptions": {
		"module": "esnext",
		"moduleResolution": "bundler"
	}
}

For Node.js/Express servers:

{
	"compilerOptions": {
		"module": "nodenext",
		"moduleResolution": "nodenext"
	}
}

⚠️ The legacy "moduleResolution": "node" is not supported and will cause build errors with subpath imports like @muhgholy/next-drive/server.

FFmpeg (for video thumbnails):

# macOS
brew install ffmpeg

# Ubuntu
sudo apt install ffmpeg

# Windows
# Download from https://ffmpeg.org and add to PATH

Styles

Styles are automatically injected when you import components from @muhgholy/next-drive/client. No additional CSS import is required!

All CSS classes are prefixed with nd- to avoid conflicts with your project's styles. CSS variables are scoped to the .nd-drive-root container class.

If styles are not loading (e.g., with certain bundler configurations), you can manually import:

import "@muhgholy/next-drive/client/styles.css";

Quick Start

1. Server Configuration

Create lib/drive.ts to configure storage, security, and authentication:

[!IMPORTANT] You must connect to your database (e.g., Mongoose) before calling driveConfiguration. The configuration initialization will fail if the database connection is not ready.

// lib/drive.ts
import { driveConfiguration } from "@muhgholy/next-drive/server";
import type { TDriveConfigInformation } from "@muhgholy/next-drive/server";

driveConfiguration({
	database: "MONGOOSE",
	apiUrl: "/api/drive",
	storage: { path: "/var/data/drive" },
	security: {
		maxUploadSizeInBytes: 50 * 1024 * 1024, // 50MB
		allowedMimeTypes: ["image/*", "video/*", "application/pdf"],
		signedUrls: {
			enabled: true,
			secret: process.env.DRIVE_SECRET!,
			expiresIn: 3600, // 1 hour
		},
	},
	information: async (input): Promise<TDriveConfigInformation> => {
		// REQUEST method — called from API handler with req
		if (input.method === "REQUEST") {
			const auth = await verifyAuth(input.req);
			if (!auth) throw new Error("Unauthenticated");
			return {
				key: { userId: auth.userId },
				storage: { quotaInBytes: 1024 * 1024 * 1024 }, // 1GB
			};
		}

		// KEY method — called from server-side code (driveUpload, etc.)
		return {
			key: input.key,
			storage: { quotaInBytes: 1024 * 1024 * 1024 }, // 1GB
		};
	},
});

2. API Route (Pages Router)

⚠️ Important: Must be in pages/ folder with body parser disabled.

// pages/api/drive.ts
import "@/lib/drive";
import { driveAPIHandler } from "@muhgholy/next-drive/server";
import type { NextApiRequest, NextApiResponse } from "next";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
	return driveAPIHandler(req, res);
}

export const config = {
	api: { bodyParser: false },
};

3. Client Provider

Wrap your app with DriveProvider:

// app/layout.tsx
import { DriveProvider } from "@muhgholy/next-drive/client";

export default function RootLayout({ children }) {
	return <DriveProvider apiEndpoint="/api/drive">{children}</DriveProvider>;
}

4. UI Components

File Explorer:

import { DriveExplorer } from "@muhgholy/next-drive/client";

export default function DrivePage() {
	return <DriveExplorer />;
}

File Picker (for forms):

import { useState } from "react";
import { DriveFileChooser } from "@muhgholy/next-drive/client";
import type { TDriveFile } from "@muhgholy/next-drive/client";

function MyForm() {
	const [file, setFile] = useState<TDriveFile | null>(null);
	return <DriveFileChooser value={file} onChange={setFile} accept="image/*" />;
}

For unauthenticated, one-time uploads, add the allowUnauthenticated prop. See Anonymous (One-Time) Uploads.


Headless Uploads

Need to build your own upload UI? Use the framework-agnostic uploadFile / uploadFiles functions. They run the same chunked, retrying upload engine that powers the built-in components — without any React or UI — so you control the markup, loading states, and cancel buttons.

Capabilities:

  • 📈 Byte-level progress – smooth percentage, even for small single-chunk files
  • 🪵 Logs – the same per-step log stream shown by DriveFileChooser
  • Cancellation – every call returns a cancel() function
  • 🔓 Anonymous – just omit accountId (works with unauthenticated server config)
  • 📦 Consistent result – returns a normalized TDriveFile on completion

Single File

import { uploadFile } from "@muhgholy/next-drive/client";

const { id, promise, cancel } = uploadFile({
	apiEndpoint: "/api/drive",
	file, // a File from an <input type="file" />
	folderId: null, // optional: target folder id (defaults to root)
	accountId: null, // optional: omit/null = anonymous upload
	withCredentials: false, // optional: send cookies cross-origin
	onProgress: ({ percent, uploadedBytes, totalBytes }) => {
		setProgress(percent); // 0–100
	},
	onLog: ({ type, message, timestamp }) => {
		console.log(`[${type}] ${message}`);
	},
});

// Call cancel() any time to abort (e.g. a Cancel button)
// <button onClick={cancel}>Cancel</button>

const result = await promise;
if (result.status === "complete") {
	result.file; // TDriveFile — same shape as DriveFileChooser's onChange
	result.driveId; // server drive item id
	result.item; // raw item (signed URL, full metadata, etc.)
} else if (result.status === "cancelled") {
	// upload was aborted
} else {
	result.error; // failure message
}

Multiple Files (with concurrency)

import { uploadFiles } from "@muhgholy/next-drive/client";

const { uploads, cancelAll, promise } = uploadFiles(files, {
	apiEndpoint: "/api/drive",
	concurrency: 2, // optional: max parallel uploads (default 2)
	onProgress: ({ id, percent }) => updateRow(id, percent),
	onFileComplete: (result) => {
		if (result.status === "complete") addFile(result.file);
	},
});

// uploads: one handle per file — each has its own { id, promise, cancel }
// cancelAll(): abort everything

const results = await promise; // resolves once all files settle

Result Type

The result is a discriminated union (no optional fields — narrow by status):

type TDriveUploadResult =
	| { id: string; status: "complete"; driveId: string; file: TDriveFile; item: unknown }
	| { id: string; status: "cancelled"; driveId: string | null }
	| { id: string; status: "error"; driveId: string | null; error: string };

Prefer React state management? The useUpload hook wraps the same engine and exposes a reactive uploads array — see the source for DriveUpload for a reference implementation.


Express Integration

Use the Express adapter instead of Next.js API routes:

// lib/drive.ts
import { driveConfigurationExpress } from "@muhgholy/next-drive/server/express";
import type { TDriveConfigInformation } from "@muhgholy/next-drive/server/express";

driveConfigurationExpress({
	database: "MONGOOSE",
	apiUrl: "/api/drive",
	storage: { path: "/var/data/drive" },
	security: {
		maxUploadSizeInBytes: 50 * 1024 * 1024,
		allowedMimeTypes: ["image/*", "video/*", "application/pdf"],
	},
	information: async (input): Promise<TDriveConfigInformation> => {
		if (input.method === "REQUEST") {
			const auth = await verifyAuth(input.req);
			if (!auth) throw new Error("Unauthenticated");
			return {
				key: { userId: auth.userId },
				storage: { quotaInBytes: 1024 * 1024 * 1024 },
			};
		}
		return {
			key: input.key,
			storage: { quotaInBytes: 1024 * 1024 * 1024 },
		};
	},
});
// routes/drive.ts
import "./lib/drive";
import express from "express";
import { driveAPIHandlerExpress } from "@muhgholy/next-drive/server/express";

const router = express.Router();
router.all("/drive", driveAPIHandlerExpress);

export default router;

⚠️ Don't use express.json() middleware on this route.


Zod Validation

Validate file data in forms or API routes:

import { z } from "zod";
import { driveFileSchemaZod } from "@muhgholy/next-drive/schemas";

const formSchema = z.object({
	asset: driveFileSchemaZod,
	title: z.string(),
});

Schema also available from /client and /server exports.


Client-Side File URLs

Generate URLs for displaying files:

import { useDrive } from "@muhgholy/next-drive/client";
import type { TDriveFile } from "@muhgholy/next-drive/client";

function MyComponent({ driveFile }: { driveFile: TDriveFile }) {
	const { createUrl } = useDrive();

	// Get file URL
	const url = createUrl(driveFile);

	return <img src={url} alt={driveFile.file.name} />;
}

Server-Side File Access

Upload File

Upload files programmatically from server-side code:

import { driveUpload } from "@muhgholy/next-drive/server";

// Upload to specific folder by ID
const file = await driveUpload(
	"/tmp/photo.jpg",
	{ userId: "123" },
	{
		name: "photo.jpg",
		folder: { id: "folderId" }, // Optional: folder ID
		accountId: "LOCAL", // Optional: storage account ID
		enforce: false, // Optional: bypass quota check
	}
);

// Upload to folder by path (creates folders if not exist)
const file = await driveUpload(
	"/tmp/photo.jpg",
	{ userId: "123" },
	{
		name: "photo.jpg",
		folder: { path: "images/2024/january" }, // Creates folders recursively
	}
);

// Upload from stream
import fs from "fs";
const stream = fs.createReadStream("/tmp/video.mp4");
const file = await driveUpload(
	stream,
	{ userId: "123" },
	{
		name: "video.mp4",
		enforce: true, // Skip quota check
	}
);

// Upload from Buffer
const buffer = Buffer.from("file content");
const file = await driveUpload(
	buffer,
	{ userId: "123" },
	{
		name: "document.txt",
		mime: "text/plain", // Optional: specify MIME type
	}
);

Options:

| Option | Type | Required | Description | | ------------ | --------------------------------- | -------- | -------------------------------------------------------- | | name | string | Yes | File name with extension | | folder.id | string | No | Parent folder ID | | folder.path| string | No | Folder path (e.g., images/2024) - creates if not exist | | accountId | string | No | Storage account ID ('LOCAL' for local storage) | | mime | string | No | MIME type (auto-detected from extension if not provided) | | enforce | boolean | No | Bypass quota check (default: false) |

Get Signed URL

import { driveGetUrl } from "@muhgholy/next-drive/server";

// Default expiry (from config)
const url = driveGetUrl(fileId);

// Custom expiry in seconds
const url = driveGetUrl(fileId, { expiry: 7200 }); // 2 hours

// Specific date
const url = driveGetUrl(fileId, { expiry: new Date("2026-12-31") });

Read File Stream

import { driveReadFile } from "@muhgholy/next-drive/server";

// Using file ID
const { stream, mime, size } = await driveReadFile(fileId);
stream.pipe(response);

// Using database document
const drive = await Drive.findById(fileId);
const { stream, mime, size } = await driveReadFile(drive);

Get File/Folder Information

import { driveInfo } from "@muhgholy/next-drive/server";

// Using file ID
const info = await driveInfo("694f5013226de007be94fcc0");
console.log(info.name, info.size, info.createdAt);
console.log(info.dimensions); // { width: 1920, height: 1080 } for images
console.log(info.duration); // 120 (seconds) for videos

// Using TDriveFile
const file = { id: "123", isFolder: false, file: { name: "photo.jpg", mime: "image/jpeg", size: 1024 } };
const info = await driveInfo(file);

Returns TDriveInformation:

| Property | Type | Description | | ------------ | -------------------- | ------------------------------------ | | id | string | File/folder ID | | name | string | File/folder name | | type | 'FILE' \| 'FOLDER' | Item type | | mime | string? | MIME type (files only) | | size | number? | Size in bytes (files only) | | hash | string? | Content hash (files only) | | dimensions | {width, height}? | Image dimensions | | duration | number? | Video duration in seconds | | status | string | Processing status | | provider | object | Storage provider info (LOCAL/GOOGLE) | | parent | {id, name}? | Parent folder | | createdAt | Date | Creation timestamp | | trashedAt | Date \| null? | Trash timestamp if deleted |

Get Local File Path

For libraries requiring file paths (Sharp, FFmpeg, etc.):

import { driveFilePath } from "@muhgholy/next-drive/server";

const { path, mime, size, provider } = await driveFilePath(fileId);

// Use with Sharp
await sharp(path).resize(800, 600).toFile("output.jpg");

// Use with FFmpeg
await ffmpeg(path).format("mp4").save("output.mp4");

Google Drive files are automatically downloaded to local cache.

List Files and Folders

List files and folders in a directory:

import { driveList } from "@muhgholy/next-drive/server";

// List root folder
const items = await driveList({ key: { userId: "123" } });

// List specific folder
const items = await driveList({
	key: { userId: "123" },
	folderId: "folderIdHere",
	limit: 50,
});

// Pagination
const items = await driveList({
	key: { userId: "123" },
	folderId: "root",
	limit: 20,
	afterId: "lastItemId",
});

Options:

| Option | Type | Required | Description | | ----------- | ------------------------- | -------- | ---------------------------------------------- | | key | Record<string, unknown> | Yes | Owner key (must match authenticated user) | | folderId | string \| null | No | Folder ID to list (null or 'root' for root) | | accountId | string | No | Storage account ID ('LOCAL' for local storage) | | limit | number | No | Maximum items to return (default: 100) | | afterId | string | No | Last item ID for pagination |

List Files (Paginated)

List files only (not folders) with offset-based pagination:

import { driveListFiles } from "@muhgholy/next-drive/server";

// List all files (page 1, default limit 50)
const result = await driveListFiles({});

// List files for a specific owner
const result = await driveListFiles({ key: { userId: "123" } });

// List files in a folder with pagination
const result = await driveListFiles({
	key: { userId: "123" },
	folderId: "folderIdHere",
	page: 2,
	limit: 20,
});

// Access pagination info
console.log(result.pagination);
// { page: 2, limit: 20, totalCount: 87, totalPages: 5, hasMore: true }

Options:

| Option | Type | Required | Description | | ----------- | -------------------------- | -------- | ---------------------------------------------- | | key | Record<string, unknown> | No | Owner key (omit to list all files) | | folderId | string \| null | No | Folder ID (null/'root' for root, omit for all) | | accountId | string | No | Storage account ID | | page | number | No | Page number (default: 1) | | limit | number | No | Items per page (default: 50, max: 100) |

Returns: { items: TDatabaseDrive[], pagination: { page, limit, totalCount, totalPages, hasMore } }

Direct Database Access

For custom queries, use the exposed Mongoose model:

import { DatabaseMongoDrive } from "@muhgholy/next-drive/server";
import type { IDatabaseDriveDocument } from "@muhgholy/next-drive/server";

// Custom query
const files = await DatabaseMongoDrive.find({
	owner: { userId: "123" },
	"information.type": "FILE",
	"information.mime": { $regex: "^image/" },
});

// Aggregation
const stats = await DatabaseMongoDrive.aggregate([
	{ $match: { owner: { userId: "123" }, trashedAt: null } },
	{ $group: { _id: "$information.type", count: { $sum: 1 } } },
]);

Cleanup Orphaned Files

Remove orphaned file folders from storage that no longer exist in the database, and clean up leftover temp directories:

import { driveCleanup } from "@muhgholy/next-drive/server";

const result = await driveCleanup();
console.log(`Removed ${result.removed.length} orphaned folders`);
console.log(`Freed ${result.totalFreedInBytes} bytes`);

Returns: { removed: string[], totalFreedInBytes: number }

Delete File or Folder

Permanently delete a file or folder from the drive system:

import { driveDelete } from "@muhgholy/next-drive/server";

// Delete a file
await driveDelete("694f5013226de007be94fcc0");

// Delete a folder recursively (default behavior)
await driveDelete(folderId, { recurse: true });

// Delete only if folder is empty
try {
	await driveDelete(folderId, { recurse: false });
} catch (error) {
	// Throws error if folder contains items
	console.error("Cannot delete non-empty folder");
}

// Delete using database document
const drive = await Drive.findById(fileId);
await driveDelete(drive);

// Delete using TDatabaseDrive object
const items = await driveList({ key: { userId: "123" } });
await driveDelete(items[0]);

Parameters:

| Parameter | Type | Description | | --------- | ---------------------------------------------------- | -------------------------------------- | | source | string \| IDatabaseDriveDocument \| TDatabaseDrive | File/folder ID or object to delete | | options | { recurse?: boolean } | Delete options (default: recurse=true) |

Options:

| Option | Type | Default | Description | | --------- | --------- | ------- | ----------------------------------------------------------------------------------------- | | recurse | boolean | true | If true, deletes folder and all children. If false, throws error if folder contains items |

Note: This permanently deletes the file/folder. For soft deletion (trash), use the trash API action instead.


Anonymous (One-Time) Uploads

Let unauthenticated users upload a file once — without giving them access to a drive. The user picks a file, it uploads with progress (just like the drive explorer), and you receive a normal TDriveFile (with a real drive.id). The file is temporary: it is automatically deleted after a TTL (default 60 minutes) unless your backend explicitly confirms it.

The same DriveFileChooser adapts to the visitor automatically: logged-in users get the full drive explorer (browse + pick existing files or upload to their own drive, stored permanently), while anonymous visitors get a one-time upload (temporary, auto-expiring). You opt in with a single allowUnauthenticated prop.

This is useful for forms where you need a drive.id for an anonymous visitor (e.g. a contact form attachment, a job application, or a guest submission) but don't want to expose your authenticated drive.

Lifecycle: pick file → upload → returns TDriveFile (temporary) → your backend saves drive.iddriveConfirm(id) keeps it → otherwise drivePurgeExpired() deletes it after the TTL.

1. Enable in your server configuration

Add an unauthenticated block to security. It is completely separate from your authenticated limits (it has its own size and MIME allow-list). The feature is disabled unless enabled: true.

security: {
	// ...your normal authenticated limits
	maxUploadSizeInBytes: 50 * 1024 * 1024,
	allowedMimeTypes: ["image/*", "video/*", "application/pdf"],

	unauthenticated: {
		enabled: true,
		ttlMinutes: 60, // Lifetime before auto-delete (default 60)
		maxUploadSizeInBytes: 25 * 1024 * 1024, // 25MB (separate from authenticated limit)
		allowedMimeTypes: ["image/*", "application/pdf"],

		// Optional abuse prevention (all fields optional)
		abuse: {
			perIp: { windowMinutes: 10, max: 20 }, // Max 20 uploads / 10 min per IP
			hourlyPerIp: 60, // Max 60 uploads / hour per IP
			maxConcurrent: 10, // Max simultaneous anonymous uploads (global)
			maxLiveBytes: 2 * 1024 * 1024 * 1024, // Max total live temporary storage (global)
		},
	},
}

Anonymous uploads do not count against any user's quota and are stored with no owner.

2. Client component

Pass the allowUnauthenticated prop to DriveFileChooser. It checks the visitor's auth status once on mount:

  • Logged in → opens the normal drive explorer (browse and pick existing files or upload to their own drive).
  • Not logged in → opens the native file picker and uploads anonymously, returning the uploaded TDriveFile.
import { useState } from "react";
import { DriveFileChooser } from "@muhgholy/next-drive/client";
import type { TDriveFile } from "@muhgholy/next-drive/client";

function GuestForm() {
	const [file, setFile] = useState<TDriveFile | null>(null);

	return (
		<DriveFileChooser
			allowUnauthenticated
			value={file}
			onChange={setFile}
			accept="image/*"
			placeholder="Upload an attachment"
		/>
	);
}

DriveFileChooser must be inside a DriveProvider (it reads apiEndpoint and resolves auth from context). The multiple and accept props work the same as the standard chooser.

3. Confirm to keep the file

After you persist the returned drive.id (e.g. attach it to a submitted form record), call driveConfirm to clear its expiry so it is never auto-deleted:

import { driveConfirm } from "@muhgholy/next-drive/server";

const kept = await driveConfirm(driveFile.id);
// kept === true if the file exists

Returns: Promise<boolean>true if the file exists. Safe to call on files uploaded while logged in (they are already permanent, so it is a harmless no-op) — so your form handler can always call driveConfirm regardless of whether the visitor was authenticated.

4. Purge expired (unconfirmed) files

Run drivePurgeExpired on a schedule (cron job or interval) to permanently delete temporary files whose TTL has passed and were never confirmed:

import { drivePurgeExpired } from "@muhgholy/next-drive/server";

// e.g. in a cron route or a setInterval on your server
const result = await drivePurgeExpired();
console.log(`Purged ${result.removed.length} expired files, freed ${result.totalFreedInBytes} bytes`);

Returns: { removed: string[], totalFreedInBytes: number }

[!IMPORTANT] Confirmed files are never purged. Only files that are both expired and unconfirmed are removed. If you never call drivePurgeExpired, temporary files will remain — schedule it (e.g. hourly).

Determining the client IP (behind a proxy)

Per-IP limits resolve the client IP from cf-connecting-ip, then the first entry of x-forwarded-for, then the socket address. Override the trusted headers or supply your own resolver:

unauthenticated: {
	enabled: true,
	maxUploadSizeInBytes: 25 * 1024 * 1024,
	allowedMimeTypes: ["image/*"],
	abuse: {
		perIp: { windowMinutes: 10, max: 20 },
		trustedHeaders: ["cf-connecting-ip", "x-forwarded-for"], // checked in order
		clientId: (req) => (req.headers["cf-connecting-ip"] as string) ?? "unknown", // full override
	},
}

[!WARNING] Only trust forwarded IP headers if every request reaches your origin through a proxy you control (e.g. Cloudflare). If clients can reach the origin directly, these headers can be spoofed — lock your origin down to your proxy's IPs.

Abuse counters are kept in-memory per process. In a multi-instance deployment, each instance enforces limits independently; use a shared store (e.g. Redis) if you need cross-instance limits.


Configuration Options

Security

security: {
	maxUploadSizeInBytes: 50 * 1024 * 1024, // 50MB
	allowedMimeTypes: ['image/*', 'video/*', 'application/pdf'],
	signedUrls: {
		enabled: true,
		secret: process.env.DRIVE_SECRET!,
		expiresIn: 3600, // seconds
	},
	trash: { retentionDays: 30 },
	// Anonymous one-time uploads (see "Anonymous (One-Time) Uploads" section)
	unauthenticated: {
		enabled: true,
		ttlMinutes: 60,
		maxUploadSizeInBytes: 25 * 1024 * 1024,
		allowedMimeTypes: ['image/*', 'application/pdf'],
		abuse: {
			perIp: { windowMinutes: 10, max: 20 },
			hourlyPerIp: 60,
			maxConcurrent: 10,
			maxLiveBytes: 2 * 1024 * 1024 * 1024,
		},
	},
}

| Option | Type | Default | Description | | ------------------------------------- | ---------- | ----------- | ---------------------------------------------------------------- | | unauthenticated.enabled | boolean | false | Enable anonymous one-time uploads | | unauthenticated.ttlMinutes | number | 60 | Minutes before an unconfirmed upload becomes eligible for purge | | unauthenticated.maxUploadSizeInBytes| number | — | Max size per anonymous upload (separate from authenticated) | | unauthenticated.allowedMimeTypes | string[] | — | Allowed MIME types for anonymous uploads | | unauthenticated.abuse.perIp | { windowMinutes, max } | — | Sliding-window per-IP upload limit | | unauthenticated.abuse.hourlyPerIp | number | — | Max anonymous uploads per IP per hour | | unauthenticated.abuse.maxConcurrent | number | — | Max simultaneous anonymous uploads (global, per process) | | unauthenticated.abuse.maxLiveBytes | number | — | Max total live temporary storage in bytes (global) | | unauthenticated.abuse.trustedHeaders| string[] | ['cf-connecting-ip', 'x-forwarded-for'] | Headers checked in order to resolve client IP | | unauthenticated.abuse.clientId | (req) => string | — | Full override for resolving the client identifier |

CORS (Cross-Origin)

Required when client and API are on different domains:

cors: {
	enabled: true,
	origins: ['https://app.example.com'],
	credentials: true, // Allow cookies/auth headers
	maxAge: 86400, // Preflight cache (24 hours)
}

Client setup for CORS:

<DriveProvider apiEndpoint="https://api.example.com/drive" withCredentials={true}>
	{children}
</DriveProvider>

| Option | Type | Default | Description | | ---------------- | -------------------- | ----------------------------------------------------------- | ------------------------------- | | enabled | boolean | false | Enable CORS | | origins | string \| string[] | '*' | Allowed origins | | methods | string[] | ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] | Allowed HTTP methods | | allowedHeaders | string[] | ['Content-Type', 'Authorization', 'X-Drive-Account'] | Headers clients can send | | exposedHeaders | string[] | ['Content-Length', 'Content-Type', 'Content-Disposition'] | Headers exposed to client | | credentials | boolean | false | Allow credentials | | maxAge | number | 86400 | Preflight cache duration (secs) |

When credentials: true, you must specify explicit origins (not '*').

Drive Modes

Next Drive supports two operation modes:

  1. NORMAL (Default): Requires information callback to identify the user and set quota. Enforces security limits.
  2. ROOT: System/Admin mode. No authentication required by default (unless you provide information).

[!WARNING] In both modes, ensure your database connection is active before initializing the configuration.

Root Mode Configuration:

driveConfiguration({
    mode: 'ROOT', // Enable root mode
    database: 'MONGOOSE',
    apiUrl: '/api/drive',
    storage: { path: '/var/data/drive' },
    // Optional: Security defaults to 10GB limit and all mime types if omitted
    security: {
        maxUploadSizeInBytes: 10 * 1024 * 1024 * 1024, // 10GB
        allowedMimeTypes: ['*/*']
    }
});

Key differences in ROOT mode:

  • information callback is optional.
  • If information is omitted:
    • key is null (files owned by system).
    • quota is unlimited.
  • Default security.maxUploadSizeInBytes is 10GB.
  • Default security.allowedMimeTypes is ['*/*'] (all files allowed).

Google Drive Integration

1. Google Cloud Setup

  1. Go to Google Cloud Console
  2. Create/select a project
  3. Enable Google Drive API
  4. Create OAuth 2.0 credentials (Web application)
  5. Add redirect URI (e.g., http://localhost:3000/api/drive?action=callback)

2. Configuration

storage: {
	path: '/var/data/drive',
	google: {
		clientId: process.env.GOOGLE_CLIENT_ID!,
		clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
		redirectUri: process.env.GOOGLE_REDIRECT_URI!,
	},
}

3. Environment Variables

GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=http://localhost:3000/api/drive?action=callback

OAuth Scopes

| Scope | Description | | ------------------------------------------------ | ---------------------- | | https://www.googleapis.com/auth/drive | Full Drive access | | https://www.googleapis.com/auth/drive.file | App-created files only | | https://www.googleapis.com/auth/drive.readonly | Read-only access |


Image Optimization

Serve optimized images with dynamic compression, resizing, and format conversion using query parameters.

URL Format

/api/drive?action=serve&id={fileId}&quality={preset}&display={context}&size={scale}&fit={mode}&position={anchor}&format={format}

Parameters

| Parameter | Type | Description | |-----------|------|-------------| | quality | low / medium / high / 1-100 | Compression level | | display | string | Sets aspect ratio, base dimensions, quality factor, and default fit | | size | string | Scale factor (xs/sm/md/lg/xl) or standalone dimension preset | | fit | cover / contain / fill / inside / outside | How image fits into dimensions | | position | center / top / bottom / left / right / attention / entropy | Crop anchor point (for cover/contain) | | format | jpeg / webp / avif / png | Output format |

Fit Options

| Fit | Behavior | Use Case | |-----|----------|----------| | cover | Crop to fill exact dimensions | Thumbnails, avatars, cards | | contain | Fit within dimensions (may letterbox) | Logos, icons | | fill | Stretch to exact dimensions (may distort) | Background fills | | inside | Fit within, no upscaling (default) | Article images | | outside | Cover minimum dimensions | Backgrounds |

Position Options (for cover/contain)

| Position | Anchor Point | |----------|--------------| | center | Center (default) | | top | Top center | | bottom | Bottom center | | left | Left center | | right | Right center | | attention | Focus on most "interesting" area (AI-based) | | entropy | Focus on highest entropy area |

How Display + Size Work Together

When display is specified, it defines the aspect ratio, base dimensions, and default fit. The size parameter then scales those dimensions:

display=article-image + size=sm  → 400×225  (16:9, half size, fit=inside)
display=thumbnail + size=md     → 150×150  (1:1, fit=cover)
display=avatar + fit=contain    → 128×128  (override default cover to contain)

When no display is specified, size uses standalone presets (fixed dimensions).

Quality Presets

| Preset | Base Quality | Use Case | |--------|--------------|----------| | low | 30 | Thumbnails, previews | | medium | 50 | General content | | high | 75 | High-quality display | | 1-100 | Custom | Fine-tuned control |

Quality is dynamically adjusted based on file size. Larger files get more aggressive compression.

Display Presets (Aspect Ratio + Dimensions + Fit)

| Display | Aspect Ratio | Base Size | Quality | Default Fit | |---------|--------------|-----------|---------|-------------| | article-header | 16:9 | 1200×675 | 0.9 | inside | | article-image | 16:9 | 800×450 | 0.85 | inside | | thumbnail | 1:1 | 150×150 | 0.7 | cover | | avatar | 1:1 | 128×128 | 0.8 | cover | | logo | 2:1 | 200×100 | 0.95 | contain | | card | 4:3 | 400×300 | 0.8 | cover | | gallery | 1:1 | 600×600 | 0.85 | cover | | og | ~1.9:1 | 1200×630 | 0.9 | cover | | icon | 1:1 | 48×48 | 0.75 | cover | | cover | 16:9 | 1920×1080 | 0.9 | cover | | story | 9:16 | 1080×1920 | 0.85 | cover | | video | 16:9 | 1280×720 | 0.85 | cover | | banner | 3:1 | 1200×400 | 0.9 | cover | | portrait | 3:4 | 600×800 | 0.85 | inside | | landscape | 4:3 | 800×600 | 0.85 | inside |

Size Scale (with Display)

When used with a display preset, size scales the dimensions:

| Size | Scale | Example with article-image (800×450) | |------|-------|----------------------------------------| | xs | 0.25× | 200×113 | | sm | 0.5× | 400×225 | | md | 1.0× | 800×450 | | lg | 1.5× | 1200×675 | | xl | 2.0× | 1600×900 | | 2xl | 2.5× | 2000×1125 |

Standalone Size Presets (without Display)

When no display is specified, use these fixed dimension presets:

| Size | Dimensions | Size | Dimensions | |------|------------|------|------------| | xs | 64×64 | landscape-sm | 480×270 | | sm | 128×128 | landscape | 800×450 | | md | 256×256 | landscape-lg | 1280×720 | | lg | 512×512 | portrait-sm | 270×480 | | xl | 1024×1024 | portrait | 450×800 | | icon | 48×48 | wide | 1200×630 | | thumb | 150×150 | banner | 1200×400 | | video | 1280×720 | card | 400×300 |

Examples

<!-- Article image, smaller variant (400×225, fit=inside) -->
<img src="/api/drive?action=serve&id=123&display=article-image&size=sm&format=webp">

<!-- Thumbnail with cover fit (crops to fill 150×150 square) -->
<img src="/api/drive?action=serve&id=123&display=thumbnail&format=webp">

<!-- Avatar with top-focused crop (for face photos) -->
<img src="/api/drive?action=serve&id=123&display=avatar&fit=cover&position=top&format=webp">

<!-- Gallery with AI-based attention crop -->
<img src="/api/drive?action=serve&id=123&display=gallery&fit=cover&position=attention&format=webp">

<!-- Card image, override default cover to contain -->
<img src="/api/drive?action=serve&id=123&display=card&fit=contain&format=webp">

<!-- Banner with custom position -->
<img src="/api/drive?action=serve&id=123&display=banner&position=bottom&format=webp">

<!-- Standalone size, no display -->
<img src="/api/drive?action=serve&id=123&size=landscape&fit=cover&format=webp">

<!-- Just quality, no resize -->
<img src="/api/drive?action=serve&id=123&quality=medium&format=webp">

License

MIT