import * as fs from 'fs' import * as path from 'path' import { migrateConfigIfNeeded } from './config' // Possible lock file names const LOCK_FILE_NAMES = ['zzup.lock', 'zup.lock'] as const /** * Get the lock file path based on the active config file */ export function getLockPath(): string { const configPath = migrateConfigIfNeeded() const configDir = path.dirname(configPath) // Check for existing lock files const existingLockFiles = LOCK_FILE_NAMES.filter(name => fs.existsSync(path.join(configDir, name)) ) // If we have a zup.lock but no zzup.lock, migrate it if ( existingLockFiles.includes('zup.lock') && !existingLockFiles.includes('zzup.lock') ) { const oldPath = path.join(configDir, 'zup.lock') const newPath = path.join(configDir, 'zzup.lock') console.log('Migrating from zup.lock to zzup.lock...') fs.renameSync(oldPath, newPath) return newPath } // If we have both files, throw an error if (existingLockFiles.length > 1) { throw new Error( `Multiple lock files found: ${existingLockFiles.join(', ')}. Please use only one of them.` ) } // Return the path of the existing lock file or the new standard name return path.join(configDir, 'zzup.lock') } /** * Interface for lock file entry */ export interface LockEntry { imageName: string tag: string digest: string } /** * Interface for lock file */ export interface LockFile { version: string lastUpdated: string entries: Record } /** * Create a new lock file with default values * @returns A new lock file object */ export function createLockFile(): LockFile { return { version: '1.0', lastUpdated: new Date().toISOString(), entries: {}, } } /** * Load the lock file * @returns Lock file contents or a new lock file if it doesn't exist */ export function loadLockFile(): LockFile { try { const lockPath = getLockPath() // Ensure the config directory exists if (!fs.existsSync(path.dirname(lockPath))) { fs.mkdirSync(path.dirname(lockPath), { recursive: true }) } // Check if lock file exists if (!fs.existsSync(lockPath)) { return createLockFile() } // Load lock file const lockFileContent = fs.readFileSync(lockPath, 'utf8') const lockFile = JSON.parse(lockFileContent) as LockFile return lockFile } catch (error) { console.warn(`Error loading lock file: ${error}`) return createLockFile() } } /** * Save the lock file * @param lockFile Lock file to save */ export function saveLockFile(lockFile: LockFile): void { try { const lockPath = getLockPath() // Ensure the config directory exists if (!fs.existsSync(path.dirname(lockPath))) { fs.mkdirSync(path.dirname(lockPath), { recursive: true }) } // Update the lastUpdated timestamp lockFile.lastUpdated = new Date().toISOString() // Save lock file fs.writeFileSync(lockPath, JSON.stringify(lockFile, null, 2), 'utf8') } catch (error) { console.error(`Error saving lock file: ${error}`) } } /** * Generate a lock key from image name and tag * @param imageName Image name * @param tag Tag * @returns Lock key */ export function getLockKey(imageName: string, tag: string): string { return `${imageName}` } /** * Find an existing lock entry key for an image * This is useful when an image may be in the lock file under a different tag * @param imageName Image name * @returns The existing lock key or undefined if not found */ export function findExistingLockEntry( imageName: string ): { key: string; entry: LockEntry } | undefined { const lockFile = loadLockFile() // Search for entries with the matching image name for (const [key, entry] of Object.entries(lockFile.entries)) { if (entry.imageName === imageName) { return { key, entry } } } return undefined } /** * Check if an image needs updating by comparing with the lock file * @param imageName Image name * @param tag Tag * @param currentDigest Current digest * @returns Whether the image needs updating */ export function needsUpdate( imageName: string, tag: string, currentDigest: string ): boolean { // Load lock file const lockFile = loadLockFile() // Get lock key (image name + tag) const lockKey = getLockKey(imageName, tag) // Check if lock file has an entry for this image if (!lockFile.entries[lockKey]) { return true } // Check if digest is different return lockFile.entries[lockKey].digest !== currentDigest } /** * Update the lock file with a new digest * If the image already exists in the lock file (with any tag), update that entry * instead of creating a new one. * @param imageName Image name * @param tag Tag * @param digest Digest * @param updateExisting Whether to update an existing entry if found (default: true) */ export function updateLockEntry( imageName: string, tag: string, digest: string, updateExisting: boolean = true ): void { // Load lock file const lockFile = loadLockFile() // Generate lock key (image name + tag) const lockKey = getLockKey(imageName, tag) // Check if the entry already exists with the same digest const existingLockEntry = lockFile.entries[lockKey] if (existingLockEntry && existingLockEntry.digest === digest) { // No changes to save, skip update return } // If updateExisting is true, check for an existing entry to update if (updateExisting) { const existingEntry = findExistingLockEntry(imageName) if (existingEntry) { // Check if the digest is actually changing if (existingEntry.entry.digest === digest) { // No changes to save, skip update return } // Update the existing entry lockFile.entries[existingEntry.key] = { imageName, tag, // Update to the new tag digest, } // Save lock file and return saveLockFile(lockFile) return } } // If we didn't update an existing entry or updateExisting is false, // create a new entry lockFile.entries[lockKey] = { imageName, tag, digest, } // Save lock file saveLockFile(lockFile) } /** * Get the digest from the lock file * @param imageName Image name * @param tag Tag * @returns Digest or undefined if not found */ export function getLockedDigest( imageName: string, tag: string ): string | undefined { // Load lock file const lockFile = loadLockFile() // Get lock key (image name + tag) const lockKey = getLockKey(imageName, tag) // Check if lock file has an entry for this image if (!lockFile.entries[lockKey]) { return undefined } // Return digest return lockFile.entries[lockKey].digest }