Core module for the extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more
—
Uppy provides comprehensive file validation through a configurable restrictions system that supports both individual file constraints and aggregate restrictions across all files.
interface Restrictions {
maxFileSize: number | null; // Maximum individual file size in bytes
minFileSize: number | null; // Minimum individual file size in bytes
maxTotalFileSize: number | null; // Maximum total size of all files
maxNumberOfFiles: number | null; // Maximum number of files
minNumberOfFiles: number | null; // Minimum number of files
allowedFileTypes: string[] | null; // Allowed MIME types/extensions
requiredMetaFields: string[]; // Required metadata fields
}All restriction values default to null (no restriction) except:
requiredMetaFields: defaults to empty array []validateSingleFile(file: ValidateableFile<M, B>): string | null;Validates a single file against individual restrictions (size, type, etc.).
Parameters:
file: File to validateReturns: Error message string if validation fails, null if valid
validateAggregateRestrictions(files: ValidateableFile<M, B>[]): string | null;Validates aggregate restrictions across all files (total size, file count).
Parameters:
files: Array of all files to validateReturns: Error message string if validation fails, null if valid
validateRestrictions(
file: ValidateableFile<M, B>,
files?: ValidateableFile<M, B>[]
): RestrictionError<M, B> | null;Performs both individual and aggregate validation.
Parameters:
file: File to validatefiles: All files for aggregate validation (optional)Returns: RestrictionError object if validation fails, null if valid
type ValidateableFile<M extends Meta, B extends Body> = Pick<
UppyFile<M, B>,
'type' | 'extension' | 'size' | 'name'
> & {
isGhost?: boolean; // Optional ghost file marker
};Minimal file interface required for validation. Both UppyFile and CompanionFile objects can be validated.
class RestrictionError<M extends Meta, B extends Body> extends Error {
isUserFacing: boolean; // Whether error should be shown to user
file?: UppyFile<M, B>; // Associated file (if applicable)
isRestriction: true; // Marker property for error type identification
constructor(
message: string,
opts?: {
isUserFacing?: boolean;
file?: UppyFile<M, B>;
}
);
}Properties:
isUserFacing: Defaults to true, indicates if error should be displayed to userfile: Optional file reference for file-specific errorsisRestriction: Always true, used to identify restriction errors// Exact MIME types
allowedFileTypes: ['image/jpeg', 'image/png', 'text/plain']
// Wildcard patterns
allowedFileTypes: ['image/*', 'video/*', 'audio/*']
// Mixed patterns
allowedFileTypes: ['image/*', 'application/pdf', 'text/plain']// File extensions
allowedFileTypes: ['.jpg', '.jpeg', '.png', '.gif']
// Mixed MIME types and extensions
allowedFileTypes: ['image/*', '.pdf', '.doc', '.docx']// Multiple specific types
allowedFileTypes: [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'text/plain',
'text/csv'
]
// Category-based with exceptions
allowedFileTypes: ['image/*', 'video/*', 'application/pdf']// Maximum 5MB per file
maxFileSize: 5 * 1024 * 1024
// Minimum 1KB per file
minFileSize: 1024
// Both min and max
restrictions: {
minFileSize: 1024, // 1KB minimum
maxFileSize: 10485760 // 10MB maximum
}// Maximum 50MB total across all files
maxTotalFileSize: 50 * 1024 * 1024
// Combine with individual limits
restrictions: {
maxFileSize: 5 * 1024 * 1024, // 5MB per file
maxTotalFileSize: 25 * 1024 * 1024 // 25MB total
}// Maximum 10 files
maxNumberOfFiles: 10
// Minimum 2 files required
minNumberOfFiles: 2
// Range restrictions
restrictions: {
minNumberOfFiles: 1,
maxNumberOfFiles: 5
}// Require specific metadata fields
requiredMetaFields: ['name', 'description', 'category']
// Usage with custom metadata
const uppy = new Uppy({
restrictions: {
requiredMetaFields: ['author', 'tags']
},
meta: {
author: '', // Default values
tags: ''
}
});
// Files must have these metadata fields set
uppy.setFileMeta(fileId, {
author: 'John Doe',
tags: 'important,work'
});import Uppy from '@uppy/core';
const uppy = new Uppy({
restrictions: {
maxFileSize: 2 * 1024 * 1024, // 2MB per file
maxNumberOfFiles: 3, // Maximum 3 files
allowedFileTypes: ['image/*'], // Images only
requiredMetaFields: ['caption'] // Require caption
}
});
// Listen for validation failures
uppy.on('restriction-failed', (file, error) => {
console.log('Validation failed:', error.message);
// Handle specific error types
if (error.message.includes('size')) {
showError('File is too large. Maximum size is 2MB.');
} else if (error.message.includes('type')) {
showError('Only image files are allowed.');
} else if (error.message.includes('meta')) {
showError('Please provide a caption for your image.');
}
});const uppy = new Uppy({
restrictions: {
maxFileSize: 5 * 1024 * 1024,
allowedFileTypes: ['image/*', 'application/pdf']
},
onBeforeFileAdded: (currentFile, files) => {
// Custom validation beyond standard restrictions
// Check filename pattern
if (!currentFile.name.match(/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9]+$/)) {
uppy.info('Filename can only contain letters, numbers, hyphens, and underscores', 'error');
return false;
}
// Check for duplicates
const isDuplicate = Object.values(files).some(
file => file.name === currentFile.name
);
if (isDuplicate) {
uppy.info(`File "${currentFile.name}" is already added`, 'error');
return false;
}
// Custom business logic
if (currentFile.type.startsWith('image/') && currentFile.size < 10000) {
uppy.info('Image files must be at least 10KB', 'error');
return false;
}
return true;
}
});class DynamicUploadRestrictions {
private uppy: Uppy;
private userPlan: 'free' | 'pro' | 'enterprise';
constructor(userPlan: 'free' | 'pro' | 'enterprise') {
this.userPlan = userPlan;
this.uppy = new Uppy();
this.updateRestrictions();
}
updateRestrictions() {
const restrictions = this.getRestrictionsForPlan(this.userPlan);
this.uppy.setOptions({ restrictions });
}
getRestrictionsForPlan(plan: string): Partial<Restrictions> {
switch (plan) {
case 'free':
return {
maxFileSize: 1024 * 1024, // 1MB
maxNumberOfFiles: 3,
maxTotalFileSize: 5 * 1024 * 1024, // 5MB
allowedFileTypes: ['image/*']
};
case 'pro':
return {
maxFileSize: 10 * 1024 * 1024, // 10MB
maxNumberOfFiles: 20,
maxTotalFileSize: 100 * 1024 * 1024, // 100MB
allowedFileTypes: ['image/*', 'video/*', 'application/pdf']
};
case 'enterprise':
return {
maxFileSize: 100 * 1024 * 1024, // 100MB
maxNumberOfFiles: null, // Unlimited
maxTotalFileSize: null, // Unlimited
allowedFileTypes: null // All types
};
default:
return {};
}
}
upgradeUser(newPlan: 'free' | 'pro' | 'enterprise') {
this.userPlan = newPlan;
this.updateRestrictions();
// Revalidate existing files
const files = this.uppy.getFiles();
files.forEach(file => {
const error = this.uppy.validateRestrictions(file, files);
if (error) {
this.uppy.emit('restriction-failed', file, error);
}
});
}
}// Enhanced file type validation
const uppy = new Uppy({
restrictions: {
allowedFileTypes: ['image/*']
},
onBeforeFileAdded: (currentFile, files) => {
// Validate actual file content, not just extension
return new Promise((resolve) => {
if (currentFile.type.startsWith('image/')) {
// For images, verify it's actually an image
const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => {
uppy.info('File appears corrupted or is not a valid image', 'error');
resolve(false);
};
img.src = URL.createObjectURL(currentFile.data);
} else {
resolve(true);
}
});
}
});const uppy = new Uppy({
restrictions: {
maxFileSize: 5 * 1024 * 1024,
maxNumberOfFiles: 10,
allowedFileTypes: ['image/*', 'video/*', 'application/pdf']
}
});
uppy.on('restriction-failed', (file, error) => {
const fileName = file?.name || 'Unknown file';
// Provide detailed, user-friendly error messages
if (error.message.includes('size')) {
const maxSize = formatBytes(5 * 1024 * 1024);
const fileSize = formatBytes(file?.size || 0);
showDetailedError(
'File Too Large',
`"${fileName}" (${fileSize}) exceeds the maximum allowed size of ${maxSize}.`,
'Please choose a smaller file or compress the current file.'
);
} else if (error.message.includes('type')) {
showDetailedError(
'File Type Not Allowed',
`"${fileName}" is not an allowed file type.`,
'Please select an image, video, or PDF file.'
);
} else if (error.message.includes('number')) {
showDetailedError(
'Too Many Files',
'You can upload a maximum of 10 files at once.',
'Please remove some files and try again.'
);
}
});
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function showDetailedError(title: string, message: string, suggestion: string) {
// Display rich error message to user
const errorDialog = document.createElement('div');
errorDialog.innerHTML = `
<div class="error-dialog">
<h3>${title}</h3>
<p><strong>${message}</strong></p>
<p><em>${suggestion}</em></p>
<button onclick="this.parentElement.remove()">OK</button>
</div>
`;
document.body.appendChild(errorDialog);
}// Pre-upload validation of entire batch
uppy.on('upload', (files) => {
const fileArray = Object.values(files);
// Validate aggregate restrictions manually
const totalSize = fileArray.reduce((sum, file) => sum + (file.size || 0), 0);
const maxTotal = 50 * 1024 * 1024; // 50MB
if (totalSize > maxTotal) {
uppy.info(
`Total file size (${formatBytes(totalSize)}) exceeds limit (${formatBytes(maxTotal)})`,
'error'
);
return false; // Cancel upload
}
// Check for required metadata on all files
const missingMeta = fileArray.filter(file =>
!file.meta.description || file.meta.description.trim() === ''
);
if (missingMeta.length > 0) {
uppy.info(
`${missingMeta.length} file(s) missing required description`,
'error'
);
return false;
}
return true;
});Install with Tessl CLI
npx tessl i tessl/npm-uppy--core