8 |
9 | {% if page.last_updated or page.meta %}
10 |
11 |
12 | {% if page.last_updated %}
13 |
Last updated {{ page.last_updated | date: site.date_format }}
14 | {% endif %}
15 | {% for m in page.meta %}
16 |
{{ m }}
17 | {% endfor %}
18 |
19 |
20 | {% endif %}
21 |
22 | {{ content | cleanup_text | smartify }}
23 |
24 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-05-boosting-an-individual-field-in-a-simple-query-string-query.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Boosting an individual field in a Simple Query String query
4 | date: 2018-11-05 16:12:23 +0000
5 | tags: elasticsearch
6 | ---
7 |
8 | By default, a Simple Query String query will search all fields (`*`).
9 |
10 | If you want to preserve that behaviour but just boost a couple of fields beyond the rest, the following query seems to do it:
11 |
12 | ```json
13 | {
14 | "query": {
15 | "simple_query_string": {
16 | "fields": ["description^10", "title^5", ".*"],
17 | "query": "legs",
18 | "default_operator": "and"
19 | }
20 | }
21 | }
22 | ```
23 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-05-23-sort-by-extname-basename-dirname-to-reduce-the-size-of-compressed-streams.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Sort by extname/basename/dirname to reduce the size of compressed streams"
4 | date: 2019-05-23 07:17:33 +0100
5 | tags: compression git
6 | ---
7 |
8 | Chris Dickson on Twitter:
9 |
10 | > I got to use a trick I learned from git today: if you're going to throw a directory full of files into a compressed stream, wait, take a second,
11 | >
12 | > sort those files by extname -> basename -> dirname first so files that are likely to be similar end up next to each other
13 | >
14 | > I just decreased the size of my compressed stream by 10% using this one weird trick
15 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-06-04-latency-issues-with-nlb-and-ecs-tasks.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Latency issues with NLB and ECS tasks"
4 | date: 2019-06-04 19:52:55 +0100
5 | tags: aws
6 | ---
7 |
8 | If the number of ECS tasks is less than the number of AZs served by an NLB, you get latent issues.
9 |
10 | See [Register Targets with your Target Group](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/target-group-register-targets.html):
11 |
12 | > You register your targets with one or more target groups. Each target group must have at least one registered target in each Availability Zone that is enabled for the load balancer. You can register targets by instance ID or by IP address.
13 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-05-experiments-with-the-linode-terraform-provider.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Experiments with the Linode Terraform provider
4 | date: 2018-11-05 21:23:17 +0000
5 | tags: linode terraform
6 | ---
7 |
8 | The docs don't yet have a list of instance types/regions (see [terraform-providers/terraform-provider-linode#7](https://github.com/terraform-providers/terraform-provider-linode/issues/7)); this is what I've managed to work out by guessing:
9 |
10 | * The London, UK is `eu-west`
11 | * The Linode 1GB plan is `g6-nanode-1`
12 | * The Linode 2GB plan is `g6-standard-1`
13 |
14 | I'm hoping they'll add more to the docs at some point, but those are the values most useful to me for now.
15 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-12-replace-white-parts-of-an-image-with-transparency.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "ImageMagick: Replace white parts of an image with transparency"
4 | date: 2018-11-12 09:09:23 +0000
5 | tags: imagemagick
6 | ---
7 |
8 | Replace white sections of an image with transparency:
9 |
10 | ```console
11 | $ convert myimage.jpg -transparent white myimage.png
12 | ```
13 |
14 | If it's not pure white, and you need a bit of extra:
15 |
16 | ```console
17 | $ convert myimage.jpg -fuzz 10% -transparent white myimage.png
18 | ```
19 |
20 | If you want to additionally crop the image to the nontransparent portion:
21 |
22 | ```console
23 | $ convert myimage.jpg -fuzz 10% -transparent white -trim myimage.png
24 | ```
25 |
--------------------------------------------------------------------------------
/src/_scss/_layout.scss:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | article {
7 | @include central_element();
8 | padding-top: 3px;
9 | padding-bottom: 15px;
10 | }
11 |
12 | .dot_list {
13 | list-style-type: none;
14 | padding-left: 0px !important;
15 |
16 | // Ensure they all display in a line
17 | li {
18 | display: inline;
19 | &:not(:first-child)::before {
20 | content: " · ";
21 | }
22 | }
23 | }
24 |
25 | hr {
26 | background-color: $light-grey;
27 | height: 1px;
28 | border: 0px;
29 | }
30 |
31 | // Images never expand beyond the article bounds, and are centred when
32 | // they're too small.
33 | img, video {
34 | @include centred(100%);
35 | display: block;
36 | }
37 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-05-delete-elasticsearch-indexes-to-improve-performance.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Delete Elasticsearch indexes to improve performance
4 | date: 2018-11-05 10:15:19 +0000
5 | tags: elasticsearch
6 | ---
7 |
8 | If your Elasticsearch cluster is having performance problems (visible through requests timing out under load), and against the limits of CPU and memory, try deleting unused indexes.
9 | We had a cluster with ~70 indexes, and deleting a bunch of indexes we weren't using made a noticeable difference.
10 |
11 | Before:
12 |
13 | 
14 |
15 | After:
16 |
17 | 
18 |
19 | (Screenshots from the Performance tab of the Elastic Cloud console.)
20 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-10-25-rendering-markdown-without-p-tags-in-jekyll.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Rendering Markdown without <p> tags in Jekyll"
4 | date: 2018-10-25 07:54:00 +0000
5 | tags: markdown jekyll
6 | ---
7 |
8 | The `markdownify` filter can render Markdown as HTML:
9 |
10 | ```html
11 | {% raw %}{{ page.title | markdownify }}{% endraw %}
12 | ```
13 |
14 | That adds `
` tags to the output.
15 | If you're in a context where that's undesirable (for example, a heading), add two `remove` filters afterwards:
16 |
17 | ```html
18 | {% raw %}{{ page.title | markdownify | remove: '
' | remove: '
' }}{% endraw %}
19 | ```
20 |
21 | (via [jekyll/jekyll#3571](https://github.com/jekyll/jekyll/issues/3571#issuecomment-372061718))
22 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-05-09-github-set-co-commit-credit-with-co-authored-by.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "GitHub: Set co-author commit credit with “Co-authored-by”"
4 | date: 2019-05-09 08:54:03 +0100
5 | tags: git github
6 | ---
7 |
8 | If you add the following lines to your commit message, the GitHub UI will show them as co-authors:
9 |
10 | ```
11 | Co-authored-by: your name
12 | Co-authored-by: your co-author
13 | ```
14 |
15 | I have a `;co` snippet for this.
16 |
17 | References:
18 |
19 | * [Indu Alagarsamy's tweet where I saw this](https://twitter.com/Indu_alagarsamy/status/1125641581904551936)
20 | * [The GitHub blog post about the feature](https://github.blog/2018-01-29-commit-together-with-co-authors/)
21 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-05-12-is-apache-using-threaded-mpms-or-pre-fork.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Is Apache using threaded MPMs or pre-fork?"
4 | date: 2019-05-12 08:56:13 +0100
5 | tags: apache dreamwidth
6 | ---
7 |
8 | As part of setting up [a Dreamwidth installation](http://wiki.dreamwidth.net/notes/Dreamwidth_Scratch_Installation#Configure_Apache_2), you have to check if Apache is using threaded MPMs or pre-fork.
9 |
10 | Even from the linked page, it wasn't clear to me how to work out which it was using.
11 | Poking around on Stack Overflow let me to this solution:
12 |
13 | ```console
14 | # apache2ctl -t -D DUMP_MODULES | grep mpm_
15 | mpm_event_module (shared)
16 | ```
17 |
18 | This is using threaded MPMs; you'd see a different module if it was using pre-fork.
19 |
--------------------------------------------------------------------------------
/create_post.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "fileutils"
4 |
5 | now = Time.now
6 |
7 | unless ARGV.length == 1
8 | puts "Usage: create_post.rb "
9 | exit 1
10 | end
11 |
12 | title = ARGV[0]
13 | name = title
14 | .downcase
15 | .gsub(/[^a-z]/, "-")
16 | .gsub(/\-{2,}/, "-")
17 | .chomp("-")
18 |
19 | out_dir = File.join("src", "_posts", now.strftime("%Y"))
20 | FileUtils.mkdir_p out_dir
21 |
22 | path = File.join(out_dir, "#{now.strftime('%Y-%m-%d')}-#{name}.md")
23 |
24 | def finish(path)
25 | puts path
26 | `open #{path}`
27 | exit 0
28 | end
29 |
30 | if File.exist? path
31 | finish(path)
32 | end
33 |
34 | doc = "---\nlayout: post\ntitle: \"#{title}\"\ndate: #{now}\ntags: \n---\n\n"
35 | File.open(path, 'w') { |f| f.write(doc) }
36 |
37 | finish(path)
38 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-04-10-standard-ia-in-s-incurs-a-minimum-day-charge.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Standard IA in S3 incurs a minimum 30 day charge
4 | date: 2018-04-10 08:52:00 +0000
5 | tags: aws aws:s3
6 | ---
7 |
8 | From the [S3 pricing page](https://aws.amazon.com/s3/pricing/):
9 |
10 | > S3 Standard-Infrequent Access and S3 One Zone-Infrequent Access Storage are charged for a minimum storage duration of 30 days. Objects that are deleted, overwritten, or transitioned to a different storage class before 30 days will incur the normal usage charge plus a pro-rated request charge for the remainder of the 30 day minimum.
11 |
12 | This means it's less suitable for objects that are frequently replaced or deleted.
13 |
14 | (Context was the S3 bucket saving snapshots for the Catalogue API.)
15 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-08-28-installing-mimetype-on-alpine-linux.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Installing mimetype on Alpine Linux"
4 | date: 2019-08-28 13:49:55 +0100
5 | tags: alpine
6 | ---
7 |
8 | I needed to install [mimetype(1)] in Alpine because it's used by the [preview-generator] library.
9 |
10 | ```console
11 | $ docker run -it alpine sh
12 |
13 | # apk add apkbuild-cpan build-base perl perl-dev shared-mime-info
14 | # PERL_MM_USE_DEFAULT=1 cpan File::BaseDir
15 | # PERL_MM_USE_DEFAULT=1 cpan File::MimeInfo
16 | # apk del apkbuild-cpan build-base perl-dev
17 |
18 | # echo "# README" > README.md
19 | # mimetype README.md
20 | README.md: text/markdown
21 | ```
22 |
23 | [mimetype(1)]: https://linux.die.net/man/1/mimetype
24 | [preview-generator]: https://pypi.org/project/preview-generator/
25 |
--------------------------------------------------------------------------------
/src/_includes/footer.html:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | ---
4 |
5 | ## Notebook entries
6 |
7 |
20 | Tagged with:
21 | {% assign sorted_tags = post.tags | sort %}
22 | {% for tag in sorted_tags %}
23 | {{ tag }}
24 | {% endfor %}
25 |
26 | {% endif %}
27 |
28 |
29 | {% endfor %}
30 |
31 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-03-05-sharing-files-with-my-work-computer-using-dropbox.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Sharing files with my work computer using Dropbox
4 | date: 2018-03-05 20:02:00 +0000
5 | tags: dropbox
6 | ---
7 |
8 | Problem: I have some files in my Dropbox that I want to search with my work computer (notes, my Alfred preferences, maybe a few other things).
9 |
10 | I could log into my personal Dropbox at work, and then use Selective Sync to download just those folders locally, but that feels like it's skirting around poor data management practices.
11 |
12 | Solution: I created a new Dropbox account with my work email, then shared the directories I want at work into that account.
13 | I get granular control of what's shared with work, and it's harder for stuff to accidentally get into my personal account.
14 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-04-20-getting-the-cover-of-an-epub-file.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Getting the cover of an epub file"
4 | date: 2019-04-20 21:24:30
5 | tags: epub
6 | ---
7 |
8 | The preview-generator library doesn't have support for epub files, so I have to create thumbnails for those separately.
9 | The first step is to get the cover image of the book.
10 |
11 | I found some useful code for doing this on GitHub: (GPL)
12 |
13 | The basic gist:
14 |
15 | * An epub is a zip file, so look inside the zipfile and assume the biggest image entry is the cover image
16 | * Poke around inside the `container.xml` inside the epub
17 |
18 | The code in the GitHub repo is Python, but is fairly simple and could be ported to another language if necessary, licence allowing.
19 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-26-how-to-suppress-installing-rdoc-ri-docs-when-running-gem-install.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: How to suppress installing rdoc/ri docs when running 'gem install'
4 | date: 2018-11-26 10:13:25 +0000
5 | tags: ruby docker
6 | ---
7 |
8 | If you try to run `gem install` in a Docker container which only contains a Ruby package (and no rdoc or ri), you get an error:
9 |
10 | ```
11 | Step 3/5 : RUN gem install rack
12 | ---> Running in 8ee24453f7a9
13 | ERROR: While executing gem ... (Gem::DocumentError)
14 | RDoc is not installed: cannot load such file -- rdoc/rdoc
15 | ```
16 |
17 | If you add the following line to your `.gemrc`, it skips trying to install the docs.
18 |
19 | ```
20 | install: --no-rdoc --no-ri
21 | ```
22 |
23 | Handy if you're in a Docker image that will never run interactively!
24 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-10-24-using-xargs-for-parallel-processing.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Using xargs for parallel processing
4 | date: 2018-10-24 08:55:00 +0000
5 | tags: shell
6 | ---
7 |
8 | If you have a file full of arguments (`inputs.txt`), and a script that takes a single ID as an argument (`process_single_id.py`), you can run the script in parallel with `xargs`:
9 |
10 | ```shell
11 | $ xargs -P 84 -I '{}' python process_single_id.py '{}' < inputs.txt
12 | ```
13 |
14 | Customise the number of parallel processes with the `-P` flag.
15 |
16 | You'll want to experiment with the number of processes you run -- although 84 was about the limit of my laptop's CPU, it caused more errors in the Tandem Vault/S3 APIs, so the overall throughput was actually less.
17 |
18 | (I discovered this during the Miro migration project in October 2018.)
19 |
20 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-05-11-how-do-dreamwidth-post-ids-increment.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Dreamwidth: How/why do post IDs increment?"
4 | date: 2019-05-11 23:26:38 +0100
5 | tags: dreamwidth
6 | ---
7 |
8 | Dreamwidth IDs (posts, comments) don't increment one-by-one, but via algebraic manipulations. Comments don't go 1, 2, 3, they go 256, 540, 721, whatever.
9 |
10 | The exact pattern is [something like](https://github.com/dreamwidth/dw-free/blob/2c5f1a9a11efbcf43a9eaa73a6ae43a533ec439d/cgi-bin/DW/Collection.pm#L54):
11 |
12 | ```
13 | display_id = collection_id * 256 + internal_id
14 | ```
15 |
16 | This is an old anti-bot measure: if a bot saw URLs with `/1.html`, `/2.html`, `/3.html`, it assumes it's a sequence and continues in progression, which could overwhelm the site.
17 | These days it's not needed (hooray CDNs!) but it's baked into the site now.
18 |
--------------------------------------------------------------------------------
/src/_scss/_aside.scss:
--------------------------------------------------------------------------------
1 | aside {
2 | border-bottom: 2px solid $primary-color;
3 | background: url('/theme/specktre.png') rgba(114,83,237,0.25);
4 | background-size: auto 100%;
5 |
6 | #aside_inner {
7 | @include central_element();
8 |
9 | font-family: Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', serif;;
10 |
11 | a:not([class]) {
12 | background-image: none;
13 | }
14 |
15 | a:hover {
16 | text-decoration: none;
17 | }
18 |
19 | a:visited {
20 | color: white;
21 | }
22 |
23 | #brand {
24 | margin-bottom: 18px;
25 | font-weight: normal;
26 | font-size: 2em;
27 | a {
28 | text-decoration: none;
29 | }
30 | }
31 |
32 | #aside_links ul {
33 | padding-top: 0px;
34 | padding-bottom: 0px;
35 | margin-bottom: 10px;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/_scss/_settings.scss:
--------------------------------------------------------------------------------
1 | $primary-color: darken(#7253ed, 10%);
2 | $primary-dark: darken($primary-color, 25%);
3 |
4 | $main-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
5 | $mono-font: Menlo, Consolas, monospace;
6 |
7 | $body-color: #3c3942;
8 | $accent-grey: #999;
9 | $light-grey: #f0f0f0;
10 | $midtone-gray: #ccc;
11 |
12 | $default-font-size: 1em;
13 | $line-height: 1.45em;
14 |
15 | $meta-size: 0.82;
16 | $meta-font-size: $meta-size * $default-font-size;
17 | $meta-line-height: $meta-size * $line-height * 1.15;
18 |
19 | $max-width: 750px !default;
20 | $default-padding: 20px !default;
21 | $big-font-size: 2em !default;
22 |
23 | $sidebar-border-width: 1px;
24 |
25 | $scaling-factor: 0.95;
26 | $code-scaling-factor: 0.88;
27 |
28 | $jekyll-red: #d50000;
29 | $linode-green: green;
30 | $github-grey: #171515;
31 | $twitter-blue: #55acee;
32 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-04-20-http-the-content-disposition-header.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "HTTP: The Content-Disposition header"
4 | date: 2019-04-20 21:24:30 +0100
5 | tags: web-dev http
6 | ---
7 |
8 | The `Content-Disposition` header can be used to tell a browser the filename of an HTTP response.
9 | It's used for "Save As" or when downloading the file.
10 |
11 | For example, you might access a URL of the form:
12 |
13 | /files/0645c33f-0be6-44e1-8059-228ec9594867.pdf
14 |
15 | If you include a `Content-Disposition` header of the form:
16 |
17 | filename*=utf-8''original_filename.pdf
18 |
19 | the browser will download this filename as `original_filename.pdf`.
20 |
21 | Read more:
22 |
23 | * MDN docs for Content-Disposition:
24 | * Encoding a filename as UTF-8:
25 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-05-be-careful-of-assembling-partial-databases-with-concurrency.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Be careful of assembling partial databases with concurrency
4 | date: 2018-11-05 16:30:20 +0000
5 | tags: concurrency miro-migration
6 | ---
7 |
8 | This bit me during the Miro migration.
9 |
10 | A brief reminder of the setup: I had an "inventory" of destinations for each of the images.
11 | When I ran a script to move an image, it wrote a "partial inventory" update, which could be reassembled into the proper inventory later.
12 | In practice, these are all JSON files on disk.
13 |
14 | 
15 |
16 | Remembering to run the reassembly script was tedious, so I had the "smart" idea of writing the partial file and then automatically triggering the inventory script.
17 |
18 | But because the main JSON file can't be updated concurrently, this promptly exploded.
19 | Don't do that!
20 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-10-24-using-force-with-lease.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Using --force-with-lease
4 | date: 2018-10-24 09:20:00 +0000
5 | tags: git
6 | ---
7 |
8 | When doing a `git push`, if there are different commits in the remote, your push is rejected.
9 | This often happens if I've rebased against master.
10 |
11 | You can get round this by running `git push --force`, but what if your rebase wasn't the only change?
12 | What if somebody else pushed commits while you weren't looking?
13 | This can be dangerous!
14 |
15 | Using `git push --force-with-lease` is safer, because it checks the branch hasn't moved in the meantime.
16 | Quoting [`--force` considered harmful; understanding git's `--force-with-lease`](https://developer.atlassian.com/blog/2015/04/force-with-lease/):
17 |
18 | > What --force-with-lease does is refuse to update a branch unless it is the state that we expect; i.e. nobody has updated the branch upstream.
19 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-10-28-replacing-map-flatten-with-flatmap.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Replacing .map.flatten with .flatMap
4 | date: 2018-10-28 09:30:00 +0000
5 | tags: scala
6 | ---
7 |
8 | This is something IntelliJ whinges about.
9 | I'm still not entirely sure what `flatMap` does, but here's an example which illustrates the change I made.
10 |
11 | Before:
12 |
13 | ```scala
14 | val listOfListOfNames: List[List[String]]
15 |
16 | val people: List[People] =
17 | listOfListOfNames
18 | .map { names: List[String] =>
19 | val maybeFirst: Option[String] = getFirstName(names)
20 | maybeFirst.map { name => Person(name) }
21 | }
22 | .flatten
23 | ```
24 |
25 | After:
26 |
27 | ```scala
28 | val people: List[People] =
29 | listOfListOfNames
30 | .flatMap { names: List[String] =>
31 | val maybeFirst: Option[String] = getFirstName(names)
32 | maybeFirst.map { name => Person(name) }
33 | }
34 | ```
35 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-12-failing-to-find-an-implicit-objectstore-when-it-wants-an-executioncontext.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Failing to find an implicit ObjectStore when it wants an ExecutionContext
4 | date: 2018-11-12 15:49:07 +0000
5 | tags: scala wellcome
6 | ---
7 |
8 | I was getting a complaint about being unable to find an implicit ObjectStore when running some Goobi reader tests:
9 |
10 | ```
11 | GoobiReaderFeatureTest.scala:75: could not find implicit value for parameter objectStore: uk.ac.wellcome.storage.ObjectStore[java.io.InputStream]
12 |
13 | withTypeVHS[InputStream, GoobiRecordMetadata, R](bucket, table) { vhs =>
14 | ^
15 | ```
16 |
17 | I have no idea why that was the error message, but to save future head-scratching, this was the import I needed to add before it compiled:
18 |
19 | ```scala
20 | import scala.concurrent.ExecutionContext.Implicits.global
21 | ```
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # notebook.alexwlchan.net
2 |
3 | This repo has the code for my notebook site, [notebook.alexwlchan.net][root].
4 |
5 | My notebook is for short blog posts that I don't think are worth writing up as a full post on my [main site][main], but which are useful information that I want to find later.
6 | I make them public so they can be indexed by Google.
7 | Often it's for solutions to very specific problems.
8 |
9 | Not everything will make sense if you're not me -- these are written for me first, and may be missing context or assumptions that I've forgotten I'm making.
10 | On my main blog I try to write those out explicitly, but here I don't bother.
11 |
12 | 
13 |
14 | This is a static site built with [Jekyll][jekyll], using a bunch of build machinery taken from my main site.
15 |
16 | [root]: https://notebook.alexwlchan.net/
17 | [main]: https://github.com/alexwlchan/alexwlchan.net
18 | [jekyll]: https://jekyllrb.com/
19 |
20 | # License
21 |
22 | MIT.
23 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-05-11-hashes-and-hash-references.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Perl: Hashes and hash references"
4 | date: 2019-05-11 21:37:24 +0100
5 | tags: perl
6 | ---
7 |
8 | Working in [the Dreamwidth codebase][s2_pm]:
9 |
10 | ```perl
11 | sub sitescheme_secs_to_iso {
12 | my ( $secs, %opts ) = @_;
13 |
14 | ...
15 |
16 | # if opts has a true tz key, get the remote user's timezone if possible
17 | if ( $opts{tz} ) {
18 | ```
19 |
20 | It wasn't clear to me what the difference is between `%opts` and `$opts` is.
21 |
22 | I asked in Discord and [momijizukamori] explained that swapping `%opts` to `$opts` changes it from a hash to a hash reference.
23 | You can only access the values of a hash reference as `$hash{key}`, and changing it to `$hash->{key}` should fix it.
24 |
25 | [s2_pm]: https://github.com/dreamwidth/dw-free/blob/fa394ce0e47ea83d5b5d0db994de324b049a9ccb/cgi-bin/LJ/S2.pm#L2640-L2656
26 | [momijizukamori]: https://momijizukamori.dreamwidth.org/
27 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-28-beware-of-dynamic-arguments-in-apply-methods.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Scala: Beware of dynamic arguments in apply() methods"
4 | date: 2018-11-28 13:41:00 +0000
5 | tags: scala
6 | ---
7 |
8 | Here's a minimal of a case class we had in the platform:
9 |
10 | ```scala
11 | import java.time.Instant
12 |
13 | case class Modifiable(
14 | createdDate: Instant = Instant.now,
15 | lastModifiedDate: Instant = Instant.now
16 | )
17 | ```
18 |
19 | We had a test that created an instance of `Modifiable`, then asserted that the creation
20 | and last modified date were the same -- and normally that's fine.
21 |
22 | But occasionally there'd be a delay, and you'd get a new instance of `Modifiable` that had a different created and last modified date.
23 | The fix:
24 |
25 | ```scala
26 | case object Modifiable {
27 | def apply(createdDate: Instant = Instant.now): Modifiable =
28 | Modifiable(
29 | createdDate = createdDate,
30 | lastModifiedDate = createdDate
31 | )
32 | }
33 | ```
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 Alex Chan
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-15-notes-on-vpc-networking-and-acls.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Notes on AWS networking and ACLs
4 | date: 2018-11-15 13:05:46 +0000
5 | tags: aws networking
6 | ---
7 |
8 | Some brief notes from a whiteboard session with RK.
9 |
10 | 
11 |
12 | An availability zone is assigned a CIDR block (here `120.0.0.0/16`).
13 |
14 | The AZ has a default route table and ACL (access control list), which let everything through.
15 |
16 | Within the AZ, you create subnets.
17 | There are ACLs attached to the subnets, and route table entries on the subnets.
18 | (This lets you keep public and private subnets separate, and have different routes to the public Internet.)
19 |
20 | The route table has the VPC CIDR, and the Internet gateway (or a NAT Gateway for private subnets).
21 |
22 | ACL rules take precedence over security rules.
23 | They have to be stateless -- only for inbound traffic or only for outbound traffic.
24 |
25 | **Don't play with ACL and route table entries! That way lies unplanned outages.**
26 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-08-14-condition-parameter-type-does-not-match-schema-type.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Condition parameter type does not match schema type"
4 | date: 2019-08-14 16:48:07 +0100
5 | tags: aws aws:dynamodb
6 | ---
7 |
8 | If you get the following error from DynamoDB:
9 |
10 | > One or more parameter values were invalid: Condition parameter type does not match schema type
11 |
12 | it's a sign that you're requesting the wrong type of paramater in a query.
13 |
14 | In boto3/Python, it's code like:
15 |
16 | ```python
17 | import boto3
18 |
19 | dynamodb = boto3.client("dynamodb")
20 |
21 | dynamodb.query(
22 | TableName="storage-staging-ingests",
23 | KeyConditions={
24 | "id": {
25 | "AttributeValueList": [{"N": "1"}],
26 | "ComparisonOperator": "EQ"
27 | }
28 | }
29 | )
30 | ```
31 |
32 | In Scala-land, we've seen this when we pass a case class as the key (e.g. `ReplicaPath`) and the custom DynamoFormat is wrong or missing, so it tries to query on a map instead of a string/int.
33 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-10-24-beware-ambiguous-dates-with-dateutil-parse.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Beware ambiguous dates with dateutil.parse
4 | date: 2018-10-24 09:01:00 +0000
5 | tags: python datetime-handling
6 | ---
7 |
8 | The [dateutil module](https://pypi.org/project/python-dateutil/) is useful for parsing ambiguous dates, but you want to be careful parsing lists -- it defaults to American "month-first" style dates.
9 | Compare:
10 |
11 | ```python
12 | import dateutil.parser as dp
13 |
14 | dates = ["1/2/2018", "11/2/2018", "21/2/2018"]
15 |
16 | for date_str in dates:
17 | print(dp.parse(date_str))
18 |
19 | # 2018-01-02 00:00:00
20 | # 2018-11-02 00:00:00
21 | # 2018-02-21 00:00:00
22 |
23 | for date_str in dates:
24 | print(dp.parse(date_str, dayfirst=True))
25 |
26 | # 2018-02-01 00:00:00
27 | # 2018-02-11 00:00:00
28 | # 2018-02-21 00:00:00
29 | ```
30 |
31 | If you don't pass the `dayfirst=True`, it makes a guess at what seems sensible, and only uses the British format if it's unambiguous.
32 | This can cause unexpected results!
33 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-05-15-getting-the-latest-version-of-a-range-key.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "DynamoDB: Getting the latest version of a range key associated with a hash key"
4 | date: 2019-05-15 20:25:24 +0100
5 | tags: aws aws:dynamodb java scala
6 | ---
7 |
8 | I have yet to come up with a way to describe this that isn't completely horrible; this DynamoDB query with the document client will get you the lowest/highest range key row associated with a particular hash key value
9 |
10 | ```scala
11 | val querySpec = new QuerySpec()
12 | .withHashKey(hashKeyName, hashKeyValue)
13 | .withConsistentRead(true)
14 | .withScanIndexForward(lowestValueFirst)
15 | .withMaxResultSize(1)
16 | ```
17 |
18 | A query returns results ordered by range key -- the trick is making sure results arrive in the right order.
19 |
20 | See also:
21 |
22 | * [DynamoHashKeyLookup.scala @ cc3b434](https://github.com/wellcometrust/scala-storage/blob/cc3b434c5cfeb264f14e7da0504dbf796a528141/storage/src/main/scala/uk/ac/wellcome/storage/dynamo/DynamoHashKeyLookup.scala) (MIT)
23 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-06-beware-of-using-val-in-abstract-traits.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Beware of NullPointerException when using 'val' in abstract traits
4 | date: 2018-11-06 13:37:57 +0000
5 | tags: scala
6 | ---
7 |
8 | A simplified version of the code I was using is below:
9 |
10 | ```scala
11 | class NotifierApp() extends WellcomeApp {
12 | val configModule = new Configurable { }
13 | }
14 |
15 | trait WellcomeApp {
16 | val configModule: Configurable
17 | val injector: Injector = Guice.createInjector(configModule)
18 | }
19 | ```
20 |
21 | Whenever I tried to run it, the code threw a NullPointerException while creating the Guice injector.
22 | Why?
23 |
24 | I think it's because `injector` was being evaluated when the trait was created, and before the new value of `configModule` had been set in `NotifierApp`.
25 | Thus `injector` is nil, and that blows up the injector.
26 | Changing this to use "def" instead of "val" seemed to fix the bug.
27 |
28 | For more context, see [wellcometrust/platform#2971](https://github.com/wellcometrust/platform/pull/2971).
29 |
--------------------------------------------------------------------------------
/src/_plugins/cleanup_text.rb:
--------------------------------------------------------------------------------
1 | # Various text cleanups.
2 |
3 | module Jekyll
4 | module CleanupsFilter
5 | def cleanup_text(input)
6 | # Replace mentions of RFCs with a non-breaking space version.
7 | text = input.gsub(/RFC (\d+)/, 'RFC \1')
8 |
9 | # Also: "part X" or "Part X"
10 | text = text.gsub(/([Pp]art) (\d+)/, '\1 \2')
11 |
12 | # Display "LaTeX" in a nice way, if you have CSS enabled
13 | text = text.gsub(
14 | "LaTeX",
15 | "LaTeX")
16 |
17 | # Replace any mention of "PyCon" with the appropriate non-breaking space
18 | text = text.gsub("PyCon ", "PyCon ")
19 |
20 | # Get rid of the trailing space after the dollar in language-console
21 | # blocks. The space is added in CSS and is unselectable.
22 | text = text.gsub(
23 | "$ ",
24 | "$")
25 |
26 | text
27 | end
28 | end
29 | end
30 |
31 | Liquid::Template::register_filter(Jekyll::CleanupsFilter)
32 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-04-20-javascript-manipulating-url-query-parameters.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "JavaScript: manipulating URL query parameters"
4 | date: 2019-04-20 21:24:30 +0100
5 | tags: javascript
6 | ---
7 |
8 | Last time I did this, I had to use some moderately fiddly code from Stack Overflow.
9 | There are built-in tools for this now:
10 |
11 | ```javascript
12 | function addQueryParameter(name, value) {
13 | var url = new URL(window.location.href);
14 | url.searchParams.append(name, value);
15 | return url.href
16 | }
17 |
18 | function setQueryParameter(name, value) {
19 | var url = new URL(window.location.href);
20 | url.searchParams.set(name, value);
21 | return url.href
22 | }
23 |
24 | function deleteQueryParameter(name) {
25 | var url = new URL(window.location.href);
26 | url.searchParams.delete(name);
27 | return url.href
28 | }
29 | ```
30 |
31 | All these methods return a string based on the current window location.
32 |
33 | Read more:
34 |
35 | * MDN docs for URLSearchParams:
36 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BUILD_IMAGE = jekyll/jekyll:3.8
2 |
3 | RSYNC_HOST = 139.162.244.147
4 | RSYNC_USER = alexwlchan
5 | RSYNC_DIR = /home/alexwlchan/sites/notebook.alexwlchan.net
6 |
7 | ROOT = $(shell git rev-parse --show-toplevel)
8 | DST = $(ROOT)/_site
9 |
10 |
11 | build:
12 | docker run --tty --rm \
13 | --volume $(ROOT):$(ROOT) \
14 | --workdir $(ROOT) \
15 | --env JEKYLL_UID=0 \
16 | $(BUILD_IMAGE) jekyll build
17 |
18 | serve:
19 | docker run \
20 | --publish 6060:6060 \
21 | --volume $(ROOT):$(ROOT) \
22 | --workdir $(ROOT) \
23 | --tty --rm $(BUILD_IMAGE) \
24 | jekyll serve --host "0.0.0.0" --port 6060 --watch --drafts
25 |
26 | deploy: build
27 | docker run --rm --tty \
28 | --volume ~/.ssh/id_rsa:/root/.ssh/id_rsa \
29 | --volume $(DST):/data \
30 | instrumentisto/rsync-ssh \
31 | rsync \
32 | --archive \
33 | --verbose \
34 | --compress \
35 | --delete \
36 | --exclude=".DS_Store" \
37 | --rsh="ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \
38 | /data/ "$(RSYNC_USER)"@"$(RSYNC_HOST)":"$(RSYNC_DIR)"
39 |
40 |
41 | .PHONY: build serve deploy
42 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-05-09-jekyll-creating-permalinks-to-posts.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Jekyll: creating permalinks to posts"
4 | date: 2019-05-09 12:39:53 +0100
5 | tags: jekyll
6 | ---
7 |
8 | It's a bit buried in the Jekyll docs, but you can use the `post_url` tag to generate permalink URLs to posts:
9 |
10 | ```
11 | {% raw %}{% post_url 2019-05-09-jekyll-creating-permalinks-to-posts %}{% endraw %}
12 | ```
13 |
14 | If you get this warning:
15 |
16 | > Deprecation: A call to {% raw %}{% post_url 2019-05-09-java-conditional-updates-in-dynamodb %}{% endraw %} did not match a post using the new matching method of checking name (path-date-slug) equality. Please make sure that you change this tag to match the post's name exactly.
17 |
18 | It's because I put each year of posts in a separate folder.
19 | You need to prefix the year to get the "path" part, like so:
20 |
21 | ```
22 | {% raw %}{% post_url 2019/2019-05-09-jekyll-creating-permalinks-to-posts %}{% endraw %}
23 | ```
24 |
25 | References:
26 |
27 | - [Tags Filters/Linking to posts](https://jekyllrb.com/docs/liquid/tags/#linking-to-posts) in the Jekyll docs
28 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-04-20-python-include-the-filename-content-type-and-content-length-in-a-requests-upload.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Python: Include the filename, Content-Type and Content-Length in a requests upload"
4 | date: 2019-04-20 21:24:30 +0100
5 | tags: python
6 | ---
7 |
8 | When you upload a file through an HTML form:
9 |
10 | ```html
11 |
15 | ```
16 |
17 | it gets sent to the server with a filename, content-type and content-length (along with the contents, of course).
18 |
19 | You can pass optional content-type and filename when uploading a file with requests by passing a 2-tuple or 3-tuple in the `files` list.
20 | Two examples:
21 |
22 | ```python
23 | import requests
24 |
25 | requests.post(
26 | "/upload",
27 | files={"file": ("mydocument.pdf", open("mydocument.pdf", "rb"))},
28 | )
29 |
30 | requests.post(
31 | "/upload",
32 | files={"file": ("mydocument.pdf", open("mydocument.pdf", "rb"), "application/pdf")},
33 | )
34 | ```
35 |
36 | Relevant docs:
37 |
--------------------------------------------------------------------------------
/src/_posts/2019/2019-04-20-python-use-the-whitenoise-library-to-serve-static-files.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: "Python: Use the whitenoise library to serve static files"
4 | date: 2019-04-20 21:24:30 +0100
5 | tags: python
6 | ---
7 |
8 | [whitenoise] is a Python library for serving static files in a WSGI application.
9 | When you create an instance of whitenoise, you pass it a folder, and it learns all the files in that folder.
10 | If you save a new file in that folder, you need to tell whitenoise about it.
11 |
12 | Here's a quick example, taken from the [whitenoise docs][wn_docs]:
13 |
14 | ```python
15 | from whitenoise import WhiteNoise
16 |
17 | from my_project import MyWSGIApp
18 |
19 | application = MyWSGIApp()
20 | application = WhiteNoise(application, root='/path/to/static/files')
21 | application.add_files('/path/to/more/static/files', prefix='more-files/')
22 | ```
23 |
24 | It gets unhappy if the size of a file changes underneath it, after the initial load.
25 |
26 | Read more:
27 |
28 | * Whitenoise docs:
29 |
30 | [whitenoise]: https://pypi.org/project/whitenoise/
31 | [wn_docs]: http://whitenoise.evans.io/en/stable/#quickstart-for-other-wsgi-apps
32 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-07-logging-into-the-aws-console-when-your-alias-isn-t-working.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Logging into the AWS Console when your alias isn't working
4 | date: 2018-11-07 10:47:58 +0000
5 | tags: aws
6 | ---
7 |
8 | Trying to log into the AWS Console today, and we all saw this error:
9 |
10 |
11 |
12 | Meep!
13 | Had our account been compromised?
14 | (Spoiler: no.)
15 |
16 | Our IAM accounts were all working through the CLI, and we eventually tracked down the root credentials -- and then discovered that the IAM account alias had changed.
17 | No longer was the account alias *wellcomedigitalplatform*, instead it was *otherthing*.
18 |
19 | Two ways we could have worked around this:
20 |
21 | 1. Logging in with the account ID (7600...) instead of the account alias.
22 | This always works, whether or not you have an alias set.
23 |
24 | 2. Used the AWS CLI to discover the current aliases (if any):
25 |
26 | ```console
27 | $ aws iam list-account-aliases
28 | {
29 | "AccountAliases": [
30 | "wellcomedigitalplatform"
31 | ]
32 | }
33 |
34 | $ aws iam list-account-aliases
35 | {
36 | "AccountAliases": []
37 | }
38 | ```
--------------------------------------------------------------------------------
/src/_plugins/escape_email.rb:
--------------------------------------------------------------------------------
1 | # This does some quick HTML encoding on email addresses to make them
2 | # slightly harder to find for spam bots. The idea and implementation
3 | # are both copied directly from Markdown.pl.
4 |
5 | require 'cgi'
6 |
7 | module Jekyll
8 | module EmailFilter
9 | def encode_mailto(input)
10 | "mailto:#{input}".chars.map { |ch| _encode_char(ch) }.join("")
11 | end
12 |
13 | def encode_mail(input)
14 | "#{input}".chars.map { |ch| _encode_char(ch) }.join("")
15 | end
16 |
17 | def _encode_char(char)
18 | if char == ":"
19 | char
20 | elsif char == "@"
21 | _encode_char_with_method(char, method = "hex")
22 | else
23 | r = rand()
24 | if r > 0.9
25 | _encode_char_with_method(char)
26 | elsif r < 0.45
27 | _encode_char_with_method(char, method = "hex")
28 | else
29 | _encode_char_with_method(char, method = "dec")
30 | end
31 | end
32 | end
33 |
34 | def _encode_char_with_method(char, method = nil)
35 | if method == "hex"
36 | "#{char.ord.to_s(16).upcase};"
37 | elsif method == "dec"
38 | "#{char.ord};"
39 | else
40 | char
41 | end
42 | end
43 |
44 | end
45 | end
46 |
47 | Liquid::Template::register_filter(Jekyll::EmailFilter)
48 |
--------------------------------------------------------------------------------
/src/_scss/_footer.scss:
--------------------------------------------------------------------------------
1 | footer {
2 | border-top: 1px solid $light-grey;
3 | padding-top: 15px;
4 | padding-bottom: 15px;
5 |
6 | @media print {
7 | display: none;
8 | }
9 | }
10 |
11 | #footer_inner {
12 | @include central_element();
13 |
14 | font-size: $meta-font-size;
15 | &, a, a:visited {
16 | color: $accent-grey;
17 | }
18 |
19 | #contact_links, ul.dot_list {
20 | display: inline-block;
21 | margin-top: 0px;
22 | }
23 |
24 | p:first-child {
25 | margin-bottom: 0px;
26 | }
27 |
28 | // These definitions have to live in the #footer_inner block so they
29 | // override the a:visited style defined above.
30 | #footer__cc:hover, #footer__mit:hover {
31 | color: black;
32 | background-image: linear-gradient(rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0.45) 100%);
33 | background-size: 1px 1px
34 | }
35 |
36 | #footer__email:hover, #footer__about:hover {
37 | color: $primary-color;
38 | }
39 |
40 | #footer__github:hover {
41 | color: $github-grey;
42 | background-image: linear-gradient(rgba(23, 21, 21, 0.45) 0%, rgba(23, 21, 21, 0.45) 100%);
43 | background-size: 1px 1px
44 | }
45 | #footer__twitter:hover {
46 | color: $twitter-blue;
47 | background-image: linear-gradient(rgba(85, 172, 238, 0.45) 0%, rgba(85, 172, 238, 0.45) 100%);
48 | background-size: 1px 1px
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/_scss/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin centred($max-width) {
2 | max-width: $max-width;
3 | margin-left: auto;
4 | margin-right: auto;
5 | }
6 |
7 | @mixin central_element() {
8 | @include centred($max-width);
9 | padding: 1px $default-padding;
10 | }
11 |
12 | @mixin disable_select() {
13 | // Disable text selection highlighting
14 | // https://stackoverflow.com/a/4407335/1558022
15 | -webkit-touch-callout: none; /* iOS Safari */
16 | -webkit-user-select: none; /* Safari */
17 | -khtml-user-select: none; /* Konqueror HTML */
18 | -moz-user-select: none; /* Firefox */
19 | -ms-user-select: none; /* Internet Explorer/Edge */
20 | user-select: none; /* Non-prefixed version, currently
21 | supported by Chrome and Opera */
22 | }
23 |
24 | @mixin purple_box() {
25 | background-color: rgba(114, 83, 237, 0.04);
26 | border: $sidebar-border-width solid rgba(114, 83, 237, 0.45);
27 | border-radius: 5px;
28 | }
29 |
30 | @mixin fullwidth_box() {
31 | padding: ($default-padding / 2) ($default-padding * 0.6 - $sidebar-border-width);
32 | line-height: $line-height * $code-scaling-factor * 1.08;
33 |
34 | margin-left: -$default-padding * 0.6;
35 | margin-right: -$default-padding * 0.6;
36 |
37 | @media screen and (max-width: $max-width + $default-padding * 3) {
38 | margin-left: 0px;
39 | }
40 | }
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-06-running-after-deleting-the-overlay-directory.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Running Docker after deleting the /var/lib/docker/overlay2 directory
4 | date: 2018-11-06 08:07:17 +0000
5 | tags: docker
6 | ---
7 |
8 | When my Linode wouldn't boot, I managed to get in via rescue mode, and I thought maybe the boot disk was full -- so I went looking for big files to delete.
9 | More than half the disk was taken up by `/var/lib/docker/overlay2`.
10 | The VM was already hosed, so trashing Docker wouldn't make it worse!
11 | Thus:
12 |
13 | ```console
14 | $ rm -rf /var/lib/docker/overlay2
15 | $ mkdir -p /var/lib/docker/overlay2
16 | ```
17 |
18 | Thanks to Linode support, I got the box up and running, but now trying to run any Docker commands fails with errors like:
19 |
20 | ```
21 | No such file or directory: /var/lib/docker/overlay2/a37c8253bbefa7ea641a110a5e6e2f5efd7d403f89b3319ef97a8038c2db229b
22 | ```
23 |
24 | All the image/container definitions live in this directory, but are indexed separately -- so Docker still thought it had a complete collection of images and containers.
25 | When I asked it to run an image, it failed because it couldn't find the local image it thought it had.
26 |
27 | Fix was to purge the index of local images and containers:
28 |
29 | ```console
30 | $ docker rm $(docker ps -a -q)
31 | $ docker rmi $(docker images -q)
32 | ```
33 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-09-17-list-all-git-object-ids-and-their-type.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: List all Git object IDs and their type
4 | date: 2018-09-17 12:39:00 +0000
5 | tags: git
6 | ---
7 |
8 | From Michal Grochmal in the PyCon UK Slack:
9 |
10 | ```console
11 | $ find .git/objects/ -type f | sed -e s^\.git/objects/^^ -e s^/^^ | sort | while read x; do echo -n "$x "; git cat-file -t $x; done
12 | ```
13 |
14 | That didn't work for me on macOS, because I have a different version of sed (I think).
15 | This is less pretty but has the same effect.
16 |
17 | ```console
18 | $ find .git/objects -type f | tr '/' ' ' | awk '{print $3 $4}' | grep -v pack | while read x; do echo -n "$x "; git cat-file -t "$x"; done
19 | ```
20 |
21 | With either command, output is of the form:
22 |
23 | ```
24 | 12779b2e3b24fded5f817525a416a625e9f1a356 tree
25 | 38246dfb9d83a2c7be5ee0dda3a62cb223e9a764 blob
26 | 5d7cd731f9beefea46efe0d13fb1ec11bfb09001 blob
27 | 95c8c8a03c2c037b5de2a1eb80d55ec8dd80a528 blob
28 | cfaa614fd42ee1408341d3db3a1570713ea3494c blob
29 | de9e0026f9d6c1948096dfefb093aa25a188577d blob
30 | e977704a7442a6e80b3d1119a7fcc44b29e22f06 tree
31 | ef2fc0e9ecc83352a410860f6baf8b33c66f82fb tree
32 | f25c461b1674c1d67146f57f7ac9c3626958ff3e blob
33 | f741f48754937179332d2f1fb6f670065c5f69bd commit
34 | f8292a790c79453822afaa6f8fee4dd4a14c5cd1 tree
35 | ff6e0f7a0e941b152ccb63e656b110f48f65515e commit
36 | ```
37 |
--------------------------------------------------------------------------------
/src/_scss/_archive.scss:
--------------------------------------------------------------------------------
1 | #notebook_index {
2 | margin-bottom: 1.2em;
3 | padding-left: 0px;
4 |
5 | li {
6 | list-style-type: none;
7 |
8 | &:before {
9 | content: '\25A0';
10 | display: block;
11 | position: relative;
12 | max-width: 0;
13 | max-height: 0;
14 | left: -12px;
15 | top: -1px;
16 | color: $primary-color;
17 | font-size: 10px;
18 | }
19 | }
20 |
21 | .notebook_index__tags {
22 | font-size: 75%;
23 | color: $accent-grey;
24 |
25 | a, a:visited {
26 | color: $accent-grey;
27 | margin-left: 0.15rem;
28 | margin-right: 0.15rem;
29 |
30 | &:hover {
31 | color: darken($accent-grey, 25%);
32 | }
33 | }
34 | }
35 |
36 | li:not(:last-child) {
37 | margin-bottom: 0.85em;
38 | }
39 | }
40 |
41 | #notebook_filters {
42 | @include purple_box();
43 | @include fullwidth_box();
44 |
45 | display: none;
46 |
47 | .tag_filter {
48 | margin-left: 0.25rem;
49 | margin-right: 0.4rem;
50 | font-weight: bold;
51 |
52 | a.remove_tag {
53 | font-weight: normal;
54 | color: #d01c11;
55 | background-image: none;
56 | padding-left: 3px;
57 |
58 | &::before {
59 | content: "[";
60 | }
61 |
62 | &::after {
63 | content: "]";
64 | }
65 |
66 | &:hover {
67 | text-decoration: underline;
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/_includes/socialgraph.html:
--------------------------------------------------------------------------------
1 | {% if page.theme and page.theme.card_type %}
2 |
3 | {% else %}
4 |
5 | {% endif %}
6 |
7 |
8 | {% if page.summary %}
9 |
10 | {% endif %}
11 | {% if page.theme and page.theme.image %}
12 |
13 | {% elsif page.theme and page.theme.touch_icon %}
14 |
15 | {% else %}
16 |
17 | {% endif %}
18 |
19 |
20 |
21 |
22 | {% if page.theme and page.theme.image %}
23 |
24 | {% elsif page.theme and page.theme.touch_icon %}
25 |
26 | {% else %}
27 |
28 | {% endif %}
29 | {% if page.summary %}
30 |
31 | {% endif %}
32 |
--------------------------------------------------------------------------------
/src/_scss/_code.scss:
--------------------------------------------------------------------------------
1 | code, pre {
2 | @include purple_box();
3 | font-family: $mono-font;
4 | overflow-x: auto;
5 | }
6 |
7 | code {
8 | margin: 2px;
9 | padding: 3px 3px;
10 | font-size: 1em * $code-scaling-factor;
11 |
12 | }
13 |
14 | // Disable selecting the $ or the following space in ``console`` code
15 | // blocks. The original space is removed in the `cleanup_text.rb` plugin.
16 | .language-console > pre > code > span.w {
17 | @include disable_select();
18 | &::after {
19 | content: " ";
20 | }
21 | }
22 |
23 | pre {
24 | @include fullwidth_box();
25 |
26 | // This ensures that code blocks don't get blown up to big sizes
27 | // on iPhone displays.
28 | -webkit-text-size-adjust: 100%;
29 |
30 | // This ensures the first line of
" and _pres.size == 1 %}{{ _pres_after }}{% endunless %}{% endcapture %}{% endfor %}{% if _profile %}{% assign _profile_collapse = _content | size | plus: 1 %}{% endif %}{% if site.compress_html.clippings == "all" %}{% assign _clippings = "html head title base link meta style body article section nav aside h1 h2 h3 h4 h5 h6 hgroup header footer address p hr blockquote ol ul li dl dt dd figure figcaption main div table caption colgroup col tbody thead tfoot tr td th" | split: " " %}{% else %}{% assign _clippings = site.compress_html.clippings %}{% endif %}{% for _element in _clippings %}{% assign _edges = " ;; ;" | replace: "e", _element | split: ";" %}{% assign _content = _content | replace: _edges[0], _edges[1] | replace: _edges[2], _edges[3] | replace: _edges[4], _edges[5] %}{% endfor %}{% if _profile and _clippings %}{% assign _profile_clippings = _content | size | plus: 1 %}{% endif %}{{ _content }}{% if _profile %}
Step
Bytes
raw
{{ content | size }}{% if _profile_endings %}
endings
{{ _profile_endings }}{% endif %}{% if _profile_startings %}
startings
{{ _profile_startings }}{% endif %}{% if _profile_comments %}
comments
{{ _profile_comments }}{% endif %}{% if _profile_collapse %}
collapse
{{ _profile_collapse }}{% endif %}{% if _profile_clippings %}
clippings
{{ _profile_clippings }}{% endif %}
{% endif %}{% endif %}
11 |
--------------------------------------------------------------------------------
/src/_posts/2018/2018-11-23-notes-from-working-through-the-ruby-koans.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Notes from working through the Ruby Koans
4 | date: 2018-11-23 12:01:12 +0000
5 | tags: ruby
6 | ---
7 |
8 | Some notes from when I worked through the [Ruby Koans](http://rubykoans.com/).
9 | Grouped by name of the file where I learnt something.
10 |
11 | about_asserts.rb:
12 |
13 | * The following are equivalent:
14 |
15 | ```ruby
16 | assert foo == bar
17 | assert_equal foo, bar
18 | ```
19 |
20 | about_nil.rb:
21 |
22 | * `nil` is an Object.
23 | * You can use `foo.nil?` to check if an object is `nil`.
24 | * You can check for string matches with a regex, e.g.:
25 |
26 | ```ruby
27 | assert_match(/undefined method/, ex.message)
28 | ```
29 |
30 | about_objects.rb
31 |
32 | * The ID of an object is a `Fixnum`.
33 | * All objects have a different object ID.
34 | * Small ints have a fixed object ID.
35 | See [Object IDs in Ruby](http://thepaulrayner.com/blog/2013/02/06/object-ids-in-ruby/) for more details.
36 |
37 | about_arrays.rb
38 |
39 | * There are helper methods `arr.first` and `arr.last`.
40 | * Array slices work differently to Python: the structure is `arr[index, length]`.
41 | If the index is out of range, it returns `nil`.
42 | * Dots work as follows (the opposite of what you expected!):
43 |
44 | ```ruby
45 | a..b # a <= i <= b
46 | a...b # a <= i < b
47 | ```
48 |
49 | * More convenience methods and their Python equivalents:
50 |
51 | ```ruby
52 | arr.unshift(x) # list.insert(0, x)
53 | arr.shift # list.pop(0)
54 | ```
55 |
56 | about_hashes.rb
57 |
58 | * Behaviour is opposite to Python: default hash lookup (`h[key]`) returns `nil` if the key doesn't exist.
59 | To get an explicit `KeyError`, you need to use `h.fetch(key)`.
60 | * You can provide default values to get similar behaviour to Python's defaultdict:
61 |
62 | ```ruby
63 | Hash.new(default)
64 | Hash.new { |hash, key| hash[key] = default }
65 | ```
66 |
67 | Be careful about mutating the default!
68 | In the first line, the same value of `default` is used everywhere, so editing it will affect every key where it's used.
69 |
70 | about_strings.rb:
71 |
72 | * You can be flexible about quoting strings if it's helpful, including multi-line strings.
73 |
74 | ```ruby
75 | %{"I don't have the answer" he said}
76 | %("I don't have the answer" he said)
77 | %!"I don't have the answer" he said!
78 | ```
79 |
80 | * You also have `<