aws/lambdas/create-s3-index-html/index.js (139 lines of code) (raw):

'use strict' /** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const AWS = require('aws-sdk'); const path = require('path'); const React = require('react'); const ReactDOMServer = require('react-dom/server'); const prettysize = require('prettysize'); const FOLDER_EMOJI = String.fromCodePoint(0x1F4C2); const FILE_EMOJI = String.fromCodePoint(0x1F4C3); function handle_s3_response(err, data, event, callback, acc) { if (err) { callback(err, data); return; } acc = acc.concat(data.Contents); if (data.IsTruncated) { const s3 = new AWS.S3(); s3.listObjectsV2( { Bucket: event.bucket, ContinuationToken: data.NextContinuationToken }, (err2, data2) => handle_s3_response(err2, data2, event, callback, acc) ); return; } publish_indices(event, acc, callback); } function publish_indices(event, all_objs, callback) { const month_ago = Date.now() - 30 * 24 * 60 * 60 * 1000; const filtered_objs = all_objs.filter(obj => !obj.Key.endsWith('/index.html') && obj.LastModified.getTime() > month_ago ); let dirs = new Set(['']); filtered_objs.forEach(obj => { let parts = obj.Key.split('/'); parts.pop(); while (parts.length > 0) { dirs.add(parts.join('/')); parts.pop(); } }); dirs = Array.from(dirs.values()); const s3 = new AWS.S3(); const s3_upload_promises = dirs.map(dir => s3.putObject({ ContentType: 'text/html', Body: get_index_html_string(dir, all_objs), Bucket: event.bucket, Key: (dir+'/index.html').replace(/^\/+/, '') }).promise()); Promise.all(s3_upload_promises).catch(err => callback(err)).then(data => { if (!event.cloudfront) { callback(null, data); } // Invalidate the CDN const cloudfront = new AWS.CloudFront(); cloudfront.createInvalidation( { DistributionId: event.cloudfront, InvalidationBatch: { CallerReference: 's3-index-update-'+(new Date()).toISOString(), Paths: { Quantity: dirs.length, Items: dirs.map( dir => dir === '' ? '/' : ('/'+dir+'/') ) } } }, (err, data) => { try { const paths = data.Invalidation.InvalidationBatch.Paths.Items; if (paths.length > 17) { data.Invalidation.InvalidationBatch.Paths.Items = [ ...paths.slice(0, 8), '-- truncated --', ...paths.slice(-8), ]; } } catch (e) {} callback(err, data); } ); }).catch(err => callback(err)); } function get_index_html_string(dir, objs) { console.log(dir); const parent_dir_row = dir === '' ? null : ( <tr key="parent"><td>{FOLDER_EMOJI} <a href={('/' + path.dirname(dir)).replace(/^\/.$/, '/')}>..</a></td></tr> ); let files = []; let dirs = new Set(); objs.forEach(obj => { if (path.dirname(obj.Key) === (dir === '' ? '.' : dir)) { if (path.basename(obj.Key) !== 'index.html') { files.push(obj); } return; } if (dir === '' || obj.Key.startsWith(dir + '/')) { const subdir = (dir === '' ? obj.Key : obj.Key.replace(dir+'/', '')).split('/')[0]; dirs.add(subdir); } }); const dir_rows = Array.from(dirs.values()).sort().map(subdir => <tr key={subdir}><td>{FOLDER_EMOJI} <a href={subdir+'/'}>{subdir}/</a></td></tr> ); const file_rows = files.sort( (a, b) => (a.Key < b.Key) ? -1 : 1 ).map(item => { if (item.Size === 0) { console.log(item); } const basename = path.basename(item.Key); return ( <tr key={item.Key}> <td>{FILE_EMOJI} <a href={basename}>{basename}</a></td> <td>{item.LastModified.toUTCString()}</td> <td>{prettysize(item.Size)}</td> </tr> ); }); const pretty_dir = dir === '' ? '/' : (dir + '/'); const html_tree = ( <html> <head> <meta charSet="UTF-8" /> <title>{pretty_dir} - HHVM Downloads</title> </head> <body> <h1>HHVM Downloads</h1> <h2 style={{fontFamily: 'monospace'}}>{pretty_dir}</h2> <table style={{width: '100%', fontFamily: 'monospace'}}> {parent_dir_row} {dir_rows} {file_rows} </table> <footer style={{fontSize: 'x-small', fontStyle: 'italic', color: '#aaa', marginTop: '1em'}}> Generated at {(new Date()).toISOString()} </footer> </body> </html> ); return ReactDOMServer.renderToStaticMarkup(html_tree); } exports.handler = (event, context, callback) => { if (!event.bucket) { callback('bucket must be specified'); return; } const s3 = new AWS.S3(); s3.listObjectsV2( { Bucket: event.bucket }, (err, data) => handle_s3_response(err, data, event, callback, []) ); };