feat(tools): Add a horrifying tool to write blog posts into DNS
This commit is contained in:
parent
28a9c01d36
commit
2a16740445
7 changed files with 350 additions and 0 deletions
|
|
@ -11,6 +11,7 @@ let
|
||||||
# Local projects should be added here:
|
# Local projects should be added here:
|
||||||
tazjin = {
|
tazjin = {
|
||||||
blog = import ./services/tazblog { inherit pkgs; };
|
blog = import ./services/tazblog { inherit pkgs; };
|
||||||
|
blog_cli = pkgs.callPackage ./tools/blog_cli {};
|
||||||
gemma = import ./services/gemma { inherit pkgs; };
|
gemma = import ./services/gemma { inherit pkgs; };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ case "${TARGET_TOOL}" in
|
||||||
kontemplate)
|
kontemplate)
|
||||||
attr="kontemplate"
|
attr="kontemplate"
|
||||||
;;
|
;;
|
||||||
|
blog_cli)
|
||||||
|
attr="tazjin.blog_cli"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "The tool '${TARGET_TOOL}' is currently not installed in this repository."
|
echo "The tool '${TARGET_TOOL}' is currently not installed in this repository."
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
||||||
1
tools/bin/blog_cli
Symbolic link
1
tools/bin/blog_cli
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
__dispatch.sh
|
||||||
41
tools/blog_cli/README.md
Normal file
41
tools/blog_cli/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
tazblog CLI
|
||||||
|
===========
|
||||||
|
|
||||||
|
My blog stores its content in DNS, spread out over three types of `TXT` entries:
|
||||||
|
|
||||||
|
* `TXT _posts.blog.tazj.in.`: A sorted list of posts, serialised as a JSON list of
|
||||||
|
strings (e.g. `["1486830338", "1476807384"]`)
|
||||||
|
|
||||||
|
* `TXT _chunks.$postID.blog.tazj.in`: JSON chunks containing the blog post text
|
||||||
|
|
||||||
|
* `TXT _meta.$postID.blog.tazj.in`: JSON blob with blog post metadata
|
||||||
|
|
||||||
|
All JSON blobs are base64-encoded.
|
||||||
|
|
||||||
|
This CLI tool helps to update those records.
|
||||||
|
|
||||||
|
Each blog post data is a series of JSON-encoded structures which follow one of
|
||||||
|
these formats:
|
||||||
|
|
||||||
|
```
|
||||||
|
struct metadata {
|
||||||
|
chunks: int
|
||||||
|
title: string
|
||||||
|
date: date
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `chunks` describes the number of chunks following this format:
|
||||||
|
|
||||||
|
```
|
||||||
|
struct chunk {
|
||||||
|
c: int
|
||||||
|
t: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Writing a blog post to DNS means taking its text and metadata, chunking it up
|
||||||
|
and writing the chunks.
|
||||||
|
|
||||||
|
Reading a blog post means retrieving all data, reading the metadata and then
|
||||||
|
assembling the chunks in order.
|
||||||
8
tools/blog_cli/default.nix
Normal file
8
tools/blog_cli/default.nix
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{ buildGoPackage }:
|
||||||
|
|
||||||
|
buildGoPackage {
|
||||||
|
name = "blog_cli";
|
||||||
|
goPackagePath = "github.com/tazjin/personal/blog_cli";
|
||||||
|
src = ./.;
|
||||||
|
goDeps = ./deps.nix;
|
||||||
|
}
|
||||||
111
tools/blog_cli/deps.nix
Normal file
111
tools/blog_cli/deps.nix
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
# This file was generated by https://github.com/kamilchm/go2nix v1.3.0
|
||||||
|
[
|
||||||
|
{
|
||||||
|
goPackagePath = "cloud.google.com/go";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://code.googlesource.com/gocloud";
|
||||||
|
rev = "76e973f7c1e722b4859698ace0daed4e7eccdc60";
|
||||||
|
sha256 = "0m3ncaz0br67zmzsi76wwk6c00rqr8r7kiqgirnkcd1iwlql0qdr";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
goPackagePath = "github.com/golang/protobuf";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://github.com/golang/protobuf";
|
||||||
|
rev = "4c88cc3f1a34ffade77b79abc53335d1e511f25b";
|
||||||
|
sha256 = "0chbdc4q55z7myiwnbvhryc5ihf6cxh8p4w3c1imy2gyzjn9sf4r";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
goPackagePath = "github.com/googleapis/gax-go";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://github.com/googleapis/gax-go";
|
||||||
|
rev = "bd5b16380fd03dc758d11cef74ba2e3bc8b0e8c2";
|
||||||
|
sha256 = "1lxawwngv6miaqd25s3ba0didfzylbwisd2nz7r4gmbmin6jsjrx";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
goPackagePath = "github.com/hashicorp/golang-lru";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://github.com/hashicorp/golang-lru";
|
||||||
|
rev = "7f827b33c0f158ec5dfbba01bb0b14a4541fd81d";
|
||||||
|
sha256 = "1p2igd58xkm8yaj2c2wxiplkf2hj6kxwrg6ss7mx61s5rd71v5xb";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
goPackagePath = "go.opencensus.io";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://github.com/census-instrumentation/opencensus-go";
|
||||||
|
rev = "b4a14686f0a98096416fe1b4cb848e384fb2b22b";
|
||||||
|
sha256 = "1aidyp301v5ngwsnnc8v1s09vvbsnch1jc4vd615f7qv77r9s7dn";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
goPackagePath = "golang.org/x/net";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://go.googlesource.com/net";
|
||||||
|
rev = "74dc4d7220e7acc4e100824340f3e66577424772";
|
||||||
|
sha256 = "0563yswwqknxx2gsvl0qikn0lmwalilbng8i12iw4d3v40n23s0l";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
goPackagePath = "golang.org/x/oauth2";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://go.googlesource.com/oauth2";
|
||||||
|
rev = "0f29369cfe4552d0e4bcddc57cc75f4d7e672a33";
|
||||||
|
sha256 = "06jwpvx0x2gjn2y959drbcir5kd7vg87k0r1216abk6rrdzzrzi2";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
goPackagePath = "golang.org/x/sys";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://go.googlesource.com/sys";
|
||||||
|
rev = "fde4db37ae7ad8191b03d30d27f258b5291ae4e3";
|
||||||
|
sha256 = "16k4w4pzziq1kln18k5fg01qgk4hpzb5xsm7175kaky6d6gwyhg3";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
goPackagePath = "golang.org/x/text";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://go.googlesource.com/text";
|
||||||
|
rev = "342b2e1fbaa52c93f31447ad2c6abc048c63e475";
|
||||||
|
sha256 = "0flv9idw0jm5nm8lx25xqanbkqgfiym6619w575p7nrdh0riqwqh";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
goPackagePath = "google.golang.org/api";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://code.googlesource.com/google-api-go-client";
|
||||||
|
rev = "573115b31dcba90b19c25508412495d63f86e804";
|
||||||
|
sha256 = "0vs0783azc1ja5l2ijs9d2szysrs4gpg8blvpkjbslyr07ca0dha";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
goPackagePath = "google.golang.org/genproto";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://github.com/google/go-genproto";
|
||||||
|
rev = "55e96fffbd486c27fc0d5b4468c497d0de3f2727";
|
||||||
|
sha256 = "1nnz3rb3ppj9dvalyf7xz6cpndlvi4ra5wm5hlv34nn498zpqc70";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
goPackagePath = "google.golang.org/grpc";
|
||||||
|
fetch = {
|
||||||
|
type = "git";
|
||||||
|
url = "https://github.com/grpc/grpc-go";
|
||||||
|
rev = "451cf373a706e089ca34abd9ea14cbc20b34c1fc";
|
||||||
|
sha256 = "11sza0g00v8xlmm3sah51l93m2993g4lw89df76z9d9lyk094dl8";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]
|
||||||
185
tools/blog_cli/main.go
Normal file
185
tools/blog_cli/main.go
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
// The tazblog CLI implements updating my blog records in DNS, see the
|
||||||
|
// README in this folder for details.
|
||||||
|
//
|
||||||
|
// The post input format is a file with the title on one line,
|
||||||
|
// followed by the date on a line, followed by an empty line, followed
|
||||||
|
// by the post text.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/api/dns/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
project = flag.String("project", "tazjins-infrastructure", "Target GCP project")
|
||||||
|
zone = flag.String("zone", "blog-tazj-in", "Target Cloud DNS zone")
|
||||||
|
title = flag.String("title", "", "Title of the blog post")
|
||||||
|
infile = flag.String("text", "", "Text file containing the blog post")
|
||||||
|
id = flag.String("id", "", "Post ID - will be generated if unset")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Number of runes to include in a single chunk. If any chunks exceed
|
||||||
|
// the limit of what can be encoded, the chunk size is reduced and we
|
||||||
|
// try again.
|
||||||
|
var chunkSize = 200
|
||||||
|
|
||||||
|
type metadata struct {
|
||||||
|
Chunks int `json:"chunks"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type chunk struct {
|
||||||
|
Chunk int `json:"c"`
|
||||||
|
Text string `json:"t"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type post struct {
|
||||||
|
ID string
|
||||||
|
Meta metadata
|
||||||
|
Chunks []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *post) writeToDNS() error {
|
||||||
|
metaRecord := dns.ResourceRecordSet{
|
||||||
|
Name: fmt.Sprintf("_meta.%s.blog.tazj.in.", p.ID),
|
||||||
|
Type: "TXT",
|
||||||
|
Ttl: 1200,
|
||||||
|
Rrdatas: []string{
|
||||||
|
encodeJSON(p.Meta),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkRecord := dns.ResourceRecordSet{
|
||||||
|
Name: fmt.Sprintf("_chunks.%s.blog.tazj.in.", p.ID),
|
||||||
|
Type: "TXT",
|
||||||
|
Ttl: 1200,
|
||||||
|
Rrdatas: p.Chunks,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
dnsSvc, err := dns.NewService(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
change := dns.Change{
|
||||||
|
Additions: []*dns.ResourceRecordSet{&metaRecord, &chunkRecord},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dnsSvc.Changes.Create(*project, *zone, &change).Do()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode given value as JSON and base64-encode it.
|
||||||
|
func encodeJSON(v interface{}) string {
|
||||||
|
outer, _ := json.Marshal(v)
|
||||||
|
return base64.RawStdEncoding.EncodeToString(outer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode a chunk and check whether it is too large
|
||||||
|
func encodeChunk(c chunk) (string, bool) {
|
||||||
|
tooLarge := false
|
||||||
|
|
||||||
|
j := encodeJSON(c)
|
||||||
|
|
||||||
|
if len(j) >= 255 {
|
||||||
|
tooLarge = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return j, tooLarge
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPost(id, title, text string, date time.Time) post {
|
||||||
|
runes := []rune(text)
|
||||||
|
n := 0
|
||||||
|
tooLarge := false
|
||||||
|
|
||||||
|
var chunks []string
|
||||||
|
|
||||||
|
for chunkSize < len(runes) {
|
||||||
|
n++
|
||||||
|
|
||||||
|
c, l := encodeChunk(chunk{
|
||||||
|
Chunk: n,
|
||||||
|
Text: string(runes[0:chunkSize:chunkSize]),
|
||||||
|
})
|
||||||
|
|
||||||
|
tooLarge = tooLarge || l
|
||||||
|
chunks = append(chunks, c)
|
||||||
|
runes = runes[chunkSize:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) > 0 {
|
||||||
|
n++
|
||||||
|
|
||||||
|
c, l := encodeChunk(chunk{
|
||||||
|
Chunk: n,
|
||||||
|
Text: string(runes),
|
||||||
|
})
|
||||||
|
|
||||||
|
tooLarge = tooLarge || l
|
||||||
|
chunks = append(chunks, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tooLarge {
|
||||||
|
log.Println("Too large at chunk size", chunkSize)
|
||||||
|
chunkSize -= 5
|
||||||
|
return createPost(id, title, text, date)
|
||||||
|
}
|
||||||
|
|
||||||
|
return post{
|
||||||
|
ID: id,
|
||||||
|
Meta: metadata{
|
||||||
|
Chunks: n,
|
||||||
|
Title: title,
|
||||||
|
Date: date,
|
||||||
|
},
|
||||||
|
Chunks: chunks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *title == "" {
|
||||||
|
log.Fatalln("Post title must be set (-title)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *infile == "" {
|
||||||
|
log.Fatalln("Post text file must be set (-text)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *id == "" {
|
||||||
|
log.Fatalln("Post ID must be set (-id)")
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := ioutil.ReadFile(*infile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to read post:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
post := createPost(*id, *title, string(t), time.Now())
|
||||||
|
|
||||||
|
log.Println("Writing post to DNS ...")
|
||||||
|
err = post.writeToDNS()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to write post:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Successfully wrote entries")
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue