CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-uppy--core

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

Pending
Overview
Eval results
Files

file-validation.mddocs/

File Validation and Restrictions

Uppy provides comprehensive file validation through a configurable restrictions system that supports both individual file constraints and aggregate restrictions across all files.

Restrictions Interface

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
}

Default Values

All restriction values default to null (no restriction) except:

  • requiredMetaFields: defaults to empty array []

Validation Methods

Individual File Validation

validateSingleFile(file: ValidateableFile<M, B>): string | null;

Validates a single file against individual restrictions (size, type, etc.).

Parameters:

  • file: File to validate

Returns: Error message string if validation fails, null if valid

Aggregate Validation

validateAggregateRestrictions(files: ValidateableFile<M, B>[]): string | null;

Validates aggregate restrictions across all files (total size, file count).

Parameters:

  • files: Array of all files to validate

Returns: Error message string if validation fails, null if valid

Complete Validation

validateRestrictions(
  file: ValidateableFile<M, B>,
  files?: ValidateableFile<M, B>[]
): RestrictionError<M, B> | null;

Performs both individual and aggregate validation.

Parameters:

  • file: File to validate
  • files: All files for aggregate validation (optional)

Returns: RestrictionError object if validation fails, null if valid

ValidateableFile Type

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.

RestrictionError Class

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 user
  • file: Optional file reference for file-specific errors
  • isRestriction: Always true, used to identify restriction errors

File Type Validation

MIME Type Patterns

// 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 Extension Patterns

// File extensions
allowedFileTypes: ['.jpg', '.jpeg', '.png', '.gif']

// Mixed MIME types and extensions
allowedFileTypes: ['image/*', '.pdf', '.doc', '.docx']

Complex Type Restrictions

// 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']

Size Restrictions

Individual File Size

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

Total Size Restrictions

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

File Count Restrictions

// Maximum 10 files
maxNumberOfFiles: 10

// Minimum 2 files required
minNumberOfFiles: 2

// Range restrictions
restrictions: {
  minNumberOfFiles: 1,
  maxNumberOfFiles: 5
}

Required Metadata Fields

// 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'
});

Validation Examples

Basic Restrictions Setup

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.');
  }
});

Custom Validation Logic

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;
  }
});

Dynamic Restrictions

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);
      }
    });
  }
}

File Type Detection

// 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);
      }
    });
  }
});

Validation with User Feedback

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);
}

Batch Validation

// 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;
});

Validation Best Practices

User Experience

  • Provide clear, actionable error messages
  • Show file size limits in human-readable format
  • Validate files immediately when added, not just before upload
  • Allow users to fix validation errors easily

Performance

  • Use client-side validation to reduce server load
  • Implement progressive validation (quick checks first)
  • Cache validation results when possible
  • Avoid expensive validation operations in tight loops

Security

  • Always validate file types by content, not just extension
  • Implement server-side validation as well as client-side
  • Be cautious with file metadata validation
  • Consider implementing virus scanning for uploaded files

Accessibility

  • Ensure error messages are accessible to screen readers
  • Provide multiple ways to understand restrictions (text, icons, examples)
  • Make restriction information discoverable before file selection

Install with Tessl CLI

npx tessl i tessl/npm-uppy--core

docs

event-management.md

file-validation.md

index.md

plugin-development.md

type-system.md

uppy-class.md

tile.json