From 2a16740445291366540f8ce3fa8589c3f9b7740a Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Mon, 19 Aug 2019 16:58:19 +0100 Subject: feat(tools): Add a horrifying tool to write blog posts into DNS --- tools/blog_cli/README.md | 41 ++++++++++ tools/blog_cli/default.nix | 8 ++ tools/blog_cli/deps.nix | 111 +++++++++++++++++++++++++++ tools/blog_cli/main.go | 185 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 345 insertions(+) create mode 100644 tools/blog_cli/README.md create mode 100644 tools/blog_cli/default.nix create mode 100644 tools/blog_cli/deps.nix create mode 100644 tools/blog_cli/main.go (limited to 'tools/blog_cli') diff --git a/tools/blog_cli/README.md b/tools/blog_cli/README.md new file mode 100644 index 0000000000..7afa0fe920 --- /dev/null +++ b/tools/blog_cli/README.md @@ -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. diff --git a/tools/blog_cli/default.nix b/tools/blog_cli/default.nix new file mode 100644 index 0000000000..c755d273a2 --- /dev/null +++ b/tools/blog_cli/default.nix @@ -0,0 +1,8 @@ +{ buildGoPackage }: + +buildGoPackage { + name = "blog_cli"; + goPackagePath = "github.com/tazjin/personal/blog_cli"; + src = ./.; + goDeps = ./deps.nix; +} diff --git a/tools/blog_cli/deps.nix b/tools/blog_cli/deps.nix new file mode 100644 index 0000000000..4d75fcfdd8 --- /dev/null +++ b/tools/blog_cli/deps.nix @@ -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"; + }; + } +] diff --git a/tools/blog_cli/main.go b/tools/blog_cli/main.go new file mode 100644 index 0000000000..f376f256ea --- /dev/null +++ b/tools/blog_cli/main.go @@ -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") +} -- cgit 1.4.1