import stream from 'node:stream'; import util from 'node:util'; import { isNonEmptyString } from '@sindresorhus/is'; import { findUp } from 'find-up'; import fs from 'fs-extra'; import upath from 'upath'; import { GlobalConfig } from '../../config/global.ts'; import { logger } from '../../logger/index.ts'; import { logWarningIfUnicodeHiddenCharactersInPackageFile } from '../unicode.ts'; import { ensureCachePath, ensureLocalPath, isValidPath } from './util.ts'; export const pipeline = util.promisify(stream.pipeline); export function getParentDir(fileName: string): string { return upath.parse(fileName).dir; } export function getSiblingFileName( fileName: string, siblingName: string, ): string { const subDirectory = getParentDir(fileName); return upath.join(subDirectory, siblingName); } export async function readLocalFile(fileName: string): Promise; export async function readLocalFile( fileName: string, encoding: 'utf8', ): Promise; export async function readLocalFile( fileName: string, encoding?: BufferEncoding, ): Promise { const localFileName = ensureLocalPath(fileName); try { const fileContent = encoding ? await fs.readFile(localFileName, encoding) : await fs.readFile(localFileName); logWarningIfUnicodeHiddenCharactersInPackageFile(fileName, fileContent); return fileContent; } catch (err) { logger.trace({ err }, 'Error reading local file'); return null; } } export async function readLocalSymlink( fileName: string, ): Promise { const localFileName = ensureLocalPath(fileName); try { const linkContent = await fs.readlink(localFileName); return linkContent; } catch (err) { logger.trace({ err }, 'Error reading local symlink'); return null; } } export async function writeLocalFile( fileName: string, fileContent: string | Buffer, ): Promise { const localFileName = ensureLocalPath(fileName); await fs.outputFile(localFileName, fileContent); } export async function deleteLocalFile(fileName: string): Promise { // This a failsafe and hopefully will never be triggered if (GlobalConfig.get('platform') === 'local') { throw new Error('Cannot delete file when platform=local'); } const localDir = GlobalConfig.get('localDir'); if (localDir) { const localFileName = ensureLocalPath(fileName); await fs.remove(localFileName); } } export async function renameLocalFile( fromFile: string, toFile: string, ): Promise { const fromPath = ensureLocalPath(fromFile); const toPath = ensureLocalPath(toFile); await fs.move(fromPath, toPath); } export async function renameCacheFile( fromFile: string, toFile: string, ): Promise { const fromPath = ensureCachePath(fromFile); const toPath = ensureCachePath(toFile); await fs.rename(fromPath, toPath); } export async function ensureDir(dirName: string): Promise { // v8 ignore else -- TODO: add test #40625 if (isNonEmptyString(dirName)) { await fs.ensureDir(dirName); } } export async function ensureLocalDir(dirName: string): Promise { const fullPath = ensureLocalPath(dirName); await fs.ensureDir(fullPath); return fullPath; } export async function ensureCacheDir(name: string): Promise { const cacheDirName = ensureCachePath(`others/${name}`); await fs.ensureDir(cacheDirName); return cacheDirName; } /** * Return the path of the private cache directory. This directory is wiped * between repositories, so they can be used to store private registries' index * without risk of that information leaking to other repositories/users. */ export function privateCacheDir(): string { // TODO: types (#22198) const cacheDir = GlobalConfig.get('cacheDir'); return upath.join(cacheDir, '__renovate-private-cache'); } export async function localPathExists(pathName: string): Promise { // Works for both files as well as directories const path = ensureLocalPath(pathName); try { const s = await fs.stat(path); return !!s; } catch { return false; } } /** * Validate local path without throwing. * @param path Path to check * @returns `true` if given `path` is a valid local path, otherwise `false`. */ export function isValidLocalPath(path: string): boolean { return isValidPath(path, 'localDir'); } /** * Tries to find `otherFileName` in the directory where * `existingFileNameWithPath` is, then in its parent directory, then in the * grandparent, until we reach the top-level directory. All paths * must be relative to `localDir`. */ export async function findLocalSiblingOrParent( existingFileNameWithPath: string, otherFileName: string, ): Promise { if (upath.isAbsolute(existingFileNameWithPath)) { return null; } if (upath.isAbsolute(otherFileName)) { return null; } let current = existingFileNameWithPath; while (current !== '') { current = getParentDir(current); const candidate = upath.join(current, otherFileName); if (await localPathExists(candidate)) { return candidate; } } return null; } /** * Get files by name from directory */ export async function readLocalDirectory(path: string): Promise { const localPath = ensureLocalPath(path); const fileList = await fs.readdir(localPath); return fileList; } export function createCacheWriteStream(path: string): fs.WriteStream { const fullPath = ensureCachePath(path); return fs.createWriteStream(fullPath); } export function createCacheReadStream(path: string): fs.ReadStream { const fullPath = ensureCachePath(path); return fs.createReadStream(fullPath); } export async function localPathIsFile(pathName: string): Promise { const path = ensureLocalPath(pathName); try { const s = await fs.stat(path); return s.isFile(); } catch { return false; } } export async function localPathIsSymbolicLink( pathName: string, ): Promise { const path = ensureLocalPath(pathName); try { const s = await fs.lstat(path); return s.isSymbolicLink(); } catch { return false; } } /** * Find a file or directory by walking up parent directories within localDir */ export async function findUpLocal( fileName: string | string[], cwd: string, ): Promise { // TODO: types (#22198) const localDir = GlobalConfig.get('localDir'); const absoluteCwd = upath.join(localDir, cwd); const normalizedAbsoluteCwd = upath.normalizeSafe(absoluteCwd); const res = await findUp(fileName, { cwd: normalizedAbsoluteCwd, type: 'file', }); // Return null if nothing found if (!isNonEmptyString(res) || !isNonEmptyString(localDir)) { return null; } const safePath = upath.normalizeSafe(res); // Return relative path if file is inside of local dir if (safePath.startsWith(localDir)) { let relativePath = safePath.replace(localDir, ''); // v8 ignore else -- TODO: add test #40625 if (relativePath.startsWith('/')) { relativePath = relativePath.substring(1); } return relativePath; } // Return null if found file is outside of localDir return null; } export function chmodLocalFile( fileName: string, mode: string | number, ): Promise { const fullFileName = ensureLocalPath(fileName); return fs.chmod(fullFileName, mode); } export async function statLocalFile( fileName: string, ): Promise { const fullFileName = ensureLocalPath(fileName); try { return await fs.stat(fullFileName); } catch { return null; } } export async function statCacheFile( pathName: string, ): Promise { const path = ensureCachePath(pathName); try { return await fs.stat(path); } catch { return null; } } export function listCacheDir( path: string, options: { recursive: boolean } = { recursive: false }, ): Promise { const fullPath = ensureCachePath(path); return fs.readdir(fullPath, { encoding: 'utf-8', recursive: options.recursive, }); } export async function rmCache(path: string): Promise { const fullPath = ensureCachePath(path); // force: a missing path is a no-op, callers use this for cleanup await fs.rm(fullPath, { recursive: true, force: true }); } export async function cachePathExists(pathName: string): Promise { const path = ensureCachePath(pathName); try { const s = await fs.stat(path); return !!s; } catch { return false; } } export async function cachePathIsFile(pathName: string): Promise { const path = ensureCachePath(pathName); try { const s = await fs.stat(path); return s.isFile(); } catch { return false; } } export async function readCacheFile(fileName: string): Promise; export async function readCacheFile( fileName: string, encoding: 'utf8', ): Promise; export function readCacheFile( fileName: string, encoding?: BufferEncoding, ): Promise { const fullPath = ensureCachePath(fileName); return encoding ? fs.readFile(fullPath, encoding) : fs.readFile(fullPath); } export function outputCacheFile( file: string, data: string | NodeJS.ArrayBufferView, ): Promise { const filePath = ensureCachePath(file); return fs.outputFile(filePath, data); } export async function readSystemFile(fileName: string): Promise; export async function readSystemFile( fileName: string, encoding: 'utf8', ): Promise; export function readSystemFile( fileName: string, encoding?: BufferEncoding, ): Promise { return encoding ? fs.readFile(fileName, encoding) : fs.readFile(fileName); } export async function writeSystemFile( fileName: string, data: string | Buffer, ): Promise { await fs.outputFile(fileName, data); } export async function getLocalFiles( fileNames: string[], ): Promise> { const fileContentMap: Record = {}; for (const fileName of fileNames) { fileContentMap[fileName] = await readLocalFile(fileName, 'utf8'); } return fileContentMap; }