UNPKG

osls

Version:

Open-source alternative to Serverless Framework

329 lines (289 loc) 9.49 kB
'use strict'; const path = require('path'); const os = require('os'); const URL = require('url'); const download = require('@serverless/utils/download'); const BbPromise = require('bluebird'); const fse = require('fs-extra'); const qs = require('querystring'); const fetch = require('node-fetch'); const spawn = require('child-process-ext/spawn'); const renameService = require('./rename-service').renameService; const ServerlessError = require('../serverless-error'); const copyDirContentsSync = require('./fs/copy-dir-contents-sync'); const dirExistsSync = require('./fs/dir-exists-sync'); /** * Returns directory path * @param {Number} length * @param {Array} parts * @returns {String} directory path */ function getPathDirectory(length, parts) { if (!parts) { return ''; } return parts.slice(length).filter(Boolean).join(path.sep); } /** * Validates URL * @param {Object} url * @param {String} hostname * @param {String} service * @param {String} owner * @param {String} repo */ function validateUrl({ url, hostname, service, owner, repo }) { // validate if given url is a valid url if (url.hostname !== hostname || !owner || !repo) { const errorMessage = `The URL must be a valid ${service} URL in the following format: https://${hostname}/serverless/serverless`; throw new ServerlessError(errorMessage, 'INVALID_TEMPLATE_URL'); } } /** * Check if the URL is pointing to a Git repository * @param {String} url */ function isPlainGitURL(url) { return (url.startsWith('https') || url.startsWith('git@')) && url.endsWith('.git'); } /** * @param {Object} url * @returns {Object} */ function parseGitHubURL(url) { const pathLength = 4; const parts = url.pathname.split('/'); const isSubdirectory = parts.length > pathLength; const owner = parts[1]; const repo = parts[2]; const branch = isSubdirectory ? parts[pathLength] : 'master'; const isGitHubEnterprise = url.hostname !== 'github.com'; if (!isGitHubEnterprise) { // validate if given url is a valid GitHub url validateUrl({ url, hostname: 'github.com', service: 'GitHub', owner, repo }); } const downloadUrl = `https://${ isGitHubEnterprise ? url.hostname : 'github.com' }/${owner}/${repo}/archive/${branch}.zip`; return { owner, repo, branch, downloadUrl, isSubdirectory, pathToDirectory: getPathDirectory(pathLength + 1, parts), username: url.username || '', password: url.password || '', }; } /** * @param {Object} url * @returns {Object} */ function parseBitbucketURL(url) { const pathLength = 4; const parts = url.pathname.split('/'); const isSubdirectory = parts.length > pathLength; const owner = parts[1]; const repo = parts[2]; const query = qs.parse(url.query); const branch = 'at' in query ? query.at : 'master'; // validate if given url is a valid Bitbucket url validateUrl({ url, hostname: 'bitbucket.org', service: 'Bitbucket', owner, repo }); const downloadUrl = `https://bitbucket.org/${owner}/${repo}/get/${branch}.zip`; return { owner, repo, branch, downloadUrl, isSubdirectory, pathToDirectory: getPathDirectory(pathLength + 1, parts), username: url.username || '', password: url.password || '', }; } function parseBitbucketServerURL(url) { const pathLength = 9; const parts = url.pathname.split('/'); const isSubdirectory = parts.length > pathLength; const owner = parts[5]; const repo = parts[7]; const query = qs.parse(url.query); const branch = 'at' in query ? decodeURIComponent(query.at) : 'master'; const downloadUrl = `${url.protocol}//${url.hostname}/rest/api/latest/projects/${owner}/repos/${repo}/archive${url.search}&format=zip`; return { owner, repo, branch, downloadUrl, isSubdirectory, pathToDirectory: getPathDirectory(pathLength + 1, parts), username: url.username || '', password: url.password || '', }; } /** * Call `/rest/api/1.0/application-properties` to retrieve server info * @param {Object} url * @returns {Boolean} */ async function retrieveBitbucketServerInfo(url) { const versionInfoPath = `${url.protocol}//${url.hostname}/rest/api/1.0/application-properties`; return fetch(versionInfoPath) .then((resp) => resp.json()) .then((body) => body.displayName === 'Bitbucket'); } /** * @param {Object} url * @returns {Object} */ function parseGitlabURL(url) { const pathLength = 4; const parts = url.pathname.split('/'); const isSubdirectory = parts.length > pathLength; const owner = parts[1]; const repo = parts[2]; const branch = isSubdirectory ? parts[pathLength] : 'master'; // validate if given url is a valid GitLab url validateUrl({ url, hostname: 'gitlab.com', service: 'Bitbucket', owner, repo }); const downloadUrl = `https://gitlab.com/${owner}/${repo}/-/archive/${branch}/${repo}-${branch}.zip`; return { owner, repo, branch, downloadUrl, isSubdirectory, pathToDirectory: getPathDirectory(pathLength + 1, parts), username: url.username || '', password: url.password || '', }; } /** * Parses a URL which points to a plain Git repository * such as https://example.com/jdoe/project.git * * @param {String} url * @returns {Object} */ function parsePlainGitURL(url) { const branch = 'master'; const downloadUrl = url; const isSubdirectory = false; const repo = url.match(/.+\/(.+)\.git/)[1]; return { repo, branch, downloadUrl, isSubdirectory, username: url.username || '', password: url.password || '', }; } /** * Parse URL and call the appropriate adaptor * * @param {string} inputUrl * @throws {ServerlessError} * @returns {Promise} */ async function parseRepoURL(inputUrl) { return new BbPromise((resolve, reject) => { if (!inputUrl) { return reject(new ServerlessError('URL is required', 'MISSING_TEMPLATE_URL')); } const url = URL.parse(inputUrl.replace(/\/$/, '')); if (url.auth) { const [username, password] = url.auth.split(':'); url.username = username; url.password = password; } // check if url parameter is a valid url if (!url.host && !url.href.startsWith('git@')) { return reject(new ServerlessError('The URL you passed is not valid', 'INVALID_TEMPLATE_URL')); } if (isPlainGitURL(url.href)) { return resolve(parsePlainGitURL(inputUrl)); } else if (url.hostname === 'github.com' || url.hostname.indexOf('github.') !== -1) { return resolve(parseGitHubURL(url)); } else if (url.hostname === 'bitbucket.org') { return resolve(parseBitbucketURL(url)); } else if (url.hostname === 'gitlab.com') { return resolve(parseGitlabURL(url)); } const msg = 'The URL you passed is not one of the valid providers: "GitHub", "GitHub Entreprise", "Bitbucket", "Bitbucket Server" or "GitLab".'; const err = new ServerlessError(msg, 'INVALID_TEMPLATE_PROVIDER'); // test if it's a private bitbucket server return retrieveBitbucketServerInfo(url) .then((isBitbucket) => { if (!isBitbucket) { return reject(err); } // build download URL return resolve(parseBitbucketServerURL(url)); }) .catch(() => reject(err)); }); } /** * @param {string} inputUrl * @param {string} [templateName] * @param {string} [path] * @returns {Promise} */ async function downloadTemplateFromRepo(inputUrl, templateName, downloadPath) { return parseRepoURL(inputUrl).then((repoInformation) => { let serviceName; let dirName; let downloadServicePath; const { username, password } = repoInformation; if (repoInformation.isSubdirectory) { const folderName = repoInformation.pathToDirectory.split('/').splice(-1)[0]; serviceName = folderName; dirName = downloadPath || templateName || folderName; downloadServicePath = path.join(os.tmpdir(), repoInformation.repo); } else { serviceName = repoInformation.repo; dirName = downloadPath || templateName || repoInformation.repo; downloadServicePath = path.join(process.cwd(), dirName); } const serviceDir = path.join(process.cwd(), dirName); const renamed = dirName !== repoInformation.repo; if (dirExistsSync(path.join(process.cwd(), dirName))) { const errorMessage = `A folder named "${dirName}" already exists.`; throw new ServerlessError(errorMessage, 'TARGET_FOLDER_ALREADY_EXISTS'); } if (isPlainGitURL(inputUrl)) { return spawn('git', ['clone', inputUrl, downloadServicePath]).then(() => { if (renamed) renameService(dirName, serviceDir); return serviceName; }); } const downloadOptions = { timeout: 30000, extract: true, strip: 1, mode: '755', username, password, }; // download service return download(repoInformation.downloadUrl, downloadServicePath, downloadOptions) .then(() => { // if it's a directory inside of git if (repoInformation.isSubdirectory) { const directory = path.join(downloadServicePath, repoInformation.pathToDirectory); copyDirContentsSync(directory, serviceDir); fse.removeSync(downloadServicePath); } }) .then(() => { if (renamed) renameService(dirName, serviceDir); return BbPromise.resolve(serviceName); }); }); } module.exports = { downloadTemplateFromRepo, parseRepoURL, };