DownloadFile Area System Proposal
DRAFT DOCUMENT: This proposal is a draft that was generated by AI and may not have been reviewed for accuracy. Implementation details may change based on technical constraints, security considerations, and user feedback.
Executive Summary
This proposal outlines a comprehensive file area system for BinktermPHP that enables users to upload, download, and share files through both the web interface and Fidonet networking. The system includes virus scanning, credit-based economy integration, sysop approval workflows, and future support for Fidonet file requests (FREQ).
Table of Contents
-
Overview
-
Design Decisions
-
Architecture
-
Database Schema
-
Core Components
-
API Endpoints
-
User Interface
-
Integration Pointsthe system
-
Security Considerations
-
Implementation Roadmap
-
Future Enhancements
Overview
The file area system extends BinktermPHP's capabilities beyond message-based communication to include file distribution, similar to traditional BBS file bases. This feature enables:
-
User File Sharing: Upload and download files through the web interface
-
Fidonet File Distribution: Receive and distribute files via binkp protocol
-
Private File Storage: Users receive netmail attachments in their private file area
-
Credit Economy: Reward uploads and charge for downloads (public files only)
-
Quality Control: Sysop approval queue for user-uploaded content
-
Security: Optional ClamAV virus scanning for all files
-
Multi-Network Support: File areas per network domain
-
Future FREQ Support: Framework for Fidonet file request protocol
Use Cases
-
Software Distribution: Share utilities, tools, and applications
-
Document Libraries: Technical documentation, guides, tutorials
-
Media Sharing: Images, music, videos (subject to size limits)
-
Archive Preservation: Historical BBS files, vintage software
-
Network Collaboration: Share files between Fidonet nodes
-
User Contributions: Allow users to upload content for community benefit
-
Private File Storage: Users receive files via netmail attachments in their private file area
Design Decisions
Based on stakeholder input and architectural review, the following design decisions have been made:
1. Credits Integration
Decision: Integrate with existing credit system
Rationale: Incentivizes quality uploads and prevents abuse of downloads
Implementation:
- Charge configurable credits per download
- Award configurable credits for approved uploads
- Use existing UserCredit class for consistency
2. Storage Structure
Decision: Organize by file area tag (data/files/{area_tag}/{filename})
Rationale: Mirrors Fidonet file area structure, simplifies backup/management
Benefits:
- Easy to locate files for specific areas
- Supports area-level operations (archive, delete, backup)
- Aligns with Fidonet conventions
3. Metadata Tracking
Decision: Store both short and long descriptions plus download statistics
Rationale: Follows BBS tradition, provides user-friendly browsing
Fields:
- Short description (255 chars) - Quick summary
- Long description (unlimited) - Detailed information
- Download count, last download date
- Uploader information
4. File Request Support
Decision: Plan for both binkp FREQ and netmail-based requests
Rationale: Maximum compatibility with Fidonet ecosystem
Phases:
- Phase 1: Database schema and queue system (this proposal)
- Phase 2: Binkp M_GET frame handling
- Phase 3: Netmail command parsing
5. Virus Detection Strategy
Decision: Flag infected files with visual indicator, don't auto-delete
Rationale: Allows sysop review of false positives, maintains audit trail
Workflow:
- Scan uploads with ClamAV (optional)
- Mark infected files with status='quarantined'
- Move to .quarantine/ directory
- Display prominent warning in approval queue
- Sysop makes final decision
6. Size Limits
Decision: Per-area configurable maximum file size
Rationale: Flexibility for different area purposes
Examples:
- Text files area: 1 MB limit
- General files: 10 MB limit
- Large archives: 100 MB limit
- Sysop-only area: Unlimited
7. Duplicate Handling
Decision: Reject duplicates with error message (no auto-rename)
Rationale: Prevents confusion, maintains file integrity
Implementation:
- Calculate SHA256 hash of uploaded file
- Check for existing file with same hash in same area
- Return error if duplicate found
- UNIQUE constraint: (file_area_id, file_hash)
8. File Type Control
Decision: Configurable blacklist per area (e.g., .exe, .bat)
Rationale: Security and content control flexibility
Features:
- Whitelist mode: allowed_extensions (e.g., .txt,.zip,.pdf)
- Blacklist mode: blocked_extensions (e.g., .exe,.bat,.com)
- Admin can configure per-area
9. Private File Storage
Decision: Use is_private flag instead of creating private file areas per user
Rationale: Simpler implementation, reuses existing infrastructure
Implementation:
- Set is_private = TRUE on file record
- Set file_area_id = NULL (not associated with public area)
- Store in data/files/private/{user_id}/ directory
- Access controlled by uploaded_by_user_id ownership
- No approval workflow (auto-approved)
- No credit costs for downloading own files
Benefits:
- Single files table for both public and private files
- Reuses virus scanning, download handlers, storage logic
- Simple access control (owner check)
- Easy to implement file transfer from private ? public
Architecture
Architecture Pattern
The file area system follows existing BinktermPHP patterns for consistency:
| Component | Pattern Source | Similarity |
|-----------|----------------|------------|
| Database Schema | echoareas table | Multi-network support, subscriptions, domains |
| Approval Queue | crashmail_queue | Pending status, admin review, approval/rejection |
| Admin UI | templates/echoareas.twig | List view, modals, color coding |
| Queue Management | templates/admin/crashmail_queue.twig | Pending items, approve/reject actions |
| API Endpoints | routes/admin-routes.php | RESTful design, admin authentication |
| Credit Integration | Crashmail debit system | UserCredit class usage |
System Flow Diagram
???????????????????
? User Upload ?
? (Web Form) ?
???????????????????
?
?
???????????????????????????????
? FileUploadHandler ?
? - Validate size/extension ?
? - Calculate SHA256 hash ?
? - Check duplicates ?
? - Scan virus (optional) ?
???????????????????????????????
?
?
???????????????????????????????
? Store to disk ?
? data/files/{tag}/{file} ?
???????????????????????????????
?
?
???????????????????????????????
? Create database record ?
? status = 'pending' ?
???????????????????????????????
?
?
???????????????????????????????
? Admin Approval Queue ?
? (FileApprovalController) ?
???????????????????????????????
?
???????????
? ?
?????????? ???????????
?Approve ? ? Reject ?
?????????? ???????????
? ?
? ?
?????????? ????????????
?Award ? ? Notify ?
?Credits ? ? User ?
?????????? ????????????
?
?
???????????????????????????????
? File available for ?
? download ?
???????????????????????????????
???????????????????
? User Download ?
???????????????????
?
?
???????????????????????????????
? FileDownloadHandler ?
? - Check subscription ?
? - Check credits ?
? - Deduct credits ?
? - Log download ?
???????????????????????????????
?
?
???????????????????????????????
? Stream file to user ?
???????????????????????????????
Database Schema
Migration File
Table: file_areas
File distribution areas configuration (similar to echoareas).
CREATE TABLE file_areas (
id SERIAL PRIMARY KEY,
tag VARCHAR(255) NOT NULL,
description TEXT,
domain VARCHAR(255) NOT NULL DEFAULT 'fidonet',
is_local BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
is_sysop_only BOOLEAN DEFAULT FALSE,
color VARCHAR(7) DEFAULT '#007bff',
-- File-specific settings
max_file_size BIGINT DEFAULT 10485760, -- 10MB default
allowed_extensions TEXT, -- Comma-separated: .txt,.zip,.pdf
blocked_extensions TEXT, -- Comma-separated: .exe,.bat,.com
download_credit_cost INTEGER DEFAULT 0,
upload_credit_reward INTEGER DEFAULT 0,
require_approval BOOLEAN DEFAULT TRUE,
scan_virus BOOLEAN DEFAULT TRUE,
-- Statistics
file_count INTEGER DEFAULT 0,
total_size BIGINT DEFAULT 0,
download_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_tag_domain UNIQUE(tag, domain)
);
Key Fields:
- tag: File area identifier (e.g., GENERAL_FILES, SOFTWARE)
- domain: Network domain for multi-network support
- is_local: If true, files not transmitted to uplinks
- max_file_size: Maximum file size in bytes
- require_approval: If false, uploads auto-approved
- scan_virus: Enable ClamAV scanning for this area
Table: files
File metadata and tracking.
CREATE TABLE files (
id SERIAL PRIMARY KEY,
file_area_id INTEGER REFERENCES file_areas(id) ON DELETE CASCADE,
-- File information
filename VARCHAR(255) NOT NULL,
filesize BIGINT NOT NULL,
file_hash VARCHAR(64) NOT NULL, -- SHA256
storage_path TEXT NOT NULL,
-- Upload information
uploaded_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
uploaded_from_address VARCHAR(255), -- Fidonet address (e.g., 1:123/456)
source_type VARCHAR(20) DEFAULT 'user', -- 'user', 'fidonet', 'freq', 'netmail_attachment'
-- Privacy
is_private BOOLEAN DEFAULT FALSE, -- If true, only uploaded_by_user_id can access
-- Descriptions
short_description VARCHAR(255),
long_description TEXT,
-- Approval workflow (not used for private files)
status VARCHAR(20) DEFAULT 'pending', -- pending, approved, rejected, quarantined
approved_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
approved_at TIMESTAMP,
rejection_reason TEXT,
-- Virus scanning
virus_scanned BOOLEAN DEFAULT FALSE,
virus_scan_result VARCHAR(20), -- clean, infected, error, skipped
virus_signature VARCHAR(255), -- Name of detected virus
virus_scanned_at TIMESTAMP,
-- Download statistics
download_count INTEGER DEFAULT 0,
last_downloaded_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_file_hash_per_area UNIQUE(file_area_id, file_hash)
);
Key Fields:
- file_area_id: Can be NULL for private files (not associated with public area)
- file_hash: SHA256 hash for duplicate detection
- source_type: Track origin (user upload, fidonet, netmail_attachment, freq)
- is_private: If TRUE, only the owning user can access (bypasses approval workflow)
- status: Workflow state (pending ? approved/rejected/quarantined) - auto-set to 'approved' for private files
- virus_scan_result: ClamAV scan outcome
Private Files Behavior:
- Private files have is_private = TRUE and file_area_id = NULL
- Stored in user-specific directory: data/files/private/{user_id}/
- No approval workflow (auto-approved upon receipt)
- Only accessible by the owning user (via uploaded_by_user_id)
- Created when files are received as netmail attachments
- No credit costs for downloading your own private files
- Still subject to virus scanning for security
Table: user_file_area_subscriptions
Access control for file areas.
CREATE TABLE user_file_area_subscriptions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
file_area_id INTEGER NOT NULL REFERENCES file_areas(id) ON DELETE CASCADE,
subscription_type VARCHAR(20) DEFAULT 'read_write', -- read_only, read_write, upload_only
is_active BOOLEAN DEFAULT TRUE,
subscribed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_user_file_area UNIQUE(user_id, file_area_id)
);
Subscription Types:
- read_write: Browse and upload (most common)
- read_only: Browse and download only
- upload_only: Upload only (rare, for contribution areas)
Table: file_downloads
Download history and audit trail.
CREATE TABLE file_downloads (
id SERIAL PRIMARY KEY,
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
credits_charged INTEGER DEFAULT 0,
ip_address INET,
user_agent TEXT,
downloaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Purpose:
- Track who downloaded what and when
- Audit trail for credit charges
- Statistics for popular files
- Security monitoring
Table: file_requests
File request queue (FREQ) - future implementation.
CREATE TABLE file_requests (
id SERIAL PRIMARY KEY,
request_type VARCHAR(20) NOT NULL, -- 'binkp_freq', 'netmail_command'
requested_filename VARCHAR(255) NOT NULL,
requesting_address VARCHAR(255), -- Fidonet address
requesting_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
status VARCHAR(20) DEFAULT 'pending', -- pending, matched, sent, failed
matched_file_id INTEGER REFERENCES files(id) ON DELETE SET NULL,
password VARCHAR(255), -- FREQ password (if required)
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP,
sent_at TIMESTAMP
);
Future Use:
- Queue incoming FREQ requests
- Match requests to available files
- Track fulfillment status
Indexes
-- Performance indexes
CREATE INDEX idx_files_file_area_id ON files(file_area_id);
CREATE INDEX idx_files_status ON files(status);
CREATE INDEX idx_files_hash ON files(file_hash);
CREATE INDEX idx_files_uploaded_by ON files(uploaded_by_user_id);
CREATE INDEX idx_files_virus_scan ON files(virus_scanned, virus_scan_result);
CREATE INDEX idx_subscriptions_user_id ON user_file_area_subscriptions(user_id);
CREATE INDEX idx_subscriptions_file_area_id ON user_file_area_subscriptions(file_area_id);
CREATE INDEX idx_downloads_file_id ON file_downloads(file_id);
CREATE INDEX idx_downloads_user_id ON file_downloads(user_id);
CREATE INDEX idx_downloads_date ON file_downloads(downloaded_at);
CREATE INDEX idx_requests_status ON file_requests(status);
CREATE INDEX idx_requests_address ON file_requests(requesting_address);
Default Data
-- Create default file areas
INSERT INTO file_areas (tag, description, domain, is_local, color, max_file_size, download_credit_cost, upload_credit_reward) VALUES
('GENERAL_FILES', 'General purpose file area for miscellaneous files', 'fidonet', TRUE, '#007bff', 10485760, 10, 25),
('LOCALTEST_FILES', 'Testing area for local file uploads', 'fidonet', TRUE, '#6c757d', 5242880, 0, 0);
Core Components
1. FileAreaManager
File: src/FileAreaManager.php
Pattern: Similar to EchoareaSubscriptionManager.php
Purpose: Business logic for file area management and subscriptions
Key Methods
class FileAreaManager
{
/
* Get all file areas user is subscribed to
*/
public function getUserSubscribedFileAreas(int $userId): array;
/
* Subscribe user to file area
*/
public function subscribeUser(int $userId, int $fileAreaId, string $type = 'read_write'): bool;
/
* Unsubscribe user from file area
*/
public function unsubscribeUser(int $userId, int $fileAreaId): bool;
/
* Check if user has access to file area
*/
public function isUserSubscribed(int $userId, int $fileAreaId): bool;
/
* Get file area details with statistics
*/
public function getFileAreaWithStats(int $fileAreaId): ?array;
/
* Get all file areas with statistics (admin)
*/
public function getFileAreasWithStats(string $domain = 'fidonet'): array;
/
* Create new file area (admin)
*/
public function createFileArea(array $data): int;
/
* Update file area (admin)
*/
public function updateFileArea(int $id, array $data): bool;
/
* Delete file area (admin)
*/
public function deleteFileArea(int $id): bool;
/
* Update file area statistics
*/
public function updateStats(int $fileAreaId): void;
}
Responsibilities
-
Subscription management
-
Access control checks
-
CRUD operations for file areas
-
Statistics aggregation
2. FileUploadHandler
File: src/FileUploadHandler.php
Pattern: Similar to packet processing in BinkdProcessor.php
Purpose: Process and validate file uploads from users and Fidonet
Key Methods
class FileUploadHandler
{
/
* Handle user upload from web interface
*/
public function handleUserUpload(
int $fileAreaId,
array $uploadedFile,
int $userId,
array $metadata
): array;
/
* Handle file from Fidonet (binkp)
*/
public function handleFidonetFile(
int $fileAreaId,
string $filePath,
string $fromAddress,
array $metadata
): array;
/
* Validate file against area rules
*/
protected function validateFile(array $fileArea, string $filePath): array;
/
* Calculate SHA256 hash
*/
protected function calculateFileHash(string $filePath): string;
/
* Check for duplicate files
*/
protected function isDuplicate(int $fileAreaId, string $hash): bool;
/
* Move file to storage location
*/
protected function moveToStorage(string $tempPath, string $areaTag, string $filename): string;
/
* Create database record
*/
protected function storeFileRecord(array $data): int;
/
* Trigger virus scan
*/
protected function scanForViruses(string $filePath): array;
}
Upload Workflow
-
Validation:
- Check file size ? `max_file_size`
- Validate extension against whitelist/blacklist
- Calculate SHA256 hash
- Check for duplicate hash in area
-
Virus Scanning (if enabled):
- Call `VirusScanner::scanFile()`
- If infected: status='quarantined', move to `.quarantine/`
- If clean: continue to storage
-
Storage:
- Move from temp to `data/files/{area_tag}/{filename}`
- Set appropriate permissions (644)
-
Database Record:
- Insert into `files` table
- Set status='pending' (user) or 'approved' (Fidonet if auto-approve enabled)
- Record uploader, source, descriptions
-
Logging:
- Log upload via error_log or dedicated file logger
- Log admin action if auto-approved
Return: ['success' => true, 'file_id' => 123] or ['success' => false, 'error' => 'message']
3. FileDownloadHandler
File: src/FileDownloadHandler.php
Pattern: Similar to credit deduction in crashmail
Purpose: Handle file downloads with credit integration
Key Methods
class FileDownloadHandler
{
/
* Initiate download process
*/
public function initiateDownload(int $fileId, int $userId): array;
/
* Check user has access to file area
*/
protected function checkAccess(int $fileId, int $userId): bool;
/
* Check user has sufficient credits
*/
protected function checkCredits(int $fileId, int $userId): bool;
/
* Deduct credits from user balance
*/
protected function deductCredits(int $fileId, int $userId): bool;
/
* Record download in history
*/
protected function recordDownload(int $fileId, int $userId, int $creditsCharged): void;
/
* Update file statistics
*/
protected function updateDownloadStats(int $fileId): void;
/
* Stream file to user
*/
public function streamFile(int $fileId): void;
}
Download Workflow
-
Access Check:
- Verify user subscribed to file area
- Check file status = 'approved'
- Verify file exists on disk
-
Credit Check:
- Get `download_credit_cost` from file area
- Check user balance via `UserCredit` class
- If insufficient: return error
-
Credit Deduction:
- Call `UserCredit::debit($userId, $cost, $description)`
- Transaction description: "Downloaded: {filename}"
-
Download Logging:
- Insert into `file_downloads` table
- Record: file_id, user_id, credits_charged, IP, user agent, timestamp
-
Statistics Update:
- Increment `files.download_count`
- Update `files.last_downloaded_at`
- Increment `file_areas.download_count`
-
File Streaming:
- Set headers: Content-Type, Content-Disposition, Content-Length
- Stream file in chunks (memory efficient)
- Support resume/partial content (HTTP 206)
Error Handling:
- Insufficient credits: Return error, no charge
- File missing: Log error, refund if charged
- Download interrupted: No refund (user initiated)
4. VirusScanner
File: src/VirusScanner.php
Purpose: Optional ClamAV integration for virus detection
Key Methods
class VirusScanner
{
/
* Check if virus scanning is enabled
*/
public static function isEnabled(): bool;
/
* Scan file for viruses
*/
public static function scanFile(string $filePath): array;
/
* Update virus scan result in database
*/
public static function updateVirusScanResult(int $fileId, array $result): void;
/
* Get scanner status/version
*/
public static function getStatus(): array;
}
Configuration
Environment variables in .env:
CLAMAV_ENABLED=false
CLAMAV_PATH=/usr/bin/clamdscan
CLAMAV_SOCKET=/var/run/clamav/clamd.ctl
CLAMAV_TIMEOUT=30
Scan Results
Return array structure: [
'scanned' => true,
'result' => 'clean|infected|error|skipped',
'signature' => 'Win.Trojan.Eicar-1', // If infected
'error' => 'Scanner timeout', // If error
'scanned_at' => '2025-01-30 12:34:56'
]
Result Values:
- clean: No virus detected
- infected: Virus found (signature in 'signature' field)
- error: Scan failed (error in 'error' field)
- skipped: Scanning disabled
Implementation
public static function scanFile(string $filePath): array
{
if (!self::isEnabled()) {
return ['scanned' => false, 'result' => 'skipped'];
}
$clamPath = getenv('CLAMAV_PATH') ?: '/usr/bin/clamdscan';
$timeout = intval(getenv('CLAMAV_TIMEOUT') ?: 30);
$command = escapeshellarg($clamPath) . ' --no-summary ' . escapeshellarg($filePath);
exec($command, $output, $returnCode);
// Return code 0 = clean, 1 = infected, 2 = error
switch ($returnCode) {
case 0:
return ['scanned' => true, 'result' => 'clean', 'scanned_at' => date('Y-m-d H:i:s')];
case 1:
// Parse signature from output
$signature = self::parseSignature($output);
return [
'scanned' => true,
'result' => 'infected',
'signature' => $signature,
'scanned_at' => date('Y-m-d H:i:s')
];
default:
return [
'scanned' => true,
'result' => 'error',
'error' => implode("\n", $output),
'scanned_at' => date('Y-m-d H:i:s')
];
}
}
Testing:
- Use EICAR test file for infected file testing
- ClamAV must be installed and running (clamd daemon)
5. FileApprovalController
File: src/FileApprovalController.php
Pattern: Similar to crashmail queue management
Purpose: Admin approval workflow for user uploads
Key Methods
class FileApprovalController
{
/
* Get pending files for approval
*/
public function getPendingFiles(?int $fileAreaId = null, int $limit = 50): array;
/
* Approve file
*/
public function approveFile(int $fileId, int $adminUserId): bool;
/
* Reject file
*/
public function rejectFile(int $fileId, int $adminUserId, string $reason): bool;
/
* Get approval statistics
*/
public function getApprovalStats(): array;
/
* Get file details for approval review
*/
public function getFileDetails(int $fileId): ?array;
}
Approval Workflow
Approve Action:
1. Update files table:
- Set status='approved'
- Set approved_by_user_id=$adminUserId
- Set approved_at=NOW()
-
Award Credits:
- If source_type='user' and uploaded_by_user_id exists
- Get upload_credit_reward from file area
- Call `UserCredit::credit($userId, $amount, "File approved: {filename}", $adminUserId, UserCredit::TYPE_SYSTEM_REWARD)`
-
Update Statistics:
- Increment `file_areas.file_count`
- Add to `file_areas.total_size`
-
Log Action:
- `AdminActionLogger::logAction($adminUserId, 'file_approved', ['file_id' => $fileId, 'filename' => $filename])`
-
Notify User (optional):
- Send netmail notification
- Or web notification
Reject Action:
1. Update files table:
- Set status='rejected'
- Set rejection_reason
- Set approved_by_user_id=$adminUserId (who rejected)
- Set approved_at=NOW() (when rejected)
-
File Handling Options:
- Move to `.quarantine/` directory
- Or delete from disk
- Configurable via admin UI
-
Log Action:
- `AdminActionLogger::logAction($adminUserId, 'file_rejected', ['file_id' => $fileId, 'reason' => $reason])`
-
Notify User:
- Send rejection reason via netmail or web notification
Statistics
Return array: [
'pending_count' => 15,
'approved_today' => 8,
'rejected_today' => 2,
'quarantined_count' => 3,
'total_pending_size' => 125829120 // bytes
]
6. FileAreaController
File: src/FileAreaController.php
Purpose: Web/API controller for user and admin endpoints
User Methods
class FileAreaController
{
/
* List file areas user has access to
*/
public function handleListFileAreas(Request $request): Response;
/
* Browse files in a specific area
*/
public function handleBrowseArea(Request $request, int $fileAreaId): Response;
/
* Upload file to area
*/
public function handleUploadFile(Request $request, int $fileAreaId): Response;
/
* Download file
*/
public function handleDownloadFile(Request $request, int $fileId): Response;
/
* Get file details
*/
public function handleFileDetails(Request $request, int $fileId): Response;
/
* Subscribe to file area
*/
public function handleSubscribe(Request $request, int $fileAreaId): Response;
/
* Unsubscribe from file area
*/
public function handleUnsubscribe(Request $request, int $fileAreaId): Response;
/
* List user's private files (netmail attachments)
*/
public function handleListPrivateFiles(Request $request): Response;
/
* Delete user's private file
*/
public function handleDeletePrivateFile(Request $request, int $fileId): Response;
}
Admin Methods
/
* File area management page
*/
public function handleAdminFileAreas(Request $request): Response;
/
* Approval queue page
*/
public function handleApprovalQueue(Request $request): Response;
/
* CRUD operations (via API)
*/
public function handleCreateFileArea(Request $request): Response;
public function handleUpdateFileArea(Request $request, int $id): Response;
public function handleDeleteFileArea(Request $request, int $id): Response;
/
* Approval actions (via API)
*/
public function handleApproveFile(Request $request, int $fileId): Response;
public function handleRejectFile(Request $request, int $fileId): Response;
Private File Methods
/
* List user's private files
*/
public function handleListPrivateFiles(Request $request): Response
{
$userId = $request->user['id'];
$db = Database::getInstance()->getPdo();
$stmt = $db->prepare("
SELECT id, filename, filesize, short_description, long_description,
uploaded_from_address, virus_scan_result, created_at,
download_count
FROM files
WHERE is_private = TRUE
AND uploaded_by_user_id = ?
AND status != 'deleted'
ORDER BY created_at DESC
");
$stmt->execute([$userId]);
$files = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Calculate storage usage
$totalSize = array_sum(array_column($files, 'filesize'));
$storageQuota = intval(getenv('FILES_PRIVATE_STORAGE_QUOTA') ?: 104857600);
return $this->json([
'success' => true,
'files' => $files,
'storage' => [
'used' => $totalSize,
'quota' => $storageQuota,
'percent' => round(($totalSize / $storageQuota) * 100, 1)
]
]);
}
/
* Delete user's private file
*/
public function handleDeletePrivateFile(Request $request, int $fileId): Response
{
$userId = $request->user['id'];
$db = Database::getInstance()->getPdo();
// Verify ownership
$stmt = $db->prepare("
SELECT id, storage_path, is_private, uploaded_by_user_id
FROM files
WHERE id = ?
");
$stmt->execute([$fileId]);
$file = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$file) {
return $this->json(['success' => false, 'error' => 'File not found'], 404);
}
if (!$file['is_private'] || $file['uploaded_by_user_id'] != $userId) {
return $this->json(['success' => false, 'error' => 'Access denied'], 403);
}
// Delete from disk
if (file_exists($file['storage_path'])) {
unlink($file['storage_path']);
}
// Delete from database
$stmt = $db->prepare("DELETE FROM files WHERE id = ?");
$stmt->execute([$fileId]);
return $this->json([
'success' => true,
'message' => 'File deleted successfully'
]);
}
7. TicFileProcessor
File: src/TicFileProcessor.php
Purpose: Process incoming TIC files and handle file distribution
Key Methods
class TicFileProcessor
{
/
* Process TIC file received via binkp
*/
public function processTicFile(string $ticPath, string $filePath): array;
/
* Parse TIC file content
*/
protected function parseTicFile(string $ticPath): array;
/
* Validate file against TIC metadata
*/
protected function validateFile(array $ticData, string $filePath): bool;
/
* Calculate CRC32 checksum
*/
protected function calculateCrc32(string $filePath): string;
/
* Store file in file area
*/
protected function storeFile(array $ticData, string $filePath): int;
/
* Update TIC path and seenby lines
*/
protected function updateTicRouting(array &$ticData, string $ourAddress): void;
/
* Forward TIC to downstream nodes
*/
protected function forwardTic(array $ticData, int $fileId): void;
/
* Create TIC file for outbound
*/
public function createTicFile(int $fileId, string $destAddress): string;
/
* Check if file area is subscribed
*/
protected function isAreaSubscribed(string $areaTag): bool;
}
TIC File Format
Standard TIC Fields:
- Area - File area tag (required)
- File - Filename (required)
- Desc - Short description (required)
- From - Origin address (required)
- Path - Routing path (required)
- Seenby - Distribution tracking (required)
- Size - File size in bytes
- Crc - CRC32 checksum (8 hex digits)
- Created - Creation program
- Date - Creation timestamp
- Pw - Password (optional)
- Replaces - File being replaced (optional)
- LDesc - Long description (multi-line, optional)
Processing Workflow
public function processTicFile(string $ticPath, string $filePath): array
{
try {
// Parse TIC file
$ticData = $this->parseTicFile($ticPath);
// Check if we're subscribed to this area
if (!$this->isAreaSubscribed($ticData['Area'])) {
return [
'success' => false,
'error' => "Not subscribed to file area: {$ticData['Area']}"
];
}
// Validate file
if (!$this->validateFile($ticData, $filePath)) {
return [
'success' => false,
'error' => 'File validation failed (size/CRC mismatch)'
];
}
// Store file
$fileId = $this->storeFile($ticData, $filePath);
// Update routing
$this->updateTicRouting($ticData, $this->getOurAddress());
// Forward to downstream nodes if configured
$this->forwardTic($ticData, $fileId);
return [
'success' => true,
'file_id' => $fileId,
'area' => $ticData['Area']
];
} catch (\Exception $e) {
error_log("TIC processing error: " . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
protected function parseTicFile(string $ticPath): array
{
$content = file_get_contents($ticPath);
$lines = explode("\n", $content);
$ticData = [
'Path' => [],
'Seenby' => [],
'LDesc' => []
];
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
// Parse key value pairs
if (preg_match('/^(\w+)\s+(.+)$/', $line, $matches)) {
$key = $matches[1];
$value = trim($matches[2]);
if ($key === 'Path' || $key === 'Seenby') {
$ticData[$key][] = $value;
} elseif ($key === 'LDesc') {
$ticData['LDesc'][] = $value;
} else {
$ticData[$key] = $value;
}
}
}
// Validate required fields
$required = ['Area', 'File', 'From'];
foreach ($required as $field) {
if (!isset($ticData[$field])) {
throw new \Exception("Missing required TIC field: $field");
}
}
return $ticData;
}
protected function validateFile(array $ticData, string $filePath): bool
{
// Validate size
if (isset($ticData['Size'])) {
$actualSize = filesize($filePath);
if ($actualSize != intval($ticData['Size'])) {
error_log("TIC size mismatch: expected {$ticData['Size']}, got $actualSize");
return false;
}
}
// Validate CRC32
if (isset($ticData['Crc'])) {
$actualCrc = $this->calculateCrc32($filePath);
if (strtoupper($actualCrc) !== strtoupper($ticData['Crc'])) {
error_log("TIC CRC mismatch: expected {$ticData['Crc']}, got $actualCrc");
return false;
}
}
return true;
}
protected function calculateCrc32(string $filePath): string
{
$hash = hash_file('crc32b', $filePath);
return strtoupper($hash);
}
8. FileRequestHandler
File: src/FileRequestHandler.php
Purpose: Stub for future FREQ implementation
Planned Methods (Phase 2+)
class FileRequestHandler
{
/
* Create file request in queue
*/
public function createRequest(array $requestData): int;
/
* Process binkp FREQ (M_GET frame)
*/
public function handleBinkpFreq(string $address, string $filename, ?string $password = null): array;
/
* Process netmail file request command
*/
public function handleNetmailFreq(int $netmailId): array;
/
* Match request to available file
*/
protected function matchFile(string $filename): ?int;
/
* Verify FREQ password
*/
protected function verifyFreqPassword(int $fileAreaId, string $password): bool;
/
* Queue file for transmission
*/
protected function queueFileTransmission(int $fileId, string $address): bool;
}
Note: This is a placeholder for future implementation. Phase 1 focuses on core upload/download functionality.
API Endpoints
User Endpoints
Add to routes/api-routes.php:
// File area browsing
$router->get('/api/file-areas',
[FileAreaController::class, 'handleListFileAreas']);
$router->get('/api/file-areas/{id}/files',
[FileAreaController::class, 'handleBrowseArea']);
$router->get('/api/files/{id}',
[FileAreaController::class, 'handleFileDetails']);
// File upload
$router->post('/api/file-areas/{id}/upload',
[FileAreaController::class, 'handleUploadFile']);
// File download
$router->get('/api/files/{id}/download',
[FileAreaController::class, 'handleDownloadFile']);
// Subscriptions
$router->post('/api/file-areas/{id}/subscribe',
[FileAreaController::class, 'handleSubscribe']);
$router->post('/api/file-areas/{id}/unsubscribe',
[FileAreaController::class, 'handleUnsubscribe']);
// Private files
$router->get('/api/files/private',
[FileAreaController::class, 'handleListPrivateFiles']);
$router->delete('/api/files/private/{id}',
[FileAreaController::class, 'handleDeletePrivateFile']);
Admin Endpoints
Add to routes/admin-routes.php:
// File area management pages
$router->get('/admin/file-areas',
[FileAreaController::class, 'handleAdminFileAreas']);
$router->get('/admin/file-approval',
[FileAreaController::class, 'handleApprovalQueue']);
// File area CRUD API
$router->get('/admin/api/file-areas',
[FileAreaController::class, 'handleListFileAreasAdmin']);
$router->post('/admin/api/file-areas',
[FileAreaController::class, 'handleCreateFileArea']);
$router->put('/admin/api/file-areas/{id}',
[FileAreaController::class, 'handleUpdateFileArea']);
$router->delete('/admin/api/file-areas/{id}',
[FileAreaController::class, 'handleDeleteFileArea']);
// Approval queue API
$router->get('/admin/api/files/pending',
[FileAreaController::class, 'handleGetPendingFiles']);
$router->post('/admin/api/files/{id}/approve',
[FileAreaController::class, 'handleApproveFile']);
$router->post('/admin/api/files/{id}/reject',
[FileAreaController::class, 'handleRejectFile']);
$router->get('/admin/api/files/stats',
[FileAreaController::class, 'handleApprovalStats']);
Request/Response Examples
Upload File (POST /api/file-areas/1/upload)
Request: POST /api/file-areas/1/upload HTTP/1.1
Content-Type: multipart/form-data
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="example.zip"
Content-Type: application/zip
[binary data]
------WebKitFormBoundary
Content-Disposition: form-data; name="short_description"
Useful utility for X
------WebKitFormBoundary
Content-Disposition: form-data; name="long_description"
This is a detailed description of the utility...
------WebKitFormBoundary--
Response (Success): {
"success": true,
"file_id": 42,
"message": "File uploaded successfully and awaiting approval",
"status": "pending"
}
Response (Error): {
"success": false,
"error": "File exceeds maximum size of 10MB"
}
Download File (GET /api/files/42/download)
Response (Success): HTTP/1.1 200 OK
Content-Type: application/zip
Content-Disposition: attachment; filename="example.zip"
Content-Length: 1024000
[binary file stream]
Response (Insufficient Credits): {
"success": false,
"error": "Insufficient credits. This download costs 20 credits, you have 15."
}
Approve File (POST /admin/api/files/42/approve)
Request: {
"admin_user_id": 1
}
Response: {
"success": true,
"message": "File approved successfully",
"credits_awarded": 25
}
User Interface
Admin Templates
1. File Areas Management (templates/admin/file_areas.twig)
Pattern: Similar to templates/echoareas.twig
Features:
- List all file areas with statistics
- Color-coded area tags
- Add/Edit modal dialogs
- Delete confirmation
- Real-time updates via AJAX
Layout: ???????????????????????????????????????????????????????????
? File Areas Management [+ Add Area] ?
???????????????????????????????????????????????????????????
? Filter: [All ?] [Active] [Inactive] ?
???????????????????????????????????????????????????????????
? TAG ? DESC ? FILES ? SIZE ? DL ?
??????????????????????????????????????????????????????????
? GENERAL_FILES ? General... ? 145 ? 2.1GB ? 1,234 ?
? ? ? [Edit] [Del]? ? ? ?
??????????????????????????????????????????????????????????
? SOFTWARE ? Software... ? 78 ? 890MB ? 567 ?
? ? ? [Edit] [Del]? ? ? ?
???????????????????????????????????????????????????????????
Add/Edit Modal: ???????????????????????????????????????????
? Add File Area [×] ?
???????????????????????????????????????????
? Tag: [________________] ?
? Description: [________________] ?
? Domain: [fidonet ?] ?
? Color: [#007bff] [?] ?
? ?
? File Settings: ?
? Max Size: [10] MB ?
? Allowed Ext: [.txt,.zip,.pdf] ?
? Blocked Ext: [.exe,.bat,.com] ?
? ?
? Credits: ?
? Download Cost: [10] credits ?
? Upload Reward: [25] credits ?
? ?
? Options: ?
? ? Active ?
? ? Local Only ?
? ? Sysop Only ?
? ? Require Approval ?
? ? Scan for Viruses ?
? ?
? [Cancel] [Save Area] ?
???????????????????????????????????????????
2. File Approval Queue (templates/admin/file_approval.twig)
Pattern: Similar to templates/admin/crashmail_queue.twig
Features:
- List pending files
- Virus scan results prominently displayed
- File preview (images)
- Approve/Reject actions
- Statistics sidebar
Layout: ???????????????????????????????????????????????????????????????????
? File Approval Queue ?
???????????????????????????????????????????????????????????????????
? ??????????????????? ?
? ? Statistics ? ?
? ? Pending: 15 ? ?
? ? Approved: 8 ? ?
? ? Rejected: 2 ? ?
? ??????????????????? ?
???????????????????????????????????????????????????????????????????
? Filter by Area: [All ?] Status: [All ?] ?
???????????????????????????????????????????????????????????????????
? ????????????????????????????????????????????????????????????? ?
? ? ? example.zip (2.4 MB) ? ?
? ? Area: GENERAL_FILES ? ?
? ? Uploaded by: JohnDoe (user_id: 42) on 2025-01-30 10:23 ? ?
? ? From: 1:123/456.0 ? ?
? ? ? ?
? ? ? Virus Scan: CLEAN (Scanned: 2025-01-30 10:23) ? ?
? ? ? ?
? ? Short: Useful utility for X ? ?
? ? Long: This is a detailed description... ? ?
? ? ? ?
? ? [? Approve] [? Reject] ? ?
? ????????????????????????????????????????????????????????????? ?
? ?
? ????????????????????????????????????????????????????????????? ?
? ? ? suspicious.exe (142 KB) ? ?
? ? Area: SOFTWARE ? ?
? ? Uploaded by: JaneSmith (user_id: 99) on 2025-01-30 11:45? ?
? ? ? ?
? ? ?? Virus Scan: INFECTED - Win.Trojan.Generic ? ?
? ? Signature: Win.Trojan.Generic ? ?
? ? ? ?
? ? Short: Productivity tool ? ?
? ? ? ?
? ? [? Approve] [? Reject] ? ?
? ????????????????????????????????????????????????????????????? ?
???????????????????????????????????????????????????????????????????
Virus Scan Display:
- Green checkmark + "CLEAN" for clean files
- Red warning + "INFECTED" with signature for infected
- Yellow warning + "ERROR" if scan failed
- Gray "SKIPPED" if scanning disabled
Reject Modal: ???????????????????????????????????????????
? Reject File [×] ?
???????????????????????????????????????????
? Filename: suspicious.exe ?
? ?
? Reason for rejection: ?
? ??????????????????????????????????????? ?
? ? File contains malware detected by ? ?
? ? virus scanner ? ?
? ? ? ?
? ??????????????????????????????????????? ?
? ?
? Action: ?
? ? Move to quarantine ?
? ? Delete from disk ?
? ?
? [Cancel] [Reject File] ?
???????????????????????????????????????????
User Templates
3. File Browsing (templates/file_browse.twig)
Features:
- Tabbed interface for file areas
- File list with icons and descriptions
- Download buttons with credit cost display
- Upload button
- Search and filter
Layout: ???????????????????????????????????????????????????????????????????
? File Areas ?
???????????????????????????????????????????????????????????????????
? [GENERAL_FILES] [SOFTWARE] [DOCS] ?
???????????????????????????????????????????????????????????????????
? Search: [__________] Sort: [Name ?] [? Upload File] ?
???????????????????????????????????????????????????????????????????
? Your Balance: 150 credits ?
???????????????????????????????????????????????????????????????????
? ? example.zip (2.4 MB) - Downloaded 145 times ?
? Useful utility for X ?
? [? Download (10 credits)] ?
???????????????????????????????????????????????????????????????????
? ? another-file.txt (24 KB) - Downloaded 67 times ?
? Text documentation for Y ?
? [? Download (5 credits)] ?
???????????????????????????????????????????????????????????????????
? ? large-archive.7z (45 MB) - Downloaded 12 times ?
? Complete archive of Z ?
? [? Download (50 credits)] ?? Large file ?
???????????????????????????????????????????????????????????????????
File Icons:
- ? Generic file
- ? Archive (.zip, .rar, .7z)
- ? Text file (.txt, .md)
- ?? Image (.jpg, .png, .gif)
- ? Audio (.mp3, .wav)
- ? Video (.mp4, .avi)
4. File Upload (templates/file_upload.twig)
Features:
- Drag-and-drop file picker
- Description fields
- Progress bar
- Upload status
Layout: ???????????????????????????????????????????????????????????????????
? Upload File ?
???????????????????????????????????????????????????????????????????
? File Area: [GENERAL_FILES ?] ?
? ?
? ????????????????????????????????????????????????????????????? ?
? ? ? ?
? ? ? Drag and drop file here ? ?
? ? or click to browse ? ?
? ? ? ?
? ? Max size: 10 MB ? ?
? ? Allowed: .txt, .zip, .pdf ? ?
? ? ? ?
? ????????????????????????????????????????????????????????????? ?
? ?
? Short Description (255 chars): ?
? [_________________________________________________________] ?
? ?
? Long Description: ?
? ??????????????????????????????????????????????????????????? ?
? ? ? ?
? ? ? ?
? ? ? ?
? ??????????????????????????????????????????????????????????? ?
? ?
? Reward: 25 credits upon approval ?
? ?
? [Cancel] [Upload File] ?
???????????????????????????????????????????????????????????????????
Upload Progress: ???????????????????????????????????????????
? Uploading: example.zip ?
? ?????????????????????????? 65% ?
? 1.5 MB / 2.4 MB ?
???????????????????????????????????????????
Success Message: ???????????????????????????????????????????
? ? Upload Successful ?
? ?
? Your file has been uploaded and is ?
? awaiting approval by a sysop. ?
? ?
? You will receive 25 credits once ?
? approved. ?
? ?
? [OK] ?
???????????????????????????????????????????
5. Private Files (templates/file_private.twig)
Features:
- List user's private files (received via netmail)
- Download private files (no credit cost)
- Delete unwanted files
- Show file origin (sender address/name)
- Virus scan status
Layout: ???????????????????????????????????????????????????????????????????
? My Private Files ?
???????????????????????????????????????????????????????????????????
? Files received via netmail are stored here. ?
? Storage used: 15.2 MB / 100 MB ?
???????????????????????????????????????????????????????????????????
? ? document.pdf (2.4 MB) ?
? Received from: John Doe (1:123/456) ?
? Date: 2025-01-30 10:23 ?
? Subject: RE: Project files ?
? ? Virus Scan: CLEAN ?
? [? Download] [?? Delete] ?
???????????????????????????????????????????????????????????????????
? ? archive.zip (5.1 MB) ?
? Received from: Jane Smith (1:234/567) ?
? Date: 2025-01-29 14:15 ?
? Subject: Files you requested ?
? ? Virus Scan: CLEAN ?
? [? Download] [?? Delete] ?
???????????????????????????????????????????????????????????????????
? ? infected.exe (142 KB) ?
? Received from: Unknown (1:999/999) ?
? Date: 2025-01-28 08:30 ?
? ?? Virus Scan: QUARANTINED - Win.Trojan.Generic ?
? This file cannot be downloaded (infected) ?
? [?? Delete] ?
???????????????????????????????????????????????????????????????????
Features:
- Read-only list (files added automatically via netmail)
- No upload button (files come from netmail only)
- Free downloads (no credit cost for own files)
- Delete option to manage storage
- Shows sender information from netmail
- Quarantined files clearly marked and not downloadable
Integration Points
1. BinkpSession.php Integration
File: src/Binkp/Protocol/BinkpSession.php
Location: handleFileFrame() method (around line 743)
Current Behavior:
- Receives M_FILE frame from binkp session
- Saves all files to inbound directory
- Only processes .pkt files
New Behavior:
- Process .pkt files (existing behavior)
- Detect .TIC files and their associated data files
- Process TIC files via TicFileProcessor
- Store files in appropriate file areas
Implementation:
protected function handleFileFrame($filename, $fileData)
{
$inboundPath = $this->config->getInboundPath();
$tempPath = $inboundPath . '/' . $filename;
// Save to temp location first
file_put_contents($tempPath, $fileData);
if (str_ends_with(strtolower($filename), '.pkt')) {
// Existing packet processing
$this->logger->info("Received packet: $filename");
// ... existing packet handling code
} elseif (str_ends_with(strtolower($filename), '.tic')) {
// NEW: TIC file processing
$this->logger->info("Received TIC file: $filename from {$this->remoteAddress}");
$this->pendingTicFiles[$filename] = $tempPath;
// TIC processing happens after associated file is received
} else {
// Regular file - check if there's a pending TIC for it
$ticFile = $filename . '.tic';
if (isset($this->pendingTicFiles[$ticFile])) {
// Process TIC + file together
try {
$ticProcessor = new \BinktermPHP\TicFileProcessor();
$result = $ticProcessor->processTicFile(
$this->pendingTicFiles[$ticFile],
$tempPath
);
if ($result['success']) {
$this->logger->info("TIC processed: {$result['area']} - $filename (file_id={$result['file_id']})");
// Clean up TIC file
unlink($this->pendingTicFiles[$ticFile]);
unset($this->pendingTicFiles[$ticFile]);
} else {
$this->logger->error("TIC processing failed: {$result['error']}");
}
} catch (\Exception $e) {
$this->logger->error("TIC handling exception: " . $e->getMessage());
}
} else {
// File without TIC - log warning and skip
$this->logger->warning("Received file without TIC: $filename");
}
}
}
Session Variables: class BinkpSession
{
// Add to class properties
private array $pendingTicFiles = [];
// At end of session, process any orphaned TIC files
protected function cleanup()
{
foreach ($this->pendingTicFiles as $ticFile => $ticPath) {
$this->logger->warning("TIC file received without data file: $ticFile");
unlink($ticPath);
}
$this->pendingTicFiles = [];
}
}
2. BinkdProcessor.php Integration
File: src/BinkdProcessor.php
Purpose: Process files from inbound directory
New Method:
/
* Process non-packet files from inbound directory
*/
public function processInboundFiles(): void
{
$allFiles = glob($this->inboundPath . '/*');
if ($allFiles === false) {
return;
}
// Filter out .pkt files (already handled by processInbound)
$pktFiles = glob($this->inboundPath . '/*.pkt') ?: [];
$otherFiles = array_diff($allFiles, $pktFiles);
foreach ($otherFiles as $filePath) {
if (!is_file($filePath) || is_dir($filePath)) {
continue;
}
try {
$this->processFileAreaFile($filePath);
} catch (\Exception $e) {
error_log("Error processing file {$filePath}: " . $e->getMessage());
}
}
}
/
* Process a single file area file
*/
protected function processFileAreaFile(string $filePath): void
{
$filename = basename($filePath);
// Determine file area (simple implementation)
$db = Database::getInstance()->getPdo();
$stmt = $db->prepare("SELECT id FROM file_areas WHERE tag = ? AND is_active = TRUE LIMIT 1");
$stmt->execute(['GENERAL_FILES']);
$area = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$area) {
error_log("No active file area found for: $filename");
return;
}
$fileHandler = new FileUploadHandler();
$result = $fileHandler->handleFidonetFile(
$area['id'],
$filePath,
'unknown', // Address not available in batch processing
['domain' => 'fidonet']
);
if ($result['success']) {
error_log("File processed successfully: $filename (file_id={$result['file_id']})");
// Remove from inbound after successful processing
unlink($filePath);
} else {
error_log("File processing failed: $filename - {$result['error']}");
}
}
Integration Point:
Modify the main processing loop or create a separate cron job:
// In scripts/binkp_poll.php or similar
$processor = new BinktermPHP\BinkdProcessor();
$processor->processInbound(); // Existing packet processing
$processor->processInboundFiles(); // NEW: File area processing
3. UserCredit Integration
Purpose: Award credits for approved uploads, charge for downloads
Download Credit Deduction
File: src/FileDownloadHandler.php
use BinktermPHP\UserCredit;
protected function deductCredits(int $fileId, int $userId): bool
{
$file = $this->getFileDetails($fileId);
$fileArea = $this->getFileArea($file['file_area_id']);
$cost = $fileArea['download_credit_cost'];
if ($cost <= 0) {
return true; // Free download
}
return UserCredit::debit(
$userId,
$cost,
"Downloaded: {$file['filename']}",
null, // No source user
UserCredit::TYPE_PAYMENT
);
}
Upload Credit Reward
File: src/FileApprovalController.php
use BinktermPHP\UserCredit;
public function approveFile(int $fileId, int $adminUserId): bool
{
$file = $this->getFileDetails($fileId);
// Only award credits for user uploads (not Fidonet)
if ($file['source_type'] === 'user' && $file['uploaded_by_user_id']) {
$fileArea = $this->getFileArea($file['file_area_id']);
$reward = $fileArea['upload_credit_reward'];
if ($reward > 0) {
UserCredit::credit(
$file['uploaded_by_user_id'],
$reward,
"File approved: {$file['filename']}",
$adminUserId,
UserCredit::TYPE_SYSTEM_REWARD
);
}
}
// Update file status
$db = Database::getInstance()->getPdo();
$stmt = $db->prepare("
UPDATE files
SET status = 'approved',
approved_by_user_id = ?,
approved_at = NOW(),
updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$adminUserId, $fileId]);
// Update file area stats
$this->updateFileAreaStats($file['file_area_id']);
return true;
}
4. Netmail File Attachment Integration
File: Netmail packet processing (location TBD based on current netmail handling)
Purpose: Extract file attachments from netmail and store in user's private area
Implementation
When processing incoming netmail packets, detect file attachments and handle them:
/
* Process netmail with file attachments
*/
protected function processNetmailAttachments(array $netmail, array $attachedFiles): void
{
// Get recipient user
$db = Database::getInstance()->getPdo();
$stmt = $db->prepare("SELECT id FROM users WHERE ftn_address = ? OR real_name = ?");
$stmt->execute([$netmail['to_address'], $netmail['to_name']]);
$recipient = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$recipient) {
error_log("Cannot store attachments: recipient not found for {$netmail['to_name']}");
return;
}
$fileHandler = new FileUploadHandler();
foreach ($attachedFiles as $filePath) {
$filename = basename($filePath);
// Store in user's private area
$result = $fileHandler->handleNetmailAttachment(
$recipient['id'],
$filePath,
$netmail['from_address'],
[
'netmail_subject' => $netmail['subject'],
'from_name' => $netmail['from_name']
]
);
if ($result['success']) {
error_log("Netmail attachment stored: $filename (file_id={$result['file_id']})");
// Optionally: Add note to netmail body about received file
$this->addAttachmentNote($netmail['id'], $filename, $result['file_id']);
} else {
error_log("Failed to store netmail attachment: $filename - {$result['error']}");
}
}
}
/
* Add note to netmail about received file attachment
*/
protected function addAttachmentNote(int $netmailId, string $filename, int $fileId): void
{
// Could append to message body or add system message
$note = "\n\n--- File Attachment ---\nFile: $filename\nStored in your private files. View at: /files/private\n";
$db = Database::getInstance()->getPdo();
$stmt = $db->prepare("UPDATE netmail SET body = body || ? WHERE id = ?");
$stmt->execute([$note, $netmailId]);
}
FileUploadHandler New Method
Add to FileUploadHandler class:
/
* Handle file received as netmail attachment
*
* @param int $userId Owner of the file
* @param string $filePath Path to temporary file
* @param string $fromAddress Sender's Fidonet address
* @param array $metadata Additional metadata
* @return array Result array
*/
public function handleNetmailAttachment(
int $userId,
string $filePath,
string $fromAddress,
array $metadata = []
): array {
try {
$filename = basename($filePath);
$filesize = filesize($filePath);
// Calculate hash
$fileHash = $this->calculateFileHash($filePath);
// Scan for viruses
$scanResult = $this->scanForViruses($filePath);
if ($scanResult['result'] === 'infected') {
// Quarantine infected files even in private area
$this->moveToQuarantine($filePath, $userId, $filename);
return [
'success' => false,
'error' => 'File infected with virus: ' . $scanResult['signature'],
'quarantined' => true
];
}
// Move to private storage
$storagePath = $this->moveToPrivateStorage($filePath, $userId, $filename);
// Create database record
$fileId = $this->storePrivateFileRecord([
'filename' => $filename,
'filesize' => $filesize,
'file_hash' => $fileHash,
'storage_path' => $storagePath,
'uploaded_by_user_id' => $userId,
'uploaded_from_address' => $fromAddress,
'source_type' => 'netmail_attachment',
'is_private' => true,
'status' => 'approved', // Auto-approved
'short_description' => $metadata['netmail_subject'] ?? '',
'long_description' => "Received from {$metadata['from_name']} ({$fromAddress})",
'virus_scanned' => $scanResult['scanned'],
'virus_scan_result' => $scanResult['result'],
'virus_scanned_at' => $scanResult['scanned_at'] ?? null
]);
return [
'success' => true,
'file_id' => $fileId,
'storage_path' => $storagePath
];
} catch (\Exception $e) {
error_log("Netmail attachment handling error: " . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/
* Move file to user's private storage directory
*/
protected function moveToPrivateStorage(string $tempPath, int $userId, string $filename): string
{
$filename = $this->sanitizeFilename($filename);
// Build storage path: data/files/private/{user_id}/
$baseDir = realpath(__DIR__ . '/../data/files/private');
$userDir = $baseDir . '/' . $userId;
// Create user directory if needed
if (!is_dir($userDir)) {
mkdir($userDir, 0755, true);
}
// Handle filename conflicts
$storagePath = $userDir . '/' . $filename;
$counter = 1;
while (file_exists($storagePath)) {
$pathInfo = pathinfo($filename);
$newFilename = $pathInfo['filename'] . '_' . $counter . '.' . $pathInfo['extension'];
$storagePath = $userDir . '/' . $newFilename;
$counter++;
}
// Move file
if (!rename($tempPath, $storagePath)) {
throw new \Exception("Failed to move file to private storage");
}
// Set permissions
chmod($storagePath, 0644);
return $storagePath;
}
/
* Store private file record in database
*/
protected function storePrivateFileRecord(array $data): int
{
$db = Database::getInstance()->getPdo();
$stmt = $db->prepare("
INSERT INTO files (
file_area_id, filename, filesize, file_hash, storage_path,
uploaded_by_user_id, uploaded_from_address, source_type,
is_private, status, short_description, long_description,
virus_scanned, virus_scan_result, virus_scanned_at,
created_at, updated_at
) VALUES (
NULL, ?, ?, ?, ?,
?, ?, ?,
TRUE, 'approved', ?, ?,
?, ?, ?,
NOW(), NOW()
) RETURNING id
");
$stmt->execute([
$data['filename'],
$data['filesize'],
$data['file_hash'],
$data['storage_path'],
$data['uploaded_by_user_id'],
$data['uploaded_from_address'],
$data['source_type'],
$data['short_description'],
$data['long_description'],
$data['virus_scanned'],
$data['virus_scan_result'],
$data['virus_scanned_at']
]);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
return $result['id'];
}
5. AdminActionLogger Integration
Purpose: Audit trail for all file area admin actions
Actions to Log:
use BinktermPHP\AdminActionLogger;
// File area created
AdminActionLogger::logAction($userId, 'file_area_created', [
'tag' => $tag,
'domain' => $domain,
'max_file_size' => $maxFileSize
]);
// File area updated
AdminActionLogger::logAction($userId, 'file_area_updated', [
'file_area_id' => $id,
'tag' => $tag,
'changes' => $changedFields
]);
// File area deleted
AdminActionLogger::logAction($userId, 'file_area_deleted', [
'file_area_id' => $id,
'tag' => $tag
]);
// File approved
AdminActionLogger::logAction($adminUserId, 'file_approved', [
'file_id' => $fileId,
'filename' => $filename,
'uploader_id' => $uploaderId,
'credits_awarded' => $creditsAwarded
]);
// File rejected
AdminActionLogger::logAction($adminUserId, 'file_rejected', [
'file_id' => $fileId,
'filename' => $filename,
'uploader_id' => $uploaderId,
'reason' => $rejectionReason
]);
// File quarantined (virus detected)
AdminActionLogger::logAction($adminUserId, 'file_quarantined', [
'file_id' => $fileId,
'filename' => $filename,
'virus_signature' => $signature
]);
Security Considerations
1. File Extension Blacklist
Implementation:
protected function validateExtension(string $filename, array $fileArea): bool
{
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
// Check blacklist
if (!empty($fileArea['blocked_extensions'])) {
$blocked = array_map('trim', explode(',', $fileArea['blocked_extensions']));
$blocked = array_map('strtolower', $blocked);
if (in_array('.' . $extension, $blocked) || in_array($extension, $blocked)) {
return false;
}
}
// Check whitelist (if configured)
if (!empty($fileArea['allowed_extensions'])) {
$allowed = array_map('trim', explode(',', $fileArea['allowed_extensions']));
$allowed = array_map('strtolower', $allowed);
if (!in_array('.' . $extension, $allowed) && !in_array($extension, $allowed)) {
return false;
}
}
return true;
}
Default Blacklist:
- Executables: .exe, .bat, .com, .cmd, .scr, .pif
- Scripts: .vbs, .js, .wsf, .hta
- System files: .sys, .dll, .drv
2. Path Traversal Prevention
Implementation:
protected function sanitizeFilename(string $filename): string
{
// Remove path components
$filename = basename($filename);
// Remove dangerous characters
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename);
// Prevent hidden files
$filename = ltrim($filename, '.');
// Ensure not empty
if (empty($filename)) {
$filename = 'file_' . bin2hex(random_bytes(8));
}
return $filename;
}
protected function moveToStorage(string $tempPath, string $areaTag, string $filename): string
{
// Sanitize area tag
$areaTag = preg_replace('/[^a-zA-Z0-9_-]/', '', $areaTag);
// Sanitize filename
$filename = $this->sanitizeFilename($filename);
// Build safe storage path
$storagePath = realpath(__DIR__ . '/../data/files') . '/' . $areaTag . 'FileAreas_Proposal.md/' . $filename;
// Verify path is within allowed directory
$allowedBase = realpath(__DIR__ . '/../data/files');
if (!str_starts_with($storagePath, $allowedBase)) {
throw new \Exception("Invalid storage path detected");
}
// Create directory if needed
$areaDir = $allowedBase . '/' . $areaTag;
if (!is_dir($areaDir)) {
mkdir($areaDir, 0755, true);
}
// Move file
if (!rename($tempPath, $storagePath)) {
throw new \Exception("Failed to move file to storage");
}
// Set permissions
chmod($storagePath, 0644);
return $storagePath;
}
3. Access Control
Access Check with Private File Support:
protected function checkAccess(int $fileId, int $userId): bool
{
$db = Database::getInstance()->getPdo();
$stmt = $db->prepare("
SELECT f.id, f.is_private, f.uploaded_by_user_id,
f.file_area_id, fa.is_sysop_only
FROM files f
LEFT JOIN file_areas fa ON f.file_area_id = fa.id
WHERE f.id = ? AND f.status = 'approved'
");
$stmt->execute([$fileId]);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$result) {
return false; // File not found or not approved
}
// Private files: only owner can access
if ($result['is_private']) {
return $result['uploaded_by_user_id'] == $userId;
}
// Public files: check subscriptions and sysop restrictions
if (!$result['file_area_id']) {
return false; // Public file must have area
}
// Check if sysop-only and user is not sysop
if ($result['is_sysop_only']) {
$userStmt = $db->prepare("SELECT is_sysop FROM users WHERE id = ?");
$userStmt->execute([$userId]);
$user = $userStmt->fetch(\PDO::FETCH_ASSOC);
if (!$user || !$user['is_sysop']) {
return false;
}
}
// Check subscription
$subStmt = $db->prepare("
SELECT id FROM user_file_area_subscriptions
WHERE user_id = ? AND file_area_id = ?
AND is_active = TRUE
");
$subStmt->execute([$userId, $result['file_area_id']]);
return $subStmt->fetch() !== false;
}
4. Credit Balance Checks
Pre-Download Validation with Private File Support:
protected function checkCredits(int $fileId, int $userId): array
{
$file = $this->getFileDetails($fileId);
// Private files are always free for the owner
if ($file['is_private']) {
return ['sufficient' => true, 'cost' => 0, 'balance' => null];
}
$fileArea = $this->getFileArea($file['file_area_id']);
$cost = $fileArea['download_credit_cost'];
if ($cost <= 0) {
return ['sufficient' => true, 'cost' => 0, 'balance' => null];
}
$balance = UserCredit::getBalance($userId);
return [
'sufficient' => $balance >= $cost,
'cost' => $cost,
'balance' => $balance,
'shortage' => max(0, $cost - $balance)
];
}
5. Virus Scanning
See VirusScanner component above.
Key Security Points:
- Scan before making file available
- Quarantine infected files immediately
- Don't auto-delete (allow sysop review)
- Update virus definitions regularly
- Timeout protection (30 seconds default)
6. Audit Logging
All Critical Actions Logged:
- File uploads (user, file, size, timestamp)
- File downloads (user, file, credits, IP)
- Admin approvals/rejections
- File area changes
- Virus detections
Example Log Queries:
-- Recent file uploads
SELECT f.filename, u.username, f.created_at
FROM files f
LEFT JOIN users u ON f.uploaded_by_user_id = u.id
ORDER BY f.created_at DESC
LIMIT 50;
-- Download history for file
SELECT u.username, fd.downloaded_at, fd.ip_address
FROM file_downloads fd
JOIN users u ON fd.user_id = u.id
WHERE fd.file_id = ?
ORDER BY fd.downloaded_at DESC;
-- Virus detections
SELECT f.filename, f.virus_signature, f.virus_scanned_at
FROM files f
WHERE f.virus_scan_result = 'infected'
ORDER BY f.virus_scanned_at DESC;
Implementation Roadmap
Phase 1: Foundation (Week 1)
Tasks:
1. Create database migration
2. Run migration and verify tables created
3. Create directory structure (data/files/, .quarantine/, etc.). Place .gitkeep in. Update setup.php to make these world writable just as data/outbound is
4. Create FileAreaManager class
5. Create FileUploadHandler class (basic validation)
6. Create FileDownloadHandler class (basic download)
7. Unit tests for core classes
Deliverables:
- Working database schema
- Core class functionality
- Basic upload/download flow
Phase 2: Web Interface (Week 2)
Tasks:
1. Create API endpoints (user routes)
2. Create admin API endpoints
3. Create FileAreaController
4. Create admin templates (file_areas.twig, file_approval.twig)
5. Create user templates (file_browse.twig, file_upload.twig)
6. Create JavaScript files (file_areas.js, file_approval.js, file_browse.js)
7. CSS styling for file area pages
Deliverables:
- Working admin interface
- Working user interface
- AJAX-based interactions
Phase 3: Advanced Features (Week 3)
Tasks:
1. Implement VirusScanner class
2. Configure ClamAV integration
3. Implement FileApprovalController
4. Add credit integration (UserCredit)
5. Add admin action logging (AdminActionLogger)
6. File area subscription management
7. Download statistics and tracking
Deliverables:
- Virus scanning functional
- Credit economy integrated
- Approval workflow complete
- Statistics tracking
Phase 4: Fidonet Integration (Week 4)
Tasks:
1. Modify BinkpSession.php for file handling
2. Modify BinkdProcessor.php for batch processing
3. Implement file area routing logic
4. Test receiving files via binkp
5. Auto-approval for Fidonet sources
6. File area routing table (optional)
7. Implement netmail attachment extraction
8. Private file storage for netmail attachments
9. Create private files UI template
Deliverables:
- Files received via binkp processed correctly
- Auto-approval for trusted nodes
- Routing logic functional
- Netmail attachments stored in user private areas
- Users can browse and download their private files
Phase 5: Testing & Documentation (Week 5)
Tasks:
1. End-to-end testing (see test plan below)
2. Security audit (penetration testing)
3. Performance testing (large files, many users)
4. Documentation: UPGRADING_1.9.0.md
5. Documentation: Update FAQ.md
6. Bug fixes and refinements
Deliverables:
- Complete test coverage
- Documentation complete
- Production-ready system
Phase 6: Future Enhancements (Post-Release)
Planned Features:
1. Binkp FREQ protocol (M_GET frame)
2. Netmail file request commands
3. File area statistics dashboard
4. Automatic file aging/archival
5. File tagging and categories
6. Thumbnail generation for images
7. File versioning system
8. Bandwidth quotas per user
9. File area mirroring between nodes
10. Advanced routing rules
Testing Plan
Unit Tests
FileAreaManager Tests:
- Create/update/delete file areas
- Subscription management
- Access control checks
- Statistics aggregation
FileUploadHandler Tests:
- File size validation
- Extension validation
- Duplicate detection (hash)
- Path traversal prevention
- Filename sanitization
FileDownloadHandler Tests:
- Access control
- Credit checks
- Credit deduction
- Download logging
- File streaming
VirusScanner Tests:
- Clean file detection
- Infected file detection (EICAR)
- Error handling
- Timeout handling
Integration Tests
Upload Flow:
1. User uploads valid file ? status='pending'
2. Virus scan runs ? result='clean'
3. File stored in correct directory
4. Database record created
5. Admin sees file in approval queue
Approval Flow:
1. Admin approves file
2. User awarded credits
3. File status='approved'
4. File appears in browse list
5. Download statistics initialized
Download Flow:
1. User initiates download
2. Access checked
3. Credits checked and deducted
4. Download logged
5. Statistics updated
6. File streamed to user
Fidonet Flow:
1. File received via binkp
2. Routed to correct file area
3. Auto-approved (if configured)
4. Available for download immediately
End-to-End Tests
Test Scenario 1: User Upload to Approval to Download
-
Login as regular user
-
Navigate to file upload page
-
Select GENERAL_FILES area
-
Upload test.txt (1 KB)
-
Add descriptions
-
Submit upload
-
Verify status: "Awaiting approval"
-
Logout
-
Login as admin
-
Navigate to approval queue
-
See test.txt in pending list
-
Review file details
-
Approve file
-
Verify user awarded credits
-
Logout
-
Login as different user (with credits)
-
Navigate to file browse
-
See test.txt in GENERAL_FILES
-
Click download
-
Verify credit deduction
-
Verify file downloaded correctly
Expected Results:
- Upload successful
- Approval workflow correct
- Credits awarded/deducted correctly
- File download successful
- All statistics updated
Test Scenario 2: Virus Detection
-
Login as user
-
Upload EICAR test file
-
Verify virus scan detects infection
-
Verify file status='quarantined'
-
Verify file moved to .quarantine/
-
Login as admin
-
See prominent virus warning in approval queue
-
Review virus signature
-
Reject file with reason
-
Verify file not available for download
Expected Results:
- Virus detected correctly
- File quarantined
- Admin notified
- Rejection logged
Test Scenario 3: Insufficient Credits
-
Login as user with 10 credits
-
Attempt to download file costing 20 credits
-
Verify error: "Insufficient credits"
-
Verify no download
-
Verify no credit deduction
-
Verify download not logged
Expected Results:
- Download blocked
- Clear error message
- No credit charge
- No false statistics
Test Scenario 4: Duplicate Detection
-
Upload file: example.zip
-
Approve and publish
-
Attempt to upload same file again (same hash)
-
Verify error: "Duplicate file exists"
-
Verify no database record created
-
Verify no duplicate file on disk
Expected Results:
- Duplicate detected
- Upload rejected
- No duplicate storage
Test Scenario 5: Access Control
-
Create sysop-only file area
-
Upload file to that area
-
Login as regular user
-
Verify area not visible in browse list
-
Attempt direct URL access to file
-
Verify access denied
-
Login as sysop
-
Verify area visible
-
Verify download allowed
Expected Results:
- Regular users blocked
- Sysops have access
- No security bypass
Test Scenario 6: Private File Delivery via Netmail
-
User sends netmail with file attachment to user "alice"
-
Netmail processed by system
-
File attachment extracted
-
Virus scan performed
-
File stored in alice's private area
-
Verify file record created with is_private=TRUE
-
Verify storage path: `data/files/private/{alice_user_id}/filename`
-
Login as alice
-
Navigate to "My Private Files"
-
See attached file listed
-
Verify sender information displayed
-
Download file
-
Verify no credit charge
-
Verify download successful
-
Login as different user (bob)
-
Attempt to access alice's private file via direct URL
-
Verify access denied
Expected Results:
- Netmail attachment extracted correctly
- File stored in private area
- Only owner can access
- No credit charge for owner
- Other users blocked from access
Test Scenario 7: Private File Deletion
-
Login as user with private files
-
Navigate to "My Private Files"
-
Select file to delete
-
Click delete button
-
Confirm deletion
-
Verify file removed from list
-
Verify file deleted from disk
-
Verify database record removed/marked deleted
-
Verify storage quota updated
Expected Results:
- File deleted successfully
- Storage space reclaimed
- Database cleaned up
Test Scenario 8: Private File Virus Quarantine
-
User receives netmail with infected file (EICAR test)
-
System extracts attachment
-
Virus scan detects infection
-
File quarantined (not placed in private area)
-
Database record created with status='quarantined'
-
Login as recipient user
-
Navigate to "My Private Files"
-
See quarantined file with warning
-
Verify download button disabled
-
Verify prominent virus warning displayed
-
Option to delete quarantined file
Expected Results:
- Infected file quarantined
- User notified of virus
- Download prevented
- Clear security warning
Future Enhancements
Phase 2: File Request (FREQ) Support
Binkp FREQ Protocol:
- Implement M_GET frame handling in BinkpSession
- Match requested filenames to file_areas
- Queue files for transmission
- Support password-protected FREQ
- Implement file request limits (rate limiting)
Netmail File Requests:
- Parse netmail body for FREQ commands
- Support commands: REQUEST, FREQ, SENDFILE
- Respond with availability status
- Queue files for next binkp session
Database Extensions: -- Already created in Phase 1
CREATE TABLE file_requests (
...
);
-- Add routing rules
CREATE TABLE file_freq_rules (
id SERIAL PRIMARY KEY,
file_area_id INTEGER REFERENCES file_areas(id),
require_password BOOLEAN DEFAULT FALSE,
password_hash VARCHAR(255),
max_requests_per_day INTEGER DEFAULT 10,
max_file_size BIGINT,
allowed_addresses TEXT -- Comma-separated patterns
);
Phase 3: Statistics Dashboard
Features:
- Top downloaded files
- Most active uploaders
- File area popularity
- Credit economy statistics
- Virus detection trends
- Storage usage graphs
Implementation:
- New template: templates/admin/file_statistics.twig
- Chart.js for visualizations
- API endpoint: /admin/api/file-stats
Phase 4: File Aging & Archival
Features:
- Auto-archive files not downloaded in X days
- Move old files to archive area
- Delete files older than X days (configurable)
- Sysop notifications before deletion
Configuration: // Per file area
'auto_archive_days' => 180, // Move to archive after 180 days
'auto_delete_days' => 365, // Delete after 1 year
'notify_before_delete' => true
Phase 5: File Tagging & Categories
Features:
- User-defined tags (e.g., "utility", "game", "documentation")
- Category browsing
- Tag-based search
- Tag clouds
Database: CREATE TABLE file_tags (
id SERIAL PRIMARY KEY,
tag_name VARCHAR(50) UNIQUE NOT NULL
);
CREATE TABLE file_tag_assignments (
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
tag_id INTEGER REFERENCES file_tags(id) ON DELETE CASCADE,
PRIMARY KEY (file_id, tag_id)
);
Phase 6: Thumbnail Generation
Features:
- Auto-generate thumbnails for images
- Preview for videos (first frame)
- PDF preview (first page)
- Archive content listing (.zip, .rar)
Implementation:
- GD library or ImageMagick for image processing
- Store thumbnails in data/files/.thumbnails/
- Lazy loading in file browse interface
Phase 7: File Versioning
Features:
- Upload new version of existing file
- Keep version history
- Download specific versions
- Compare versions
Database: ALTER TABLE files ADD COLUMN version INTEGER DEFAULT 1;
ALTER TABLE files ADD COLUMN parent_file_id INTEGER REFERENCES files(id);
Phase 8: Bandwidth Quotas
Features:
- Daily/weekly/monthly download limits per user
- Quota tracking
- Reset schedules
- Quota exceeded notifications
Database: CREATE TABLE user_bandwidth_quotas (
user_id INTEGER PRIMARY KEY REFERENCES users(id),
daily_limit BIGINT DEFAULT 104857600, -- 100 MB
weekly_limit BIGINT,
monthly_limit BIGINT,
current_period_usage BIGINT DEFAULT 0,
period_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Phase 9: File Area Mirroring
Features:
- Designate file areas for mirroring
- Automatically send new files to linked nodes
- Receive mirrored files from uplinks
- Conflict resolution
Configuration: CREATE TABLE file_area_mirrors (
id SERIAL PRIMARY KEY,
file_area_id INTEGER REFERENCES file_areas(id),
remote_address VARCHAR(255), -- Fidonet address
direction VARCHAR(10), -- 'send', 'receive', 'both'
is_active BOOLEAN DEFAULT TRUE
);
Conclusion
This proposal outlines a comprehensive file area system for BinktermPHP that:
-
Enhances User Experience: Modern web interface for file sharing
-
Maintains BBS Tradition: Classic file base features with credit economy
-
Ensures Security: Virus scanning, approval workflows, access control
-
Integrates Seamlessly: Works with existing code patterns and architecture
-
Supports Growth: Extensible design for future FREQ and advanced features
Key Benefits:
- ? Multi-network file distribution
- ? Private file storage for netmail attachments
- ? Credit-based economy integration (public files)
- ? Virus protection with ClamAV for all files
- ? Sysop approval workflow (public files)
- ? Download statistics and tracking
- ? Fidonet file reception via binkp
- ? Future FREQ protocol support
- ? Scalable storage architecture
- ? Simple ownership-based access control for private files
Implementation Timeline: 5 weeks for core features, ongoing enhancements
Risk Mitigation:
- Follows existing code patterns (low integration risk)
- Incremental rollout (Phase 1 ? 2 ? 3...)
- Extensive testing plan
- Security audit before production
Appendix: Configuration Examples
.env Configuration
# File Area Settings
FILES_MAX_UPLOAD_SIZE=10485760 # 10MB default
FILES_AUTO_APPROVE_FIDONET=true # Auto-approve from trusted nodes
FILES_QUARANTINE_INFECTED=true # Move infected to .quarantine/
FILES_DEFAULT_DOWNLOAD_COST=10 # Default credits per download
FILES_DEFAULT_UPLOAD_REWARD=25 # Default credits per approved upload
# Private File Settings
FILES_PRIVATE_STORAGE_QUOTA=104857600 # 100MB per user default
FILES_PRIVATE_MAX_FILE_SIZE=10485760 # 10MB max size for netmail attachments
FILES_PRIVATE_SCAN_VIRUSES=true # Scan private files for viruses
# ClamAV Virus Scanning (optional)
CLAMAV_ENABLED=false
CLAMAV_PATH=/usr/bin/clamdscan
CLAMAV_SOCKET=/var/run/clamav/clamd.ctl
CLAMAV_TIMEOUT=30
# File Area Routing (future)
FILES_AUTO_ROUTE_ENABLED=false
FILES_ROUTING_CONFIG=/path/to/routing.json
TIC File Configuration
File areas use the TIC (File Catalog) system for Fidonet file distribution. Configuration similar to binkp.json for uplink file area subscriptions.
File: config/fileareas.json
{
"file_areas": [
{
"tag": "COOKING_FILES",
"description": "Cooking recipes and food-related files",
"domain": "fidonet",
"uplinks": ["1:123/456"],
"forward_to": [],
"is_passive": false
},
{
"tag": "BBS_ADS",
"description": "BBS advertisements and ANSI art",
"domain": "fidonet",
"uplinks": ["1:123/456"],
"forward_to": ["1:234/567"],
"is_passive": false
}
],
"tic_settings": {
"inbound_path": "data/inbound/tic",
"validate_crc": true,
"validate_size": true,
"require_password": false,
"replace_existing": false
}
}
TIC File Format (received with files): Area COOKING_FILES
File recipe.txt
Desc A great recipe for chocolate cake
From 1:123/456
Path 1:123/456 1234567890
Seenby 1:123/456
Created by BinktermPHP v1.9.0
Date Wed, 30 Jan 2025 12:34:56 -0500
Size 2048
Crc A3B5C7D9
Processing Flow:
1. Receive .TIC + file via binkp
2. Parse TIC metadata
3. Validate CRC and size
4. Look up file area by tag
5. Store file in area
6. Update Path/Seenby
7. Forward to downstream nodes if configured
Appendix: SQL Schema Summary
Tables Created:
1. file_areas - File distribution areas (8 rows expected initially)
2. files - File metadata (grows over time)
3. user_file_area_subscriptions - Access control (per user/area)
4. file_downloads - Download history (audit trail)
5. file_requests - FREQ queue (future use)
Total Storage Estimate:
- Database: ~100 MB for 10,000 files
- Files: Varies (configurable limits)
- Thumbnails: ~50 MB for 10,000 images (future)
Document Version: 1.1
Last Updated: 2025-01-30
Status: DRAFT - Pending Review
|