<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Projects on James Jarvis</title><link>https://jamesjarvis.io/tags/projects/</link><description>Recent content in Projects on James Jarvis</description><generator>Hugo</generator><language>en</language><managingEditor>hello@jamesjarvis.io (James Jarvis)</managingEditor><webMaster>hello@jamesjarvis.io (James Jarvis)</webMaster><lastBuildDate>Sun, 02 Nov 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://jamesjarvis.io/tags/projects/index.xml" rel="self" type="application/rss+xml"/><item><title>🏚️-&gt;🏠 A year of house renovations</title><link>https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/</link><pubDate>Sat, 01 Nov 2025 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/</guid><category>Thoughts</category><category>Projects</category><description>Lessons learnt from a whole year of house renovations</description><content:encoded>&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/kitchen-before-and-after_hu_60ddf8efc8930e94.jpeg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="8000"
 height="3000"
 class="mx-auto my-0 rounded-md"
 alt="Kitchen before and after"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/kitchen-before-and-after_hu_c366c7fc05eea316.jpeg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;








&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/office-before-and-after_hu_64a49780573e03dd.jpeg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="8506"
 height="3000"
 class="mx-auto my-0 rounded-md"
 alt="Office before and after"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/office-before-and-after_hu_201c0a9419fdc3aa.jpeg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;








&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/bathroom-before-and-after_hu_561ad5155c9bc4cb.jpeg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="8489"
 height="3000"
 class="mx-auto my-0 rounded-md"
 alt="Bathroom before and after"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/bathroom-before-and-after_hu_b6c4cbdbc40174f1.jpeg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;I bought my first house at the age of 26 in late October 2024, almost exactly 1 year ago.&lt;/p&gt;
&lt;p&gt;I was up for a challenge, so even though I didn&amp;rsquo;t start looking out for a complete fixer-upper, I slowly convinced myself (and my girlfriend) to take a chance on a project house.
I wrote a bit previously about how we went about searching for the right house 
 
 &lt;a href="https://jamesjarvis.io/posts/2024/10/on-buying-a-house/"&gt;here&lt;/a&gt;, but TL;DR is that we could have probably lost less money by renting 😂.&lt;/p&gt;
&lt;p&gt;So, a year &lt;del&gt;wiser&lt;/del&gt; older, after hundreds of hours of work demolishing, planning, painting, plumbing, laying floors, building a kitchen, bathroom and generally fixing-up, this is my advice&amp;hellip;&lt;/p&gt;
&lt;h2 id="planning-is-everything" class="relative group"&gt;Planning is everything &lt;/h2&gt;&lt;p&gt;When selecting flooring, I ordered 12 samples from 4 suppliers, I spent hours planning the herringbone sequence to make sure we didn&amp;rsquo;t get stuck.
The kitchen went through 5 different designs, the bathroom went through 6 different designs, and the front fireplace took hours to just measure out.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a bit annoying but once you&amp;rsquo;ve got experience with it, it&amp;rsquo;s fairly painless and well worth the investment.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Outside -&amp;gt; in.
Top -&amp;gt; bottom.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sage advice when working on buildings is that you should always sequence your work outside -&amp;gt; in, and top -&amp;gt; bottom.&lt;/p&gt;
&lt;p&gt;Roof need fixing? Any leaky windows? Do those first before decorating the inside.&lt;/p&gt;
&lt;p&gt;Want to plaster the ceiling? Better do that before you paint the walls, and definitely do that before you lay a new floor.&lt;/p&gt;
&lt;p&gt;And any significant plumbing work should sequence neatly between outside work and any inner work, as you are definitely going to be ripping stuff apart in the process.&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/planning-drawings_hu_a7ea874c4c470767.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 &lt;img
 width="1206"
 height="1606"
 class="mx-auto my-0 rounded-md"
 alt="Planning Drawings"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/planning-drawings_hu_269ac3e2436c0bd3.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/flooring-plan-of-attack_hu_bdc09e52be882479.jpeg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="2048"
 height="1536"
 class="mx-auto my-0 rounded-md"
 alt="Flooring plan of attack"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/flooring-plan-of-attack_hu_18f2e71faf733207.jpeg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="you-cant-do-it-alone" class="relative group"&gt;You can&amp;rsquo;t do it alone &lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been incredibly lucky to have taken advantage of a wonderful bunch of wilful workers from my family and friends.
None of knew what we were doing, but they&amp;rsquo;ve all gotten stuck in and helped out massively.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d like to thank Feiyang, Jason, Celia, Penny, Owen, Jon, Matthew, Brandon, Raphael, Filip and Sam for all of their help.&lt;/p&gt;
&lt;h2 id="invest-in-tools" class="relative group"&gt;Invest in tools &lt;/h2&gt;&lt;p&gt;The first things I bought were a mitre saw and handheld combi drill. Alongside a stanley knife, some paint brushes/rollers and a borrowed handheld jigsaw I can&amp;rsquo;t imagine getting this far without them. I had to do almost a thousand cuts with that saw when laying the downstairs floor, and I&amp;rsquo;ve been through several thousand screws with the drill.&lt;/p&gt;
&lt;p&gt;A borrowed chainsaw was way better than the 50 year old wood saw I had for removing some trees, and the laser marker or dedicated scribing tools were far better than me eyeballing a cut.&lt;/p&gt;
&lt;h2 id="doubletriple-the-budget" class="relative group"&gt;Double/Triple the budget &lt;/h2&gt;&lt;p&gt;I think the last year&amp;rsquo;s work (~100sqm of wood flooring, every room redecorated/furnished, all radiators replaced and a new kitchen / bathroom) cost me approximately ~£45k.
I had originally thought ~£35k would cover everything comfortably including the gardens and some more furniture - turns out I was off by about double!&lt;/p&gt;
&lt;p&gt;Timing is also something not to be underestimated.&lt;/p&gt;
&lt;h2 id="how-hard-can-it-be" class="relative group"&gt;How hard can it be? &lt;/h2&gt;&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/how-hard-can-it-be_hu_8b5933bce48d679f.jpeg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="1536"
 height="2048"
 class="mx-auto my-0 rounded-md"
 alt="How hard can it be?"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/how-hard-can-it-be_hu_d92c25359c4ed7f8.jpeg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;How hard can it be?&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Fuck it, let&amp;rsquo;s give it a go&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This kind of attitude is the most important thing required (possibly more so than capital imho) when taking on a big project.&lt;/p&gt;
&lt;p&gt;You have to be willing to jump into a domain you know absolutely nothing about, mess stuff up, and figure it.&lt;/p&gt;
&lt;h2 id="understand-what-you-are-signing-up-for" class="relative group"&gt;Understand what you are signing up for &lt;/h2&gt;&lt;p&gt;This isn&amp;rsquo;t for the faint-hearted, it&amp;rsquo;s deliberately a massive pain in the arse and can be extremely stressful, not to mention economically exhausting.&lt;/p&gt;
&lt;p&gt;Getting 
 
 &lt;a href="https://jamesjarvis.io/posts/2025/08/uk-tax-burden-2025/"&gt;taxed 43%&lt;/a&gt;, handing over stamp duty (worse tax ever), only to remember that 20% of your new kitchen&amp;rsquo;s cost is also going to HMRC is a bit of a kick in the teeth.&lt;/p&gt;
&lt;p&gt;Working a full time job, then spending evenings and weekends on DIY whilst living without heating or running water in a dusty house, eating only microwaveable food gets old real quick - you do need to be slightly stupid to do this.&lt;/p&gt;
&lt;h2 id="now-the-itch-is-scratched-im-itching-to-do-it-again" class="relative group"&gt;Now the itch is scratched, I&amp;rsquo;m itching to do it again &lt;/h2&gt;&lt;p&gt;&amp;hellip;and I&amp;rsquo;m not talking about loft insulation!&lt;/p&gt;
&lt;p&gt;It has been a genuine pain in the ass sometimes, but I can&amp;rsquo;t help but have a huge sense of accomplishment sitting in my home built home office.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not looking to repeat some of the stress of the last year (I almost lost my eyesight twice due to stress!) but I am looking forward to taking on more big projects like this in the future.&lt;/p&gt;
&lt;p&gt;Hopefully now, with a little more experience behind me!&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/me-pirate_hu_675726a8801a7cd8.jpeg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="2316"
 height="3088"
 class="mx-auto my-0 rounded-md"
 alt="No pirate builders here!"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2025/11/a-year-of-house-renovations/me-pirate_hu_2fe28af408f23363.jpeg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;No pirate builders here&amp;hellip;&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;</content:encoded></item><item><title>Secret Santa: A Quote-a-day E-Ink display</title><link>https://jamesjarvis.io/posts/2025/02/secret-santa-a-quote-a-day-e-ink-display/</link><pubDate>Fri, 07 Feb 2025 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2025/02/secret-santa-a-quote-a-day-e-ink-display/</guid><category>Projects</category><description>&lt;p&gt;This is the story of how I built (and how you can build) a cute little display that shows you a new famous quote every day.&lt;/p&gt;</description><content:encoded>&lt;p&gt;This is the story of how I built (and how you can build) a cute little display that shows you a new famous quote every day.&lt;/p&gt;
&lt;h2 id="the-idea" class="relative group"&gt;The idea &lt;/h2&gt;&lt;p&gt;Way back in December 2022, my family decided that this christmas we would DIY each other some presents.&lt;/p&gt;
&lt;p&gt;For a few years prior we had been experimenting with a secret santa to alleviate some of the pains of having everyone each buy 10 gifts to give to each person, but when we realised most people&amp;rsquo;s &amp;ldquo;wants&amp;rdquo; aren&amp;rsquo;t within a £50 budget, we gave the &amp;ldquo;make something and give that&amp;rdquo; idea a go.&lt;/p&gt;
&lt;p&gt;My &lt;del&gt;unlucky victim&lt;/del&gt; adoring cousin was to be the recipient of whatever thing I could make, and since she was at the time completing her doctorate, enjoyed reading, and had recently started programming, I thought I would try and make something that combined all of those ridiculously nerdy attributes into one:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Thou shalt build a display to show a new quote every day&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;m sure the baby Jesus would&amp;rsquo;ve preferred one of these over some frankincense and myrrh any day.&lt;/p&gt;
&lt;p&gt;I had come back home for christmas and thought I could start work on making a V2 of my original e-ink display (&lt;a href="https://jamesjarvis.io/posts/2020/06/eink-dashboard/"&gt;read more about that one here&lt;/a&gt;), so this particular type of display was on my mind, and it fit perfectly with the goal of having a dumb little trinket sitting on a desk for months at a time without having to charge the battery.&lt;/p&gt;
&lt;h2 id="the-parts-list" class="relative group"&gt;The parts list &lt;/h2&gt;&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2025/02/secret-santa-a-quote-a-day-e-ink-display/parts-list_hu_c2de971aefe5708b.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="1774"
 height="1458"
 class="mx-auto my-0 rounded-md"
 alt="Parts List"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2025/02/secret-santa-a-quote-a-day-e-ink-display/parts-list_hu_6f409ba76ebcd44f.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;Screenshot of the parts list on pimoroni.com&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;You could 100% do this project for about half the price if you source components on AliExpress / some other site.&lt;/p&gt;
&lt;p&gt;However, I only had 1 week left to finish this project by the time I finally stopped procrastinating, I have always had a positive experience with Pimoroni and their shipping is normally only a couple days.&lt;/p&gt;
&lt;p&gt;Besides, I wouldn&amp;rsquo;t have to pull my hair out working out how to build a driver for this display, since the Badger2040 project is pretty well documented, and has a lot of support on &lt;a href="https://github.com/pimoroni/badger2040" target="_blank" rel="noreferrer"&gt;their GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The full parts list is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1x Pimoroni Badger 2040 (&lt;a href="https://shop.pimoroni.com/products/badger-2040?variant=39752959852627" target="_blank" rel="noreferrer"&gt;link&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;1x LiPo battery pack 1200mAh (&lt;a href="https://shop.pimoroni.com/products/lipo-battery-pack?variant=20429082183" target="_blank" rel="noreferrer"&gt;link&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;1x LiPo Amigo (&lt;a href="https://shop.pimoroni.com/products/lipo-amigo?variant=39779302506579" target="_blank" rel="noreferrer"&gt;link&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2025/02/secret-santa-a-quote-a-day-e-ink-display/back_hu_366811899c63fb5e.jpeg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="4032"
 height="3024"
 class="mx-auto my-0 rounded-md"
 alt="All plugged together"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2025/02/secret-santa-a-quote-a-day-e-ink-display/back_hu_bbed0819e1f2db5f.jpeg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;Back of the Badger2040 with battery attached&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2025/02/secret-santa-a-quote-a-day-e-ink-display/charging_hu_b7f0e671fddd4d05.jpeg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="3024"
 height="4032"
 class="mx-auto my-0 rounded-md"
 alt="LiPo battery charging with Amigo"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2025/02/secret-santa-a-quote-a-day-e-ink-display/charging_hu_af6a5ccd6f4e3929.jpeg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;Charging the battery pack with LiPo Amigo&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;You can choose to instead power the badger via a AAA battery pack, but I&amp;rsquo;m not a fan of disposable batteries.&lt;/p&gt;
&lt;p&gt;P.S. Pimoroni if you want to sponsor me I have my eyes on a few other things from your site! 😉&lt;/p&gt;
&lt;h2 id="the-quotes-list" class="relative group"&gt;The quotes list &lt;/h2&gt;&lt;p&gt;For this project I had about ~1.5MB of memory to play with on the Badger2040, so I couldn&amp;rsquo;t have too much data sitting around.
Though I did want enough to cover the goal of 1 quote a day, for as long as it takes to forget the other quotes, so around 2,040 quotes should do, assuming 200 characters per quote this&amp;rsquo;ll fit in under 0.5MB.&lt;/p&gt;
&lt;p&gt;Finding the quotes was quite hard, as most sites only host useless stuff like &amp;ldquo;Top 50 inspirational quotes&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Goodreads had probably the best corpus of material over on &lt;a href="https://www.goodreads.com/quotes" target="_blank" rel="noreferrer"&gt;https://www.goodreads.com/quotes&lt;/a&gt;, and if I were doing this again I would just write a quick script that scraped that page and trim by length of quote so it still fit on the small display.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s even a whole GitHub project for this: &lt;a href="https://github.com/maxhumber/quote" target="_blank" rel="noreferrer"&gt;github.com/maxhumber/quote&lt;/a&gt;, and nowadays a quick bit of ChatGPT / using your brain &lt;em&gt;audible groan&lt;/em&gt; could get you this data within a few moments.&lt;/p&gt;
&lt;p&gt;However, back in my last minute frantic I used a now-defunct API to scrape this data quickly. You can see my efforts at &lt;a href="https://github.com/jamesjarvis/secret-santa/blob/master/scraper/scraper.py" target="_blank" rel="noreferrer"&gt;github.com/jamesjarvis/secret-santa&lt;/a&gt; but don&amp;rsquo;t bother running it as the API seemed to have gone offline sometime in the last few years.&lt;/p&gt;
&lt;h2 id="displaying-quotes-on-the-badger2040" class="relative group"&gt;Displaying quotes on the Badger2040 &lt;/h2&gt;&lt;p&gt;Pimoroni offer a really good repository for writing software for the Badger2040.&lt;/p&gt;
&lt;p&gt;They have an embedded-systems subset of Python with very low resource consumption called &amp;ldquo;micropython&amp;rdquo;, that allows you to run pretty reasonable scripts on the RP2040 chip.&lt;/p&gt;
&lt;p&gt;Combining that with an alright badger2040 library to interact with the buttons and display, and it&amp;rsquo;s a good solid base point to write simple apps for the Badger2040 product.&lt;/p&gt;
&lt;p&gt;You can view the full source at &lt;a href="https://github.com/jamesjarvis/badger2040/blob/main/badger_os/examples/quotes.py" target="_blank" rel="noreferrer"&gt;github.com/jamesjarvis/badger2040&lt;/a&gt; but the gist of it is the &lt;code&gt;display_random_quote&lt;/code&gt; function, where we seek to a random line in a &amp;ldquo;quotes&amp;rdquo; file, and write it to the display, wrapping around the screen.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;display_random_quote&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_pen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_thickness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FONT_THICKNESS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Open the quotes file.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;quotes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;r&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOTAL_QUOTES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;quotes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;current_quote_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quotes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Read a full line and split it into words.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_quote_json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;latest_line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;latest_line_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;measure_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest_line&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;latest_line_length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;TEXT_WIDTH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest_line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;latest_line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;latest_line&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;latest_line&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest_line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;- &amp;#34;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;current_quote_json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;author&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;text_spacing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_spacing&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TEXT_PADDING&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_pen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# last line is the author, make a bit bolder.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_thickness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FONT_THICKNESS&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT_PADDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT_WIDTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If I were to ever productionise this, I would look to find a more efficient way to scan through the file to a random line, as that&amp;rsquo;ll probably save a bit of energy.&lt;/p&gt;
&lt;h3 id="uploading-code-to-the-badger2040" class="relative group"&gt;Uploading code to the Badger2040 &lt;/h3&gt;&lt;p&gt;Normally when you get this kind of project it&amp;rsquo;s all dependent on some random toolchain you haven&amp;rsquo;t installed on your machine, and it&amp;rsquo;s so out of date that you&amp;rsquo;ll spend your first day just debugging how to program the damn thing.&lt;/p&gt;
&lt;p&gt;With Pimoroni&amp;rsquo;s Badger2040 repo (forked to &lt;a href="https://github.com/jamesjarvis/badger2040" target="_blank" rel="noreferrer"&gt;github.com/jamesjarvis/badger2040&lt;/a&gt;) you can exploit the repo&amp;rsquo;s Github actions to build the necessary compiled target for you!&lt;/p&gt;
&lt;p&gt;The development flow was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clone badger2040 repo&lt;/li&gt;
&lt;li&gt;Make small changes (adding a &lt;a href="https://github.com/jamesjarvis/badger2040/blob/main/badger_os/examples/quotes.py" target="_blank" rel="noreferrer"&gt;quotes.py&lt;/a&gt; example App)&lt;/li&gt;
&lt;li&gt;Push commit to github and wait for the Action to build&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;badger2040-micropython-with-badger-os.uf2&lt;/code&gt; file from the Action artifact list&lt;/li&gt;
&lt;li&gt;Connect badger2040 to computer in boot mode (hold down &lt;code&gt;boot/usr&lt;/code&gt; and press &lt;code&gt;rst&lt;/code&gt; button as it is plugged in)&lt;/li&gt;
&lt;li&gt;Copy over the downloaded &lt;code&gt;.uf2&lt;/code&gt; file from step 4 to the badger&lt;/li&gt;
&lt;li&gt;Bobs your Uncle&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="the-outcome" class="relative group"&gt;The outcome &lt;/h2&gt;&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2025/02/secret-santa-a-quote-a-day-e-ink-display/secret-santa_hu_209973017f11c6b.gif"
 
 &gt;
 








 
 
 
 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 &lt;img
 width="640"
 height="480"
 class="mx-auto my-0 rounded-md"
 alt="Working Quotes Display"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2025/02/secret-santa-a-quote-a-day-e-ink-display/secret-santa_hu_f991e0e900fdb1c4.gif"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;Quotes Display refreshing to new quote&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;The outcome is quite nice if I say so myself.&lt;/p&gt;
&lt;p&gt;It lasts a few weeks on a single charge, and when you press an up or down button it&amp;rsquo;ll display a new random quote.
A simple but effective desk ornament.&lt;/p&gt;
&lt;p&gt;I never took a photo of the original I gave to my cousin, and I&amp;rsquo;m fairly sure it was &amp;ldquo;lost&amp;rdquo; quickly after christmas, so I made myself another one :smile:&lt;/p&gt;</content:encoded></item><item><title>Role Based Access Control (RBAC) Authorisation in Golang with Permit.io</title><link>https://jamesjarvis.io/posts/2024/11/role-based-access-control-rbac-authorisation-in-golang-with-permit.io/</link><pubDate>Mon, 04 Nov 2024 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2024/11/role-based-access-control-rbac-authorisation-in-golang-with-permit.io/</guid><category>Projects</category><description>What is &amp;lsquo;Authorisation&amp;rsquo;, and how to implement a working solution with Permit.io</description><content:encoded>&lt;p&gt;When building an application, you need to know who your users are, and what they should have access to.&lt;/p&gt;
&lt;p&gt;That’s when “Authentication” (are you who you say you are?) and “Authorisation” (are you allowed to do this thing?) come in to play.&lt;/p&gt;
&lt;p&gt;These are two similar but separate problems, each with their own challenges.&lt;/p&gt;
&lt;p&gt;Most developers don’t build their own authentication systems. It&amp;rsquo;s not-secure, and more importantly - not part of your products&amp;rsquo; core offering. Thus, it&amp;rsquo;s just not worth the effort / time you&amp;rsquo;ll spend building it.&lt;/p&gt;
&lt;p&gt;Authorisation is something traditionally built by developers in-house (Just like authentication was a few years ago), but in recent years, there are emerging options in the market that allow you to implement it without building it yourself.&lt;/p&gt;
&lt;p&gt;In this article we are going to focus on authorisation (authorization for my U.S. friends), covering a step by step integration and implementation with an example Key-Value service, written in the Go programming language.&lt;/p&gt;
&lt;p&gt;We are going to build our own naive authorisation system to get a feel for how it all works, and compare this with a production-ready implementation from &lt;a href="http://permit.io" target="_blank" rel="noreferrer"&gt;Permit.io&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="what-is-authorisation" class="relative group"&gt;What is &amp;ldquo;Authorisation&amp;rdquo;? &lt;/h2&gt;&lt;p&gt;At some point in the development of any &amp;ldquo;System&amp;rdquo; with &amp;ldquo;Users&amp;rdquo;, you will need some method of determining:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Is this &amp;ldquo;User&amp;rdquo; &lt;em&gt;actually&lt;/em&gt; &lt;em&gt;allowed&lt;/em&gt; to do the &lt;em&gt;thing&lt;/em&gt; they are trying to do?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is inevitable, whether the &amp;ldquo;system&amp;rdquo; you are building is physical (i.e. a Hotel), or virtual (i.e. a Social Network website).&lt;/p&gt;
&lt;p&gt;For any sensitive action, we need to make sure that the person performing that action is &lt;em&gt;allowed&lt;/em&gt; to do the action.
If the scenario is as simple as &lt;code&gt;if request.IsAuthenticated()&lt;/code&gt;, then congrats, you can end here, you have &amp;ldquo;authorisation through authentication&amp;rdquo;. &lt;strong&gt;But&lt;/strong&gt;, this means that &lt;strong&gt;any user who can authenticate into your system, is also authorised do to anything in your system&lt;/strong&gt;. For everything but a simple POC, this won&amp;rsquo;t fly.&lt;/p&gt;
&lt;p&gt;If the scenario is more complex, i.e. &lt;code&gt;if request.GetUser() not in authorisedUsers&lt;/code&gt;, then you are now able to &lt;strong&gt;restrict access to parts of your system&lt;/strong&gt;, but you have now inherited a complexity: you need to &lt;strong&gt;store and maintain a list of every single user allowed to perform every action in your system&lt;/strong&gt;.
If you have 100 users, and 5 actions in your system (a very small example), this can become a headache to manage very quickly, let alone if you have &amp;gt;1000 users, or more actions to perform.&lt;/p&gt;
&lt;p&gt;To simplify this, we can instead assign abilities to an abstract concept (Roles), this is where RBAC comes in.&lt;/p&gt;
&lt;h2 id="what-is-role-based-access-control-rbac" class="relative group"&gt;What is &amp;ldquo;Role Based Access Control&amp;rdquo; (RBAC) &lt;/h2&gt;&lt;p&gt;Rather than storing a map of user -&amp;gt; allowed actions, or actions -&amp;gt; allowed user, let&amp;rsquo;s abstract this a little.
Introducing the concept of &amp;ldquo;Roles&amp;rdquo;: A label you can pin to a whole bunch of users, and check against that.&lt;/p&gt;
&lt;p&gt;Benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Low cardinality
&lt;ul&gt;
&lt;li&gt;You may have millions of users, but you&amp;rsquo;ll rarely need more than a few dozen &amp;ldquo;roles&amp;rdquo;&lt;/li&gt;
&lt;li&gt;This is why the CIA has &amp;ldquo;security clearance levels&amp;rdquo;, rather than granting access to information individually&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Easier to manage
&lt;ul&gt;
&lt;li&gt;Easier to remove 1 &amp;ldquo;role&amp;rdquo; from 1 &amp;ldquo;user&amp;rdquo;, than updating the 1000s of actions a &amp;ldquo;user&amp;rdquo; may have access to&lt;/li&gt;
&lt;li&gt;This is why the CIA has &amp;ldquo;security clearance levels&amp;rdquo;, rather than granting access to information individually&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Semantically&lt;/em&gt; relevant
&lt;ul&gt;
&lt;li&gt;Wherever possible, as a developer your goal is to simplify the system&lt;/li&gt;
&lt;li&gt;Example: Seeing that a user has the roles of &lt;code&gt;[&amp;quot;intern&amp;quot;, &amp;quot;employee&amp;quot;, &amp;quot;admin&amp;quot;]&lt;/code&gt;, it is clearer to spot the problem than seeing a user with access to &lt;code&gt;[&amp;quot;receptionDoor1&amp;quot;, &amp;quot;receptionDoor2&amp;quot;, &amp;quot;cafeDoor3&amp;quot;, &amp;quot;floor4Door4&amp;quot;, &amp;quot;floor2Door3&amp;quot;]&lt;/code&gt;, if &lt;code&gt;&amp;quot;floor4Door4&amp;quot;&lt;/code&gt; is your server room&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now that we understand the benefits of simplifying access control through the use of “Roles” users can be assigned to, let’s look at a demo application in Golang.&lt;/p&gt;
&lt;h2 id="example-application" class="relative group"&gt;Example Application &lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s keep this super simple, and say that you have 2 endpoints to an application that allows users to save and retrieve key-value pairs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /v1/map/{key}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /v1/map/{key}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each &amp;ldquo;user&amp;rdquo;, once authenticated, can be in one of two groups (&amp;ldquo;roles&amp;rdquo;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reader&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writer&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;ldquo;Readers&amp;rdquo; can &lt;strong&gt;GET&lt;/strong&gt;, &amp;ldquo;Writers&amp;rdquo; can &lt;strong&gt;POST&lt;/strong&gt;, a user can have either or both of these roles.&lt;/p&gt;
&lt;p&gt;i.e. If I am a user (”Alice”), and I have the ability to &lt;strong&gt;write&lt;/strong&gt;, I should be able to write a value of “world” to a key of “hello”.&lt;/p&gt;
&lt;p&gt;If I am a user (”Bob”), and I have the ability to &lt;strong&gt;read&lt;/strong&gt;, I should be able to see the value “world” under key “hello” that alice has just written for me.&lt;/p&gt;
&lt;p&gt;However, since Bob cannot write, he should not be able to override the value of “world”, nor should he be able to add any new values to our system.&lt;/p&gt;
&lt;p&gt;To show how to implement this sort of authorisation control to a system, we are going to start off with a working key-value service for the above, without any access controls in place.&lt;/p&gt;
&lt;p&gt;We will then build our own RBAC system given the users and roles we know about and integrate it with our system.&lt;/p&gt;
&lt;p&gt;To top it all off, we are then we are going to showcase a scalable, production ready setup built using the &lt;a href="http://Permit.io" target="_blank" rel="noreferrer"&gt;Permit.io&lt;/a&gt; Golang SDK for RBAC.&lt;/p&gt;
&lt;p&gt;First, our Service setup:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// StorageClient provides access to a key-value map.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;StorageClient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;exists&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Service is an implementation of the key-value service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;StorageClient&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Get accepts a user and a key, and returns either the value associated with that key,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// or an error if encountered.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;error getting value for key:%q error:%w&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;error getting value for key:%q error:%w&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Error_NOTFOUND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Set accepts a user, key and value, and will attempt to set the value with that key,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// or return an error if encountered.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;error setting value for key:%q error:%w&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Using our &lt;code&gt;StorageClient&lt;/code&gt; interface, and the &lt;code&gt;Get&lt;/code&gt; and &lt;code&gt;Set&lt;/code&gt; application APIs, we have the ability to read and write any key-value pair we desire, without any authorisation logic to safeguard access.
Get allows any user to extract the value for a given key, and Set allows any user to write the value at a given key.&lt;/p&gt;
&lt;p&gt;Simple, right? Now let’s try and implement our own RBAC!&lt;/p&gt;
&lt;h3 id="restricting-access-to-our-api" class="relative group"&gt;Restricting Access to our API &lt;/h3&gt;&lt;p&gt;Remember, we now want to restrict access to &lt;code&gt;Get&lt;/code&gt; and &lt;code&gt;Set&lt;/code&gt; based on whether the given user is a &amp;ldquo;reader&amp;rdquo; or a &amp;ldquo;writer&amp;rdquo;.
To build an API for checking the authorisation level of the user, we really only care about whether they are or aren&amp;rsquo;t authorised to access the resource.
A good API for this, therefore, would enable us to supply our &lt;code&gt;userID&lt;/code&gt;, and their intended &lt;code&gt;action&lt;/code&gt;, and allow the API to resolve the user&amp;rsquo;s &lt;code&gt;roles&lt;/code&gt;, and the &lt;code&gt;permitted roles&lt;/code&gt; for each action, and return us &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, depending on whether the user can or cannot perform the intended action.&lt;/p&gt;
&lt;p&gt;A good interface for this then, would follow:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// AccessClient enables the ability to check authorisation for a given user.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AccessClient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Popping this into our implementation is quite simple, as an initial validation step for our API:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;get&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Error_UNAUTHORISED&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;set&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Error_UNAUTHORISED&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For this &lt;code&gt;AccessClient&lt;/code&gt;to work however, we need an implementation&amp;hellip;&lt;/p&gt;
&lt;h3 id="diy-in-memory-rbac" class="relative group"&gt;DIY: In-memory RBAC &lt;/h3&gt;&lt;p&gt;Those with a keen eye for Go would notice we have defined our authorisation API first, ignoring any eventual implementation.
This is a good way to focus on your API first, without getting too bogged down with how your API implementation should actually work.&lt;/p&gt;
&lt;p&gt;To get our heads around the problem, we can look into a simple implementation of this API, using an in-memory map to keep track of our users, the actions that may be performed, and the roles permitted to complete those actions.&lt;/p&gt;
&lt;p&gt;Our objects &lt;code&gt;User&lt;/code&gt; and &lt;code&gt;Action&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// User is a user object, containing the roles this user is a member of.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Roles&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Action is an action that can be performed, along with the roles allowed to perform this action.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PermittedRoles&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Our &lt;code&gt;Check&lt;/code&gt; function, needs to answer the question of &lt;code&gt;Does the given user have a role, that is permitted to complete the given action?&lt;/code&gt;
In reality, this manifests quite easily as a reverse lookup:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Check returns true if the user is allowed to perform the action, false otherwise.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Access&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;actionID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;actionID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Roles&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PermittedRoles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the above, if the user exists, the action exists, and at least one of the user&amp;rsquo;s roles match with at least one of the action&amp;rsquo;s permitted roles, then we &lt;code&gt;return true&lt;/code&gt;, and the action is permitted!&lt;/p&gt;
&lt;p&gt;Conversely, if the user is unknown, or the action is unknown or none of the user&amp;rsquo;s roles match with any of the action&amp;rsquo;s permitted roles, then we &lt;code&gt;return false&lt;/code&gt;, and the action is denied, at which point an UNAUTHORISED error is returned to the the caller.&lt;/p&gt;
&lt;h3 id="flaws-with-diy" class="relative group"&gt;Flaws with DIY &lt;/h3&gt;&lt;p&gt;In practise however, I don’t recommend you to actually use the above DIY approach.
DIY-ing your own authorisation implementation is something that should really only be attempted by the Googles of this world, as there is a good chance you simply will &lt;strong&gt;not have the resources&lt;/strong&gt; to do it &lt;strong&gt;correctly&lt;/strong&gt;, let alone &lt;strong&gt;quickly&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Focus on outsourcing the implementation of all elements of your system that are not specifically critical to the value you intend to provide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Don&amp;rsquo;t build your own database&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t build your own container orchestrator&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t build your own authorisation system&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t do anything that you don&amp;rsquo;t have to&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="alternative-rbac-with-permitio" class="relative group"&gt;Alternative: RBAC with &lt;a href="http://permit.io/" target="_blank" rel="noreferrer"&gt;permit.io&lt;/a&gt; &lt;/h3&gt;&lt;p&gt;&lt;a href="https://permit.io/" target="_blank" rel="noreferrer"&gt;Permit.io&lt;/a&gt; is an authorisation as a service provider, with a whole suite of features that cover use cases from this simple example, all the way to the largest enterprise applications with millions of users and umpteen levels of complexity.&lt;/p&gt;
&lt;p&gt;This solution was easy enough to work with that I could build this example implementation in &amp;lt;30 mins from reading through their docs (ChatGPT could probably do it faster though!)&lt;/p&gt;
&lt;p&gt;Conveniently, the Permit Golang SDK also has it&amp;rsquo;s own &lt;code&gt;Check&lt;/code&gt; function, that is similar to ours:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// PermitClient is the permit.io client interface.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PermitClient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;enforcement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;enforcement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;enforcement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For our case, to provide a drop in replacement to our DIY solution, we are going to wrap this function.
The models in this API are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;User&amp;rdquo;
&lt;ul&gt;
&lt;li&gt;Identifiers for a given user to the system&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Action&amp;rdquo;
&lt;ul&gt;
&lt;li&gt;The intent for a given call to &amp;ldquo;Check&amp;rdquo;, in our case this will be &amp;ldquo;get&amp;rdquo; or &amp;ldquo;set&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Resource&amp;rdquo;
&lt;ul&gt;
&lt;li&gt;The domain for which this action is being performed against&lt;/li&gt;
&lt;li&gt;In our simple example, we do not have a need for this &amp;ldquo;resource&amp;rdquo;, as we only have a single map domain, but this allows for other rich customisations later on, such as namespaced maps with alternative access controls.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our wrapper:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Check returns true if the user is allowed to perform the action, false otherwise.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Permit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;actionID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;enforcement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UserBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;enforcement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actionID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;enforcement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResourceBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;map&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permitClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;permit error: %s\\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="our-final-application" class="relative group"&gt;Our final application &lt;/h3&gt;&lt;p&gt;If you want to dive deeper into the code, and run the application yourself (with both DIY and Permit based authorisation), you can follow the steps on &lt;a href="https://github.com/jamesjarvis/rbac-example" target="_blank" rel="noreferrer"&gt;github.com/jamesjarvis/rbac-example&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Our test data includes the following users, and their roles:&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;UserID&lt;/th&gt;
					&lt;th&gt;Role: reader&lt;/th&gt;
					&lt;th&gt;Role: writer&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;alice&lt;/td&gt;
					&lt;td&gt;Y&lt;/td&gt;
					&lt;td&gt;Y&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;bob&lt;/td&gt;
					&lt;td&gt;&lt;/td&gt;
					&lt;td&gt;Y&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;charli&lt;/td&gt;
					&lt;td&gt;Y&lt;/td&gt;
					&lt;td&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Our server is a HTTP server running on port &lt;code&gt;:8080&lt;/code&gt;, so we can perform a simple test by setting a value &amp;ldquo;world&amp;rdquo; for key &amp;ldquo;hello&amp;rdquo; as user &amp;ldquo;alice&amp;rdquo;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -H &lt;span class="s2"&gt;&amp;#34;User: alice&amp;#34;&lt;/span&gt; -X POST &amp;lt;http://localhost:8080/v1/map/hello&amp;gt; -d &lt;span class="s2"&gt;&amp;#34;world&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt; world
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Fantastic, this works! Alice is indeed able to write to the &lt;code&gt;POST&lt;/code&gt; endpoint, as they have the role of &amp;ldquo;writer&amp;rdquo;. We can see since they also have the role of &amp;ldquo;reader&amp;rdquo;, they should also be able to &lt;code&gt;GET&lt;/code&gt; this value again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -H &lt;span class="s2"&gt;&amp;#34;User: alice&amp;#34;&lt;/span&gt; -X GET &amp;lt;http://localhost:8080/v1/map/hello&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt; world
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But what if Bob tries to read this value?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -H &lt;span class="s2"&gt;&amp;#34;User: bob&amp;#34;&lt;/span&gt; -X GET &amp;lt;http://localhost:8080/v1/map/hello&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt; UNAUTHORISED
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bob cannot perform any GET calls, because Bob does not have the &amp;ldquo;reader&amp;rdquo; role, which is required to perform the &amp;ldquo;get&amp;rdquo; action. Bob can, however, perform POST requests.&lt;/p&gt;
&lt;p&gt;Similarly, our final user Charli does not have the &amp;ldquo;writer&amp;rdquo; role, preventing them from accessing the POST endpoint, but since they are a &amp;ldquo;reader&amp;rdquo;, they can still perform &lt;code&gt;GET&lt;/code&gt; requests:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -H &lt;span class="s2"&gt;&amp;#34;User: charli&amp;#34;&lt;/span&gt; -X GET &amp;lt;http://localhost:8080/v1/map/hello&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt; world
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="summary" class="relative group"&gt;Summary &lt;/h2&gt;&lt;p&gt;Authorisation is the challenge of answering “Is this User &lt;em&gt;actually&lt;/em&gt; &lt;em&gt;allowed&lt;/em&gt; to do the &lt;em&gt;thing&lt;/em&gt; they are trying to do?”&lt;/p&gt;
&lt;p&gt;By focusing on Roles, rather than Users (RBAC), application writers can greatly simplify both their application logic, and the operation of their application.&lt;/p&gt;
&lt;p&gt;We have built a working HTTP Key-Value service in Golang, that allows a user to &lt;code&gt;GET&lt;/code&gt; (read) and &lt;code&gt;POST&lt;/code&gt; (write) values at a given key.&lt;/p&gt;
&lt;p&gt;We have then extended this application to restrict access to these endpoints based on whether the user has the associated role of “reader” or “writer”.&lt;/p&gt;
&lt;p&gt;We have explored the pros and cons of two different implementation to this RBAC solution:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DIY using just the Golang standard library&lt;/li&gt;
&lt;li&gt;Using &lt;a href="http://Permit.io" target="_blank" rel="noreferrer"&gt;Permit.io&lt;/a&gt; as our Authentication as a service provider&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Phew!&lt;/p&gt;
&lt;p&gt;By this point, you should have a decent understanding of how a simple Role Based Access Control (RBAC) implementation can be integrated into your application.&lt;/p&gt;
&lt;p&gt;Focusing on &amp;ldquo;roles&amp;rdquo; rather than &amp;ldquo;users&amp;rdquo;, provides an easier abstraction for both yourself as a developer, but also your system administrators, and ultimately, your users themselves.&lt;/p&gt;
&lt;p&gt;Finally, another word of caution.&lt;/p&gt;
&lt;p&gt;Focus on outsourcing the implementation of all elements of your system that are &lt;strong&gt;not specifically critical to the value you intend to provide&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Don&amp;rsquo;t build your own databases&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t build your own container orchestrators&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t build your own authorisation systems&lt;/li&gt;
&lt;li&gt;Do focus on the core offering of your product&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks for reading!&lt;/p&gt;</content:encoded></item><item><title>📖 Blogging Site</title><link>https://jamesjarvis.io/posts/2023/07/blogging-site/</link><pubDate>Mon, 10 Jul 2023 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2023/07/blogging-site/</guid><category>Projects</category><description>Owning my place on the internet, like the good old days</description><content:encoded>&lt;p&gt;I am growing up &lt;em&gt;&lt;strong&gt;[citation needed]&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;With this, comes a lot of realisations about what I value from the internet, and in particular about the things I build.&lt;/p&gt;
&lt;h3 id="the-rewrite" class="relative group"&gt;The rewrite &lt;/h3&gt;&lt;p&gt;My first iteration of this personal website was written in GatsbyJS, primarily as a means to show off the previous things I have built &lt;a href="https://jamesjarvis.io/posts/2019/02/portfolio-site/"&gt;more info in this previous article&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However, I rarely wrote to this website, and I never really promoted it on other platforms for the purpose of getting views.&lt;/p&gt;
&lt;p&gt;I also wanted to write about things that didn&amp;rsquo;t necessarily fall under the rather specific category of &amp;ldquo;project&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;As such, my website was starting to develop an identity crisis and the weakest part of the website was, frustratingly, my own writing.&lt;/p&gt;
&lt;p&gt;I was originally hoping that this website would offer me a creative outlet where I could practise and improve my own writing style, with the auxillary goal of basically being a verbose CV to show to prospective employers or my peers.&lt;/p&gt;
&lt;p&gt;But, GatsbyJS was simply too flexible for this purpose and in turn became a burden, drawing away my own attention from writing and providing content for this site.&lt;/p&gt;
&lt;p&gt;This rewrite, is deliberately hands-off.&lt;/p&gt;
&lt;p&gt;I wanted to keep the same design language, but with a further emphasis on &lt;strong&gt;writing&lt;/strong&gt;, both project-style where I want to write a summary, but also to create the space for more long-form content in the future.
Eventually, I would like to expand this approach to also create the space for engagement with readers in the future (once I get readers!), however this will not be a priority yet until I am confident in the content.&lt;/p&gt;
&lt;p&gt;For now, &lt;strong&gt;the rewrite&lt;/strong&gt; has resulted in removing a few thousands lines of JavaScript, leaving the entire website as just a collection of markdown files and image assets.
Much nicer.&lt;/p&gt;
&lt;h3 id="why-not-medium-devto-xcom-etc" class="relative group"&gt;Why not [medium, dev.to, x.com, etc]? &lt;/h3&gt;&lt;p&gt;I want full control over my presence on the internet.&lt;/p&gt;
&lt;p&gt;I want to be able to decide entirely by myself whether I am public, private, free for all to read, or behind a paywall.&lt;/p&gt;
&lt;p&gt;I want to have at least a false sense of security that my writing will not be too easy to crawl for LLM&amp;rsquo;s to reproduce.&lt;/p&gt;
&lt;p&gt;But most of all, I want to be able to go back to the old micro-web of days gone past, where each person has their own bubble on the internet.
I think it&amp;rsquo;s quite a charming approach, and I hope you do too.&lt;/p&gt;
&lt;p&gt;I will possibly start to link to my website from other platforms when I post a new article, but I want the location to stay here, on this domain.&lt;/p&gt;
&lt;h3 id="learning-to-write" class="relative group"&gt;Learning to write &lt;/h3&gt;&lt;p&gt;I am going to take a mini-sabbatical from work later this year, spending 4.5 months out of the U.K., working and travelling around South East Asia.&lt;/p&gt;
&lt;p&gt;I will be leaving my personal laptop here, only taking my phone, camera and a notebook.
I&amp;rsquo;m hoping to teach myself how to enjoy the creative process.
If that works, You&amp;rsquo;ll be hearing more from me soon!&lt;/p&gt;</content:encoded></item><item><title>⠪ Mapping the Internet, part deux</title><link>https://jamesjarvis.io/posts/2021/02/mapping-the-internet-part-two/</link><pubDate>Fri, 05 Feb 2021 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2021/02/mapping-the-internet-part-two/</guid><category>Projects</category><description>Links on links on links with a UI, man</description><content:encoded>&lt;p&gt;Did reading the previous instalment of &amp;ldquo;mapping the internet&amp;rdquo; &lt;a href="https://jamesjarvis.io/posts/2020/10/backlink-crawler/"&gt;read here&lt;/a&gt; leave you a bit&amp;hellip; underwhelmed?&lt;/p&gt;
&lt;p&gt;Same here.
I mean, sure I had built a little program that could follow and store links on the internet, but it couldn&amp;rsquo;t be ran continuously (could not store the crawling state between runs), and there was just no easy way to traverse or visualise the graph &lt;em&gt;yawn&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So, in this chapter I am going to go through how we have improved this service to become the beautifully graphical service that is:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://jamesjarvis.github.io/web-graph/" target="_blank" rel="noreferrer"&gt;jamesjarvis.github.io/web-graph&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: By default, the graph starts from &lt;a href="https://jamesjarvis.io" target="_blank" rel="noreferrer"&gt;jamesjarvis.io&lt;/a&gt;, but if you want to change it to start from a page of your choosing (provided it has managed to scrape that part of the web already), you can give it a shot by changing the url query parameter such as: &lt;a href="https://jamesjarvis.github.io/web-graph/?url=https://en.wikipedia.org/wiki/London" target="_blank" rel="noreferrer"&gt;/?url=https://en.wikipedia.org/wiki/London&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="part-1-maintaining-state" class="relative group"&gt;Part 1: Maintaining state &lt;/h2&gt;&lt;p&gt;So I wanted to eventually run this service on a raspberry pi 4 I currently have just sitting in my attic doing &lt;a href="https://jamesjarvis.io/posts/2020/01/speedtest/"&gt;occasional speedtests&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also decided that I want to be able to stop and start the crawls (say, when performing an upgrade), without losing any data between runs.&lt;/p&gt;
&lt;p&gt;In order to facilitate this, I need to have a list of URL&amp;rsquo;s that have been &amp;ldquo;seen&amp;rdquo; but not &amp;ldquo;crawled&amp;rdquo; yet.&lt;/p&gt;
&lt;p&gt;I had 3 ways I could think of doing this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Spin up a message queue system, like &lt;a href="https://kafka.apache.org" target="_blank" rel="noreferrer"&gt;kafka&lt;/a&gt; or &lt;a href="https://www.rabbitmq.com" target="_blank" rel="noreferrer"&gt;rabbitMQ&lt;/a&gt; to store unprocessed URLs and yeet them out to processors for the actual crawling.&lt;/li&gt;
&lt;li&gt;Add another table in the database, with the pending URLs.&lt;/li&gt;
&lt;li&gt;Store this information in a simple queue file (read from the top, append to the bottom).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I tried all 3:&lt;/p&gt;
&lt;h3 id="rabbitmq" class="relative group"&gt;RabbitMQ &lt;/h3&gt;&lt;p&gt;First, I tried RabbitMQ, and got it working after a few hours of mucking about with it.&lt;/p&gt;
&lt;p&gt;However, it &lt;em&gt;ate&lt;/em&gt; my poor raspberry pi&amp;rsquo;s RAM as soon as the queue was over a few thousand URLs, and I couldn&amp;rsquo;t work out how to persist messages properly.&lt;/p&gt;
&lt;p&gt;I also didn&amp;rsquo;t really need the complexity, as I didn&amp;rsquo;t want my ISP to boot me off the network for making a continuous 100 requests/s, so I wouldn&amp;rsquo;t have bothered with more than one consumer anyway.&lt;/p&gt;
&lt;p&gt;If I was building this to be an actual, scaleable service, then I&amp;rsquo;d have swallowed the extra AWS bill and continued, but this ain&amp;rsquo;t it chief.&lt;/p&gt;
&lt;h3 id="whack-it-in-a-db-table" class="relative group"&gt;Whack it in a DB table &lt;/h3&gt;&lt;p&gt;Another embarrassing thing about the infra for this project is that I&amp;rsquo;m running the raspberry pi with an 8 year old 1TB external hard drive I had lying around.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not the fastest and is in fact the biggest performance bottleneck (if I could be bothered, I would have bought an SSD but this isn&amp;rsquo;t worth £80 + dealing with this limitation is interesting enough).&lt;/p&gt;
&lt;p&gt;As a result of my cheapskate-ness, this is also out of the question, I needed something more lightweight.&lt;/p&gt;
&lt;h3 id="simple-file-based-queue-system" class="relative group"&gt;Simple file based queue system &lt;/h3&gt;&lt;p&gt;This was the way, effectively traverse the web in a breadth-first fashion by appending any found URLs to the end of a list, and crawl any URLs by taking from the top.&lt;/p&gt;
&lt;p&gt;Nice and easy.&lt;/p&gt;
&lt;h2 id="part-2-making-the-data-easy-to-query" class="relative group"&gt;Part 2: Making the data easy to query &lt;/h2&gt;&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2021/02/mapping-the-internet-part-two/api-splashscreen_hu_7a4a464dd193f5b6.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="1962"
 height="398"
 class="mx-auto my-0 rounded-md"
 alt="Splashscreen for the API"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2021/02/mapping-the-internet-part-two/api-splashscreen_hu_882532e23f69ab76.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;Screenshot of the API splashscreen&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Eventually, I wanted to expose this information, but in an easily cacheable and web friendly way.&lt;/p&gt;
&lt;p&gt;Enter: an API service.&lt;/p&gt;
&lt;p&gt;You can actually check it out if you want at &lt;a href="https://api.jamesjarvis.io" target="_blank" rel="noreferrer"&gt;api.jamesjarvis.io&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been set up to be as easily cache-able (thanks to cloudflare) as possible, and it&amp;rsquo;s open for anyone to use, provided you&amp;rsquo;re aware it&amp;rsquo;s hitting the shitbox in my attic and I don&amp;rsquo;t want my ISP to throw me off their network.&lt;/p&gt;
&lt;p&gt;Also, I used different queries than the first iteration of this project, in order to speed it up.
Indexes help a lot as well! Once the DB grew to &amp;gt;100k items queries used to take minutes, and now after some enthusiastic indexing can take &amp;lt;1s.&lt;/p&gt;
&lt;p&gt;Some fun things you can do with it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find all pages at a particular host (limited to 100): &lt;a href="https://api.jamesjarvis.io/pages/en.wikipedia.org" target="_blank" rel="noreferrer"&gt;/pages/en.wikipedia.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Get an overview of a particular page (just URL, host and links from, limited to 100): &lt;a href="https://api.jamesjarvis.io/page/e518da42a48097ceb7648a9961428e300f2db545" target="_blank" rel="noreferrer"&gt;/page/HASH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Find all links &lt;em&gt;from&lt;/em&gt; a particular page (limited to 100): &lt;a href="https://api.jamesjarvis.io/linksFrom/e518da42a48097ceb7648a9961428e300f2db545" target="_blank" rel="noreferrer"&gt;/linksFrom/HASH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Find all links &lt;em&gt;to&lt;/em&gt; a particular page (limited to 100): &lt;a href="https://api.jamesjarvis.io/linksTo/e518da42a48097ceb7648a9961428e300f2db545" target="_blank" rel="noreferrer"&gt;/linksTo/HASH&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last one is currently not being used by myself, though I may make an inverse graph traversal, whereby you can discover all the pages that link to a page&amp;hellip;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also set up my &lt;a href="https://jamesjarvis.io/posts/2020/06/eink-dashboard/"&gt;e-ink display&lt;/a&gt; to monitor the number of &lt;a href="https://api.jamesjarvis.io/countPages" target="_blank" rel="noreferrer"&gt;URL&amp;rsquo;s crawled&lt;/a&gt; and &lt;a href="https://api.jamesjarvis.io/countLinks" target="_blank" rel="noreferrer"&gt;links found&lt;/a&gt; for my own amusement as well.&lt;/p&gt;
&lt;h3 id="self-hosted" class="relative group"&gt;Self hosted?? &lt;/h3&gt;&lt;p&gt;Yup, it&amp;rsquo;s slow as shit and is as reliable as my ISP (who apparently work on a &lt;em&gt;best effort&lt;/em&gt; case), but thanks to some aggressive cloudflare caching, the most common endpoints should be relatively quick.&lt;/p&gt;
&lt;h2 id="part-3-graphical-representation" class="relative group"&gt;Part 3: Graphical representation &lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;What would a visual representation of browsing the internet look like?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2021/02/mapping-the-internet-part-two/web-graph_hu_cbfd0b32849a0905.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="2278"
 height="1716"
 class="mx-auto my-0 rounded-md"
 alt="Look mum, I&amp;rsquo;ve crashed my browser!"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2021/02/mapping-the-internet-part-two/web-graph_hu_1d7c42e8451b227f.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Check it out at &lt;a href="https://jamesjarvis.github.io/web-graph/" target="_blank" rel="noreferrer"&gt;jamesjarvis.github.io/web-graph&lt;/a&gt; , and if you want to start from a website of your choosing, you can modify the url query param like: &lt;code&gt;https://jamesjarvis.github.io/web-graph/?url={your_page_here}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Because I couldn&amp;rsquo;t be bothered to set up a proper react application + build process for this project, the whole thing is in one html file, and actually transpiles the required javascript from reactJS source on load time - maybe not the fastest but hey this isn&amp;rsquo;t exactly a performance project + it&amp;rsquo;s so easy to deploy.&lt;/p&gt;
&lt;p&gt;You can check out the whole front-end source &lt;a href="https://github.com/jamesjarvis/web-graph/blob/master/frontend/index.html" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;, ~200 lines is all you need for this &lt;em&gt;beauty&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I ended up using a react wrapper around &lt;a href="https://d3js.org" target="_blank" rel="noreferrer"&gt;D3.js&lt;/a&gt; for the actual graphing, and it made me remember just how much I love and hate javascript.
It&amp;rsquo;s a rocky relationship.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll also notice that running this site is very cpu intensive, and yes it is, nothing more to add really.
My personal record was &lt;code&gt;1874 nodes&lt;/code&gt; and &lt;code&gt;3556 links&lt;/code&gt; expanded before my browser crashed, your mileage may vary.&lt;/p&gt;
&lt;h2 id="part-4-what-have-i-learnt-from-this" class="relative group"&gt;Part 4: What have I learnt from this? &lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Hardware can be a real bottleneck, next time use an SSD for IO intensive operations.&lt;/li&gt;
&lt;li&gt;Building services to be resilient to failure is extremely liberating.&lt;/li&gt;
&lt;li&gt;JavaScript still sucks to use, package management is still hell and documentation is generally just a bit weak, considering you get zero type information from the language itself.&lt;/li&gt;
&lt;li&gt;Proper database indexes are the difference between good and shit.&lt;/li&gt;
&lt;li&gt;Caching is still amazing.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Some of my favourite snippets of code:&lt;/p&gt;
&lt;h3 id="channel-based-interface-for-reading-off-of-a-queue" class="relative group"&gt;Channel based interface for reading off of a queue &lt;/h3&gt;&lt;p&gt;Look how neat this is, as waiting to read from the queue channel / sending to the processing channel are both blocking operations, this works as fast as these two components.
We are also able to pre-fetch a number of items from the queue&amp;rsquo;s file if we wanted to improve performance a bit.&lt;/p&gt;
&lt;p&gt;This also enables an easy method of gracefully shutting down the program.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;linkProcessor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;linkprocessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewLinkProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;linkStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;failOnError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Could not initialise link processor&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Begin processing...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;urlProcessingChan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;linkProcessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SpawnWorkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;sigs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sigs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;syscall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;syscall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;syscall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SIGHUP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;syscall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SIGKILL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewTicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;processing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;processing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;sigs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;processing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Received signal %s, shutting down gracefully...\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DeQueue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;urlProcessingChan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%d urls in the queue&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;linkProcessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GracefulShutdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;===== Shut down link processor =====&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="periodic-batcher-from-a-buffered-channel" class="relative group"&gt;Periodic batcher from a buffered channel &lt;/h3&gt;&lt;p&gt;This is a slightly improved version from the original post.&lt;/p&gt;
&lt;p&gt;This turns out to be a great way to convert a message by message insert operation into a batchable operation.
It also works as a great way to rate limit database operations, as you can still fill up the channel up to the buffer size, and slowly drain it at whatever rate you choose.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;PageBatcher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endSignal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;doneChan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// We want it to die on the endSignal, but otherwise keep looping&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;endSignal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;doneChan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Page&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxBatch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufChan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;					&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;					&lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Remaining&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="c1"&gt;// The batch processing&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BatchAddPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Batch adding pages failed!: %e&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content:encoded></item><item><title>🗺 Visualising location history</title><link>https://jamesjarvis.io/posts/2020/12/mappyboi/</link><pubDate>Tue, 01 Dec 2020 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2020/12/mappyboi/</guid><category>Projects</category><description>Where did he come from, where did he go?</description><content:encoded>&lt;p&gt;&lt;a href="https://github.com/jamesjarvis/mappyboi" target="_blank" rel="noreferrer"&gt;Check it out on GitHub!&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So this one is more of a revisit than a totally unique project, but this all came about thanks to a little sprinkling of &lt;em&gt;dependency hell&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The goal of this little project was to be able to visualise my google location history (and other source of location info such as strava activities or workouts from my apple watch) on a strava style heatmap &lt;a href="https://www.strava.com/heatmap" target="_blank" rel="noreferrer"&gt;like this one&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My first foray into this was little more than a fork of luka1199/geo-heatmap &lt;a href="https://github.com/luka1199/geo-heatmap" target="_blank" rel="noreferrer"&gt;link&lt;/a&gt; as I&amp;rsquo;m a sucker for believing that a lot of GitHub stars means a project is well maintained (silly me).&lt;/p&gt;
&lt;p&gt;A previous fork I made tried to incorporate Poetry &lt;a href="https://python-poetry.org" target="_blank" rel="noreferrer"&gt;link&lt;/a&gt; for more deterministic python dependency management and execution.
Sounds good right?&lt;/p&gt;
&lt;h2 id="python-dependency-hell" class="relative group"&gt;Python dependency hell &lt;/h2&gt;&lt;p&gt;However, when trying to run this script after upgrading to macOS 11, I was getting some issues while trying to download NumPy (see more &lt;a href="https://github.com/numpy/numpy/issues/17784" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I know this may seem like a little issue, but the goal for this project was to simply rerun this script every year or so, minimal effort so I can just update a file with my latest location history.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NumPy doesn&amp;rsquo;t work? Why the fuck not? Why do I care? What does NumPy have to do with creating a .html file?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Since this is ultimately a fairly simple project I decided to just spend a couple hours and rewrite in one of the most backwards compatible languages I know of: &lt;strong&gt;Golang&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I also wanted to reduce the dependency on random remote scripts in the resultant .html file as well, so I could open this file with no internet connectivity, in 50 years if I wanted to.&lt;/p&gt;
&lt;h2 id="you-cannot-escape-dependencies" class="relative group"&gt;You cannot escape dependencies &lt;/h2&gt;&lt;p&gt;Ok so this section is a &lt;em&gt;little bit of a lie&lt;/em&gt;, given enough effort I&amp;rsquo;m sure you can.&lt;/p&gt;
&lt;p&gt;My goal of just having a simple program I can run once every year with zero code touching is kinda sorta there (I haven&amp;rsquo;t given it enough time to check).
I&amp;rsquo;m hoping that Go&amp;rsquo;s promise of constant backwards compatibility should help this out, and the removal of reliance on remote scripts means that as long as the one dependency I use in go doesn&amp;rsquo;t go offline, then we should be chill.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Why do you even have dependencies?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;m lazy, ok?&lt;/p&gt;
&lt;p&gt;Only downside going ahead is that I am completely reliant upon the following packages (and github itself) remaining publicly accessible.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-txt" data-lang="txt"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;github.com/tkrajina/gpxgo v1.0.1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;github.com/urfave/cli/v2 v2.3.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Whilst this little pain in the ass has taught me to be a little bit hesitant to blindly import random dependencies, tbh I&amp;rsquo;ll probs carry on doing so, because dependencies are hell but I love getting code for free.&lt;/p&gt;</content:encoded></item><item><title>⠪ Mapping the Internet (?)</title><link>https://jamesjarvis.io/posts/2020/10/backlink-crawler/</link><pubDate>Sun, 04 Oct 2020 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2020/10/backlink-crawler/</guid><category>Projects</category><description>Links on links on links, man</description><content:encoded>&lt;p&gt;Once upon a time I was looking at my website, and I realised that I tend to link to a lot of random sites within each post I write.&lt;/p&gt;
&lt;p&gt;I thought it would be interesting to find out all of the sites that I link to, and see if I could categorise them.&lt;/p&gt;
&lt;p&gt;Unfortunately, I never really got around to figuring out the whole categorisation thing (like what do you even do? Manually classify them? Do some kind of NLP on the page title? &lt;a href="http://data.webarchive.org.uk/opendata/ukwa.ds.1/classification/" target="_blank" rel="noreferrer"&gt;These guys&lt;/a&gt; have attempted it previously but even that dataset only contains ~27k URL&amp;rsquo;s and seems fairly limited to UK sites).&lt;/p&gt;
&lt;p&gt;This then got me thinking about what kind of sites &lt;em&gt;link to me&lt;/em&gt;.
This would be a &lt;strong&gt;backlink checker&lt;/strong&gt;, that for some reason I had never heard of or thought about before.&lt;/p&gt;
&lt;p&gt;I knew that Google, the NSA and many others regularly crawl the internet in order to improve their search products - &lt;a href="https://www.cloudflare.com/learning/bots/what-is-a-web-crawler/" target="_blank" rel="noreferrer"&gt;cloudflare explains it well&lt;/a&gt; - and I roughly knew that the more linked to your site was, the more popular it would be in search rankings.&lt;/p&gt;
&lt;p&gt;However I didn&amp;rsquo;t know how to find your individual backlinks - I just assumed it was some secretly guarded knowledge due to the sheer amount of effort required to find these backlinks.&lt;/p&gt;
&lt;p&gt;Of course, me being me I didn&amp;rsquo;t bother googling &amp;ldquo;uh how do i found out who links to my site on the interwebs pls&amp;rdquo;, I just thought it would be easier to build a crawler myself 🤦‍♂️.&lt;/p&gt;
&lt;p&gt;If you want to find out your own backlinks from an actual SEO service, skip to &lt;a href="#Part-2:-Actual-SEO-services-for-finding-your-own-backlinks"&gt;Part 2&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="part-1-building-a-link-spider" class="relative group"&gt;Part 1: Building a link spider &lt;/h2&gt;&lt;p&gt;I attempted and revisited this several times (to improve performance, mainly), but it basically started because I was heading to &lt;a href="https://en.wikipedia.org/wiki/Wales" target="_blank" rel="noreferrer"&gt;the middle of nowhere&lt;/a&gt; for a family holiday and I wanted to leave my laptop back at home doing some crawling while I couldn&amp;rsquo;t hear the fans.&lt;/p&gt;
&lt;p&gt;Originally, I thought I would just write the crawler from scratch (it&amp;rsquo;s not hard to do tbf), but then I stumbled across &lt;a href="http://go-colly.org/" target="_blank" rel="noreferrer"&gt;Colly&lt;/a&gt; that looked like it could speed up development for me.&lt;/p&gt;
&lt;p&gt;My original version just used a basic colly set up, with a simple postgresql db set up to record pages visited, and links from page to page.
Simple enough, and took an hour or so to code, so I left it running the evening before we left to see how far it could go.&lt;/p&gt;
&lt;p&gt;Well, when I came back 4 days later I found that the fans had stopped, and discovered that the process had killed itself after scraping only 1.5 million links over 36 hours (averaging only ~12 links per second).&lt;/p&gt;
&lt;p&gt;I couldn&amp;rsquo;t see an obvious reason why it would have crapped itself, so I had a deeper look using go&amp;rsquo;s fantastic inbuilt &lt;a href="https://pkg.go.dev/net/http/pprof" target="_blank" rel="noreferrer"&gt;pprof&lt;/a&gt; tool for delving into the internal stats of a running go app.&lt;/p&gt;
&lt;p&gt;What I had found out was that the crawler had likely ran out of memory, but most surprising to me was that colly&amp;rsquo;s crawlers seemed to just spawn another process for each &lt;code&gt;a[href]&lt;/code&gt; element found, causing them to spawn hundreds of goroutines &lt;em&gt;every second&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;This is a bit daft, so the first improvement I made was to switch to colly&amp;rsquo;s &lt;a href="http://go-colly.org/docs/examples/queue/" target="_blank" rel="noreferrer"&gt;queue mode&lt;/a&gt;, as that seemed to handle colly&amp;rsquo;s goroutine obsession, and allowed me to sacrifice scraping certainty (since any links found beyond the queue limit are just discarded) for runtime certainty.&lt;/p&gt;
&lt;p&gt;Secondly, let&amp;rsquo;s focus on that paltry 12 links/second.
The first improvement I could think of was just &lt;a href="https://youtu.be/uNy_MLr8mXA" target="_blank" rel="noreferrer"&gt;&lt;em&gt;spawning more processes&lt;/em&gt;&lt;/a&gt;, but the new bottleneck became the way that I was pushing data to postgresql.&lt;/p&gt;
&lt;p&gt;Previously, I was just creating a new insert transaction every time a new link was found, that was hammering my db with well over 2000 transactions per second and likely being the reason why all of these goroutines were hanging around for so long.&lt;/p&gt;
&lt;p&gt;So, I moved to &lt;strong&gt;batching&lt;/strong&gt;, by creating &lt;em&gt;n&lt;/em&gt; database workers, I could happily push as much data into them through some go channels, and then I could consume up to &lt;em&gt;x&lt;/em&gt; links at a time from these channels, and whack those in the database in one go.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Worker is the worker process for the link batcher&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// This is straight up nicked from https://blog.drkaka.com/batch-get-from-golangs-buffered-channel-9638573f0c6e&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;LinkBatcher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endSignal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// We want it to die on the endSignal, but otherwise keep looping&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;endSignal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Die on endSignal received&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Link&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Now I&amp;#39;m typing this blog, I realise there is a potential deadlock here,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// whereby the endSignal may not be received while we wait for the next link&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// oh well, turns out we never use an endSignal in an infinite scrape anyway ¯\_(ツ)_/¯&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufChan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;remains&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxBatch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Here we use a go label to break out of the repetition loop if there are no more links&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// in the channel, otherwise it will naturally break once the max batch size is reached.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;remains&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bufChan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Remaining&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Then we just whack those links into a database batch insert function.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BatchAddLinks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Batch adding links failed!: %e&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Obviously, this introduces a whole bunch of fun async issues, such as potential duplicates and foreign key constraints (this can be solved by reorganising the db structure), and to that I say &lt;em&gt;fuck it&lt;/em&gt;; &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt;, turning a blind eye to those issues I should probably engineer out of this.&lt;/p&gt;
&lt;p&gt;The only thing I did kinda care about was removing duplicates from being added to the channel, since if I was going to add 5000 links, with 4000 of those potential duplicates, it would seriously slow down any transaction.&lt;/p&gt;
&lt;p&gt;For this, I just used a simple &lt;a href="https://www.interviewcake.com/concept/java/lru-cache" target="_blank" rel="noreferrer"&gt;LRU Cache&lt;/a&gt; called &lt;a href="https://github.com/hashicorp/golang-lru" target="_blank" rel="noreferrer"&gt;golang-lru&lt;/a&gt; to filter out some of the most common URLs.&lt;/p&gt;
&lt;p&gt;In the current iteration (that I&amp;rsquo;m not going to bother improving for the foreseeable future and you can critique &lt;a href="https://github.com/jamesjarvis/web-graph" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;), I managed to collect ~ 50 million links between ~ 9 million pages in ~ 3 days.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a happy 16x improvement upon my initial attempt, but I am unfortunately starting to hit the limits of my 7 year old laptop (running the &lt;code&gt;COUNT(*)&lt;/code&gt; query just to get those numbers took several minutes with nothing else running&amp;hellip;)&lt;/p&gt;
&lt;p&gt;If I can be fully bothered, I may write another post exploring the data, or leave it running a couple weeks and upload a csv file of the links found, but for now I&amp;rsquo;ll leave you with a couple interesting things I found:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Wikipedia is a crawler&amp;rsquo;s best friend, with the #1 number of pages found.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Top 5 for reference:&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;host&lt;/th&gt;
					&lt;th&gt;num_pages&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&amp;ldquo;en.wikipedia.org&amp;rdquo;&lt;/td&gt;
					&lt;td&gt;2375201&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&amp;ldquo;twitter.com&amp;rdquo;&lt;/td&gt;
					&lt;td&gt;533493&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&amp;ldquo;web.archive.org&amp;rdquo;&lt;/td&gt;
					&lt;td&gt;464023&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&amp;ldquo;artuk.org&amp;rdquo;&lt;/td&gt;
					&lt;td&gt;394299&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&amp;ldquo;t.co&amp;rdquo;&lt;/td&gt;
					&lt;td&gt;258350&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol start="2"&gt;
&lt;li&gt;
&lt;p&gt;I only managed to find a total of 5 external backlinks to &amp;lt;jamesjarvis.io&amp;gt; pages through this crawler, all of which I also link to within my own website, so I guess I would have had to run the crawler a LOT longer in order to organically find any proper backlinks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Regrettably, I am unable to draw any further conclusions, since my old ass laptop ran out of storage before completing this simple query for finding the top 5 backlinked sites:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-SQL" data-lang="SQL"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_page_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;num_backlinks&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;links_visited&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;INNER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pages_visited&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;links_visited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_page_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pages_visited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;GROUP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;num_backlinks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(No, really, apparently running this query took up 10GB+ of temporary storage on the database and since my laptop is filled with 200GB of &amp;ldquo;other&amp;rdquo;, it just died.)&lt;/p&gt;
&lt;p&gt;#GetJamesADecentDevMachine2021&lt;/p&gt;
&lt;p&gt;In the future, I would like to continue with the original &amp;ldquo;mapping the internet&amp;rdquo; goal, and see if it would be possible to generate an image of the internet link graph?
Although certainly not with this laptop&amp;hellip;&lt;/p&gt;
&lt;h2 id="part-2-actual-seo-services-for-finding-your-own-backlinks" class="relative group"&gt;Part 2: Actual SEO services for finding your own backlinks &lt;/h2&gt;&lt;p&gt;Ok, that handcrafted web crawler might have been cool and all, but the max I could get before crashing my PC was only a few million links, at a pathetic 100 links/second.&lt;/p&gt;
&lt;p&gt;This pales to the big leagues, who &lt;a href="https://ahrefs.com/big-data" target="_blank" rel="noreferrer"&gt;according to ahrefs&lt;/a&gt; have approximately a &lt;em&gt;fuck ton&lt;/em&gt; more resources than I do for this kind of stuff.
Some things you just have to leave to the professionals&amp;hellip;&lt;/p&gt;
&lt;p&gt;So heres the list of all the free* backlink checkers if you are ever curious about your own SEO performance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ahrefs.com/backlink-checker" target="_blank" rel="noreferrer"&gt;Ahrefs&lt;/a&gt; - seems to have the largest trough of data at their disposal.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neilpatel.com/backlinks/" target="_blank" rel="noreferrer"&gt;NeilPatel&lt;/a&gt; - I&amp;rsquo;ve started using this tool now and it seems pretty cool. Might be a good game to increase your backlinks overtime.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://smallseotools.com/backlink-checker/" target="_blank" rel="noreferrer"&gt;SmallSEOTools&lt;/a&gt; - This one got some slightly weird results for me, so idk how good it is.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.seoreviewtools.com/valuable-backlinks-checker/" target="_blank" rel="noreferrer"&gt;SEOReviewTools&lt;/a&gt; - This one was kinda interesting, as it showed uses of static assets from my site. So if you want to see why one of your images is getting 1000+ views and your posts aren&amp;rsquo;t, this could help.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.thehoth.com/backlinks-checker/" target="_blank" rel="noreferrer"&gt;TheHoth&lt;/a&gt; - Wants an email, not a fan of that.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sitechecker.pro/backlinks-checker/" target="_blank" rel="noreferrer"&gt;SiteChecker&lt;/a&gt; - Limits results so whilst you can see a count of backlinks, you can&amp;rsquo;t see more than 20 of the actual referring urls.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>☂️ E-Ink Weather Dashboard</title><link>https://jamesjarvis.io/posts/2020/06/eink-dashboard/</link><pubDate>Sat, 20 Jun 2020 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2020/06/eink-dashboard/</guid><category>Projects</category><description>Fun little rarely updating dashboard.</description><content:encoded>&lt;p&gt;Another one of my lockdown projects&amp;hellip;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve always like the idea of making an always-on display.
Something like the Magic Mirror project &lt;a href="https://magicmirror.builders/" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;, or a productivity dashboard using an LCD display &lt;a href="https://www.jlwinkler.com/2017-05-25/raspberry-pi-productivity-dashboard/" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;, but I also wanted something that is a bit more visible in bright light and uses a little bit less electricity.&lt;/p&gt;
&lt;p&gt;Hence, I settled on the idea of making a giant e-ink display that I could use to show whatever I wanted.
Until I saw &lt;a href="https://amzn.to/3iHGepG" target="_blank" rel="noreferrer"&gt;how expensive the panels are&lt;/a&gt;, and I settled on a much smaller &lt;a href="https://amzn.to/2E5lol0" target="_blank" rel="noreferrer"&gt;7.5&amp;quot; e-ink 2 colour display&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="what-does-it-do" class="relative group"&gt;What does it do? &lt;/h2&gt;&lt;p&gt;Well, there&amp;rsquo;s a few modes (in case I ever get bored).&lt;/p&gt;
&lt;p&gt;Normally, I keep it in &lt;em&gt;weather&lt;/em&gt; mode, whereby every 10 minutes it will update and show the following:&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2020/06/eink-dashboard/display_weather_hu_4f74bde528523a7d.jpg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="2577"
 height="4030"
 class="mx-auto my-0 rounded-md"
 alt="Weather mode"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2020/06/eink-dashboard/display_weather_hu_5e9350a3bbb027a7.jpg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;The red splotches are rain&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Temperature&lt;/li&gt;
&lt;li&gt;Current weather conditions&lt;/li&gt;
&lt;li&gt;Rain chart for the next few hours&lt;/li&gt;
&lt;li&gt;Air quality&lt;/li&gt;
&lt;li&gt;Sunrise and Sunset times&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.isstracker.com/" target="_blank" rel="noreferrer"&gt;The next time the ISS will pass overhead&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Weather radar of the UK &lt;a href="https://www.metoffice.gov.uk/public/weather/observation/rainfall-radar#?map=Rainfall&amp;amp;zoom=5" target="_blank" rel="noreferrer"&gt;(just because it looks cool)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I won&amp;rsquo;t lie, the 3 hour rainwall forecast is nowhere near as accurate as &lt;a href="https://darksky.net/" target="_blank" rel="noreferrer"&gt;DarkSky&lt;/a&gt; was, and the display panel I was sent didn&amp;rsquo;t display red&amp;rsquo;s anywhere near as vibrantly as other panels I have seen (I think it was just a faulty panel but I couldn&amp;rsquo;t be bothered to pay for shipping to China and wait another month), but overall it&amp;rsquo;s worked exactly as I have envisioned!&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also the &lt;em&gt;cowsay&lt;/em&gt; mode, whose sole purpose is to present a random dad joke every hour, in the popular &lt;a href="https://en.wikipedia.org/wiki/Cowsay" target="_blank" rel="noreferrer"&gt;linux cli tool&lt;/a&gt; format.&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2020/06/eink-dashboard/display_cowsay_hu_46cd555b09ddb5d9.jpg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="4032"
 height="3024"
 class="mx-auto my-0 rounded-md"
 alt="Cowsay mode"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2020/06/eink-dashboard/display_cowsay_hu_4d6cffc2196960dd.jpg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;cowsay moo&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id="how-to-build-it" class="relative group"&gt;How to build it? &lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s relatively simple, really&amp;hellip;&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2020/06/eink-dashboard/display_pi_hu_b60fcc4fbe981e87.jpg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="4032"
 height="3024"
 class="mx-auto my-0 rounded-md"
 alt="The hardware"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2020/06/eink-dashboard/display_pi_hu_89878cb97343790.jpg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;I like raw materials just as much as I like an easy assembly&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;You only need a few components: A &lt;a href="https://amzn.to/2DWrynQ" target="_blank" rel="noreferrer"&gt;Raspberry Pi Zero W&lt;/a&gt;, an e-ink display (I chose &lt;a href="https://amzn.to/2E5lol0" target="_blank" rel="noreferrer"&gt;this 7.5&amp;quot; 2 colour display from Waveshare&lt;/a&gt;), and &lt;em&gt;something&lt;/em&gt; to mount it all one.&lt;/p&gt;
&lt;p&gt;The instructions for putting it together are fairly self explanatory, so I&amp;rsquo;m not going to bore you.
Setting up a headless Raspberry Pi (so you can just access it over SSH is explained beautifully simply over at &lt;a href="https://www.raspberrypi.org/documentation/configuration/wireless/headless.md" target="_blank" rel="noreferrer"&gt;the Raspberry Pi site&lt;/a&gt;)
Aside from that it&amp;rsquo;s just plug and play.&lt;/p&gt;
&lt;p&gt;The software is a little bit more of a pain in the ass, Waveshare have a &lt;a href="https://github.com/waveshare/e-Paper" target="_blank" rel="noreferrer"&gt;GitHub repo&lt;/a&gt; with drivers for each of their displays, but no perfect method of building a UI for these displays, so the best way to push something to the display is in the form of a bitmap (essentially png) image.
The refresh rate of the display is a pathetic ~1 minute so don&amp;rsquo;t worry too much about how long it takes to build that image.&lt;/p&gt;
&lt;p&gt;I have a repository with all the code for this project on my GitHub &lt;a href="https://github.com/jamesjarvis/eink-dashboard" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;, that you just need to clone into the RPI&amp;rsquo;s home directory, and then write your own &lt;code&gt;settings.py&lt;/code&gt; file from the example file provided.&lt;/p&gt;
&lt;p&gt;It makes extensive use of various API&amp;rsquo;s, including &lt;a href="https://www.climacell.co/weather-api/" target="_blank" rel="noreferrer"&gt;Climacell&lt;/a&gt; for the weather data, &lt;a href="http://api.open-notify.org/iss-pass.json" target="_blank" rel="noreferrer"&gt;ISS Open Notify&lt;/a&gt; for the ISS times, &lt;a href="https://icanhazdadjoke.com/" target="_blank" rel="noreferrer"&gt;ICanHazDadJoke&lt;/a&gt; for the dad joke service, and &lt;a href="https://helloacm.com/api/cowsay/" target="_blank" rel="noreferrer"&gt;This online cowsay generator&lt;/a&gt; because I couldn&amp;rsquo;t be bothered to generate it myself, although looking back it would probably have been easier to do it manually.&lt;/p&gt;
&lt;p&gt;For the map images, I used the &lt;a href="https://github.com/openmaptiles/maptiler-toner-gl-style" target="_blank" rel="noreferrer"&gt;OpenStreetMap Toner&lt;/a&gt; style tiles for the map and the &lt;a href="https://www.metoffice.gov.uk/public/weather/observation/rainfall-radar#?map=Rainfall&amp;amp;zoom=5" target="_blank" rel="noreferrer"&gt;MetOffice rain radar&lt;/a&gt; tiles for the rain overlays.
I originally used &lt;a href="https://openweathermap.org/weathermap" target="_blank" rel="noreferrer"&gt;OpenWeatherMap&lt;/a&gt;, but whilst it was good at global coverage of rainfall, it had very low resolution.&lt;/p&gt;
&lt;p&gt;To run the scripts all you need to do is create a cron job for it to update every &lt;em&gt;x&lt;/em&gt; minutes.
You can enter the cron editor with &lt;code&gt;crontab -e&lt;/code&gt; and just input &lt;code&gt;*/10 * * * * python3 /home/pi/mappyboi.py&lt;/code&gt; for a 10 minute update schedule.&lt;/p&gt;
&lt;h2 id="next-steps" class="relative group"&gt;Next steps &lt;/h2&gt;&lt;p&gt;I practically never do hardware projects and this has been super fun!&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m still thinking of ideas to use it for (if you have any ideas of a cool use for this format of display, contact me!)&lt;/p&gt;
&lt;p&gt;Ideas I&amp;rsquo;ve currently considered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
 
 &lt;a href="https://jamesjarvis.io/posts/2020/03/predictions/"&gt;Coronavirus cases tracker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Public transit departure times&lt;/li&gt;
&lt;li&gt;Health dashboard&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.dougswordclocks.com/" target="_blank" rel="noreferrer"&gt;Word clock&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>📸 Indoor Garden Timelapse</title><link>https://jamesjarvis.io/posts/2020/06/indoor-garden-timelapse/</link><pubDate>Tue, 02 Jun 2020 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2020/06/indoor-garden-timelapse/</guid><category>Projects</category><description>Watching plants grow</description><content:encoded>&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2020/06/indoor-garden-timelapse/timelapse_hu_63fbe563749f552f.gif"
 
 &gt;
 








 
 
 
 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 &lt;img
 width="480"
 height="639"
 class="mx-auto my-0 rounded-md"
 alt="Indoor Garden Timelapse"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2020/06/indoor-garden-timelapse/timelapse_hu_228c83f17ec9875e.gif"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;Who knew plants could move so fast?&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Another lockdown project!&lt;/p&gt;
&lt;p&gt;For some reason I&amp;rsquo;ve been getting into gardening over the lockdown, from building an automatic irrigation system for our newly built vegetable patch to taking part (and winning) a sunflower growing competition, it&amp;rsquo;s about the most interesting thing I could think of doing whilst being stuck in my house during a great spell of weather.&lt;/p&gt;
&lt;p&gt;This year, for my birthday I received an &lt;a href="https://amzn.to/31Xp40r" target="_blank" rel="noreferrer"&gt;indoor vegetable garden&lt;/a&gt;, and thought it would be fun to set up a timelapse camera to monitor the plants growing!&lt;/p&gt;
&lt;p&gt;Since I&amp;rsquo;m currently also slightly addicted to Raspberry Pi&amp;rsquo;s (now up to 4 in my collection!), I thought this would be a good use for one of them.&lt;/p&gt;
&lt;p&gt;So, I bought the &lt;a href="https://thepihut.com/products/adafruit-raspberry-pi-zero-w-camera-pack-includes-pi-zero" target="_blank" rel="noreferrer"&gt;Raspberry Pi Zero W camera combo&lt;/a&gt;, strapped it to the side of the garden and adjusted the focus of the camera with a pair of pliers to improve the focus a little bit.&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2020/06/indoor-garden-timelapse/pi-cam_hu_ce26cdc5074701af.jpg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="3024"
 height="4032"
 class="mx-auto my-0 rounded-md"
 alt="Pi Cam"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2020/06/indoor-garden-timelapse/pi-cam_hu_2b3703ccfdd36823.jpg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;The end result is this cute little video:&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
			&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube-nocookie.com/embed/DiBl9JMlBUI?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
		&lt;/div&gt;

&lt;p&gt;This little video was the result of 2 months of growing and almost 200GB of images!
I set up the camera to take a photo every 30 seconds (which in hindsight was a little bit too often, and the final video only took a frame each 16 minutes)&lt;/p&gt;
&lt;h2 id="scripts" class="relative group"&gt;Scripts &lt;/h2&gt;&lt;p&gt;I thought I would share the little script I used to pull images from the camera, sort them into good (clear, well exposed) and bad (too dark, fuzzy, etc), and then render them into a timelapse&amp;hellip;&lt;/p&gt;
&lt;p&gt;Firstly, on the camera all you need is a little script that helps with actually taking the photos:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;take &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +&lt;span class="s2"&gt;&amp;#34;%Y-%m-%d_%H_%M_%S&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; raspistill -t &lt;span class="m"&gt;100&lt;/span&gt; -q &lt;span class="m"&gt;20&lt;/span&gt; -o /home/pi/timelapse-images/&lt;span class="nv"&gt;$DATE&lt;/span&gt;.jpg
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;take
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sleep &lt;span class="m"&gt;29&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;take
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Just &lt;code&gt;chmod +x&lt;/code&gt; it and add it to the cron jobs with &lt;code&gt;crontab -e&lt;/code&gt; and if you want it to run every minute, then append this to the crontab:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* * * * * bash /home/pi/camera.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, on another computer (preferably faster and on the same network as your RPI), you can set up a little script like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;!!! Requires EXTERNAL-SSD to be plugged in at all times !!!&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Retrieving latest images&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rsync -a -v --ignore-existing --remove-source-files pi@pi-cam.local:/home/pi/timelapse-images/ /Volumes/EXTERNAL-SSD/timelapse-images
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Filtering and linking based on shutter speed to get rid of flickers - VERY SLOW STEP&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Checking and skipping a few thousand photos...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; f in /Volumes/EXTERNAL-SSD/timelapse-images/*.jpg
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;fname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;basename &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# If not bad and not good (ie: not sorted yet), process and filter&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; ! -f /Volumes/EXTERNAL-SSD/filtered-timelapse-images/&lt;span class="nv"&gt;$fname&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ! -f /Volumes/EXTERNAL-SSD/bad-timelapse-images/&lt;span class="nv"&gt;$fname&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;exiftool -exif:shutterspeedvalue -s -s -s &lt;span class="nv"&gt;$f&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; cut -c 3-&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;sss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;/.*&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# If the correct shutter speed, then place in the filtered folder, else cache in the bad folder&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$sss&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -gt &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; Linking &lt;span class="nv"&gt;$fname&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ln -s &lt;span class="nv"&gt;$f&lt;/span&gt; /Volumes/EXTERNAL-SSD/filtered-timelapse-images/&lt;span class="nv"&gt;$fname&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; Caching bad photo &lt;span class="nv"&gt;$fname&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ln -s &lt;span class="nv"&gt;$f&lt;/span&gt; /Volumes/EXTERNAL-SSD/bad-timelapse-images/&lt;span class="nv"&gt;$fname&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;#else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;#echo Skipping $fname&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Generating timelapse&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ffmpeg -r &lt;span class="m"&gt;1920&lt;/span&gt; -pattern_type glob -i &lt;span class="s1"&gt;&amp;#39;/Volumes/EXTERNAL-SSD/filtered-timelapse-images/*.jpg&amp;#39;&lt;/span&gt; -vf &lt;span class="s2"&gt;&amp;#34;transpose=1&amp;#34;&lt;/span&gt; -vcodec libx265 -r &lt;span class="m"&gt;60&lt;/span&gt; /Volumes/EXTERNAL-SSD/filtered-timelapse.mp4 -y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Done!&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;open /Volumes/EXTERNAL-SSD/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This script obviously requires an external ssd, and will require some modifications for you to use.
It also requires &lt;a href="https://ffmpeg.org/" target="_blank" rel="noreferrer"&gt;ffmpeg&lt;/a&gt; to be installed locally, so make sure that&amp;rsquo;s all set up before any annoying errors hit you in the face.&lt;/p&gt;
&lt;p&gt;Basically all this script does is download the latest images from the RPI, delete them from the RPI once done, then filter them into symlinked directories of &amp;ldquo;good&amp;rdquo; and &amp;ldquo;bad&amp;rdquo; photos - essentially caching them.&lt;/p&gt;
&lt;p&gt;Then it triggers a rendering job that takes &lt;em&gt;absolutely FOREVER&lt;/em&gt; to create a h.265 encoded timelapse of it all.
(Originally I used h.264 but h.265 took up honestly 1/8th of the space without losing much quality which is fantastic!)&lt;/p&gt;
&lt;p&gt;P.S. The gif at the start of the page is generated with a one liner:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ffmpeg -ss &lt;span class="m"&gt;3&lt;/span&gt; -t &lt;span class="m"&gt;20&lt;/span&gt; -i /Volumes/JARVIS-SSD/filtered-timelapse.mp4 -filter_complex &lt;span class="s2"&gt;&amp;#34;[0:v] fps=10,scale=480:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse&amp;#34;&lt;/span&gt; /Volumes/JARVIS-SSD/timelapse.gif
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, gifs are rather inefficient, the better option would be an &lt;a href="https://developers.google.com/speed/webp/faq" target="_blank" rel="noreferrer"&gt;animated webp&lt;/a&gt; image, which is unfortunately not supported by GatsbyJS, nor some &lt;a href="https://caniuse.com/#search=webp" target="_blank" rel="noreferrer"&gt;old browsers&lt;/a&gt;, but it would be 10% the size of the gif version (and better quality!) - to generate it I used the following command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ffmpeg -ss &lt;span class="m"&gt;3&lt;/span&gt; -t &lt;span class="m"&gt;20&lt;/span&gt; -i /Volumes/JARVIS-SSD/filtered-timelapse.mp4 -filter_complex &lt;span class="s2"&gt;&amp;#34;[0:v] fps=10,scale=640:-1&amp;#34;&lt;/span&gt; -vcodec libwebp -lossless &lt;span class="m"&gt;1&lt;/span&gt; -q &lt;span class="m"&gt;30&lt;/span&gt; -preset default -loop &lt;span class="m"&gt;0&lt;/span&gt; -an -vsync &lt;span class="m"&gt;0&lt;/span&gt; /Volumes/JARVIS-SSD/timelapse.webp -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hope you&amp;rsquo;ve enjoyed!&lt;/p&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2020/06/indoor-garden-timelapse/garden-patch_hu_a6a3fe19d4ad5a2a.jpg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="3890"
 height="3024"
 class="mx-auto my-0 rounded-md"
 alt="Garden Before"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2020/06/indoor-garden-timelapse/garden-patch_hu_4d3e44eb70f4faff.jpg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2020/06/indoor-garden-timelapse/garden-patch-grown_hu_362d937e1756d0b7.jpg"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="4032"
 height="3024"
 class="mx-auto my-0 rounded-md"
 alt="Garden After"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2020/06/indoor-garden-timelapse/garden-patch-grown_hu_e3aa607e0f9d56d9.jpg"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;</content:encoded></item><item><title>📦 IPFS Web Hosting</title><link>https://jamesjarvis.io/posts/2020/05/ipfs-web-hosting/</link><pubDate>Sat, 02 May 2020 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2020/05/ipfs-web-hosting/</guid><category>Projects</category><description>I thought IPFS was supposed to be permament&amp;hellip;</description><content:encoded>&lt;p&gt;2020 has been defined by The Lockdown &lt;a href="https://www.bbc.co.uk/news/uk-52084517" target="_blank" rel="noreferrer"&gt;(yay?)&lt;/a&gt;.
From March to July (and perhaps again, if the &lt;a href="https://www.bbc.co.uk/news/uk-53159918" target="_blank" rel="noreferrer"&gt;(potential)&lt;/a&gt; second wave is to come), basically everything shut down and we were all forced to spend time with family, and do nothing.&lt;/p&gt;
&lt;p&gt;Well, around this time I was thinking &amp;ldquo;what are the problems humanity will face in the next 100 years?&amp;rdquo;, and at some point I will write a little post outlining my daft ideas.&lt;/p&gt;
&lt;p&gt;But during this, I came across the &lt;a href="https://ipfs.io" target="_blank" rel="noreferrer"&gt;IPFS&lt;/a&gt; project, and I&amp;rsquo;m astonished at the idea of solving a problem that does not yet exist.&lt;/p&gt;
&lt;h2 id="what-is-ipfs" class="relative group"&gt;What is IPFS &lt;/h2&gt;&lt;p&gt;Their goal is to create a distributed web protocol that can help distribute content to where it is most requested.
Ideally, this aims to prevent issues of enormous latencies that may arise from traditional centralised services, if humanity becomes interplanetary.&lt;/p&gt;
&lt;p&gt;An example being, if you are living on mars, and the twitter servers are still hosted on earth, who would want to browse twitter if it took &lt;a href="https://blogs.esa.int/mex/2012/08/05/time-delay-between-mars-and-earth/" target="_blank" rel="noreferrer"&gt;13 minutes&lt;/a&gt; between opening twitter and each tweet starting to load?&lt;/p&gt;
&lt;p&gt;Exactly, &lt;a href="https://www.youtube.com/watch?v=zGxwbhkDjZM" target="_blank" rel="noreferrer"&gt;ain&amp;rsquo;t nobody got time for that&lt;/a&gt;.
IPFS is basically a mix between a conventional file system, and torrents.
The basic idea is that if an IPFS node requests a file (using a content-addressable hash), then that node automatically starts serving that file to other nodes requesting it.
The benefit could be that if for example a video were to go viral, rather than the central server being absolutely hammered with requests, each person would essentially be downloading it off their neighbour, greatly reducing network congestion.&lt;/p&gt;
&lt;p&gt;Now, I am certainly not the first to do this, and a great write up can be found &lt;a href="https://withblue.ink/2019/03/20/hugo-and-ipfs-how-this-blog-works-and-scales.html" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Long story short, I set up a little &lt;a href="https://github.com/ipfs/go-ipfs" target="_blank" rel="noreferrer"&gt;Go-IPFS&lt;/a&gt; node on my Raspberry Pi, and uploaded a copy of my site to &lt;a href="https://ipfs.jamesjarvis.io" target="_blank" rel="noreferrer"&gt;this probably out of date mirror&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, more detail:&lt;/p&gt;
&lt;h2 id="gatsbyjs-sucks-with-ipfs" class="relative group"&gt;GatsbyJS sucks with IPFS &lt;/h2&gt;&lt;p&gt;This website is (currently) built with GatsbyJS, &lt;a href="https://jamesjarvis.io/posts/2019/02/portfolio-site/"&gt;as outlined here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Only thing is, GatsbyJS does not support relative links by default.
Sure, you can enable &lt;a href="https://www.gatsbyjs.org/packages/gatsby-plugin-ipfs/" target="_blank" rel="noreferrer"&gt;gatsby-plugin-ipfs&lt;/a&gt; to try and fix the relative urls problem, but it is not compatible with most other plugins.&lt;/p&gt;
&lt;p&gt;As a result, Gatsby on IPFS does not work with manifests, sitemaps, or the most important gatsby-image integration.&lt;/p&gt;
&lt;p&gt;So sure, if you browse the mirror on a normal browser, then it will work fine, since IPFS content is being forwarded through the &lt;a href="https://www.cloudflare.com/distributed-web-gateway/" target="_blank" rel="noreferrer"&gt;cloudflare gateway&lt;/a&gt; the url is being formatted with the normal domain, and all the links work fine.&lt;/p&gt;
&lt;p&gt;However, if you browse the mirror on a browser with integrated IPFS (as it should do), then stuff like images and some other content will not load correctly.&lt;/p&gt;
&lt;h2 id="the-future" class="relative group"&gt;The future &lt;/h2&gt;&lt;p&gt;I spent a few hours on this problem, and honestly I just gave up.&lt;/p&gt;
&lt;p&gt;Perhaps in the future Gatsby will update to include relative URLs by default, but if you are absolutely desparate to host your content through IPFS, then I would recommend a different static site generator, such as &lt;a href="https://gohugo.io" target="_blank" rel="noreferrer"&gt;Hugo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you are looking at a potentially commercially viable implementation of IPFS, I would recommend you check out &lt;a href="https://filecoin.io" target="_blank" rel="noreferrer"&gt;Filecoin&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I know, I know &amp;ldquo;not another cryptocurrency&amp;rdquo;, but this one is designed to pay people to host their content on IPFS, and in the future could potentially take on the big cloud providers?&lt;/p&gt;</content:encoded></item><item><title>🍲 Risotto Play</title><link>https://jamesjarvis.io/posts/2020/04/risotto-play/</link><pubDate>Mon, 20 Apr 2020 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2020/04/risotto-play/</guid><category>Projects</category><description>Online playground for a useless programming language</description><content:encoded>&lt;p&gt;&lt;a href="https://play.risotto.dev/" target="_blank" rel="noreferrer"&gt;Try it out here!&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Sometime back in 2018, a friend of mine started building his own programming language, &amp;ldquo;Risotto&amp;rdquo; &lt;a href="https://github.com/risotto/risotto" target="_blank" rel="noreferrer"&gt;(GitHub link)&lt;/a&gt; (although I wanted the name to be &amp;ldquo;Ravioli&amp;rdquo; so the syntax could be &amp;ldquo;Ravioli Ravioli, give me the formuoli&amp;rdquo; &lt;a href="https://www.youtube.com/watch?v=z9r8Swai8_g&amp;amp;feature=youtu.be&amp;amp;t=32" target="_blank" rel="noreferrer"&gt;(I know, I&amp;rsquo;m immature)&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Anywho, I&amp;rsquo;ve enjoyed using services such as &lt;a href="https://goplay.space/" target="_blank" rel="noreferrer"&gt;The Go Play Space&lt;/a&gt; and &lt;a href="https://repl.it/" target="_blank" rel="noreferrer"&gt;Repl&lt;/a&gt; in the past, and wanted to replicate this kind of environment (whilst paying 0 for hosting) for Risotto.&lt;/p&gt;
&lt;h2 id="the-api" class="relative group"&gt;The API &lt;/h2&gt;&lt;p&gt;&lt;a href="https://github.com/risotto/play" target="_blank" rel="noreferrer"&gt;Code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This runs on &lt;a href="https://kubesail.com/" target="_blank" rel="noreferrer"&gt;Kubesail&lt;/a&gt; for easy K8s deployment (and the free tier), with the API written in Go, and spawning a process running the code passed to it.&lt;/p&gt;
&lt;p&gt;In order to keep the API service performant, there&amp;rsquo;s a little bit of IP addressed rate limiting, and runtime constraints, but overall the traffic is low enough that this hasn&amp;rsquo;t come close to being an issue yet.&lt;/p&gt;
&lt;p&gt;The API is designed so that if anyone wants, they can use it for their own purposes, which is highly encouraged (get Risotto out there!)&lt;/p&gt;
&lt;p&gt;It is also covered with Unit Tests, and automatic CI/CD.&lt;/p&gt;
&lt;h2 id="the-ui" class="relative group"&gt;The UI &lt;/h2&gt;&lt;p&gt;&lt;a href="https://play.risotto.dev" target="_blank" rel="noreferrer"&gt;play.risotto.dev&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/risotto/play-ui" target="_blank" rel="noreferrer"&gt;Code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is hosted with &lt;a href="https://pages.github.com/" target="_blank" rel="noreferrer"&gt;GitHub pages&lt;/a&gt; for easy CI/CD and just fantastic performance.&lt;/p&gt;
&lt;p&gt;Written in React (TypeScript flavour), it&amp;rsquo;s fairly simple but is optimised for responsive design and even has a cute little &amp;ldquo;Share&amp;rdquo; feature, so you can share your creations with others.&lt;/p&gt;</content:encoded></item><item><title>📆 WhatsUpKent: Everything happening at the University of Kent</title><link>https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/</link><pubDate>Mon, 04 Nov 2019 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/</guid><category>Projects</category><category>Data</category><description>Finding out better lectures than the ones I was paying for</description><content:encoded>&lt;p&gt;In October 2019, I was looking for a challenge.&lt;/p&gt;
&lt;p&gt;University &lt;a href="https://web.archive.org/web/20200322103909/https://www.kent.ac.uk/" target="_blank" rel="noreferrer"&gt;(look mum, I&amp;rsquo;m on the front page!)&lt;/a&gt; was going ok, but I didn&amp;rsquo;t feel stimulated enough, and wanted to see what else there was on offer.
Turns out paying £9k per year to learn from 20 year old textbooks isn&amp;rsquo;t exactly riveting, so I was looking to see what other interesting lectures I could discover on campus.
Also, my friends and I liked to use disused seminar rooms to work in since the library was often overcrowded and I wanted an easy way to discover which seminar rooms were not being used.&lt;/p&gt;
&lt;p&gt;I also wanted to stretch my legs and learn a bit of Golang, Kubernetes, Typescript and Graph databases.&lt;/p&gt;
&lt;p&gt;So, I decided to create a service that would gather all student&amp;rsquo;s timetables and present the information in an easy to use dashboard, to offer students the opportunity to explore other courses in their spare time.&lt;/p&gt;
&lt;p&gt;Thanks to the university (accidentally?) publishing student timetables as publicly accessible ical links, it proves fairly simple to just brute force and scrape all ical timetables from their website.
Of course, having to iterate through 50,000 timetables can be quite time consuming, so I chose &lt;a href="https://golang.org/" target="_blank" rel="noreferrer"&gt;Golang&lt;/a&gt; to write the server, thanks to it&amp;rsquo;s remarkably easy multithreading syntax.&lt;/p&gt;
&lt;p&gt;Since the data being retrieved was fundamentally relational, I chose to store it in a &lt;a href="https://dgraph.io/" target="_blank" rel="noreferrer"&gt;graph database called DGraph&lt;/a&gt;.
This allowed me to achieve not only some quick queries on the data, I was also able to just easily expose a read only graphql endpoint directly to the database. Simple! :)&lt;/p&gt;
&lt;p&gt;At this point, we are now looking at a few different services for the api: the scraper, a caching service (just to speed up repetetive large queries), and the dgraph database instance (which itself requires 2 pods).
So, for deployment I chose to look towards Kubernetes.
I bought the cheapests little VPS I could, and whacked on a &lt;a href="https://k3s.io/" target="_blank" rel="noreferrer"&gt;k3s lightweight kubernetes&lt;/a&gt; instance and set that up in the CI/CD (ran with GitHub actions).
As always, the &lt;a href="https://github.com/jamesjarvis/WhatsUpKent-UI" target="_blank" rel="noreferrer"&gt;frontend&lt;/a&gt; was hosted with GitHub pages.&lt;/p&gt;
&lt;p&gt;Overall, the project was a success.
It managed to scrape (and continuously check for updates and re-scrape) thousands of lectures and seminars on campus, from all students, and all courses.
It was hosted at &lt;a href="https://whatsupkent.com" target="_blank" rel="noreferrer"&gt;whatsupkent.com&lt;/a&gt;, with the backend code hosted in &lt;a href="https://github.com/jamesjarvis/WhatsUpKent" target="_blank" rel="noreferrer"&gt;GitHub&lt;/a&gt;.
Unfortunately, I decided to close down the service before the uni got mad at me (also, I couldn&amp;rsquo;t be bothered to pay the £5 a month hosting fees), but relaunching it would be as easy as pie if demand ever increases for a service like this.&lt;/p&gt;
&lt;h2 id="a-few-screenshots" class="relative group"&gt;A few screenshots &lt;/h2&gt;&lt;p&gt;The API:








&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/api_splashscreen_hu_6bfae2744f7b4226.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 &lt;img
 width="956"
 height="466"
 class="mx-auto my-0 rounded-md"
 alt="API splashscreen"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/api_splashscreen_hu_85c9442a8ec90c85.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Event info:








&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/day_unfiltered_hu_59afa4e33362a92d.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="1391"
 height="1112"
 class="mx-auto my-0 rounded-md"
 alt="Event info"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/day_unfiltered_hu_b164c7cc292f94c0.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Room finder:








&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/room_finder_hu_af241af7c5d4dee6.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="1391"
 height="1136"
 class="mx-auto my-0 rounded-md"
 alt="Room finder"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/room_finder_hu_8178f050e9d19edf.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Agenda view:








&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/agenda_filtered_hu_6f99eaddb47038af.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="1391"
 height="1136"
 class="mx-auto my-0 rounded-md"
 alt="Agenda"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/agenda_filtered_hu_8566b093e7106078.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Filtered week:








&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/week_filtered_hu_5ec1bcbe77398872.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="1391"
 height="1136"
 class="mx-auto my-0 rounded-md"
 alt="Filtered week"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2019/11/whatsupkent-everything-happening-at-the-university-of-kent/week_filtered_hu_112081ccb0229089.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;</content:encoded></item><item><title>🖊 Git Art</title><link>https://jamesjarvis.io/posts/2019/02/git-art/</link><pubDate>Tue, 05 Feb 2019 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2019/02/git-art/</guid><category>Projects</category><description>Commit stuff, with &lt;em&gt;style&lt;/em&gt;</description><content:encoded>&lt;p&gt;My roommate Tim &lt;a href="https://www.linkedin.com/in/timothylee0/" target="_blank" rel="noreferrer"&gt;(his LinkedIn)&lt;/a&gt; and I decided to start challenging ourselves with little applications we could develop.&lt;/p&gt;
&lt;p&gt;The first thing we thought of was a way to decorate your GitHub activity board, example below:&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2019/02/git-art/example-board_hu_1a8476a01262a9.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 &lt;img
 width="1164"
 height="254"
 class="mx-auto my-0 rounded-md"
 alt="Example board"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2019/02/git-art/example-board_hu_43c7e4e5ad5fc17a.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;The idea being that you could just draw or type (or both) anything that you wanted to on a canvas that represented your github activity board, and with one simple script you could create the required commits and upload that new repo to your GitHub.&lt;/p&gt;
&lt;p&gt;This is thanks to a feature/flaw in git that allows you to create commits (even empty commits) with any date attached.
Therefore, all we need to do to generate a cute little activity graph is to simply work out the date ranges required, how many commits to make each day, and to generate a script that allows the user to do this.&lt;/p&gt;
&lt;p&gt;We used React for the front end, and wrote simple JavaScript functions to generate the bash script which is available for you to download and run.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://jamesjarvis.github.io/git-art/" target="_blank" rel="noreferrer"&gt;Check out the website&lt;/a&gt;, or if you&amp;rsquo;d prefer, &lt;a href="https://github.com/jamesjarvis/git-art" target="_blank" rel="noreferrer"&gt;jump straight to the code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Made open source under the &lt;a href="http://www.wtfpl.net" target="_blank" rel="noreferrer"&gt;WTFPL licence&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>🖥 Open Source Portfolio Website</title><link>https://jamesjarvis.io/posts/2019/02/portfolio-site/</link><pubDate>Sat, 02 Feb 2019 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2019/02/portfolio-site/</guid><category>Projects</category><description>I wanted a personal website, I made a personal website.</description><content:encoded>&lt;p&gt;&lt;a href="https://jamesjarvis.io/"&gt;This very site&lt;/a&gt; you are reading, is open source, under the A-GPL licence with all the source code hosted on GitHub &lt;a href="https://github.com/jamesjarvis/jamesjarvis.io" target="_blank" rel="noreferrer"&gt;(fork off)&lt;/a&gt;.
You may fork it and fill in your own data if you wish, which is surprisingly easy to do, all thanks to GatsbyJS &lt;a href="https://www.gatsbyjs.org/" target="_blank" rel="noreferrer"&gt;(link)&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;I created this with GatsbyJS as I wanted to have my own personal portfolio site which simply consumes data stored externally (though within the same repo), rather than hard coding everything, which I felt was the issue with many other portfolio sites.&lt;/p&gt;
&lt;p&gt;Now, I wanted to have a personal site so I could have a place to publicise the work I have done in the past, what I am planning in the future, and my thoughts and ramblings (As well as just to have a place for my &amp;lsquo;digital identity&amp;rsquo; where I can link together all of my online personas into a single place).&lt;/p&gt;
&lt;p&gt;I am a little bit cheap however, so after spending $30 on domain fees I didn&amp;rsquo;t want to spend any more money on actually hosting it. Which brings me to some of my favourite pieces of tech: the free kind.&lt;/p&gt;
&lt;h2 id="how-to-host-for-no-cost-whatsoever" class="relative group"&gt;How to host for no cost whatsoever &lt;/h2&gt;&lt;p&gt;Since we are already using GitHub as a way to host our source code (for free!), we might as well use the latest and greatest &lt;a href="https://pages.github.com" target="_blank" rel="noreferrer"&gt;GitHub Pages&lt;/a&gt; to host the page source files for free as well! (I know right!??).&lt;/p&gt;
&lt;p&gt;Since we are making a &lt;a href="https://jamstack.org" target="_blank" rel="noreferrer"&gt;JAMStack&lt;/a&gt; site, all we have is front-end stuff which we just need to plonk in any old static web hosting provider. Other great examples are &lt;a href="https://www.netlify.com" target="_blank" rel="noreferrer"&gt;Netlify&lt;/a&gt;, and even on &lt;a href="https://zeit.co/now" target="_blank" rel="noreferrer"&gt;Zeit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To build your site, I would recommend setting up a CI/CD integration using GitHub actions, and pointing this to deploy to whatever hosting service you use (GitHub pages is the easiest I feel as you only need to push changes to another branch/folder within your repo).&lt;/p&gt;
&lt;p&gt;That is basically all you need to do.&lt;/p&gt;
&lt;p&gt;I would recommend also putting your site behind &lt;a href="https://www.cloudflare.com" target="_blank" rel="noreferrer"&gt;Cloudflare&lt;/a&gt;, as this can speed up access to your site due to the way they cache it on any of their CDN nodes - again, all for free!&lt;/p&gt;
&lt;p&gt;If you get to the point where you would like a &amp;rsquo;lil bit of backend with your site, say for sending contact information or retrieving particular information which is too client-side heavy, then go &lt;a href="https://serverless.com" target="_blank" rel="noreferrer"&gt;Serverless&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Serverless lambda hosting is extremely quick to set up, maintain, and cheap! Not to mention the fact that you can choose between Amazon, Azure or Google to host your functions, the whole world of data hungry tech corporations are just begging to let you run code with them. Again, for FREE*(or super cheap).&lt;/p&gt;
&lt;p&gt;If you are going to go down this whole rabbit hole, I&amp;rsquo;d recommend having a &lt;a href="https://github.com/DevProgress/onboarding/wiki/Using-Circle-CI-with-Github-Pages-for-Continuous-Delivery" target="_blank" rel="noreferrer"&gt;little read first&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="have-fun" class="relative group"&gt;Have fun &lt;/h2&gt;&lt;p&gt;Check out the code on GitHub &lt;a href="https://github.com/jamesjarvis/jamesjarvis.io" target="_blank" rel="noreferrer"&gt;https://github.com/jamesjarvis/jamesjarvis.io&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As mentioned before, feel free to fork it and change the content yourself.&lt;/p&gt;
&lt;p&gt;The content is hosted in &lt;code&gt;assets/&lt;/code&gt; and &lt;code&gt;src/pages/projects/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you have any more questions, reach out to me!&lt;/p&gt;</content:encoded></item><item><title>🎵 Musish: The Unofficial, Open Source Apple Music Web Player</title><link>https://jamesjarvis.io/posts/2018/12/musish/</link><pubDate>Wed, 12 Dec 2018 00:00:00 +0000</pubDate><author>hello@jamesjarvis.io (James Jarvis)</author><guid>https://jamesjarvis.io/posts/2018/12/musish/</guid><category>Projects</category><description>How I was kinda developer-famous for 5 minutes</description><content:encoded>&lt;p&gt;







&lt;/p&gt;
&lt;p&gt;On December 1st 2018, a group of friends and I attended the MLH Local Hack Day in San Francisco.&lt;/p&gt;
&lt;p&gt;Made by myself &lt;a href="https://jamesjarvis.io" target="_blank" rel="noreferrer"&gt;(me)&lt;/a&gt;, Brychan &lt;a href="https://brychan.io" target="_blank" rel="noreferrer"&gt;(link)&lt;/a&gt;, Raphael &lt;a href="https://raphaelvigee.com" target="_blank" rel="noreferrer"&gt;(link)&lt;/a&gt;, and Filip &lt;a href="https://www.linkedin.com/in/filip-grebowski-b24810116/" target="_blank" rel="noreferrer"&gt;(link)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This particular hackathon was sponsored by GitHub and Microsoft, and had a theme of creating the best open source project for the community. We came first in San Francisco for our project.&lt;/p&gt;
&lt;p&gt;We decided to make an &lt;a href="https://musi.sh" target="_blank" rel="noreferrer"&gt;Apple Music web player&lt;/a&gt;, since pretty much every single other music streaming platform already had one. Some people aren&amp;rsquo;t sure why Apple Music would need a web player, since surely you could just use iTunes for that?&lt;/p&gt;
&lt;p&gt;Well, as was &lt;a href="https://forums.macrumors.com/threads/apple-music-gets-another-unofficial-web-player-with-launch-of-musish.2166187/#post-27018681" target="_blank" rel="noreferrer"&gt;hilariously summarised in a comment on Macrumors&lt;/a&gt;, iTunes isn&amp;rsquo;t exactly lightweight.&lt;/p&gt;
&lt;p&gt;Musi.sh (Musish) is an &lt;a href="https://github.com/musish/musish" target="_blank" rel="noreferrer"&gt;open source project&lt;/a&gt; now, using the A-GPL license, and as of February 2019, has over &lt;a href="https://github.com/Musish/Musish/stargazers" target="_blank" rel="noreferrer"&gt;1400 stars&lt;/a&gt;, and &lt;a href="https://github.com/Musish/Musish/graphs/contributors" target="_blank" rel="noreferrer"&gt;9 contributors&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We released it (sent out a few emails) while walking around Central Park, NY on the 18th January 2019. We weren&amp;rsquo;t really prepared with what was to come, and have since had quite a few articles written about it.&lt;/p&gt;
&lt;h2 id="analytics-and-feedback" class="relative group"&gt;Analytics and Feedback &lt;/h2&gt;&lt;p&gt;According to Google Analytics, we have had over 50,000 unique users on Musi.sh from over 110 different countries, and we have found some very positive articles written about it at least 13 different languages found so far.&lt;/p&gt;
&lt;p&gt;Some notable mentions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.businessinsider.com/uk-tech-100-2019-most-important-interesting-and-impactful-people-uk-tech-2019-9?r=US&amp;amp;IR=T#97-the-undergraduate-students-who-beat-apple-to-building-a-web-player-for-apple-music-4" target="_blank" rel="noreferrer"&gt;Business Insider Tech 100 2019 #97&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.archive.org/web/20200322103909/https://www.kent.ac.uk/" target="_blank" rel="noreferrer"&gt;University of Kent homepage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.archive.org/web/20200322104018/https://www.kent.ac.uk/courses/profiles/undergraduate/computer-science-year-industry-musish" target="_blank" rel="noreferrer"&gt;University of Kent profile&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://9to5mac.com/2019/01/18/apple-music-web/" target="_blank" rel="noreferrer"&gt;9to5Mac&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://9to5mac.com/2019/01/21/apple-music-mac-app/" target="_blank" rel="noreferrer"&gt;9to5Mac again&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gizmodo.com/how-to-play-apple-music-from-the-web-1831977569" target="_blank" rel="noreferrer"&gt;Gizmodo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.macrumors.com/2019/01/21/apple-music-web-player-musish/" target="_blank" rel="noreferrer"&gt;Macrumors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cultofmac.com/601520/musish-apple-music-web-portal/" target="_blank" rel="noreferrer"&gt;Cult of Mac&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thenextweb.com/apps/2019/01/22/use-apple-music-in-your-browser-with-this-pretty-web-interface/" target="_blank" rel="noreferrer"&gt;The Next Web&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.01net.com/actualites/musi-sh-le-service-qui-donne-acces-a-apple-music-depuis-un-navigateur-web-1617352.html#xtor=AL-123461" target="_blank" rel="noreferrer"&gt;01Net (France)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://myapple.pl/posts/18640-kolejny-web-player-apple-music-musish-stawia-na-podobienstwo-do-pierwowzoru" target="_blank" rel="noreferrer"&gt;My Apple (Poland)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/search?q=Musi.sh" target="_blank" rel="noreferrer"&gt;Twitter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, we made it to #1 and #2 on GitHub trending for all JavaScript projects and all languages respectively on the 19th January 2019, which was our favourite achievement of all!&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2018/12/musish/javascript_trending_hu_9ff961b25cdfbe13.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="3538"
 height="2430"
 class="mx-auto my-0 rounded-md"
 alt="JavaScript trending"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2018/12/musish/javascript_trending_hu_4213d50c26a7151a.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;Number 1 on JavaScript trending&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;







&lt;figure&gt;
 &lt;a
 
 href="https://jamesjarvis.io/posts/2018/12/musish/all_trending_hu_212633dc320d1f33.png"
 
 &gt;
 








 
 
 &lt;picture
 class="mx-auto my-0 rounded-md"
 
 &gt;
 
 
 
 
 
 
 
 &lt;img
 width="3538"
 height="2430"
 class="mx-auto my-0 rounded-md"
 alt="All languages trending"
 loading="lazy" decoding="async"
 src="https://jamesjarvis.io/posts/2018/12/musish/all_trending_hu_1bf0a6dc2b20b204.png"
 /&gt;
 &lt;/picture&gt;
 


 &lt;/a&gt;
 &lt;figcaption class="text-center"&gt;Number 2 on GitHub trending&lt;/figcaption&gt;
 &lt;/figure&gt;&lt;/p&gt;</content:encoded></item></channel></rss>