Files
ReadMeABook/src/lib/utils/path-mapper.ts
T
kikootwo 682836237b Implement centralized logging with RMABLogger
Replaces scattered console statements with a unified RMABLogger across backend API routes and services. Adds LOG_LEVEL-based filtering, job-aware database persistence, and context-based logging. Updates documentation to describe the new logging system and usage patterns. Also documents qBittorrent CSRF header fix
2026-01-28 11:41:58 -05:00

132 lines
4.3 KiB
TypeScript

/**
* Path Mapper Utility
* Documentation: documentation/phase3/qbittorrent.md
*
* Handles remote-to-local path mapping for qBittorrent downloads.
* Use case: qBittorrent on remote seedbox or different mount points.
*/
import path from 'path';
import { RMABLogger } from './logger';
const logger = RMABLogger.create('PathMapper');
export interface PathMappingConfig {
enabled: boolean;
remotePath: string;
localPath: string;
}
export class PathMapper {
/**
* Transforms a qBittorrent path using remote-to-local mapping
*
* Example:
* qBittorrent reports: /remote/mnt/d/done/Audiobook.Name
* Config: { enabled: true, remotePath: '/remote/mnt/d/done', localPath: '/downloads' }
* Returns: /downloads/Audiobook.Name
*
* @param qbittorrentPath - Path reported by qBittorrent
* @param config - Path mapping configuration
* @returns Transformed path (or original if mapping disabled/no match)
*/
static transform(qbittorrentPath: string, config: PathMappingConfig): string {
// 1. If mapping disabled, return original
if (!config.enabled) {
return qbittorrentPath;
}
// 2. Handle empty paths
if (!qbittorrentPath || !config.remotePath || !config.localPath) {
logger.warn('Empty path or config, returning original');
return qbittorrentPath;
}
// 3. Normalize paths (handle trailing slashes, backslashes)
// Convert all backslashes to forward slashes for consistency
const normalizedRemote = this.normalizePath(config.remotePath);
const normalizedLocal = this.normalizePath(config.localPath);
const normalizedQbPath = this.normalizePath(qbittorrentPath);
// 4. Check if qBittorrent path starts with remote path
if (!normalizedQbPath.startsWith(normalizedRemote)) {
logger.warn(
`Path "${qbittorrentPath}" does not start with remote path "${config.remotePath}". ` +
`Returning original path unchanged.`
);
return qbittorrentPath;
}
// 5. Replace remote prefix with local prefix
const relativePath = normalizedQbPath.substring(normalizedRemote.length);
// Join local path with relative path, ensuring proper path separators
const transformedPath = path.join(normalizedLocal, relativePath);
logger.info(`Transformed "${qbittorrentPath}" to "${transformedPath}"`);
return transformedPath;
}
/**
* Validates path mapping configuration
*
* @param config - Path mapping configuration to validate
* @throws Error if paths are invalid (empty, malformed, etc.)
*/
static validate(config: PathMappingConfig): void {
if (!config.enabled) {
return; // No validation needed if disabled
}
if (!config.remotePath || config.remotePath.trim() === '') {
throw new Error('Remote path cannot be empty when path mapping is enabled');
}
if (!config.localPath || config.localPath.trim() === '') {
throw new Error('Local path cannot be empty when path mapping is enabled');
}
// Check for obviously invalid paths
const invalidChars = /[<>"|?*]/;
if (invalidChars.test(config.remotePath)) {
throw new Error('Remote path contains invalid characters');
}
if (invalidChars.test(config.localPath)) {
throw new Error('Local path contains invalid characters');
}
// Warn if paths look suspicious (but don't throw)
if (config.remotePath === config.localPath) {
logger.warn('Remote and local paths are identical - path mapping will have no effect');
}
}
/**
* Normalizes a file path for consistent comparison
* - Converts backslashes to forward slashes
* - Removes trailing slashes
* - Normalizes redundant separators
*
* @param filePath - Path to normalize
* @returns Normalized path
*/
private static normalizePath(filePath: string): string {
// Convert backslashes to forward slashes
let normalized = filePath.replace(/\\/g, '/');
// Use path.normalize to handle redundant separators and ..
normalized = path.normalize(normalized);
// Convert backslashes again (path.normalize might add them on Windows)
normalized = normalized.replace(/\\/g, '/');
// Remove trailing slash (except for root '/')
if (normalized.length > 1 && normalized.endsWith('/')) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
}