JavaScript APIs

JavaScript File API: Working with Files in the Browser

Master the File API to handle file uploads, read file contents, and process files client-side. Learn FileReader, Blob manipulation, and drag-and-drop implementations.

By JavaScript Document Team
file-apiweb-apisfile-handlinguploadblob

The File API provides a way to read, manipulate, and process files directly in the browser without server round trips. It enables powerful client-side file handling for uploads, previews, and data processing.

Understanding the File API

The File API consists of several interfaces: File, FileList, FileReader, and Blob, which work together to provide comprehensive file handling capabilities.

Basic File Input

// File input handling
const fileInput = document.getElementById('fileInput');

fileInput.addEventListener('change', (event) => {
  const files = event.target.files; // FileList object

  for (let i = 0; i < files.length; i++) {
    const file = files[i];

    console.log('File name:', file.name);
    console.log('File size:', file.size, 'bytes');
    console.log('File type:', file.type);
    console.log('Last modified:', new Date(file.lastModified));

    // Process file
    processFile(file);
  }
});

// File validation
function validateFile(file) {
  const maxSize = 10 * 1024 * 1024; // 10MB
  const allowedTypes = [
    'image/jpeg',
    'image/png',
    'image/gif',
    'application/pdf',
  ];

  if (file.size > maxSize) {
    throw new Error(`File ${file.name} is too large. Maximum size is 10MB.`);
  }

  if (!allowedTypes.includes(file.type)) {
    throw new Error(`File type ${file.type} is not allowed.`);
  }

  return true;
}

// Multiple file selection
function setupMultipleFileInput() {
  const input = document.createElement('input');
  input.type = 'file';
  input.multiple = true;
  input.accept = 'image/*,.pdf'; // Accept images and PDFs

  input.addEventListener('change', (event) => {
    const files = Array.from(event.target.files);
    console.log(`Selected ${files.length} files`);

    // Process files
    files.forEach((file) => {
      try {
        validateFile(file);
        processFile(file);
      } catch (error) {
        console.error(error.message);
      }
    });
  });

  // Trigger file selection
  input.click();
}

FileReader API

// Read file as text
function readAsText(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = (event) => {
      resolve(event.target.result);
    };

    reader.onerror = (event) => {
      reject(new Error('Failed to read file'));
    };

    reader.readAsText(file);
  });
}

// Read file as data URL (base64)
function readAsDataURL(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = (event) => {
      resolve(event.target.result);
    };

    reader.onerror = (event) => {
      reject(new Error('Failed to read file'));
    };

    reader.readAsDataURL(file);
  });
}

// Read file as array buffer
function readAsArrayBuffer(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = (event) => {
      resolve(event.target.result);
    };

    reader.onerror = (event) => {
      reject(new Error('Failed to read file'));
    };

    reader.readAsArrayBuffer(file);
  });
}

// Read file with progress
function readFileWithProgress(file, onProgress) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onloadstart = () => {
      console.log('Started reading file');
    };

    reader.onprogress = (event) => {
      if (event.lengthComputable) {
        const percentComplete = (event.loaded / event.total) * 100;
        onProgress(percentComplete);
      }
    };

    reader.onload = (event) => {
      resolve(event.target.result);
    };

    reader.onerror = () => {
      reject(new Error('Failed to read file'));
    };

    reader.onabort = () => {
      reject(new Error('File reading aborted'));
    };

    reader.readAsArrayBuffer(file);
  });
}

// Read file in chunks
async function readFileInChunks(file, chunkSize = 1024 * 1024) {
  const chunks = [];
  let offset = 0;

  while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize);
    const arrayBuffer = await readAsArrayBuffer(chunk);
    chunks.push(arrayBuffer);
    offset += chunkSize;
  }

  return chunks;
}

Practical Applications

Image Preview

class ImagePreview {
  constructor(container) {
    this.container = container;
    this.maxWidth = 300;
    this.maxHeight = 300;
  }

  async createPreview(file) {
    if (!file.type.startsWith('image/')) {
      throw new Error('File is not an image');
    }

    const dataUrl = await this.readAsDataURL(file);
    const img = await this.loadImage(dataUrl);
    const canvas = this.createThumbnail(img);

    // Create preview element
    const preview = document.createElement('div');
    preview.className = 'image-preview';
    preview.innerHTML = `
      <div class="preview-image">
        ${canvas.outerHTML}
      </div>
      <div class="preview-info">
        <p class="filename">${file.name}</p>
        <p class="filesize">${this.formatFileSize(file.size)}</p>
        <p class="dimensions">${img.width} × ${img.height}</p>
      </div>
      <button class="remove-preview" data-filename="${file.name}">×</button>
    `;

    this.container.appendChild(preview);

    // Add remove functionality
    preview.querySelector('.remove-preview').addEventListener('click', () => {
      preview.remove();
      this.onRemove(file);
    });

    return preview;
  }

  readAsDataURL(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => resolve(e.target.result);
      reader.onerror = reject;
      reader.readAsDataURL(file);
    });
  }

  loadImage(src) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = reject;
      img.src = src;
    });
  }

  createThumbnail(img) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    // Calculate dimensions
    let { width, height } = this.calculateDimensions(
      img.width,
      img.height,
      this.maxWidth,
      this.maxHeight
    );

    canvas.width = width;
    canvas.height = height;

    // Draw image
    ctx.drawImage(img, 0, 0, width, height);

    return canvas;
  }

  calculateDimensions(width, height, maxWidth, maxHeight) {
    if (width <= maxWidth && height <= maxHeight) {
      return { width, height };
    }

    const aspectRatio = width / height;

    if (width > height) {
      width = maxWidth;
      height = maxWidth / aspectRatio;
    } else {
      height = maxHeight;
      width = maxHeight * aspectRatio;
    }

    return { width: Math.round(width), height: Math.round(height) };
  }

  formatFileSize(bytes) {
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    if (bytes === 0) return '0 Bytes';
    const i = Math.floor(Math.log(bytes) / Math.log(1024));
    return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i];
  }

  onRemove(file) {
    // Override this method to handle file removal
    console.log('File removed:', file.name);
  }
}

// EXIF data reader
class ExifReader {
  async readExif(file) {
    if (!file.type.startsWith('image/')) {
      throw new Error('File is not an image');
    }

    const arrayBuffer = await this.readAsArrayBuffer(file);
    const dataView = new DataView(arrayBuffer);

    // Check for JPEG
    if (dataView.getUint16(0) !== 0xffd8) {
      throw new Error('Not a valid JPEG file');
    }

    // Find EXIF data
    const exifData = this.findExifData(dataView);

    if (!exifData) {
      return null;
    }

    return this.parseExifData(exifData);
  }

  findExifData(dataView) {
    let offset = 2;
    let marker;

    while (offset < dataView.byteLength) {
      marker = dataView.getUint16(offset);

      if (marker === 0xffe1) {
        // EXIF marker found
        const size = dataView.getUint16(offset + 2);
        const exifOffset = offset + 4;

        // Check for "Exif\0\0"
        if (
          dataView.getUint32(exifOffset) === 0x45786966 &&
          dataView.getUint16(exifOffset + 4) === 0x0000
        ) {
          return {
            dataView: dataView,
            offset: exifOffset + 6,
            size: size - 8,
          };
        }
      }

      offset += 2 + dataView.getUint16(offset + 2);
    }

    return null;
  }

  parseExifData(exifData) {
    // Simplified EXIF parsing
    const tags = {
      0x010f: 'Make',
      0x0110: 'Model',
      0x0112: 'Orientation',
      0x011a: 'XResolution',
      0x011b: 'YResolution',
      0x0132: 'DateTime',
      0x010e: 'ImageDescription',
      0x013b: 'Artist',
    };

    const result = {};

    // This is a simplified version - real EXIF parsing is more complex
    Object.entries(tags).forEach(([tag, name]) => {
      // Attempt to find and parse tag
      // In real implementation, you'd properly parse IFD structures
      result[name] = 'N/A';
    });

    return result;
  }

  readAsArrayBuffer(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => resolve(e.target.result);
      reader.onerror = reject;
      reader.readAsArrayBuffer(file);
    });
  }
}

Drag and Drop

class DragDropUploader {
  constructor(dropZone, options = {}) {
    this.dropZone = dropZone;
    this.options = {
      acceptedTypes: options.acceptedTypes || ['image/*'],
      maxFileSize: options.maxFileSize || 10 * 1024 * 1024, // 10MB
      multiple: options.multiple !== false,
      onDrop: options.onDrop || (() => {}),
      onError: options.onError || ((error) => console.error(error)),
    };

    this.init();
  }

  init() {
    // Prevent default drag behaviors
    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
      this.dropZone.addEventListener(eventName, this.preventDefaults, false);
      document.body.addEventListener(eventName, this.preventDefaults, false);
    });

    // Highlight drop area when item is dragged over it
    ['dragenter', 'dragover'].forEach((eventName) => {
      this.dropZone.addEventListener(eventName, () => this.highlight(), false);
    });

    ['dragleave', 'drop'].forEach((eventName) => {
      this.dropZone.addEventListener(
        eventName,
        () => this.unhighlight(),
        false
      );
    });

    // Handle dropped files
    this.dropZone.addEventListener('drop', (e) => this.handleDrop(e), false);

    // Handle click to select files
    this.dropZone.addEventListener('click', () => this.selectFiles(), false);
  }

  preventDefaults(e) {
    e.preventDefault();
    e.stopPropagation();
  }

  highlight() {
    this.dropZone.classList.add('drag-over');
  }

  unhighlight() {
    this.dropZone.classList.remove('drag-over');
  }

  handleDrop(e) {
    const dt = e.dataTransfer;
    const files = Array.from(dt.files);

    this.handleFiles(files);
  }

  selectFiles() {
    const input = document.createElement('input');
    input.type = 'file';
    input.multiple = this.options.multiple;
    input.accept = this.options.acceptedTypes.join(',');

    input.addEventListener('change', (e) => {
      const files = Array.from(e.target.files);
      this.handleFiles(files);
    });

    input.click();
  }

  handleFiles(files) {
    const validFiles = [];
    const errors = [];

    files.forEach((file) => {
      try {
        this.validateFile(file);
        validFiles.push(file);
      } catch (error) {
        errors.push({ file, error: error.message });
      }
    });

    if (errors.length > 0) {
      this.options.onError(errors);
    }

    if (validFiles.length > 0) {
      this.options.onDrop(validFiles);
    }
  }

  validateFile(file) {
    // Check file size
    if (file.size > this.options.maxFileSize) {
      throw new Error(
        `File "${file.name}" exceeds maximum size of ${this.formatFileSize(this.options.maxFileSize)}`
      );
    }

    // Check file type
    const acceptsAny = this.options.acceptedTypes.includes('*/*');
    if (!acceptsAny) {
      const accepted = this.options.acceptedTypes.some((type) => {
        if (type.endsWith('/*')) {
          const category = type.split('/')[0];
          return file.type.startsWith(category + '/');
        }
        return file.type === type || file.name.endsWith(type);
      });

      if (!accepted) {
        throw new Error(`File type "${file.type}" is not accepted`);
      }
    }
  }

  formatFileSize(bytes) {
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    if (bytes === 0) return '0 Bytes';
    const i = Math.floor(Math.log(bytes) / Math.log(1024));
    return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i];
  }
}

// Advanced drag and drop with preview
class AdvancedDragDrop extends DragDropUploader {
  constructor(dropZone, previewContainer, options = {}) {
    super(dropZone, {
      ...options,
      onDrop: (files) => this.handleDroppedFiles(files),
    });

    this.previewContainer = previewContainer;
    this.uploadQueue = [];
  }

  async handleDroppedFiles(files) {
    for (const file of files) {
      const preview = await this.createPreview(file);
      this.uploadQueue.push({ file, preview });
    }

    // Start upload process
    this.processUploadQueue();
  }

  async createPreview(file) {
    const preview = document.createElement('div');
    preview.className = 'file-preview';
    preview.innerHTML = `
      <div class="preview-content">
        ${await this.getPreviewContent(file)}
      </div>
      <div class="preview-info">
        <div class="filename">${file.name}</div>
        <div class="filesize">${this.formatFileSize(file.size)}</div>
        <div class="progress">
          <div class="progress-bar" style="width: 0%"></div>
        </div>
        <div class="status">Waiting...</div>
      </div>
      <button class="cancel-upload">×</button>
    `;

    this.previewContainer.appendChild(preview);

    // Add cancel functionality
    preview.querySelector('.cancel-upload').addEventListener('click', () => {
      this.cancelUpload(file, preview);
    });

    return preview;
  }

  async getPreviewContent(file) {
    if (file.type.startsWith('image/')) {
      const dataUrl = await this.readAsDataURL(file);
      return `<img src="${dataUrl}" alt="${file.name}">`;
    } else if (file.type.startsWith('video/')) {
      const dataUrl = await this.readAsDataURL(file);
      return `<video src="${dataUrl}" controls></video>`;
    } else {
      const extension = file.name.split('.').pop().toUpperCase();
      return `<div class="file-icon">${extension}</div>`;
    }
  }

  readAsDataURL(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => resolve(e.target.result);
      reader.onerror = reject;
      reader.readAsDataURL(file);
    });
  }

  async processUploadQueue() {
    // Process uploads sequentially or in parallel
    for (const item of this.uploadQueue) {
      if (!item.cancelled) {
        await this.uploadFile(item.file, item.preview);
      }
    }
  }

  async uploadFile(file, preview) {
    const formData = new FormData();
    formData.append('file', file);

    const xhr = new XMLHttpRequest();

    // Update progress
    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable) {
        const percentComplete = (e.loaded / e.total) * 100;
        this.updateProgress(preview, percentComplete);
      }
    };

    // Handle completion
    xhr.onload = () => {
      if (xhr.status === 200) {
        this.markComplete(preview);
      } else {
        this.markError(preview, 'Upload failed');
      }
    };

    xhr.onerror = () => {
      this.markError(preview, 'Network error');
    };

    // Start upload
    this.updateStatus(preview, 'Uploading...');
    xhr.open('POST', '/upload');
    xhr.send(formData);
  }

  updateProgress(preview, percent) {
    const progressBar = preview.querySelector('.progress-bar');
    progressBar.style.width = `${percent}%`;
  }

  updateStatus(preview, status) {
    const statusElement = preview.querySelector('.status');
    statusElement.textContent = status;
  }

  markComplete(preview) {
    preview.classList.add('complete');
    this.updateStatus(preview, 'Complete');
    this.updateProgress(preview, 100);
  }

  markError(preview, error) {
    preview.classList.add('error');
    this.updateStatus(preview, error);
  }

  cancelUpload(file, preview) {
    const item = this.uploadQueue.find((i) => i.file === file);
    if (item) {
      item.cancelled = true;
    }
    preview.remove();
  }
}

File Processing

class FileProcessor {
  // CSV file processor
  async processCSV(file) {
    const text = await this.readAsText(file);
    const lines = text.split('\n').filter((line) => line.trim());
    const headers = this.parseCSVLine(lines[0]);

    const data = [];
    for (let i = 1; i < lines.length; i++) {
      const values = this.parseCSVLine(lines[i]);
      const row = {};

      headers.forEach((header, index) => {
        row[header] = values[index] || '';
      });

      data.push(row);
    }

    return { headers, data };
  }

  parseCSVLine(line) {
    const result = [];
    let current = '';
    let inQuotes = false;

    for (let i = 0; i < line.length; i++) {
      const char = line[i];

      if (char === '"') {
        inQuotes = !inQuotes;
      } else if (char === ',' && !inQuotes) {
        result.push(current.trim());
        current = '';
      } else {
        current += char;
      }
    }

    result.push(current.trim());
    return result;
  }

  // JSON file processor
  async processJSON(file) {
    const text = await this.readAsText(file);

    try {
      return JSON.parse(text);
    } catch (error) {
      throw new Error(`Invalid JSON in file ${file.name}: ${error.message}`);
    }
  }

  // Image file processor
  async processImage(file) {
    const img = await this.loadImage(file);

    return {
      width: img.width,
      height: img.height,
      aspectRatio: img.width / img.height,
      dataUrl: img.src,
    };
  }

  async loadImage(file) {
    const dataUrl = await this.readAsDataURL(file);

    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = () => reject(new Error('Failed to load image'));
      img.src = dataUrl;
    });
  }

  // ZIP file processor (using JSZip library)
  async processZIP(file) {
    // This requires JSZip library
    const JSZip = window.JSZip;
    if (!JSZip) {
      throw new Error('JSZip library not loaded');
    }

    const arrayBuffer = await this.readAsArrayBuffer(file);
    const zip = await JSZip.loadAsync(arrayBuffer);

    const files = [];

    for (const [path, zipEntry] of Object.entries(zip.files)) {
      if (!zipEntry.dir) {
        files.push({
          path,
          size: zipEntry._data ? zipEntry._data.uncompressedSize : 0,
          compressed: zipEntry._data ? zipEntry._data.compressedSize : 0,
          date: zipEntry.date,
        });
      }
    }

    return {
      name: file.name,
      files,
      extract: async (path) => {
        const zipEntry = zip.file(path);
        if (zipEntry) {
          return await zipEntry.async('blob');
        }
        return null;
      },
    };
  }

  // Generic file reader helpers
  readAsText(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => resolve(e.target.result);
      reader.onerror = reject;
      reader.readAsText(file);
    });
  }

  readAsDataURL(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => resolve(e.target.result);
      reader.onerror = reject;
      reader.readAsDataURL(file);
    });
  }

  readAsArrayBuffer(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => resolve(e.target.result);
      reader.onerror = reject;
      reader.readAsArrayBuffer(file);
    });
  }
}

// Hash calculator
class FileHasher {
  async calculateHash(file, algorithm = 'SHA-256') {
    const arrayBuffer = await this.readAsArrayBuffer(file);
    const hashBuffer = await crypto.subtle.digest(algorithm, arrayBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray
      .map((b) => b.toString(16).padStart(2, '0'))
      .join('');

    return hashHex;
  }

  async calculateChunkedHash(file, algorithm = 'SHA-256', onProgress) {
    const chunkSize = 1024 * 1024; // 1MB chunks
    let offset = 0;

    // Note: This is a simplified version
    // Real implementation would need incremental hashing
    const chunks = [];

    while (offset < file.size) {
      const chunk = file.slice(offset, offset + chunkSize);
      const arrayBuffer = await this.readAsArrayBuffer(chunk);
      chunks.push(arrayBuffer);

      offset += chunkSize;

      if (onProgress) {
        const progress = (offset / file.size) * 100;
        onProgress(Math.min(progress, 100));
      }
    }

    // Combine chunks and hash
    const totalLength = chunks.reduce(
      (sum, chunk) => sum + chunk.byteLength,
      0
    );
    const combined = new Uint8Array(totalLength);
    let position = 0;

    for (const chunk of chunks) {
      combined.set(new Uint8Array(chunk), position);
      position += chunk.byteLength;
    }

    const hashBuffer = await crypto.subtle.digest(algorithm, combined);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
  }

  readAsArrayBuffer(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => resolve(e.target.result);
      reader.onerror = reject;
      reader.readAsArrayBuffer(blob);
    });
  }
}

Blob Creation and Downloads

class BlobManager {
  // Create text file
  createTextFile(content, filename = 'file.txt', mimeType = 'text/plain') {
    const blob = new Blob([content], { type: mimeType });
    return this.downloadBlob(blob, filename);
  }

  // Create JSON file
  createJSONFile(data, filename = 'data.json') {
    const json = JSON.stringify(data, null, 2);
    const blob = new Blob([json], { type: 'application/json' });
    return this.downloadBlob(blob, filename);
  }

  // Create CSV file
  createCSVFile(data, headers, filename = 'data.csv') {
    let csv = headers.join(',') + '\n';

    data.forEach((row) => {
      const values = headers.map((header) => {
        const value = row[header] || '';
        // Escape values containing commas or quotes
        if (value.toString().includes(',') || value.toString().includes('"')) {
          return `"${value.toString().replace(/"/g, '""')}"`;
        }
        return value;
      });

      csv += values.join(',') + '\n';
    });

    const blob = new Blob([csv], { type: 'text/csv' });
    return this.downloadBlob(blob, filename);
  }

  // Create and download blob
  downloadBlob(blob, filename) {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;

    // Trigger download
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);

    // Clean up
    setTimeout(() => URL.revokeObjectURL(url), 100);

    return { blob, url };
  }

  // Create blob from base64
  base64ToBlob(base64, mimeType = 'application/octet-stream') {
    const byteCharacters = atob(base64.split(',')[1] || base64);
    const byteNumbers = new Array(byteCharacters.length);

    for (let i = 0; i < byteCharacters.length; i++) {
      byteNumbers[i] = byteCharacters.charCodeAt(i);
    }

    const byteArray = new Uint8Array(byteNumbers);
    return new Blob([byteArray], { type: mimeType });
  }

  // Merge multiple blobs
  mergeBlobs(blobs, mimeType = 'application/octet-stream') {
    return new Blob(blobs, { type: mimeType });
  }

  // Slice blob
  sliceBlob(blob, start, end) {
    return blob.slice(start, end, blob.type);
  }

  // Convert blob to different formats
  async blobToBase64(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onloadend = () => resolve(reader.result);
      reader.onerror = reject;
      reader.readAsDataURL(blob);
    });
  }

  async blobToText(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onloadend = () => resolve(reader.result);
      reader.onerror = reject;
      reader.readAsText(blob);
    });
  }

  async blobToArrayBuffer(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onloadend = () => resolve(reader.result);
      reader.onerror = reject;
      reader.readAsArrayBuffer(blob);
    });
  }
}

// Image manipulation
class ImageManipulator {
  async resizeImage(file, maxWidth, maxHeight) {
    const img = await this.loadImage(file);

    // Calculate new dimensions
    let { width, height } = img;

    if (width > maxWidth || height > maxHeight) {
      const aspectRatio = width / height;

      if (width > height) {
        width = maxWidth;
        height = maxWidth / aspectRatio;
      } else {
        height = maxHeight;
        width = maxHeight * aspectRatio;
      }
    }

    // Create canvas and resize
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;

    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0, width, height);

    // Convert to blob
    return new Promise((resolve) => {
      canvas.toBlob(resolve, file.type || 'image/jpeg', 0.9);
    });
  }

  async compressImage(file, quality = 0.8) {
    const img = await this.loadImage(file);

    const canvas = document.createElement('canvas');
    canvas.width = img.width;
    canvas.height = img.height;

    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0);

    return new Promise((resolve) => {
      canvas.toBlob(resolve, 'image/jpeg', quality);
    });
  }

  async loadImage(file) {
    const dataUrl = await this.fileToDataURL(file);

    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = reject;
      img.src = dataUrl;
    });
  }

  fileToDataURL(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => resolve(e.target.result);
      reader.onerror = reject;
      reader.readAsDataURL(file);
    });
  }
}

Best Practices

  1. Always validate files before processing
function validateFile(file, options = {}) {
  const {
    maxSize = 50 * 1024 * 1024, // 50MB default
    allowedTypes = [],
    allowedExtensions = [],
  } = options;

  // Check size
  if (file.size > maxSize) {
    throw new Error(
      `File size (${formatFileSize(file.size)}) exceeds maximum allowed size (${formatFileSize(maxSize)})`
    );
  }

  // Check MIME type
  if (allowedTypes.length > 0) {
    const typeAllowed = allowedTypes.some((type) => {
      if (type.includes('*')) {
        const [category] = type.split('/');
        return file.type.startsWith(category + '/');
      }
      return file.type === type;
    });

    if (!typeAllowed) {
      throw new Error(`File type "${file.type}" is not allowed`);
    }
  }

  // Check extension
  if (allowedExtensions.length > 0) {
    const extension = file.name.split('.').pop().toLowerCase();
    if (!allowedExtensions.includes(extension)) {
      throw new Error(`File extension ".${extension}" is not allowed`);
    }
  }

  // Additional security check - verify file content matches type
  return verifyFileContent(file);
}

async function verifyFileContent(file) {
  const header = await readFileHeader(file, 12);
  const signatures = {
    'image/jpeg': [[0xff, 0xd8, 0xff]],
    'image/png': [[0x89, 0x50, 0x4e, 0x47]],
    'image/gif': [[0x47, 0x49, 0x46, 0x38]],
    'application/pdf': [[0x25, 0x50, 0x44, 0x46]],
  };

  const expectedSignatures = signatures[file.type];
  if (!expectedSignatures) return true;

  const matches = expectedSignatures.some((signature) =>
    signature.every((byte, index) => header[index] === byte)
  );

  if (!matches) {
    throw new Error('File content does not match declared type');
  }

  return true;
}
  1. Handle large files efficiently
class LargeFileHandler {
  async processLargeFile(file, processChunk, chunkSize = 1024 * 1024) {
    const totalChunks = Math.ceil(file.size / chunkSize);
    let processedChunks = 0;

    for (let start = 0; start < file.size; start += chunkSize) {
      const end = Math.min(start + chunkSize, file.size);
      const chunk = file.slice(start, end);

      await processChunk(chunk, {
        index: processedChunks,
        total: totalChunks,
        progress: (processedChunks / totalChunks) * 100,
      });

      processedChunks++;
    }
  }

  createWorkerForProcessing(file) {
    const workerCode = `
      self.onmessage = async function(e) {
        const { chunk, index } = e.data;
        
        // Process chunk in worker
        const reader = new FileReader();
        reader.onload = function(event) {
          // Process data
          const result = processData(event.target.result);
          
          self.postMessage({
            index,
            result,
            done: false
          });
        };
        
        reader.readAsArrayBuffer(chunk);
      };
      
      function processData(arrayBuffer) {
        // Your processing logic here
        return arrayBuffer.byteLength;
      }
    `;

    const blob = new Blob([workerCode], { type: 'application/javascript' });
    const workerUrl = URL.createObjectURL(blob);
    const worker = new Worker(workerUrl);

    return worker;
  }
}
  1. Clean up resources
class ResourceManager {
  constructor() {
    this.objectURLs = new Set();
    this.fileReaders = new Set();
  }

  createObjectURL(blob) {
    const url = URL.createObjectURL(blob);
    this.objectURLs.add(url);
    return url;
  }

  createFileReader() {
    const reader = new FileReader();
    this.fileReaders.add(reader);

    // Auto-remove when done
    const originalOnload = reader.onload;
    reader.onload = (e) => {
      if (originalOnload) originalOnload(e);
      this.fileReaders.delete(reader);
    };

    const originalOnerror = reader.onerror;
    reader.onerror = (e) => {
      if (originalOnerror) originalOnerror(e);
      this.fileReaders.delete(reader);
    };

    return reader;
  }

  cleanup() {
    // Revoke all object URLs
    this.objectURLs.forEach((url) => {
      URL.revokeObjectURL(url);
    });
    this.objectURLs.clear();

    // Abort any pending file reads
    this.fileReaders.forEach((reader) => {
      if (reader.readyState === FileReader.LOADING) {
        reader.abort();
      }
    });
    this.fileReaders.clear();
  }
}

// Use on page unload
window.addEventListener('beforeunload', () => {
  resourceManager.cleanup();
});
  1. Provide progress feedback
class ProgressTracker {
  constructor(totalSize) {
    this.totalSize = totalSize;
    this.loaded = 0;
    this.startTime = Date.now();
    this.callbacks = [];
  }

  update(bytesProcessed) {
    this.loaded += bytesProcessed;
    const progress = (this.loaded / this.totalSize) * 100;
    const elapsedTime = Date.now() - this.startTime;
    const speed = this.loaded / (elapsedTime / 1000); // bytes per second
    const remainingBytes = this.totalSize - this.loaded;
    const estimatedTime = remainingBytes / speed; // seconds

    const info = {
      progress: Math.min(progress, 100),
      loaded: this.loaded,
      total: this.totalSize,
      speed: speed,
      elapsedTime: elapsedTime,
      estimatedTime: estimatedTime * 1000, // milliseconds
    };

    this.callbacks.forEach((callback) => callback(info));
  }

  onProgress(callback) {
    this.callbacks.push(callback);
  }

  formatProgress(info) {
    return {
      percentage: `${info.progress.toFixed(1)}%`,
      size: `${formatFileSize(info.loaded)} / ${formatFileSize(info.total)}`,
      speed: `${formatFileSize(info.speed)}/s`,
      timeRemaining: formatTime(info.estimatedTime),
    };
  }
}

Conclusion

The File API provides powerful capabilities for handling files directly in the browser, enabling rich file upload experiences, client-side processing, and efficient file management. By combining FileReader, Blob manipulation, and drag-and-drop functionality, you can create sophisticated file handling applications that work entirely in the browser. Remember to always validate files, handle errors gracefully, provide progress feedback, and clean up resources for the best user experience and application performance.