diff --git a/bin/photos-diff.ts b/bin/photos-diff.ts index 7f1fcf4..d0de8ff 100755 --- a/bin/photos-diff.ts +++ b/bin/photos-diff.ts @@ -2,7 +2,7 @@ import { execSync, spawn } from 'child_process'; import { createHash } from 'crypto'; -import { readFileSync, statSync, writeFileSync } from 'fs'; +import { createReadStream, statSync, writeFileSync } from 'fs'; import { parseArgs } from 'util'; import Redis from 'iovalkey'; import { createInterface } from 'readline'; @@ -11,6 +11,15 @@ const CACHE_TTL = 3600; // 1 hour const CACHE_PREFIX_ANDROID = 'photo-sync:android:'; const CACHE_PREFIX_LOCAL = 'photo-sync:local:'; +function showProgress(current: number, total: number, startTime: number, prefix: string): void { + const elapsed = Date.now() - startTime; + const avgTime = elapsed / current; + const remaining = total - current; + const eta = Math.round((avgTime * remaining) / 1000); + const etaStr = eta > 60 ? `${Math.floor(eta / 60)}m ${eta % 60}s` : `${eta}s`; + process.stdout.write(`\r${prefix} Progress: ${current}/${total} files | ETA: ${etaStr} `); +} + interface FileInfo { path: string; size: number; @@ -33,7 +42,7 @@ function parseArguments() { }); if (!values.local || !values.android) { - console.error('Usage: ./photo-backup-checker.ts --local DIR --android DIR'); + console.error('Usage: ./photos-diff.ts --local DIR --android DIR'); process.exit(1); } @@ -84,18 +93,7 @@ async function getLocalFiles(dir: string, redis: Redis): Promise { await redis.setex(cacheKey, CACHE_TTL, JSON.stringify({ size: stat.size })); } - // Progress and ETA - const processed = i + 1; - const elapsed = Date.now() - startTime; - const avgTime = elapsed / processed; - const remaining = files.length - processed; - const eta = Math.round((avgTime * remaining) / 1000); - - const etaStr = eta > 60 - ? `${Math.floor(eta / 60)}m ${eta % 60}s` - : `${eta}s`; - - process.stdout.write(`\ršŸ“ Progress: ${processed}/${files.length} files | ETA: ${etaStr} `); + showProgress(i + 1, files.length, startTime, 'šŸ“'); } catch (err) { console.error(`\nāŒ Error reading local file: ${file}`); throw err; @@ -131,11 +129,11 @@ async function getAndroidFiles(dir: string, redis: Redis): Promise { const fileInfos: FileInfo[] = []; const startTime = Date.now(); - const BATCH_SIZE = 50; + const STAT_BATCH_SIZE = 50; let processed = 0; - for (let i = 0; i < files.length; i += BATCH_SIZE) { - const batch = files.slice(i, i + BATCH_SIZE); + for (let i = 0; i < files.length; i += STAT_BATCH_SIZE) { + const batch = files.slice(i, i + STAT_BATCH_SIZE); // Check cache first for this batch const cachedResults: FileInfo[] = []; @@ -214,18 +212,8 @@ async function getAndroidFiles(dir: string, redis: Redis): Promise { } } - // Progress and ETA if (processed > 0) { - const elapsed = Date.now() - startTime; - const avgTime = elapsed / processed; - const remaining = files.length - processed; - const eta = Math.round((avgTime * remaining) / 1000); - - const etaStr = eta > 60 - ? `${Math.floor(eta / 60)}m ${eta % 60}s` - : `${eta}s`; - - process.stdout.write(`\ršŸ“Š Progress: ${processed}/${files.length} files | ETA: ${etaStr} `); + showProgress(processed, files.length, startTime, 'šŸ“Š'); } } @@ -233,11 +221,15 @@ async function getAndroidFiles(dir: string, redis: Redis): Promise { return fileInfos; } -function sha256Local(path: string): string { +async function sha256Local(path: string): Promise { const hash = createHash('sha256'); - const data = readFileSync(path); - hash.update(data); - return hash.digest('hex'); + const stream = createReadStream(path); + + return new Promise((resolve, reject) => { + stream.on('data', (data) => hash.update(data)); + stream.on('end', () => resolve(hash.digest('hex'))); + stream.on('error', reject); + }); } function sha256Android(path: string): string { @@ -274,11 +266,11 @@ async function calculateHashes( if (source === 'android') { // Batch processing for Android - const BATCH_SIZE = 20; + const HASH_BATCH_SIZE = 20; let processed = 0; - for (let i = 0; i < files.length; i += BATCH_SIZE) { - const batch = files.slice(i, i + BATCH_SIZE); + for (let i = 0; i < files.length; i += HASH_BATCH_SIZE) { + const batch = files.slice(i, i + HASH_BATCH_SIZE); // Check cache first const needHash: FileInfo[] = []; @@ -349,17 +341,7 @@ async function calculateHashes( } } - // Progress and ETA - const elapsed = Date.now() - startTime; - const avgTime = elapsed / processed; - const remaining = totalFiles - processed; - const eta = Math.round((avgTime * remaining) / 1000); - - const etaStr = eta > 60 - ? `${Math.floor(eta / 60)}m ${eta % 60}s` - : `${eta}s`; - - process.stdout.write(`\ršŸ” Progress: ${processed}/${totalFiles} files | ETA: ${etaStr} `); + showProgress(processed, totalFiles, startTime, 'šŸ”'); } } else { // Local files - keep sequential for now @@ -374,25 +356,13 @@ async function calculateHashes( const data = JSON.parse(cached); if (data.hash) { file.hash = data.hash; - - // Progress and ETA - const processed = i + 1; - const elapsed = Date.now() - startTime; - const avgTime = elapsed / processed; - const remaining = totalFiles - processed; - const eta = Math.round((avgTime * remaining) / 1000); - - const etaStr = eta > 60 - ? `${Math.floor(eta / 60)}m ${eta % 60}s` - : `${eta}s`; - - process.stdout.write(`\ršŸ” Progress: ${processed}/${totalFiles} files | ETA: ${etaStr} (cached) `); + showProgress(i + 1, totalFiles, startTime, 'šŸ”'); continue; } } // Compute hash - file.hash = sha256Local(file.path); + file.hash = await sha256Local(file.path); // Update cache with hash await redis.setex(cacheKey, CACHE_TTL, JSON.stringify({ size: file.size, hash: file.hash })); @@ -402,18 +372,7 @@ async function calculateHashes( throw err; } - // Progress and ETA - const processed = i + 1; - const elapsed = Date.now() - startTime; - const avgTime = elapsed / processed; - const remaining = totalFiles - processed; - const eta = Math.round((avgTime * remaining) / 1000); - - const etaStr = eta > 60 - ? `${Math.floor(eta / 60)}m ${eta % 60}s` - : `${eta}s`; - - process.stdout.write(`\ršŸ” Progress: ${processed}/${totalFiles} files | ETA: ${etaStr} `); + showProgress(i + 1, totalFiles, startTime, 'šŸ”'); } } @@ -440,6 +399,22 @@ function findDuplicates(files: FileInfo[]): Record { return duplicates; } +async function checkDependencies(redis: Redis): Promise { + try { + await redis.ping(); + } catch (err) { + console.error('āŒ Redis is not available'); + process.exit(1); + } + + try { + execSync('adb version', { stdio: 'ignore' }); + } catch (err) { + console.error('āŒ ADB is not available'); + process.exit(1); + } +} + async function main() { const { localDir, androidDir } = parseArguments(); @@ -454,6 +429,8 @@ async function main() { }); try { + await checkDependencies(redis); + // Step 1: Collect file lists const localFiles = await getLocalFiles(localDir, redis); const androidFiles = await getAndroidFiles(androidDir, redis);