Read more here: gatsbyjs/gatsby#19543 (comment)
These scripts should be paired with: https://www.gatsbyjs.org/packages/gatsby-plugin-remove-trailing-slashes/
Read more here: gatsbyjs/gatsby#19543 (comment)
These scripts should be paired with: https://www.gatsbyjs.org/packages/gatsby-plugin-remove-trailing-slashes/
| #!/bin/sh | |
| # This is the first of the two scripts that are being used to work around | |
| # the limitations of conventional filesystems where two filesystem nodes | |
| # can't share the same name regardless of whether they're of the same type | |
| # or not (folder / file). | |
| # S3 is not a conventional filesystem, it's an object storage where filename | |
| # is the key and the value is the object and therefore this limitation does | |
| # not apply to S3. | |
| # However, since we need to build the application locally first, we need to | |
| # append an extension to the cloned files locally and then after uploading | |
| # all files to S3, we run the 2nd script that will remove the file extension. | |
| # This script will find all index.html files that are nested at least 2 levels deep and copy them | |
| # to the grandparent folder while renaming them to match their original parent folder name and | |
| # appending .collision extension because most filesystem don't allow two filesystem nodes to share | |
| # the same name regardless of their type (folder / file). However, S3 is not a typical filesystem | |
| # and allows such a configuration where folder and file share the same name and that's what we're | |
| # utilizing here. So the order of steps is as follows: | |
| # | |
| # 1. Deployment artifact is built | |
| # 2. patch-trailing-slash-locally.sh is ran which creates clones with a .collision.html extension to | |
| # prevent conflicts (this script) | |
| # 3. public folder is deployed | |
| # 4. patch-trailing-slash-remotely.sh is ran which removes the .collision.html extension | |
| echo "Cloning nested index.html files and copying them to the grandparent folder" | |
| find ./public -mindepth 2 -name "index.html" | sed 's/^\(.*\)\/index\.html$/\1/' | grep -v 404 | xargs -L1 -t -I {} cp "{}/index.html" "{}.collision.html" | |
| echo "Done" |
| // Loosely based on https://gist.github.com/cmawhorter/b706e45ba88e43bc379c | |
| // Script to work around discrepancies in s3 <=> os file and directory names. | |
| // It allows you to host your static website on s3 without trailing slashes. | |
| // e.g. example.com/products and example.com/products/mugs | |
| const http = require('http') | |
| const https = require('https') | |
| const AWS = require('aws-sdk') | |
| const async = require('async') | |
| // Resource limits | |
| http.globalAgent.maxSockets = 72 | |
| https.globalAgent.maxSockets = 72 | |
| // Script input assertions | |
| if (!process.env.S3_BUCKET) throw new Error('Must configure S3_BUCKET env var') | |
| if (!process.env.AWS_ACCESS_KEY_ID) throw new Error('Must configure AWS_ACCESS_KEY_ID env var') | |
| if (!process.env.AWS_SECRET_ACCESS_KEY) | |
| throw new Error('Must configure AWS_SECRET_ACCESS_KEY env var') | |
| // AWS SDK configuration | |
| const options = { | |
| bucket: process.env.S3_BUCKET, | |
| region: process.env.AWS_DEFAULT_REGION, | |
| key: process.env.AWS_ACCESS_KEY_ID, | |
| secret: process.env.AWS_SECRET_ACCESS_KEY, | |
| bucketPrefix: '', | |
| } | |
| AWS.config.update({ | |
| accessKeyId: options.key, | |
| secretAccessKey: options.secret, | |
| region: options.region, | |
| }) | |
| // AWS SDK error handling | |
| AWS.events.on('httpError', function onHttpError() { | |
| if (this.response.error && this.response.error.code === 'UnknownEndpoint') { | |
| this.response.error.retryable = true | |
| } | |
| }) | |
| const s3 = new AWS.S3() | |
| // This function will determine the mime type based on the file extension | |
| function determineMimeType(prefix) { | |
| const ext = prefix.split('.').pop() | |
| switch (ext.toLowerCase()) { | |
| default: | |
| case 'html': | |
| case 'aspx': | |
| return 'text/html' | |
| } | |
| } | |
| // S3 SDK does not offer a method to rename files hence we had to bake our own | |
| // This function will remove the .collision.html extension from the name of the | |
| // S3 object stored under the prefix path and configure its mime type correctly | |
| function renameObject(prefix, callback) { | |
| const renamedPrefix = prefix.replace('.collision.html', '') | |
| const mimeType = determineMimeType(prefix) | |
| console.log('renameObject %s -> %s; mime = %s', prefix, renamedPrefix, mimeType) | |
| const params = { | |
| Bucket: options.bucket, | |
| CopySource: `${options.bucket}/${prefix}`, | |
| Key: renamedPrefix, | |
| MetadataDirective: 'REPLACE', | |
| } | |
| if (mimeType) { | |
| params.ContentType = mimeType | |
| } | |
| s3.copyObject(params, function onCopyObject(err, data) { | |
| if (err) return callback(err) | |
| console.log('\t-> copied (%s)', prefix) | |
| s3.deleteObject( | |
| { | |
| Bucket: options.bucket, | |
| Key: prefix, | |
| }, | |
| function onDeleteObject(err, data) { | |
| if (err) return callback(err) | |
| console.log('\t-> removed (%s)', prefix) | |
| callback() | |
| } | |
| ) | |
| }) | |
| } | |
| // This function returns true if the prefix ends with our magic file extension | |
| function collisionsOnly(prefix) { | |
| return prefix | |
| .split('/') | |
| .pop() | |
| .endsWith('.collision.html') | |
| } | |
| function removeMagicExtension(prefix) { | |
| return async.apply(renameObject, prefix) | |
| } | |
| const allKeys = [] | |
| const listAllKeys = function listAllKeys(marker, cb) { | |
| s3.listObjects({ Bucket: options.bucket, Marker: marker }, function onListObjects(err, data) { | |
| if (err) { | |
| return cb(err) | |
| } | |
| Array.prototype.push.apply( | |
| allKeys, | |
| data.Contents.map(function onMapContents(el) { | |
| return el.Key | |
| }) | |
| ) | |
| if (data.IsTruncated) { | |
| listAllKeys(data.Contents.slice(-1)[0].Key, cb) | |
| } else { | |
| cb() | |
| } | |
| }) | |
| } | |
| // Traverse the entire tree of files and remove the magic extension | |
| // from all matching files. | |
| listAllKeys(options.bucketPrefix, function onListAllKeys(err) { | |
| if (err) { | |
| process.exit(1) | |
| } | |
| async.parallelLimit(allKeys.filter(collisionsOnly).map(removeMagicExtension), 12, function onDone( | |
| err | |
| ) { | |
| if (err) throw err | |
| console.log('Done') | |
| process.exit() | |
| }) | |
| }) |