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.11.0

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
  • 📱 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:

// 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 (req): Promise<TDriveConfigInformation> => {
		const auth = await verifyAuth(req);
		if (!auth) throw new Error("Unauthenticated");
		return {
			key: { userId: auth.userId },
			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/*" />;
}

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 (req): Promise<TDriveConfigInformation> => {
		const auth = await verifyAuth(req);
		if (!auth) throw new Error("Unauthenticated");
		return {
			key: { userId: auth.userId },
			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", 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 |

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.


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 },
}

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 '*').


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