@twilio-labs/serverless-api
Version:
API-wrapper for the Twilio Serverless API
277 lines (276 loc) • 11.1 kB
JavaScript
;
/** @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,
};
})));
});
}