diff --git a/lib/utils/downloadTemplateFromRepo.js b/lib/utils/downloadTemplateFromRepo.js index 6c9ae2d8c..9fba178fd 100644 --- a/lib/utils/downloadTemplateFromRepo.js +++ b/lib/utils/downloadTemplateFromRepo.js @@ -5,6 +5,7 @@ const download = require('download'); const BbPromise = require('bluebird'); const fse = require('fs-extra'); const chalk = require('chalk'); +const qs = require('querystring'); const { renameService } = require('./renameService'); const { ServerlessError } = require('../classes/Error'); @@ -28,23 +29,15 @@ function dirExistsSync(dirPath) { } } -function downloadTemplateFromRepo(name, inputUrl) { - const url = URL.parse(inputUrl.replace(/\/$/, '')); - - // check if url parameter is a valid url - if (!url.host) { - throw new ServerlessError('The URL you passed is not a valid URL'); - } - +function parseGitHubURL(url) { const parts = url.pathname.split('/'); - const parsedGitHubUrl = { - owner: parts[1], - repo: parts[2], - branch: parts[4] || 'master', - }; + const isSubdirectory = parts.length > 4; + const owner = parts[1]; + const repo = parts[2]; + const branch = isSubdirectory ? parts[4] : 'master'; // validate if given url is a valid GitHub url - if (url.hostname !== 'github.com' || !parsedGitHubUrl.owner || !parsedGitHubUrl.repo) { + if (url.hostname !== 'github.com' || !owner || !repo) { const errorMessage = [ 'The URL must be a valid GitHub URL in the following format:', ' https://github.com/serverless/serverless', @@ -52,35 +45,120 @@ function downloadTemplateFromRepo(name, inputUrl) { throw new ServerlessError(errorMessage); } + let pathToDirectory = ''; + for (let i = 5; i <= (parts.length - 1); i++) { + pathToDirectory = path.join(pathToDirectory, parts[i]); + } + const downloadUrl = [ 'https://github.com/', - parsedGitHubUrl.owner, + owner, '/', - parsedGitHubUrl.repo, + repo, '/archive/', - parsedGitHubUrl.branch, + branch, '.zip', ].join(''); - const endIndex = parts.length - 1; - let dirName; + return { + owner, + repo, + branch, + downloadUrl, + isSubdirectory, + pathToDirectory, + }; +} + +function parseBitBucketURL(url) { + const parts = url.pathname.split('/'); + const isSubdirectory = parts.length > 4; + 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 GitHub url + if (url.hostname !== 'bitbucket.org' || !owner || !repo) { + const errorMessage = [ + 'The URL must be a valid GitHub URL in the following format:', + ' https://github.com/serverless/serverless', + ].join(''); + throw new ServerlessError(errorMessage); + } + + let pathToDirectory = ''; + for (let i = 5; i <= (parts.length - 1); i++) { + pathToDirectory = path.join(pathToDirectory, parts[i]); + } + + const downloadUrl = [ + 'https://bitbucket.org/', + owner, + '/', + repo, + '/get/', + branch, + '.zip', + ].join(''); + + return { + owner, + repo, + branch, + downloadUrl, + isSubdirectory, + pathToDirectory, + }; +} + +function parseRepoURL(inputUrl) { + if (!inputUrl) { + throw new ServerlessError('URL is required'); + } + + const url = URL.parse(inputUrl.replace(/\/$/, '')); + + // check if url parameter is a valid url + if (!url.host) { + throw new ServerlessError('The URL you passed is not a valid URL'); + } + + switch (url.hostname) { + case 'github.com': { + return parseGitHubURL(url); + } + case 'bitbucket.org': { + return parseBitBucketURL(url); + } + default: { + const msg = 'The URL you passed is not one of the valid providers: "GitHub", "BitBucket".'; + throw new ServerlessError(msg); + } + } +} + +function downloadTemplateFromRepo(name, inputUrl) { + const repoInformation = parseRepoURL(inputUrl); + let serviceName; + let dirName; let downloadServicePath; - // check if it's a directory or the whole repository - if (parts.length > 4) { - serviceName = parts[endIndex]; - dirName = name || parts[endIndex]; - // download the repo into a temporary directory - downloadServicePath = path.join(os.tmpdir(), parsedGitHubUrl.repo); + if (repoInformation.isSubdirectory) { + const folderName = repoInformation.pathToDirectory.split('/').splice(-1)[0]; + serviceName = folderName; + dirName = name || folderName; + downloadServicePath = path.join(os.tmpdir(), repoInformation.repo); } else { - serviceName = parsedGitHubUrl.repo; - dirName = name || parsedGitHubUrl.repo; + serviceName = repoInformation.repo; + dirName = name || repoInformation.repo; downloadServicePath = path.join(process.cwd(), dirName); } const servicePath = path.join(process.cwd(), dirName); - const renamed = dirName !== (parts.length > 4 ? parts[endIndex] : parsedGitHubUrl.repo); + const renamed = dirName !== repoInformation.repo; if (dirExistsSync(path.join(process.cwd(), dirName))) { const errorMessage = `A folder named "${dirName}" already exists.`; @@ -91,16 +169,13 @@ function downloadTemplateFromRepo(name, inputUrl) { // download service return download( - downloadUrl, + repoInformation.downloadUrl, downloadServicePath, { timeout: 30000, extract: true, strip: 1, mode: '755' } ).then(() => { // if it's a directory inside of git - if (parts.length > 4) { - let directory = downloadServicePath; - for (let i = 5; i <= endIndex; i++) { - directory = path.join(directory, parts[i]); - } + if (repoInformation.isSubdirectory) { + const directory = path.join(downloadServicePath, repoInformation.pathToDirectory); copyDirContentsSync(directory, servicePath); fse.removeSync(downloadServicePath); } @@ -113,4 +188,5 @@ function downloadTemplateFromRepo(name, inputUrl) { module.exports = { downloadTemplateFromRepo, + parseRepoURL, }; diff --git a/lib/utils/downloadTemplateFromRepo.test.js b/lib/utils/downloadTemplateFromRepo.test.js index 05b89c134..f3f0e498e 100644 --- a/lib/utils/downloadTemplateFromRepo.test.js +++ b/lib/utils/downloadTemplateFromRepo.test.js @@ -13,6 +13,8 @@ const readFileSync = require('./fs/readFileSync'); const remove = BbPromise.promisify(fse.remove); +const { parseRepoURL } = require('./downloadTemplateFromRepo'); + describe('downloadTemplateFromRepo', () => { let downloadTemplateFromRepo; let downloadStub; @@ -121,4 +123,79 @@ describe('downloadTemplateFromRepo', () => { }); }); }); + + describe('parseRepoURL', () => { + + it('should throw an error if no URL is provided', () => { + expect(parseRepoURL).to.throw(Error); + }); + + it('should throw an error if URL is not valid', () => { + try { + parseRepoURL('non_valid_url') + } catch(e) { + expect(e).to.be.an.instanceOf(Error); + } + }); + + it('should throw an error if URL is not of valid provider', () => { + try { + parseRepoURL('https://kostasbariotis.com/repo/owner'); + } catch(e) { + expect(e).to.be.an.instanceOf(Error); + } + }); + + it('should parse a valid GitHub URL', () => { + const output = parseRepoURL('https://github.com/serverless/serverless'); + + expect(output).to.deep.eq({ + owner: 'serverless', + repo: 'serverless', + branch: 'master', + downloadUrl: 'https://github.com/serverless/serverless/archive/master.zip', + isSubdirectory: false, + pathToDirectory: '', + }); + }); + + it('should parse a valid GitHub URL with subdirectory', () => { + const output = parseRepoURL('https://github.com/serverless/serverless/tree/master/assets'); + + expect(output).to.deep.eq({ + owner: 'serverless', + repo: 'serverless', + branch: 'master', + downloadUrl: 'https://github.com/serverless/serverless/archive/master.zip', + isSubdirectory: true, + pathToDirectory: 'assets', + }); + }); + + it('should parse a valid BitBucket URL ', () => { + const output = parseRepoURL('https://bitbucket.org/atlassian/localstack'); + + expect(output).to.deep.eq({ + owner: 'atlassian', + repo: 'localstack', + branch: 'master', + downloadUrl: 'https://bitbucket.org/atlassian/localstack/get/master.zip', + isSubdirectory: false, + pathToDirectory: '', + }); + }); + + it('should parse a valid BitBucket URL with subdirectory', () => { + const output = parseRepoURL('https://bitbucket.org/atlassian/localstack/src/85870856fd6941ae75c0fa946a51cf756ff2f53a/localstack/dashboard/?at=mvn'); + + expect(output).to.deep.eq({ + owner: 'atlassian', + repo: 'localstack', + branch: 'mvn', + downloadUrl: 'https://bitbucket.org/atlassian/localstack/get/mvn.zip', + isSubdirectory: true, + pathToDirectory: 'localstack/dashboard', + }); + }); + }); });