Gathering detailed insights and metrics for pushduck
Gathering detailed insights and metrics for pushduck
Gathering detailed insights and metrics for pushduck
Gathering detailed insights and metrics for pushduck
🦆 Framework-agnostic file upload library with TypeScript, S3-compatible storage, presigned URLs, and edge runtime support
npm install pushduck
Typescript
Module System
Min. Node Version
Node Version
NPM Version
TypeScript (86.09%)
JavaScript (11.37%)
Shell (2.53%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
36 Stars
288 Commits
2 Branches
2 Contributors
Updated on Jul 13, 2025
Latest Version
0.1.21
Package Id
pushduck@0.1.21
Unpacked Size
227.44 kB
Size
57.38 kB
File Count
31
NPM Version
10.8.2
Node Version
20.19.3
Published on
Jul 10, 2025
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
1
23
Pushduck is a powerful, type-safe file upload library for Next.js applications with S3-compatible storage providers. Built with modern React patterns and comprehensive TypeScript support.
onStart
, onProgress
, onSuccess
, and onError
1# Using npm 2npm install pushduck 3 4# Using yarn 5yarn add pushduck 6 7# Using pnpm 8pnpm add pushduck
1// lib/upload.ts 2import { createUploadConfig } from 'pushduck/server'; 3 4const { s3, config } = createUploadConfig() 5 .provider("aws", { 6 bucket: process.env.AWS_BUCKET_NAME!, 7 region: process.env.AWS_REGION!, 8 accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 9 secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 10 }) 11 .defaults({ 12 maxFileSize: '10MB', 13 acl: 'public-read', 14 }) 15 .paths({ 16 prefix: 'uploads', 17 generateKey: (file, metadata) => { 18 const userId = metadata.userId || 'anonymous'; 19 const timestamp = Date.now(); 20 const randomId = Math.random().toString(36).substring(2, 8); 21 return `${userId}/${timestamp}/${randomId}/${file.name}`; 22 }, 23 }) 24 .build(); 25 26// Create router with your upload routes 27const router = s3.createRouter({ 28 imageUpload: s3.image().max('5MB'), 29 documentUpload: s3.file({ maxSize: '10MB' }), 30 avatarUpload: s3.image().max('2MB').middleware(async ({ metadata }) => ({ 31 ...metadata, 32 userId: metadata.userId || 'anonymous', 33 })), 34}); 35 36export { router }; 37export type AppRouter = typeof router;
1// app/api/upload/route.ts 2import { router } from '@/lib/upload'; 3 4export const { GET, POST } = router.handlers;
1// app/upload/page.tsx 2'use client'; 3 4import { useUpload } from 'pushduck/client'; 5import type { AppRouter } from '@/lib/upload'; 6 7export default function UploadPage() { 8 const { uploadFiles, files, isUploading, error, reset } = useUpload<AppRouter>('imageUpload'); 9 10 const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { 11 const selectedFiles = Array.from(e.target.files || []); 12 uploadFiles(selectedFiles); 13 }; 14 15 return ( 16 <div className="p-6"> 17 <input 18 type="file" 19 multiple 20 accept="image/*" 21 onChange={handleFileSelect} 22 disabled={isUploading} 23 className="mb-4" 24 /> 25 26 {files.map((file) => ( 27 <div key={file.id} className="mb-2 p-2 border rounded"> 28 <div className="flex justify-between items-center"> 29 <span className="font-medium">{file.name}</span> 30 <span className="text-sm text-gray-500">{file.status}</span> 31 </div> 32 33 <div className="w-full bg-gray-200 rounded-full h-2 mt-2"> 34 <div 35 className="bg-blue-600 h-2 rounded-full transition-all" 36 style={{ width: `${file.progress}%` }} 37 /> 38 </div> 39 40 {file.status === 'success' && file.url && ( 41 <a 42 href={file.url} 43 target="_blank" 44 rel="noopener noreferrer" 45 className="text-blue-600 hover:underline text-sm" 46 > 47 View uploaded file 48 </a> 49 )} 50 51 {file.status === 'error' && ( 52 <p className="text-red-600 text-sm mt-1">Error: {file.error}</p> 53 )} 54 </div> 55 ))} 56 57 <button 58 onClick={reset} 59 disabled={isUploading} 60 className="px-4 py-2 bg-gray-500 text-white rounded disabled:opacity-50" 61 > 62 Reset 63 </button> 64 </div> 65 ); 66}
Pushduck provides comprehensive callback support for handling the complete upload lifecycle:
1const { uploadFiles } = useUpload<AppRouter>('imageUpload', { 2 // Called when upload process begins (after validation passes) 3 onStart: (files) => { 4 console.log(`🚀 Starting upload of ${files.length} files`); 5 setUploadStarted(true); 6 }, 7 8 // Called with progress updates (0-100) 9 onProgress: (progress) => { 10 console.log(`📊 Progress: ${progress}%`); 11 setProgress(progress); 12 }, 13 14 // Called when all uploads complete successfully 15 onSuccess: (results) => { 16 console.log('✅ Upload complete!', results); 17 setUploadStarted(false); 18 // Update your UI with uploaded file URLs 19 }, 20 21 // Called when upload fails 22 onError: (error) => { 23 console.error('❌ Upload failed:', error.message); 24 setUploadStarted(false); 25 // Show error message to user 26 }, 27});
The callbacks follow a predictable sequence:
onError
is calledonStart
→ onProgress(0)
→ onProgress(n)
→ onSuccess
onStart
→ onProgress(0)
→ onError
The onStart
callback is perfect for:
1onStart: (files) => { 2 // Show loading state immediately 3 setIsUploading(true); 4 5 // Display file list being uploaded 6 setUploadingFiles(files); 7 8 // Show toast notification 9 toast.info(`Uploading ${files.length} files...`); 10 11 // Disable form submission 12 setFormDisabled(true); 13}
1createUploadConfig().provider("aws", { 2 bucket: 'your-bucket', 3 region: 'us-east-1', 4 accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 5 secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 6})
1createUploadConfig().provider("cloudflareR2", { 2 accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, 3 bucket: process.env.R2_BUCKET!, 4 accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID!, 5 secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY!, 6 region: 'auto', 7})
1createUploadConfig().provider("digitalOceanSpaces", { 2 region: 'nyc3', 3 bucket: 'your-space', 4 accessKeyId: process.env.DO_SPACES_ACCESS_KEY_ID!, 5 secretAccessKey: process.env.DO_SPACES_SECRET_ACCESS_KEY!, 6})
1createUploadConfig().provider("minio", { 2 endpoint: 'localhost:9000', 3 bucket: 'your-bucket', 4 accessKeyId: process.env.MINIO_ACCESS_KEY_ID!, 5 secretAccessKey: process.env.MINIO_SECRET_ACCESS_KEY!, 6 useSSL: false, 7})
1.defaults({ 2 maxFileSize: '10MB', 3 allowedFileTypes: ['image/*', 'application/pdf'], 4 acl: 'public-read', 5 metadata: { 6 uploadedBy: 'system', 7 environment: process.env.NODE_ENV, 8 }, 9})
1.paths({ 2 prefix: 'uploads', 3 generateKey: (file, metadata) => { 4 const userId = metadata.userId || 'anonymous'; 5 const timestamp = Date.now(); 6 const randomId = Math.random().toString(36).substring(2, 8); 7 const sanitizedName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_'); 8 return `${userId}/${timestamp}/${randomId}/${sanitizedName}`; 9 }, 10})
1.hooks({ 2 onUploadStart: async ({ file, metadata }) => { 3 console.log(`Starting upload: ${file.name}`); 4 }, 5 onUploadComplete: async ({ file, metadata, url, key }) => { 6 console.log(`Upload complete: ${file.name} -> ${url}`); 7 // Save to database, send notifications, etc. 8 }, 9 onUploadError: async ({ file, metadata, error }) => { 10 console.error(`Upload failed: ${file.name}`, error); 11 // Log error, send alerts, etc. 12 }, 13})
1// Image validation 2s3.image().max('5MB') 3 4// File validation 5s3.file({ maxSize: '10MB', allowedTypes: ['application/pdf'] }) 6 7// Custom validation 8s3.file().validate(async (file) => { 9 if (file.name.includes('virus')) { 10 throw new Error('Suspicious file detected'); 11 } 12}) 13 14// Middleware for metadata 15s3.image().middleware(async ({ file, metadata }) => ({ 16 ...metadata, 17 processedAt: new Date().toISOString(), 18})) 19 20// Route-specific paths 21s3.image().paths({ 22 prefix: 'avatars', 23 generateKey: (file, metadata) => `user-${metadata.userId}/avatar.${file.name.split('.').pop()}`, 24}) 25 26// Lifecycle hooks per route 27s3.image().onUploadComplete(async ({ file, url, metadata }) => { 28 await updateUserAvatar(metadata.userId, url); 29})
1const { 2 uploadFiles, // (files: File[]) => Promise<void> 3 files, // UploadFile[] - reactive file state 4 isUploading, // boolean 5 error, // Error | null 6 reset, // () => void 7} = useUpload<AppRouter>('routeName', { 8 onSuccess: (results) => console.log('Success:', results), 9 onError: (error) => console.error('Error:', error), 10});
1const { 2 uploadFiles, 3 files, 4 isUploading, 5 progress, 6 cancel, 7 retry, 8} = useUploadRoute('routeName', { 9 onProgress: (progress) => console.log(`${progress.percentage}%`), 10 onComplete: (results) => console.log('Complete:', results), 11});
For more control, use the upload client directly:
1import { createUploadClient } from 'pushduck/client'; 2import type { AppRouter } from '@/lib/upload'; 3 4const client = createUploadClient<AppRouter>({ 5 endpoint: '/api/upload', 6}); 7 8// Upload files 9await client.imageUpload.upload(files, { 10 onProgress: (progress) => console.log(`${progress.percentage}%`), 11 metadata: { userId: '123' }, 12});
1function MultiUploadForm() { 2 const imageUpload = useUpload<AppRouter>('imageUpload'); 3 const documentUpload = useUpload<AppRouter>('documentUpload'); 4 5 return ( 6 <div> 7 <div> 8 <h3>Images</h3> 9 <input 10 type="file" 11 accept="image/*" 12 multiple 13 onChange={(e) => imageUpload.uploadFiles(Array.from(e.target.files || []))} 14 /> 15 {/* Render image upload state */} 16 </div> 17 18 <div> 19 <h3>Documents</h3> 20 <input 21 type="file" 22 accept=".pdf,.doc,.docx" 23 multiple 24 onChange={(e) => documentUpload.uploadFiles(Array.from(e.target.files || []))} 25 /> 26 {/* Render document upload state */} 27 </div> 28 </div> 29 ); 30}
1function CustomUploader() { 2 const { uploadFiles, files, isUploading } = useUpload<AppRouter>('imageUpload'); 3 const [dragActive, setDragActive] = useState(false); 4 5 const handleDrop = (e: React.DragEvent) => { 6 e.preventDefault(); 7 setDragActive(false); 8 const droppedFiles = Array.from(e.dataTransfer.files); 9 uploadFiles(droppedFiles); 10 }; 11 12 return ( 13 <div 14 className={`border-2 border-dashed p-8 text-center ${ 15 dragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300' 16 }`} 17 onDragOver={(e) => e.preventDefault()} 18 onDragEnter={() => setDragActive(true)} 19 onDragLeave={() => setDragActive(false)} 20 onDrop={handleDrop} 21 > 22 {isUploading ? ( 23 <p>Uploading...</p> 24 ) : ( 25 <p>Drag and drop files here, or click to select</p> 26 )} 27 </div> 28 ); 29}
1# AWS S3 2AWS_ACCESS_KEY_ID=your_access_key 3AWS_SECRET_ACCESS_KEY=your_secret_key 4AWS_REGION=us-east-1 5AWS_BUCKET_NAME=your_bucket 6 7# Cloudflare R2 8CLOUDFLARE_ACCOUNT_ID=your_account_id 9CLOUDFLARE_R2_ACCESS_KEY_ID=your_access_key 10CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_secret_key 11R2_BUCKET=your_bucket 12 13# DigitalOcean Spaces 14DO_SPACES_ACCESS_KEY_ID=your_access_key 15DO_SPACES_SECRET_ACCESS_KEY=your_secret_key 16 17# MinIO 18MINIO_ACCESS_KEY_ID=your_access_key 19MINIO_SECRET_ACCESS_KEY=your_secret_key
If you're upgrading from an older version, see our Migration Guide for step-by-step instructions.
We welcome contributions! Please see our Contributing Guide for details.
This project is licensed under the MIT License - see the LICENSE file for details.
No vulnerabilities found.
No security vulnerabilities found.