UNPKG

@idoconfig/provider-folder

Version:

Provider for idoconfig that reads values from files within a folder Useful for Docker Secrets.

133 lines (118 loc) 4.84 kB
import * as fs from "fs"; import { ConfigurationProviderAbstract } from "@idoconfig/base"; import { isBinaryFileSync } from "isbinaryfile"; import { join, normalize, sep } from "path"; import { FolderProviderOptions } from "./folder-provider-options"; import { IFolderConfigurationValueProviderOptions } from "./interfaces/i-folder-provider-options"; /** * The size of one MegaByte (1024 * 1024 or 2^20) */ const SIZE_ONE_MB = 1048576; /** * Limits the amount of files read from given folder */ const MAX_FILES = 100; /** * Read files (content) from a folder. Filenames are mangled through filters which (may) * transform the filename into ENV_VAR_SYNTAX (or something entirely different). * * Default filename filters remove the file extension and enforce ALL_CAPS_STYLE, so * "my-secret-file.txt" becomes "MY_SECRET_FILE". Additional filename filters may be * inserted via dependency injection. */ export class FolderConfigurationValueProvider extends ConfigurationProviderAbstract { protected options: IFolderConfigurationValueProviderOptions; protected filenameFilters?: Array<(v: string) => string> = []; constructor( options?: IFolderConfigurationValueProviderOptions, additionalFilters: Array<(v: string) => string> = [], ) { super(); const defaultOptions = new FolderProviderOptions(); const opts = Object.assign({}, defaultOptions, options); if (opts.stripFileExtension) { this.filenameFilters.push(this.removeExtension); } this.options = opts; // Push additional filename filters onto list for (const filter of additionalFilters) { this.filenameFilters.push(filter); } // Read files from folder try { const normalizedPath = this.normalizePath(this.options.path); const items = fs.readdirSync(normalizedPath); if (items.length <= MAX_FILES) { for (const item of items) { if (this.options.blacklist.indexOf(item) !== -1) { continue; } const file = join(normalizedPath, item); const stat = fs.statSync(file); // Only read files that are non-binary and smaller than 1MB if (stat.isFile() && stat.size < SIZE_ONE_MB && !isBinaryFileSync(file, stat.size)) { const filename = this.applyFiltersToFilename(item); const key = this.sanitizeKey(filename); const val = fs.readFileSync(file, "utf8").trim(); // Regular version this.values[key] = val; // All underscore version this.values[key.replace(/[^0-9a-z]+/ig, "_")] = val; // All dash version this.values[key.replace(/[^0-9a-z]+/ig, "-")] = val; } } } } catch (e) { // @TODO: Implement log emitter // console.warn(`Could not read secrets from ${this.options.path}. ${e.message}`); } } /** * Try to get cached value first. Upon cache miss read file directly * @param key */ public getValue(key: string) { key = this.sanitizeKey(key); if (this.values[key]) { return this.values[key]; } return this.getValueFromFile(key); } /** * When given path starts with PATH_SEP it is absolute. Don't perform * any other action. If it is relative prepend the current path. * * @param unsafePath */ private normalizePath(unsafePath: string) { const parts = unsafePath.split("/"); return normalize(parts.join(sep)); } /** * Return trimmed value of requested file * @param file */ private getValueFromFile(file: string): string { const filepath = `${this.options.path}${file}`; if (file.indexOf(sep) !== -1) { throw new Error("File name cannot contain path separator"); } if (!fs.existsSync(filepath)) { return; } return fs.readFileSync(filepath, "utf8").trim(); } private applyFiltersToFilename(val: string): string { for (const filter of this.filenameFilters) { val = filter.call(this, val); } return val; } private removeExtension(val: string): string { const parts = val.split("."); if (parts.length > 1) { parts.pop(); } return parts.join("."); } } export default FolderConfigurationValueProvider;