The Manager class handles all file operations for the P2P system, including reading pieces from seeders and writing received pieces to disk for leechers.
Constructor
Creates a new file manager instance.
const manager = new Manager(
'./shared/file.txt',
'r',
65536
);
Path to the file to manage
File opening mode:
'r' - Read-only (for seeders with existing file)
'w+' - Read/write (for leechers downloading file)
Size of each piece in bytes (typically 64 KiB = 65536 bytes)
Properties
Path to the file being managed
Size of each piece in bytes
Node.js FileHandle for low-level file operations
Total size of the file in bytes
File opening mode (‘r’ for read-only, ‘w+’ for read/write)
Methods
openFile()
Opens the file according to the specified mode.
await manager.openFile();
Behavior:
- Read mode (
'r'): Opens existing file and retrieves its size
- Write mode (
'w+'): Creates new file (or truncates if exists), size is initially 0
// From src/manager.js
this.fileHandle = await fsp.open(this.filePath, this.mode);
if (this.mode === 'r') {
const stats = await this.fileHandle.stat();
this.fileSize = stats.size;
} else {
this.fileSize = 0;
}
Returns: Promise<void>
setSize(size)
Sets the file size (used for leechers when total size is known).
await manager.setSize(1048576); // Set to 1 MB
Behavior:
- Updates the
fileSize property
- Truncates or extends the file to the specified size
- Fills with zeros if extending
this.fileSize = size;
await this.fileHandle.truncate(size);
Returns: Promise<void>
Usage in Node class:
// When leecher receives file metadata
await this.fileManager.setSize(this.fileSize);
readPiece(index)
Reads a specific piece from the file.
const pieceData = await manager.readPiece(0); // Read first piece
console.log(`Read ${pieceData.length} bytes`);
Zero-based index of the piece to read
Behavior:
- Calculates the file offset:
index * pieceSize
- Handles the last piece (may be smaller than
pieceSize)
- Reads data into a Buffer
const offset = index * this.pieceSize;
let length = this.pieceSize;
if (offset + length > this.fileSize) {
length = this.fileSize - offset; // Last piece adjustment
}
const buffer = Buffer.alloc(length);
await this.fileHandle.read(buffer, 0, length, offset);
return buffer;
Returns: Promise<Buffer> - Buffer containing the piece data
Usage in Node class:
// Seeder sending piece to peer
this.fileManager.readPiece(index).then(buffer => {
const pieceMsg = {
type: 'piece',
index: index,
data: buffer.toString('base64')
};
socket.write(JSON.stringify(pieceMsg) + '\n');
});
writePiece(index, dataBuffer)
Writes a piece to the file at the appropriate position.
const data = Buffer.from('Hello, World!');
await manager.writePiece(0, data); // Write to first piece
Zero-based index of the piece to write
Buffer containing the piece data to write
Behavior:
- Calculates the file offset:
index * pieceSize
- Writes the buffer to the file at the calculated position
- Does not validate piece size or integrity
const offset = index * this.pieceSize;
await this.fileHandle.write(dataBuffer, 0, dataBuffer.length, offset);
Returns: Promise<void>
Usage in Node class:
// Leecher receiving piece from peer
const dataBuffer = Buffer.from(dataBase64, 'base64');
try {
await this.fileManager.writePiece(index, dataBuffer);
} catch (err) {
console.error(`Error writing piece ${index}:`, err);
}
computeHash()
Computes the SHA-1 hash of the entire file.
const hash = await manager.computeHash();
console.log(`File hash: ${hash}`);
// Output: File hash: da39a3ee5e6b4b0d3255bfef95601890afd80709
Behavior:
- Creates a read stream from the file
- Computes SHA-1 hash incrementally
- Returns hexadecimal string representation
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha1');
const stream = fs.createReadStream(this.filePath);
stream.on('data', chunk => {
hash.update(chunk);
});
stream.on('end', () => {
const result = hash.digest('hex');
resolve(result);
});
stream.on('error', err => {
reject(err);
});
});
Returns: Promise<string> - SHA-1 hash in hexadecimal format (40 characters)
Usage in Node class:
// Seeder computing file hash on initialization
try {
this.fileHash = await this.fileManager.computeHash();
} catch (err) {
console.error('Error calculating file hash:', err);
process.exit(1);
}
// Leecher verifying downloaded file integrity
const downloadedHash = await this.fileManager.computeHash();
if (downloadedHash === this.fileHash) {
console.log('Integrity verification: OK (hash matches).');
} else {
console.warn('Warning: downloaded file hash differs from expected.');
}
close()
Closes the file and releases the file descriptor.
Behavior:
- Closes the file handle if open
- Sets
fileHandle to null
- Should be called when file operations are complete
if (this.fileHandle) {
await this.fileHandle.close();
this.fileHandle = null;
}
Returns: Promise<void>
Piece Size Handling
The default piece size is 64 KiB (65,536 bytes):
this.pieceSize = 65536; // 64 KiB by default
For small files, the piece size is automatically adjusted:
if (this.fileSize < this.pieceSize) {
this.pieceSize = this.fileSize;
this.fileManager.pieceSize = this.fileSize;
}
Calculating Number of Pieces
this.numPieces = Math.ceil(this.fileSize / this.pieceSize);
Example:
- File size: 200,000 bytes
- Piece size: 65,536 bytes
- Number of pieces:
Math.ceil(200000 / 65536) = 4
- Piece 0: 65,536 bytes
- Piece 1: 65,536 bytes
- Piece 2: 65,536 bytes
- Piece 3: 3,392 bytes (last piece)
Error Handling
All methods return Promises and should be wrapped in try-catch blocks:
try {
await manager.openFile();
await manager.setSize(1048576);
const piece = await manager.readPiece(0);
await manager.writePiece(1, piece);
const hash = await manager.computeHash();
await manager.close();
} catch (err) {
console.error('File operation error:', err);
}