UNPKG

8.78 kBJavaScriptView Raw
1// MIT License
2//
3// Copyright 2016-2020 Electric Imp
4//
5// SPDX-License-Identifier: MIT
6//
7// Permission is hereby granted, free of charge, to any person obtaining a copy
8// of this software and associated documentation files (the "Software"), to deal
9// in the Software without restriction, including without limitation the rights
10// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11// copies of the Software, and to permit persons to whom the Software is
12// furnished to do so, subject to the following conditions:
13//
14// The above copyright notice and this permission notice shall be
15// included in all copies or substantial portions of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
20// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
21// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
22// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23// OTHER DEALINGS IN THE SOFTWARE.
24
25'use strict';
26
27const fs = require('fs-extra');
28const path = require('path');
29const minimatch = require('minimatch');
30const XXHash = require('xxhashjs');
31const HttpReader = require('./Readers/HttpReader');
32const GithubReader = require('./Readers/GithubReader');
33const BitbucketServerReader = require('./Readers/BitbucketServerReader');
34const AzureReposReader = require('./Readers/AzureReposReader');
35
36const DEFAULT_EXCLUDE_FILE_NAME = 'builder-cache.exclude';
37const CACHED_READERS = [GithubReader, BitbucketServerReader, AzureReposReader, HttpReader];
38const CACHE_LIFETIME = 1; // in days
39const HASH_SEED = 0xE1EC791C;
40const MAX_FILENAME_LENGTH = 250;
41
42
43class FileCache {
44
45 constructor(machine) {
46 this._cacheDir = '.' + path.sep + '.builder-cache';
47 this._excludeList = [];
48 this._machine = machine;
49 this._outdateTime = CACHE_LIFETIME * 86400000; // precalc milliseconds in one day
50 this._useCache = false;
51 }
52
53 /**
54 * Transform url or github/bitbucket/azure link to path and filename
55 * It is important, that path and filename are unique,
56 * because collision can break the build
57 * @param {string} link link to the file
58 * @return {string} folder and name, where cache file can be found
59 * @private
60 */
61 _getCachedPath(link) {
62 link = link.replace(/^bitbucket-server\:/, 'bitbucket-server#'); // replace ':' for '#' in bitbucket-server protocol
63 link = link.replace(/^github\:/, 'github#'); // replace ':' for '#' in github protocol
64 link = link.replace(/^git-azure-repos\:/, 'git-azure-repos#'); // replace ':' for '#' in azure-repos protocol
65 link = link.replace(/\:\/\//, '#'); // replace '://' for '#' in url
66 link = link.replace(/\//g, '-'); // replace '/' for '-'
67 const parts = link.match(/^([^\?]*)(\?(.*))?$/); // delete get parameters from url
68 if (parts && parts[3]) {
69 link = parts[1] + XXHash.h64(parts[3], HASH_SEED);
70 }
71 if (link.length > MAX_FILENAME_LENGTH) {
72 const startPart = link.substr(0, 100);
73 const endPart = link.substr(link.length - 100);
74 const middlePart = XXHash.h64(link, HASH_SEED);
75 link = startPart + endPart + middlePart;
76 }
77 return path.join(this._cacheDir, link);
78 }
79
80 /**
81 * Create all subfolders and write file to them
82 * @param {string} path path to the file
83 * @param {string} content content of the file
84 */
85 _cacheFile(filePath, content) {
86 const cachedPath = this._getCachedPath(filePath);
87 try {
88 fs.ensureDirSync(path.dirname(cachedPath));
89 fs.writeFileSync(cachedPath, content);
90 } catch (err) {
91 this._machine.logger.error(err);
92 }
93 }
94
95 /**
96 * Check, is file exist by link and return path if exist
97 * @param {{dirPath : string, fileName : string} | false} link link to the file
98 * @return {string|false} result
99 */
100 _findFile(link) {
101 const finalPath = this._getCachedPath(link);
102 return fs.existsSync(finalPath) ? finalPath : false;
103 }
104
105 /**
106 * Check, has reader to be cached
107 * @param {AbstractReader} reader
108 * @return {boolean} result
109 * @private
110 */
111 _isCachedReader(reader) {
112 return CACHED_READERS.some((cachedReader) => (reader instanceof cachedReader));
113 }
114
115 /**
116 * Check, has file to be excluded from cache
117 * @param {string} path to the file
118 * @return {boolean} result
119 */
120 _isExcludedFromCache(includedPath) {
121 return this._excludeList.some((regexp) => regexp.test(includedPath));
122 }
123
124 _toBeCached(includePath) {
125 return this.useCache && !this._isExcludedFromCache(includePath);
126 }
127
128 /**
129 * Check, is file outdated
130 * @param {string} path to the file
131 * @return {boolean} result
132 */
133 _isCacheFileOutdate(pathname) {
134 const stat = fs.statSync(pathname);
135 return Date.now() - stat.mtime > this._outdateTime;
136 }
137
138 /**
139 * Read includePath and use cache if needed
140 * @param {string} includePath link to the source
141 * @param {AbstractReader} reader reader
142 * @return {content: string, includePathParsed} content and parsed path
143 * @private
144 */
145 read(reader, includePath, dependencies, context) {
146 // Do this first as our includePath and reader may change on us if we have a cache hit
147 const includePathParsed = reader.parsePath(includePath);
148 const originalReader = reader;
149
150 let needCache = false;
151 // Cache file or read from cache only if --cache option is on and no
152 // --save-dependencies option is used
153 // If --use-dependencies option is used (together with --cache, of course),
154 // then cache file or read from cache if no reference to it found in the specified file
155 const depCondition = (!dependencies || dependencies.get(includePath) === undefined) && !this.machine.dependenciesSaveFile;
156 if (depCondition && this._toBeCached(includePath) && this._isCachedReader(reader)) {
157 let result;
158 if ((result = this._findFile(includePath)) && !this._isCacheFileOutdate(result)) {
159 // change reader to local reader
160 includePath = result;
161 this.machine.logger.info(`Read source from local path "${includePath}"`);
162 reader = this.machine.readers.file;
163 } else {
164 needCache = true;
165 }
166 }
167
168 const options = { dependencies: dependencies, context: context };
169
170 if (originalReader === this.machine.readers.file) {
171 // This allows the FileReader to return the actual path of the file (where it has been found)
172 // But we don't need this behavior in case of reading from cache (so we saved the original reader at the top)
173 options.resultPathParsed = includePathParsed;
174 }
175
176 let content = reader.read(includePath, options);
177
178 // if content doesn't have line separator at the end, then add it
179 if (content.length > 0 && content[content.length - 1] != '\n') {
180 content += '\n';
181 }
182
183 if (needCache && this.useCache) {
184 this.machine.logger.debug(`Caching file "${includePath}"`);
185 this._cacheFile(includePath, content);
186 }
187 return {
188 'content' : content,
189 'includePathParsed' : includePathParsed
190 };
191 }
192
193 clearCache() {
194 fs.removeSync(this.cacheDir);
195 }
196
197 /**
198 * Use cache?
199 * @return {boolean}
200 */
201 get useCache() {
202 return this._useCache || false;
203 }
204
205 /**
206 * @param {boolean} value
207 */
208 set useCache(value) {
209 this._useCache = value;
210 }
211
212 set cacheDir(value) {
213 this._cacheDir = value.replace(/\//g, path.sep);
214 }
215
216 get cacheDir() {
217 return this._cacheDir;
218 }
219
220 set machine(value) {
221 this._machine = value;
222 }
223
224 get machine() {
225 return this._machine;
226 }
227
228 get excludeList() {
229 return this._excludeList;
230 }
231
232 /**
233 * Construct exclude regexp list from filename
234 * @param {string} name of exclude file. '' for default
235 */
236 set excludeList(fileName) {
237 if (fileName == '') {
238 fileName = DEFAULT_EXCLUDE_FILE_NAME;
239 }
240
241 const newPath = fileName;
242 // check is fileName exist
243 if (!fs.existsSync(newPath)) {
244 if (fileName == DEFAULT_EXCLUDE_FILE_NAME) {
245 // if it isn't exist and it is default, then put empty list
246 this._excludeList = [];
247 return;
248 } else {
249 throw new Error(`${newPath} file does not exist`);
250 }
251 }
252
253 const content = fs.readFileSync(newPath, 'utf8');
254 const filenames = content.split(/\n|\r\n/);
255 // filters not empty strings, and makes regular expression from template
256 const patterns = filenames.map((value) => value.trimLeft()) // trim for "is commented" check
257 .filter((value) => (value != '' && value[0] != '#'))
258 .map((value) => minimatch.makeRe(value));
259 this._excludeList = patterns;
260 }
261}
262
263module.exports = FileCache;