UNPKG

@twilio-labs/serverless-api

Version:
277 lines (276 loc) 11.1 kB
"use strict"; /** @module @twilio-labs/serverless-api/dist/utils/fs */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getListOfFunctionsAndAssets = exports.getFirstMatchingDirectory = exports.getDirContent = exports.getPathAndAccessFromFileInfo = exports.checkForValidPath = exports.fileExists = exports.stat = exports.readDir = exports.writeFile = exports.readFile = exports.access = void 0; const debug_1 = __importDefault(require("debug")); const fs_1 = __importDefault(require("fs")); const path_1 = __importStar(require("path")); const upath_1 = require("upath"); const recursive_readdir_1 = __importDefault(require("recursive-readdir")); const util_1 = require("util"); const log = (0, debug_1.default)('twilio-serverless-api:fs'); exports.access = (0, util_1.promisify)(fs_1.default.access); exports.readFile = (0, util_1.promisify)(fs_1.default.readFile); exports.writeFile = (0, util_1.promisify)(fs_1.default.writeFile); exports.readDir = (0, util_1.promisify)(recursive_readdir_1.default); exports.stat = (0, util_1.promisify)(fs_1.default.stat); const READ_ONLY = fs_1.default.constants.R_OK; const READ_WRITE = fs_1.default.constants.R_OK | fs_1.default.constants.W_OK; /** * Checks if a given file exists by checking if we have read & write access * * @export * @param {string} filePath full path of the file to check * @returns */ function fileExists(filePath, hasWriteAccess = false) { return __awaiter(this, void 0, void 0, function* () { try { yield (0, exports.access)(filePath, hasWriteAccess ? READ_WRITE : READ_ONLY); return true; } catch (err) { return false; } }); } exports.fileExists = fileExists; /** * Verifies a given path against the restrictions put up by the Twilio Runtime. * * @param path a potential absolute path for a Function or Asset */ function checkForValidPath(path) { if (!path.startsWith('/')) { return { valid: false, message: `Expected path to start with "/". Got: "${path}"`, }; } if (path.includes('#')) { return { valid: false, message: `Path cannot contain a #. Got: "${path}"`, }; } const invalidCharacters = /[;,\?:\@\+&\$\(\)' "]/g; if (invalidCharacters.test(path)) { return { valid: false, message: `Path cannot contain any of the following characters: ;,?:@+&$()' ". Got: "${path}"`, }; } if (path.length >= 256) { return { valid: false, message: `Path length must be shorter than 256 characters. Got: ${path.length} characters.`, }; } return { valid: true }; } exports.checkForValidPath = checkForValidPath; /** * Determines the access and Serverless path for a filesystem resource. * If it receives an ignore extension it will drop it from the final serverless path * * @export * @param {FileInfo} file the file to get the access and path for * @param {string} [ignoreExtension] file extension to drop for serverless path * @returns {ResourcePathAndAccess} */ function getPathAndAccessFromFileInfo(file, ignoreExtension) { const relativePath = path_1.default.dirname(file.name); let access = 'public'; const ext = path_1.default.extname(file.name); let baseName = path_1.default.basename(file.name, ext); if (file.name.includes(`.protected${ext}`)) { access = 'protected'; } else if (file.name.includes(`.private${ext}`)) { access = 'private'; } baseName = baseName.replace(`.${access}`, ''); let resourcePath = `/` + path_1.default.join(relativePath, baseName); if (ext !== ignoreExtension) { resourcePath += ext; } resourcePath = resourcePath.replace(/\s/g, '-'); resourcePath = (0, upath_1.toUnix)(resourcePath); const validatedPath = checkForValidPath(resourcePath); if (!validatedPath.valid) { throw new Error(validatedPath.message); } return { path: resourcePath, access, }; } exports.getPathAndAccessFromFileInfo = getPathAndAccessFromFileInfo; /** * Retrieves all (nested) files from a given directory. * * If an extension is specified it will be used to filter the results. * * @export * @param {string} dir the directory to be checked * @param {string} [extension] extension to be ignored in the results * @returns {Promise<FileInfo[]>} */ function getDirContent(dir, extension) { return __awaiter(this, void 0, void 0, function* () { const rawFiles = (yield (0, exports.readDir)(dir)); const unfilteredFiles = yield Promise.all(rawFiles.map((filePath) => __awaiter(this, void 0, void 0, function* () { if (!path_1.default.isAbsolute(filePath)) { filePath = path_1.default.join(dir, filePath); } const entry = yield (0, exports.stat)(filePath); if (!entry.isFile()) { return undefined; } if (extension && path_1.default.extname(filePath) !== extension) { return undefined; } if (path_1.default.basename(filePath) === '.DS_Store') { return undefined; } const name = path_1.default.relative(dir, filePath); return { name: name, path: filePath, }; }))); return unfilteredFiles.filter((entry) => typeof entry !== 'undefined'); }); } exports.getDirContent = getDirContent; /** * Given a list of directory names it will return the first one that exists in * the base path. * * **Important**: Performs synchronous file system reading * * @export * @param {string} basePath * @param {string[]} directories * @returns {string} */ function getFirstMatchingDirectory(basePath, directories) { for (let dir of directories) { const fullPath = path_1.default.join(basePath, dir); try { const fStat = fs_1.default.statSync(fullPath); if (fStat.isDirectory()) { return fullPath; } } catch (err) { continue; } } throw new Error(`Could not find any of these directories "${directories.join('", "')}"`); } exports.getFirstMatchingDirectory = getFirstMatchingDirectory; /** * Retrieves a list of functions and assets existing in a given base directory * Will check for both "functions" and "src" as directory for functions and * "assets" and "static" for assets * * @export * @param {string} cwd Directory * @param {SearchConfig} config lets you override the folders to use * @returns {Promise<DirectoryContent>} */ function getListOfFunctionsAndAssets(cwd, config = {}) { return __awaiter(this, void 0, void 0, function* () { let functionsDir; try { const possibleFunctionDirs = config.functionsFolderNames || [ 'functions', 'src', ]; log('Search for directory. Options: "%s"', possibleFunctionDirs.join(',')); functionsDir = getFirstMatchingDirectory(cwd, possibleFunctionDirs); } catch (err) { functionsDir = undefined; } log('Found Functions Directory "%s"', functionsDir); let assetsDir; try { const possibleAssetDirs = config.assetsFolderNames || ['assets', 'static']; log('Search for directory. Options: "%s"', possibleAssetDirs.join(',')); assetsDir = getFirstMatchingDirectory(cwd, possibleAssetDirs); } catch (err) { assetsDir = undefined; } log('Found Assets Directory "%s"', assetsDir); const functionFiles = functionsDir ? yield getDirContent(functionsDir, '.js') : []; const functionConfigs = yield getServerlessConfigs(functionFiles, '.js'); const assetFiles = assetsDir ? yield getDirContent(assetsDir) : []; const assetConfigs = yield getServerlessConfigs(assetFiles); return { functions: functionConfigs, assets: assetConfigs }; }); } exports.getListOfFunctionsAndAssets = getListOfFunctionsAndAssets; /** * Retrieve a files from a read directory * and create access and public path from the file name * * @param {FileInfo[]} dirContent read files from a directory * @param {string} [ignoreExtension] file extension to drop for serverless path * @returns {Promise<ServerlessResourceConfigWithFilePath[]>} */ function getServerlessConfigs(dirContent, ignoreExtension) { return __awaiter(this, void 0, void 0, function* () { return Promise.all(dirContent.map((file) => __awaiter(this, void 0, void 0, function* () { const { path, access } = getPathAndAccessFromFileInfo(file, ignoreExtension); const encoding = (0, path_1.extname)(file.path) === '.js' ? 'utf8' : undefined; const content = yield (0, exports.readFile)(file.path, encoding); return { name: path, path, access, content, filePath: file.path, }; }))); }); }