Introduction to File Servers
Think of a file server as a digital librarian. Just as a librarian helps you find books, keeps them organized, and ensures they're delivered to you in good condition, a file server manages digital files, makes them accessible, and delivers them efficiently. Let's build a robust file server that handles various types of files and serves them appropriately.
Core Components of Our File Server
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
const url = require('url');
// Like a library catalog system for digital files
const MIME_TYPES = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.pdf': 'application/pdf',
'.zip': 'application/zip',
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.txt': 'text/plain'
};
// Configuration object - like library rules and policies
const SERVER_CONFIG = {
port: 3000,
rootDir: 'public',
defaultIndex: 'index.html',
cacheControl: 'public, max-age=86400',
maxAge: 86400,
compressionThreshold: 1024, // bytes
allowedMethods: ['GET', 'HEAD'],
corsOrigin: '*'
};
Request Handling and Security
Like a librarian checking visitor IDs and ensuring books are properly handled, we need to implement security measures and proper request handling:
class FileServer {
constructor(config = SERVER_CONFIG) {
this.config = config;
this.server = null;
}
// Security check - like verifying a visitor's credentials
validateRequest(req) {
// Check if method is allowed
if (!this.config.allowedMethods.includes(req.method)) {
throw new Error('Method Not Allowed');
}
// Clean and validate the path (prevent directory traversal)
const parsedUrl = url.parse(req.url);
const sanitizedPath = path.normalize(parsedUrl.pathname)
.replace(/^(\.\.[\/\\])+/, '');
const fullPath = path.join(this.config.rootDir, sanitizedPath);
// Ensure path stays within root directory
if (!fullPath.startsWith(path.resolve(this.config.rootDir))) {
throw new Error('Forbidden');
}
return fullPath;
}
// Handle errors - like dealing with missing or damaged books
handleError(res, error) {
const errorMessages = {
'ENOENT': { status: 404, message: 'File not found' },
'Forbidden': { status: 403, message: 'Access denied' },
'Method Not Allowed': { status: 405, message: 'Method not allowed' }
};
const errorInfo = errorMessages[error.code || error.message] ||
{ status: 500, message: 'Internal server error' };
res.writeHead(errorInfo.status, {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
});
res.end(JSON.stringify({
error: errorInfo.message,
status: errorInfo.status
}));
}
File Handling and Response Generation
Like a librarian retrieving and preparing books for delivery, we need to handle files and generate appropriate responses:
// Get file stats - like checking a book's availability and condition
async getFileStats(fullPath) {
const stats = await fs.stat(fullPath);
return {
isDirectory: stats.isDirectory(),
size: stats.size,
mtime: stats.mtime,
ctime: stats.ctime
};
}
// Generate response headers - like preparing a book for checkout
generateHeaders(filepath, stats) {
const ext = path.extname(filepath);
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
return {
'Content-Type': contentType,
'Content-Length': stats.size,
'Last-Modified': stats.mtime.toUTCString(),
'Cache-Control': this.config.cacheControl,
'Accept-Ranges': 'bytes',
'Access-Control-Allow-Origin': this.config.corsOrigin
};
}
// Handle directory listing - like showing available books in a section
async generateDirectoryListing(dirPath, requestPath) {
const files = await fs.readdir(dirPath);
const fileList = await Promise.all(
files.map(async file => {
const filePath = path.join(dirPath, file);
const stats = await this.getFileStats(filePath);
return {
name: file,
isDirectory: stats.isDirectory,
size: stats.size,
mtime: stats.mtime
};
})
);
const html = `
Directory: ${requestPath}
Directory: ${requestPath}
Name
Type
Size
Last Modified
${fileList.map(file => `
${file.name}
${file.isDirectory ? 'Directory' : 'File'}
${file.isDirectory ? '-' : this.formatFileSize(file.size)}
${file.mtime.toLocaleString()}
`).join('')}
`;
return html;
}
// Format file size - like describing a book's dimensions
formatFileSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
Range Requests and Streaming
Like allowing readers to request specific chapters or pages, we need to handle partial content requests:
// Handle range requests - like providing specific book pages
handleRangeRequest(req, res, fullPath, stats) {
const range = req.headers.range;
if (!range) return false;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1;
if (start >= stats.size || end >= stats.size) {
res.writeHead(416, {
'Content-Range': `bytes */${stats.size}`
});
res.end();
return true;
}
const headers = this.generateHeaders(fullPath, stats);
headers['Content-Range'] = `bytes ${start}-${end}/${stats.size}`;
headers['Content-Length'] = end - start + 1;
headers['Accept-Ranges'] = 'bytes';
res.writeHead(206, headers);
const fileStream = fs.createReadStream(fullPath, { start, end });
fileStream.pipe(res);
return true;
}
Main Request Handler
The main request handler orchestrates all components, like a head librarian coordinating different library services:
async handleRequest(req, res) {
try {
const fullPath = this.validateRequest(req);
const stats = await this.getFileStats(fullPath);
// Handle directory requests
if (stats.isDirectory) {
// Check for index file
const indexPath = path.join(fullPath, this.config.defaultIndex);
try {
await fs.access(indexPath);
const indexStats = await this.getFileStats(indexPath);
await this.serveFile(req, res, indexPath, indexStats);
return;
} catch (e) {
// No index file, show directory listing
const listing = await this.generateDirectoryListing(
fullPath,
req.url
);
res.writeHead(200, {
'Content-Type': 'text/html',
'Content-Length': Buffer.byteLength(listing)
});
res.end(listing);
return;
}
}
// Handle file requests
await this.serveFile(req, res, fullPath, stats);
} catch (error) {
this.handleError(res, error);
}
}
// Serve file content - like delivering a book to a reader
async serveFile(req, res, fullPath, stats) {
// Handle range requests
if (this.handleRangeRequest(req, res, fullPath, stats)) {
return;
}
// Regular file serving
const headers = this.generateHeaders(fullPath, stats);
res.writeHead(200, headers);
// Stream the file
const fileStream = fs.createReadStream(fullPath);
fileStream.pipe(res);
}
// Start the server - like opening the library
start() {
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
this.server.listen(this.config.port, () => {
console.log(
`File server running at http://localhost:${this.config.port}`
);
});
}
}
Usage Example
Here's how to use our file server:
// Create and start the file server
const fileServer = new FileServer({
...SERVER_CONFIG,
rootDir: 'public',
port: 3000
});
fileServer.start();
Directory structure example:
public/
├── index.html
├── css/
│ └── styles.css
├── js/
│ └── main.js
├── images/
│ ├── photo.jpg
│ └── icon.png
└── documents/
└── report.pdf
Further Topics to Explore
To enhance your file server implementation, consider exploring:
Compression - Like offering both hardcover and paperback versions
Caching strategies - Like managing frequently requested books
Authentication - Like managing library cards and memberships
Rate limiting - Like managing busy periods in the library
Logging and monitoring - Like keeping track of book checkouts
WebSocket integration - Like real-time updates on book availability