UNPKG

@atomist/automation-client

Version:

Atomist API for software low-level client

154 lines (135 loc) 4.99 kB
import * as fs from "fs-extra"; import * as path from "path"; import { registerShutdownHook } from "../../internal/util/shutdown"; import { logger } from "../../util/logger"; import { CloneDirectoryInfo, CloneOptions, DirectoryManager, } from "./DirectoryManager"; export interface StableDirectoryManagerOpts { /** * Attempt to reuse directories? */ reuseDirectories: boolean; /** * Clean up directories on exit? */ cleanOnExit?: boolean; /** * If provided, number of directories */ baseDir: string; } const DefaultOpts = { reuseDirectories: false, cleanOnExit: true, }; /** * Build a stable working directory structure. * Options determine whether it's cleared on exit. */ export class StableDirectoryManager implements DirectoryManager { public opts: StableDirectoryManagerOpts; private directoriesUsed: number = 0; constructor(pOpts: StableDirectoryManagerOpts) { this.opts = { ...DefaultOpts, ...pOpts, }; if (this.opts.cleanOnExit === true) { registerShutdownHook(() => { logger.debug("Cleaning up directories under '%s'", this.opts.baseDir); return fs.remove(this.opts.baseDir).then(() => 0); }, 3000, `directory cleanup: ${this.opts.baseDir}`); } } public invalidate(existing: CloneDirectoryInfo): Promise<void> { return fs.remove(existing.path) .then(() => { logger.debug("Deleted " + existing.path); }, err => { if (err.code === "ENOENT") { logger.debug("Cleanup: deleting %s, but it's already gone", existing.path); return; } else { logger.error("Unexpected error deleting directory %s: %s", existing.path, err); throw err; } }); } public directoryFor(owner: string, repo: string, branch: string, opts: CloneOptions): Promise<CloneDirectoryInfo> { if (this.opts.reuseDirectories) { // Attempt to reuse the directory return this.existingDirectoryFor(owner, repo, branch, opts) .then(existing => !!existing ? existing : this.freshDirectoryFor(owner, repo, branch, opts)); } else { return this.freshDirectoryFor(owner, repo, branch, opts); } } /** * Return undefined if not found */ private existingDirectoryFor(owner: string, repo: string, branch: string, opts: CloneOptions): Promise<CloneDirectoryInfo> { const expectedPath = this.pathFor(owner, repo); return fs.pathExists(expectedPath) .then(exists => { if (exists) { logger.debug("%s directories used: Reusing path '%s'", this.directoriesUsed, expectedPath); return { path: expectedPath, type: "existing-directory" as any, release: () => Promise.resolve(), invalidate: () => Promise.resolve(), transient: false, }; } else { // It doesn't exist return undefined; } }); } private freshDirectoryFor( user: string, repo: string, branch: string, opts: CloneOptions, ): Promise<CloneDirectoryInfo> { return this.createFreshDir(user, repo) .then((cdi): CloneDirectoryInfo => { this.directoriesUsed++; logger.debug("%s directories used: Returning new path '%s'", this.directoriesUsed, cdi); return { path: cdi, type: "empty-directory", release: () => Promise.resolve(), invalidate: () => Promise.resolve(), transient: false, }; }); } private pathFor(owner: string, repo: string): string { return path.join(this.opts.baseDir, "repos", "github.com", owner, repo); } private createFreshDir(owner: string, repo: string): Promise<string> { const repoDir = this.pathFor(owner, repo); return fs.ensureDir(repoDir) .then(() => assureDirectoryIsEmpty(repoDir)) .then(() => repoDir); } } function assureDirectoryIsEmpty(name: string): Promise<void> { return fs.readdir(name).then(files => { if (files.length > 0) { return fs.remove(name) .then(() => fs.ensureDir(name)) .catch(err => { throw new Error("I tried to make this directory be empty: " + name + " but it didn't work: " + err.message); }); } }); }