/ scripts / delete-all-uncropped.js
delete-all-uncropped.js
  1  #!/usr/bin/env node
  2  
  3  /**
  4   * Delete All Uncropped Screenshots
  5   *
  6   * Deletes all uncropped screenshot versions to reclaim disk space.
  7   * The assets stage will recapture these sites with the new thresholds,
  8   * only storing uncropped versions when they differ significantly (> 5KB AND > 1%).
  9   */
 10  
 11  import { readdir, stat, unlink } from 'fs/promises';
 12  import { join, dirname } from 'path';
 13  import { fileURLToPath } from 'url';
 14  
 15  const __filename = fileURLToPath(import.meta.url);
 16  const __dirname = dirname(__filename);
 17  
 18  const SCREENSHOTS_DIR = process.env.SCREENSHOT_BASE_PATH || join(__dirname, '..', 'screenshots');
 19  
 20  // Parse command line args
 21  const dryRun = process.argv.includes('--dry-run');
 22  
 23  // Uncropped screenshots to delete
 24  const UNCROPPED_FILES = [
 25    'desktop_above_uncropped.jpg',
 26    'desktop_below_uncropped.jpg',
 27    'mobile_above_uncropped.jpg',
 28  ];
 29  
 30  /**
 31   * Format bytes to human readable format
 32   */
 33  function formatBytes(bytes) {
 34    if (bytes === 0) return '0 Bytes';
 35    const k = 1024;
 36    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
 37    const i = Math.floor(Math.log(bytes) / Math.log(k));
 38    return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;
 39  }
 40  
 41  /**
 42   * Get file size in bytes
 43   */
 44  async function getFileSize(filePath) {
 45    try {
 46      const stats = await stat(filePath);
 47      return stats.size;
 48    } catch {
 49      return 0;
 50    }
 51  }
 52  
 53  async function main() {
 54    console.log('šŸ” Scanning for uncropped screenshots to delete...\n');
 55  
 56    // Get all screenshot folders
 57    let screenshotFolders;
 58    try {
 59      screenshotFolders = await readdir(SCREENSHOTS_DIR);
 60    } catch (error) {
 61      console.error('āŒ Error reading screenshots directory:', error.message);
 62      process.exit(1);
 63    }
 64  
 65    console.log(`Found ${screenshotFolders.length} screenshot folders\n`);
 66  
 67    const stats = {
 68      foldersChecked: 0,
 69      filesFound: 0,
 70      spaceToFree: 0,
 71      filesToDelete: [],
 72    };
 73  
 74    // Check each folder
 75    for (const folder of screenshotFolders) {
 76      const folderPath = join(SCREENSHOTS_DIR, folder);
 77  
 78      // Skip if not a directory
 79      try {
 80        const folderStats = await stat(folderPath);
 81        if (!folderStats.isDirectory()) {
 82          continue;
 83        }
 84      } catch {
 85        continue;
 86      }
 87  
 88      stats.foldersChecked++;
 89  
 90      // Check for each uncropped file
 91      for (const filename of UNCROPPED_FILES) {
 92        const filePath = join(folderPath, filename);
 93  
 94        try {
 95          await stat(filePath);
 96          // File exists
 97          const fileSize = await getFileSize(filePath);
 98          stats.filesFound++;
 99          stats.spaceToFree += fileSize;
100          stats.filesToDelete.push({
101            path: filePath,
102            size: fileSize,
103            folder,
104            filename,
105          });
106        } catch {
107          // File doesn't exist - skip
108        }
109      }
110  
111      // Progress indicator every 100 folders
112      if (stats.foldersChecked % 100 === 0) {
113        console.log(`  Checked ${stats.foldersChecked}/${screenshotFolders.length} folders...`);
114      }
115    }
116  
117    console.log('\nšŸ“Š Scan Results:');
118    console.log(`   Folders checked: ${stats.foldersChecked}`);
119    console.log(`   Uncropped files found: ${stats.filesFound}`);
120    console.log(`\nšŸ’¾ Space to be freed: ${formatBytes(stats.spaceToFree)}`);
121  
122    if (stats.filesFound === 0) {
123      console.log('\nāœ… No uncropped screenshots found!');
124      return;
125    }
126  
127    if (dryRun) {
128      console.log('\nšŸ” DRY RUN MODE - No files will be deleted\n');
129      console.log('Sample files that would be deleted:');
130      stats.filesToDelete.slice(0, 10).forEach(file => {
131        console.log(`  - ${file.folder}/${file.filename} (${formatBytes(file.size)})`);
132      });
133      if (stats.filesToDelete.length > 10) {
134        console.log(`  ... and ${stats.filesToDelete.length - 10} more`);
135      }
136      console.log('\nRun without --dry-run to actually delete these files');
137    } else {
138      console.log('\nšŸ—‘ļø  Deleting uncropped screenshots...');
139  
140      let deleted = 0;
141      let deleteErrors = 0;
142  
143      for (const file of stats.filesToDelete) {
144        try {
145          await unlink(file.path);
146          deleted++;
147          if (deleted % 100 === 0) {
148            console.log(`  Deleted ${deleted}/${stats.filesToDelete.length} files...`);
149          }
150        } catch (error) {
151          deleteErrors++;
152          console.error(`  āŒ Error deleting ${file.path}: ${error.message}`);
153        }
154      }
155  
156      console.log('\nāœ… Cleanup complete!');
157      console.log(`   Files deleted: ${deleted}`);
158      console.log(`   Errors: ${deleteErrors}`);
159      console.log(`   Space freed: ${formatBytes(stats.spaceToFree)}`);
160      console.log(
161        '\nšŸ’” Run "npm run assets" to recapture sites with missing screenshots (uncropped versions will only be saved if they differ significantly)'
162      );
163    }
164  }
165  
166  main().catch(error => {
167    console.error('āŒ Fatal error:', error);
168    process.exit(1);
169  });