@atomist/automation-client
Version:
Atomist API for software low-level client
154 lines (135 loc) • 4.99 kB
text/typescript
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);
});
}
});
}