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