Created
February 25, 2019 20:17
-
-
Save jkcgs/a7ffde95f4b4fa8b4259eca796ecde29 to your computer and use it in GitHub Desktop.
AWS Lambda function to redirect to latest release assets from a GitHub repository.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const https = require('https'); | |
/** | |
* This parses arguments from an URL in the following form and | |
* magically redirects to an asset of a GitHub project release. | |
* | |
* https://api.owo.cl/grr/<owner>/<repo>/[version]/[file] | |
* | |
* owner: User or org that owns a repo | |
* repo: The repo name | |
* version: (optional) Version tag of the release, defaults to latest | |
* An underscore "_" will mean "latest". | |
* file: (optional) file to retrieve, defaults to first asset, allows | |
* wildcards like *.zip, and it will fetch the first asset that | |
* matches that wildcard. | |
* | |
* Examples: | |
* - https://api.owo.cl/grr/d0k3/SafeB9SInstaller/v0.0.6/*.zip | |
* - https://api.owo.cl/grr/d0k3/SafeB9SInstaller | |
* - https://api.owo.cl/grr/d0k3/SafeB9SInstaller/_/*.zip | |
**/ | |
exports.handler = (event, context, callback) => { | |
console.log('Event: ', event); | |
// If it's LAMBDA_PROXY handled, parameters will be in pathParameters. | |
let params = 'pathParameters' in event ? event.pathParameters : event; | |
// Verify required parameters | |
if (!('username' in params) || !('project' in params)) { | |
callback(new Error(`Missing required parameters.`)); | |
return; | |
} | |
// Store parameters | |
let username = params.username; | |
let project = params.project; | |
let version = params.version || '_'; | |
let filename = params.filename || ''; | |
// Validate owner's username | |
if (!/^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(username)) { | |
callback(new Error('Invalid username')); | |
return; | |
} | |
// Validate project name | |
let rx = /^[a-zA-Z0-9_\-\.]+$/; | |
if(!rx.test(project)) { | |
callback(new Error('Invalid project name')); | |
return; | |
} | |
// Validate and transform version parameter | |
if (!version || version === '_' || version === 'latest') { | |
version = 'releases/latest'; | |
} else if(!rx.test(version)) { | |
callback(new Error('Invalid version')); | |
return; | |
} else { | |
// Only tag versions are supported | |
version = 'releases/tags/' + version; | |
} | |
// Generate request options | |
const releasesURL = `https://github.com/${username}/${project}/releases/latest`; | |
const options = { | |
hostname: 'api.github.com', | |
port: 443, | |
path: `/repos/${username}/${project}/${version}`, | |
method: 'GET', | |
headers: { | |
'User-Agent': 'GithubReleasesRedirect/0.0.1 (github.com/jkcgs)' | |
} | |
}; | |
console.log('Options:', options); | |
// Load data from GitHub | |
const req = https.get(options, (res) => { | |
let body = ''; | |
console.log('Status:', res.statusCode); | |
console.log('Headers:', JSON.stringify(res.headers)); | |
res.setEncoding('utf8'); | |
res.on('data', (chunk) => body += chunk); | |
// Process data received from GitHub | |
res.on('end', () => { | |
console.log('Successfully processed HTTPS response'); | |
// We should always receive a JSON response | |
if (!res.headers['content-type'].startsWith('application/json')) { | |
callback(new Error('Unexpected answer received: ' + res.headers['content-type'])); | |
return; | |
} | |
body = JSON.parse(body); | |
// No assets (files) found, redirect to latest release page | |
if (!('assets' in body) || body.assets.length === 0) { | |
callback(null, redirect(releasesURL)); | |
return; | |
} | |
let asset = null; | |
// Search by filename if passed | |
if (filename !== '') { | |
// Wildcard search | |
if (filename.includes('*')) { | |
let pat = wildcard(filename); | |
asset = body.assets.find(rs => pat.test(rs.name)); | |
} else { | |
// Exact name search | |
asset = body.assets.find(rs => rs.name === filename); | |
} | |
} else { | |
// No filename passed = use first asset | |
asset = body.assets[0]; | |
} | |
if (!asset) { | |
// No asset found = 404 | |
callback(null, { statusCode: 404 }); | |
} else { | |
// Send redirect to the asset's direct link | |
console.log('Found asset:', asset); | |
callback(null, redirect(asset.browser_download_url)); | |
} | |
}); | |
}); | |
req.on('error', callback); | |
}; | |
function redirect(url) { | |
return { | |
statusCode: 301, | |
headers: { | |
Location: url | |
} | |
}; | |
} | |
// https://gist.github.com/donmccurdy/6d073ce2c6f3951312dfa45da14a420f | |
function wildcard(s) { | |
return new RegExp('^' + s.split(/\*+/).map( | |
x => x.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') | |
).join('.*') + '$'); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment