All files / src/utils/fs directory.ts

95.23% Statements 20/21
87.5% Branches 7/8
100% Functions 3/3
95.23% Lines 20/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81                                        39x 4x                     110x 110x 110x 110x   103x 103x                                               10x 2x     8x 8x 8x 110x 110x 7x   103x     1x 1x 1x    
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module Utils/Fs/Directory
 * @description Directory creation, atomic claim, and unique-suffix
 * resolution used by the analysis pipeline to coordinate concurrent
 * same-day runs.
 */
 
import { randomUUID } from 'crypto';
import fs from 'fs';
import path from 'path';
 
/**
 * Ensure a directory exists, creating it recursively if needed
 *
 * @param dirPath - Directory path to ensure
 */
export function ensureDirectoryExists(dirPath: string): void {
  if (!fs.existsSync(dirPath)) {
    fs.mkdirSync(dirPath, { recursive: true });
  }
}
 
/**
 * Attempt to atomically claim a directory by creating it non-recursively.
 *
 * @param dirPath - Directory path to claim
 * @returns `true` when the directory was created by this call, otherwise `false`
 */
function claimDir(dirPath: string): boolean {
  fs.mkdirSync(path.dirname(dirPath), { recursive: true });
  try {
    fs.mkdirSync(dirPath, { recursive: false });
    return true;
  } catch (err: unknown) {
    Eif ((err as NodeJS.ErrnoException).code === 'EEXIST') {
      return false;
    }
    throw err;
  }
}
 
/**
 * Resolve a unique directory path by appending a numeric suffix (-2, -3, …)
 * when the preferred directory has already been claimed by a completed run.
 *
 * The base directory is treated as occupied when it contains `manifest.json`
 * (written at the end of a successful analysis run).  A directory without
 * `manifest.json` is considered available — this allows the `skipCompleted`
 * feature to resume an incomplete run in the same directory.
 *
 * Suffixed candidates (-2, -3, …) are claimed atomically via non-recursive
 * `mkdirSync`, preventing TOCTOU races when concurrent workflow runs
 * attempt to claim the same candidate.
 *
 * @param baseDir - The preferred directory path (e.g. `analysis/daily/2026-04-02/breaking`)
 * @returns The original `baseDir` when no completed run exists there, or a
 *          suffixed variant (e.g. `analysis/daily/2026-04-02/breaking-2`) otherwise.
 */
export function resolveUniqueAnalysisDir(baseDir: string): string {
  if (!fs.existsSync(path.join(baseDir, 'manifest.json'))) {
    return baseDir;
  }
 
  let suffix = 2;
  const MAX_SUFFIX = 100;
  while (suffix <= MAX_SUFFIX) {
    const candidate = `${baseDir}-${suffix}`;
    if (claimDir(candidate)) {
      return candidate;
    }
    suffix++;
  }
 
  const candidate = `${baseDir}-${randomUUID().slice(0, 8)}`;
  fs.mkdirSync(candidate, { recursive: true });
  return candidate;
}