UNPKG

5.61 kBPlain TextView Raw
1/*
2 * Copyright © 2019 Atomist, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {
18 GitProject,
19 guid,
20 logger,
21} from "@atomist/automation-client";
22import { resolvePlaceholders } from "@atomist/automation-client/lib/configuration";
23import {
24 GoalInvocation,
25 spawnLog,
26} from "@atomist/sdm";
27import * as fs from "fs-extra";
28import * as os from "os";
29import * as path from "path";
30import { resolvePlaceholder } from "../../machine/yaml/resolvePlaceholder";
31import {
32 FileSystemGoalCacheArchiveStore,
33} from "./FileSystemGoalCacheArchiveStore";
34import { GoalCache } from "./goalCaching";
35
36export interface GoalCacheArchiveStore {
37 /**
38 * Store a compressed goal archive
39 * @param gi The goal invocation thar triggered the caching
40 * @param classifier The classifier of the cache
41 * @param archivePath The path of the archive to be stored.
42 */
43 store(gi: GoalInvocation, classifier: string, archivePath: string): Promise<void>;
44 /**
45 * Remove a compressed goal archive
46 * @param gi The goal invocation thar triggered the cache removal
47 * @param classifier The classifier of the cache
48 */
49 delete(gi: GoalInvocation, classifier: string): Promise<void>;
50 /**
51 * Retrieve a compressed goal archive
52 * @param gi The goal invocation thar triggered the cache retrieval
53 * @param classifier The classifier of the cache
54 * @param targetArchivePath The destination path where the archive needs to be stored.
55 */
56 retrieve(gi: GoalInvocation, classifier: string, targetArchivePath: string): Promise<void>;
57}
58
59/**
60 * Cache implementation that caches files produced by goals to an archive that can then be stored,
61 * using tar and gzip to create the archives per goal invocation (and classifier if present).
62 */
63export class CompressingGoalCache implements GoalCache {
64 private readonly store: GoalCacheArchiveStore;
65
66 public constructor(store: GoalCacheArchiveStore = new FileSystemGoalCacheArchiveStore()) {
67 this.store = store;
68 }
69
70 public async put(gi: GoalInvocation, project: GitProject, files: string[], classifier?: string): Promise<void> {
71 const archiveName = "atomist-cache";
72 const teamArchiveFileName = path.join(os.tmpdir(), `${archiveName}.${guid().slice(0, 7)}`);
73 const slug = `${gi.id.owner}/${gi.id.repo}`;
74
75 const tarResult = await spawnLog("tar", ["-cf", teamArchiveFileName, ...files], {
76 log: gi.progressLog,
77 cwd: project.baseDir,
78 });
79 if (tarResult.code) {
80 const message = `Failed to create tar archive '${teamArchiveFileName}' for ${slug}`;
81 logger.error(message);
82 gi.progressLog.write(message);
83 return;
84 }
85 const gzipResult = await spawnLog("gzip", ["-3", teamArchiveFileName], {
86 log: gi.progressLog,
87 cwd: project.baseDir,
88 });
89 if (gzipResult.code) {
90 const message = `Failed to gzip tar archive '${teamArchiveFileName}' for ${slug}`;
91 logger.error(message);
92 gi.progressLog.write(message);
93 return;
94 }
95 const resolvedClassifier = await resolveClassifierPath(classifier, gi);
96 await this.store.store(gi, resolvedClassifier, teamArchiveFileName + ".gz");
97 }
98
99 public async remove(gi: GoalInvocation, classifier?: string): Promise<void> {
100 const resolvedClassifier = await resolveClassifierPath(classifier, gi);
101 await this.store.delete(gi, resolvedClassifier);
102 }
103
104 public async retrieve(gi: GoalInvocation, project: GitProject, classifier?: string): Promise<void> {
105 const archiveName = "atomist-cache";
106 const teamArchiveFileName = path.join(os.tmpdir(), `${archiveName}.${guid().slice(0, 7)}`);
107 const resolvedClassifier = await resolveClassifierPath(classifier, gi);
108 await this.store.retrieve(gi, resolvedClassifier, teamArchiveFileName);
109 if (fs.existsSync(teamArchiveFileName)) {
110 await spawnLog("tar", ["-xzf", teamArchiveFileName], {
111 log: gi.progressLog,
112 cwd: project.baseDir,
113 });
114 } else {
115 throw Error("No cache entry");
116 }
117 }
118
119}
120
121/**
122 * Interpolate information from goal invocation into the classifier.
123 */
124export async function resolveClassifierPath(classifier: string | undefined, gi: GoalInvocation): Promise<string> {
125 if (!classifier) {
126 return gi.context.workspaceId;
127 }
128 const wrapper = { classifier };
129 await resolvePlaceholders(wrapper, v => resolvePlaceholder(v, gi.goalEvent, gi, {}));
130 return gi.context.workspaceId + "/" + sanitizeClassifier(wrapper.classifier);
131}
132
133/**
134 * Sanitize classifier for use in path. Replace any characters
135 * which might cause problems on POSIX or MS Windows with "_",
136 * including path separators. Ensure resulting file is not "hidden".
137 */
138export function sanitizeClassifier(classifier: string): string {
139 return classifier.replace(/[^-.0-9A-Za-z_+]/g, "_")
140 .replace(/^\.+/, ""); // hidden
141}