Pārlūkot izejas kodu

feat: add case-insensitive directory matching for output paths

Prevents duplicate directories with different casing (e.g., Test1, TEST1, test1).
When creating output directories, checks for existing directories with
different case and uses the existing path instead of creating duplicates.

Benefits:
- Prevents fragmentation of output files across case variants
- Works correctly on both case-sensitive and case-insensitive filesystems
- Maintains consistency across macOS (case-insensitive) and Linux (case-sensitive)
- Logs when case mismatches are found for debugging

Implementation:
- findExistingDirCaseInsensitive: Searches parent dir for case-insensitive match
- ensureOutputDirectory: Builds path incrementally, checking each component
- actualOutput: Uses the discovered/created path for HandBrake processing
Timothy Pomeroy 1 mēnesi atpakaļ
vecāks
revīzija
072edab325
1 mainītis faili ar 94 papildinājumiem un 10 dzēšanām
  1. 94 10
      apps/service/src/handbrake.service.ts

+ 94 - 10
apps/service/src/handbrake.service.ts

@@ -1,6 +1,6 @@
 import { Injectable, Logger } from '@nestjs/common';
 import { spawn } from 'child_process';
-import { mkdirSync } from 'fs';
+import { existsSync, mkdirSync, readdirSync } from 'fs';
 import path from 'path';
 import { DbService } from './db.service';
 import { EventsGateway } from './events.gateway';
@@ -14,6 +14,82 @@ export class HandbrakeService {
     private readonly db: DbService,
   ) {}
 
+  /**
+   * Find existing directory with case-insensitive matching
+   * Returns the actual path if found, null otherwise
+   */
+  private findExistingDirCaseInsensitive(dirPath: string): string | null {
+    const parentDir = path.dirname(dirPath);
+    const targetName = path.basename(dirPath);
+
+    // If parent doesn't exist, can't find anything
+    if (!existsSync(parentDir)) {
+      return null;
+    }
+
+    try {
+      const entries = readdirSync(parentDir, { withFileTypes: true });
+      const match = entries.find(
+        (entry) =>
+          entry.isDirectory() &&
+          entry.name.toLowerCase() === targetName.toLowerCase(),
+      );
+
+      return match ? path.join(parentDir, match.name) : null;
+    } catch (err) {
+      this.logger.warn(
+        `Error checking for case-insensitive directory: ${dirPath}`,
+        err,
+      );
+      return null;
+    }
+  }
+
+  /**
+   * Ensure output directory exists, handling case-insensitive matches
+   * Returns the actual directory path to use
+   */
+  private ensureOutputDirectory(outputPath: string): string {
+    const parts = outputPath.split(path.sep);
+    let currentPath = '';
+
+    // Build path incrementally, checking for case-insensitive matches
+    for (let i = 0; i < parts.length; i++) {
+      const part = parts[i];
+      if (!part) continue; // Skip empty parts (e.g., leading slash)
+
+      const proposedPath = currentPath ? path.join(currentPath, part) : part;
+
+      // Check if directory exists (exact case)
+      if (existsSync(proposedPath)) {
+        currentPath = proposedPath;
+        continue;
+      }
+
+      // Check for case-insensitive match
+      const existingPath = this.findExistingDirCaseInsensitive(proposedPath);
+      if (existingPath) {
+        this.logger.log(
+          `Found existing directory with different case: ${existingPath} (wanted: ${proposedPath})`,
+        );
+        currentPath = existingPath;
+      } else {
+        // Directory doesn't exist, create it
+        try {
+          mkdirSync(proposedPath, { recursive: false });
+          this.logger.log(`Created directory: ${proposedPath}`);
+          currentPath = proposedPath;
+        } catch (err) {
+          throw new Error(
+            `Failed to create directory ${proposedPath}: ${err.message}`,
+          );
+        }
+      }
+    }
+
+    return currentPath;
+  }
+
   processWithHandbrake(
     input: string,
     output: string,
@@ -22,23 +98,31 @@ export class HandbrakeService {
   ): Promise<boolean> {
     return new Promise((resolve, reject) => {
       try {
-        // Ensure output directory exists
+        // Ensure output directory exists, handling case-insensitive matches
         const outputDir = path.dirname(output);
+        let actualOutputDir: string;
+        
         try {
-          mkdirSync(outputDir, { recursive: true });
-          this.logger.log(`Ensured output directory exists: ${outputDir}`);
+          actualOutputDir = this.ensureOutputDirectory(outputDir);
+          this.logger.log(
+            `Output directory ready: ${actualOutputDir}`,
+          );
         } catch (err) {
           this.logger.error(
-            `Failed to create output directory: ${outputDir}`,
+            `Failed to prepare output directory: ${outputDir}`,
             err,
           );
-          return reject(
-            new Error(`Cannot create output directory: ${outputDir}`),
-          );
+          return reject(err);
         }
 
+        // Update output path to use actual directory (in case of case mismatch)
+        const actualOutput = path.join(actualOutputDir, path.basename(output));
+
+        // Update output path to use actual directory (in case of case mismatch)
+        const actualOutput = path.join(actualOutputDir, path.basename(output));
+
         const inputName = path.basename(input);
-        const outputName = path.basename(output);
+        const outputName = path.basename(actualOutput);
         let progressStarted = false;
         let lastPercent = 0;
 
@@ -46,7 +130,7 @@ export class HandbrakeService {
           '-i',
           input,
           '-o',
-          output,
+          actualOutput,
           '--preset',
           preset,
         ]);