<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>file uploads on Barney Parker</title><link>https://barneyparker.com/tags/file-uploads/</link><description>Recent content in file uploads on Barney Parker</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.</copyright><lastBuildDate>Fri, 02 Jul 2021 14:50:30 +0000</lastBuildDate><atom:link href="https://barneyparker.com/tags/file-uploads/index.xml" rel="self" type="application/rss+xml"/><item><title>Uploading File Trees to S3 with Terraform</title><link>https://barneyparker.com/posts/uploading-file-trees-to-s3-with-terraform/</link><pubDate>Fri, 02 Jul 2021 14:50:30 +0000</pubDate><guid>https://barneyparker.com/posts/uploading-file-trees-to-s3-with-terraform/</guid><description>&lt;p>Uploading a single file to S3 using Terraform is pretty simple, but sometimes you need to upload a whole folder. If you&amp;rsquo;re serving the files using S3 as a website, or through CloudFront you also need to make sure you set the correct mime types and eTags. Its actually a whole lot simpler than you might think!&lt;/p>
&lt;h2 id="single-file-uploads">Single File Uploads&lt;/h2>
&lt;p>A single file is easy enough to upload:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_object&amp;#34; &amp;#34;single_file&amp;#34;&lt;/span> {
&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_s3_bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">web&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;span class="n"> key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;index.hmtl&amp;#34;&lt;/span>
&lt;span class="n"> source&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.module}/content/index.html&amp;#34;&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>While that will upload a file, it will get a mime type of &lt;code>application/octet-stream&lt;/code> which means if you&amp;rsquo;re trying to serve it through CloudFront it will download rather than loading as the right document type. To set the right mime type, all thats needed is:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_object&amp;#34; &amp;#34;content&amp;#34;&lt;/span> {
&lt;span class="p">...&lt;/span>
&lt;span class="n"> content_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;text/html&amp;#34;&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Often a browser will perform a &lt;code>HEAD&lt;/code> request to see if a file has changed since the last time it downloaded it. The easiest way to check is via the &lt;code>etag&lt;/code> header which contains a hash of the contents. We can also add that automatically:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_object&amp;#34; &amp;#34;content&amp;#34;&lt;/span> {
&lt;span class="p">...&lt;/span>
&lt;span class="n"> etag&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">filemd5&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/content/index.html&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="extending-to-multiple-files">Extending to Multiple Files&lt;/h2>
&lt;p>Modern versions of terraform let is create multiple resources using the &lt;code>for_each&lt;/code> iteration operator. To get a list of the files we want to upload we can use the &lt;code>fileset&lt;/code> function which collects a list of files at a path based on a pattern.&lt;/p>
&lt;p>&lt;code>fileset&lt;/code> takes two parameters, a root path and a pattern to match files against. The pattern field is able to allow for recursively searching a folder tree using the &lt;code>**&lt;/code> globbing operator, so we could for example do something like:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_object&amp;#34; &amp;#34;content&amp;#34;&lt;/span> {
&lt;span class="n"> for_each&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">fileset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;../web/build&amp;#34;, &amp;#34;**/*.hmtl&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_s3_bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">web&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;span class="n"> key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">each&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">key&lt;/span>
&lt;span class="n"> source&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.module}/content/${each.key}&amp;#34;&lt;/span>
&lt;span class="n"> content_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;text/html&amp;#34;&lt;/span>
&lt;span class="n"> etag&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">filemd5&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/content/${each.key}&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Which is a pretty simple way of finding all the files ending in &lt;code>.html&lt;/code> and adding the to the bucket with the right mime type. Its a bit cumbersome working this way though because now we need to add a new block for JS, another for CSS, one for JPGs and so on&amp;hellip;..&lt;/p>
&lt;h2 id="mime-type-lookups">Mime-Type Lookups&lt;/h2>
&lt;p>Using some clever indexing we can save ourselves an awful lot of effort by creating a map of file extensions and the mime type they map to. Inside the &lt;code>for_each&lt;/code> loop we can dissect the filename to get the file extension, and then look up what mime type it maps to:&lt;/p>
&lt;p>Our map of mime types should look like:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">locals&lt;/span> {
&lt;span class="n"> mime_types&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;span class="n"> &amp;#34;css&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;text/css&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;html&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;text/html&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;ico&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;image/vnd.microsoft.icon&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;js&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;application/javascript&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;json&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;application/json&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;map&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;application/json&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;png&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;image/png&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;svg&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;image/svg+xml&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;txt&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;text/plain&amp;#34;&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>we need to cover every file type we have, and Terraform will fail to plan if a file extension is added that we don&amp;rsquo;t have a type for, but at least we don&amp;rsquo;t end up with the wrong mime type!&lt;/p>
&lt;p>To do the lookup we need to &lt;code>split&lt;/code> the file name and path by the &lt;code>.&lt;/code> character, and then get the last element and use that in a &lt;code>lookup&lt;/code> against the map. Using a local actually gives us an &lt;code>object&lt;/code> rather than a &lt;code>map&lt;/code>, but you can convert using the &lt;code>toset&lt;/code> function.&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_object&amp;#34; &amp;#34;content&amp;#34;&lt;/span> {
&lt;span class="n"> for_each&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">fileset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/content&amp;#34;, &amp;#34;**/*.*&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_s3_bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">web&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;span class="n"> key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">each&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">key&lt;/span>
&lt;span class="n"> source&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.module}/content/${each.key}&amp;#34;&lt;/span>
&lt;span class="n"> content_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">lookup&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">tomap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">mime_types&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="k">element&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;.&amp;#34;, each.key), length(split(&amp;#34;.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">each&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">key&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="err">-&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="n"> etag&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">filemd5&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/content/${each.key}&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now every file will be found, mapped to a valid mime-type, an &lt;code>etag&lt;/code> calculated and then be individually uploaded to S3! Since the &lt;code>etag&lt;/code> is set, subsequent runs will only upload files that have changed!&lt;/p></description></item></channel></rss>