UNPKG

@shutootaki/gwm

Version:
278 lines 10.6 kB
import { execSync } from 'child_process'; import { existsSync, readdirSync, statSync } from 'fs'; import { copyFile, mkdir, lstat, readlink, symlink, realpath, } from 'fs/promises'; import { cpus } from 'os'; import { join, dirname, relative, resolve } from 'path'; import { loadConfig } from '../../config.js'; import { escapeShellArg } from '../shell.js'; import { isVirtualEnv } from '../virtualenv.js'; /** * ファイル名がパターンにマッチするかチェック * 簡易的なワイルドカードマッチング */ function matchesPattern(file, pattern) { // 簡易的なワイルドカードマッチング // * を任意の文字列に変換 const regexPattern = pattern .split('*') .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) .join('.*'); const regex = new RegExp(`^${regexPattern}$`); return regex.test(file); } /** * ディレクトリをスキャンしてファイルを探索 */ function scanDirectory(dir, baseDir, patterns, excludePatterns, skipVirtualEnvs, matchedFiles) { try { const entries = readdirSync(dir); for (const entry of entries) { const fullPath = join(dir, entry); const relativePath = relative(baseDir, fullPath); // .gitディレクトリはスキップ if (entry === '.git') continue; // 除外パターンのチェック if (excludePatterns && excludePatterns.some((p) => matchesPattern(entry, p) || matchesPattern(relativePath, p))) { continue; } // 仮想環境ディレクトリの除外 if (skipVirtualEnvs && isVirtualEnv(relativePath)) { continue; } try { const stat = statSync(fullPath); if (stat.isDirectory()) { // ディレクトリの場合は再帰的に検索 scanDirectory(fullPath, baseDir, patterns, excludePatterns, skipVirtualEnvs, matchedFiles); } else if (stat.isFile()) { // ファイルの場合はパターンマッチング const shouldInclude = patterns.some((pattern) => matchesPattern(entry, pattern) || matchesPattern(relativePath, pattern)); if (shouldInclude) { // gitで追跡されていないファイルのみを対象とする try { execSync(`git ls-files --error-unmatch ${escapeShellArg(relativePath)}`, { cwd: baseDir, stdio: 'ignore', }); // ファイルが追跡されている場合はスキップ } catch { // ファイルが追跡されていない場合は含める matchedFiles.push(relativePath); } } } } catch { // ファイルアクセスエラーは無視 } } } catch { // ディレクトリスキャンエラーは無視 } } /** * gitignoreされたファイルのリストを取得 * @param workdir 検索対象のディレクトリ * @param patterns 検索パターン(ワイルドカード対応) * @param excludePatterns 除外パターン * @param skipVirtualEnvs 仮想環境ディレクトリをスキップするかどうか */ export function getIgnoredFiles(workdir, patterns, excludePatterns, skipVirtualEnvs = true) { const matchedFiles = []; scanDirectory(workdir, workdir, patterns, excludePatterns, skipVirtualEnvs, matchedFiles); return matchedFiles; } /** * ファイルサイズ制限をチェック */ function checkFileSizeLimit(file, size, maxFileSizeBytes) { return maxFileSizeBytes !== undefined && size > maxFileSizeBytes; } /** * ディレクトリサイズ制限をチェック */ function checkDirectorySizeLimit(file, size, maxDirSizeBytes, dirSizeMap) { if (maxDirSizeBytes === undefined) return false; const dirRel = dirname(file) || '.'; let cursor = dirRel; while (true) { const current = dirSizeMap.get(cursor) ?? 0; if (current + size > maxDirSizeBytes) { return true; } if (cursor === '.') break; const parent = dirname(cursor); if (parent === cursor) break; cursor = parent; } return false; } /** * ディレクトリサイズを更新 */ function updateDirectorySize(file, size, dirSizeMap) { const dirRel = dirname(file) || '.'; let cursor = dirRel; while (true) { const current = dirSizeMap.get(cursor) ?? 0; dirSizeMap.set(cursor, current + size); if (cursor === '.') break; const parent = dirname(cursor); if (parent === cursor) break; cursor = parent; } } /** * シンボリックリンクを処理 */ async function processSymbolicLink(sourcePath, targetPath, sourceDir, targetDir, isIsolationEnabled) { const linkTarget = await readlink(sourcePath); const absoluteLinkTarget = resolve(dirname(sourcePath), linkTarget); let rewrittenTarget = absoluteLinkTarget; if (isIsolationEnabled) { try { const realSourceDir = await realpath(sourceDir); const targetReal = await realpath(absoluteLinkTarget); if (targetReal.startsWith(realSourceDir)) { const relFromSource = relative(realSourceDir, targetReal); rewrittenTarget = join(targetDir, relFromSource); } } catch { /* ignore */ } } let linkSrc = rewrittenTarget; if (rewrittenTarget !== absoluteLinkTarget) { linkSrc = relative(dirname(targetPath), rewrittenTarget); } await symlink(linkSrc, targetPath); } /** * 個別ファイルを処理 */ async function processFile(file, sourceDir, targetDir, isIsolationEnabled, maxFileSizeBytes, maxDirSizeBytes, dirSizeMap, skippedVirtualEnvSet, skippedOversize) { try { const sourcePath = join(sourceDir, file); const targetPath = join(targetDir, file); if (!existsSync(sourcePath)) { return null; } if (isIsolationEnabled && isVirtualEnv(file)) { skippedVirtualEnvSet.add(file); return null; } const lst = await lstat(sourcePath); if (!lst.isSymbolicLink() && checkFileSizeLimit(file, lst.size, maxFileSizeBytes)) { skippedOversize.push(file); return null; } if (!lst.isSymbolicLink() && checkDirectorySizeLimit(file, lst.size, maxDirSizeBytes, dirSizeMap)) { skippedOversize.push(file); return null; } if (!lst.isSymbolicLink() && maxDirSizeBytes !== undefined) { updateDirectorySize(file, lst.size, dirSizeMap); } // ディレクトリ作成 const targetDirName = dirname(targetPath); if (!existsSync(targetDirName)) { await mkdir(targetDirName, { recursive: true }); } if (lst.isSymbolicLink()) { await processSymbolicLink(sourcePath, targetPath, sourceDir, targetDir, isIsolationEnabled); } else { await copyFile(sourcePath, targetPath); } return file; } catch { return null; } } /** * ファイルを別のディレクトリにコピー * シンボリックリンクを適切に処理し、仮想環境は除外 */ export async function copyFiles(sourceDir, targetDir, files) { const copiedFiles = []; const skippedVirtualEnvSet = new Set(); const skippedOversize = []; const { virtual_env_handling } = loadConfig(); const isIsolationEnabled = (() => { if (!virtual_env_handling) return false; if (typeof virtual_env_handling.isolate_virtual_envs === 'boolean') { return virtual_env_handling.isolate_virtual_envs; } return virtual_env_handling.mode === 'skip'; })(); // サイズ上限の設定 const maxFileSizeBytes = virtual_env_handling?.max_file_size_mb !== undefined ? virtual_env_handling.max_file_size_mb >= 0 ? virtual_env_handling.max_file_size_mb * 1024 * 1024 : undefined : virtual_env_handling?.max_copy_size_mb && virtual_env_handling.max_copy_size_mb > 0 ? virtual_env_handling.max_copy_size_mb * 1024 * 1024 : undefined; const maxDirSizeBytes = virtual_env_handling?.max_dir_size_mb !== undefined ? virtual_env_handling.max_dir_size_mb >= 0 ? virtual_env_handling.max_dir_size_mb * 1024 * 1024 : undefined : undefined; const dirSizeMap = new Map(); // 並列度 const parallelism = virtual_env_handling?.copy_parallelism !== undefined ? virtual_env_handling.copy_parallelism === 0 ? cpus().length : virtual_env_handling.copy_parallelism : 4; // 並列実行制御用のタスク配列 const tasks = files.map((file) => () => processFile(file, sourceDir, targetDir, isIsolationEnabled, maxFileSizeBytes, maxDirSizeBytes, dirSizeMap, skippedVirtualEnvSet, skippedOversize)); // 並列実行制御 const results = []; const executing = []; for (let i = 0; i < tasks.length; i++) { const task = tasks[i]; const promise = task().then((result) => { results[i] = result; const index = executing.indexOf(promise); if (index >= 0) { executing.splice(index, 1); } }); executing.push(promise); if (executing.length >= parallelism) { await Promise.race(executing); } } // 残りのタスクの完了を待機 await Promise.all(executing); // 成功したファイルのみを結果に含める(元の順序を保持) for (let i = 0; i < results.length; i++) { if (results[i] !== null) { copiedFiles.push(results[i]); } } return { copied: copiedFiles, skippedVirtualEnvs: [...skippedVirtualEnvSet], skippedOversize, }; } //# sourceMappingURL=copy.js.map