// 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")
date = flag.String("date", "", "Date the post was written on")
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 day time.Time
func (d day) MarshalJSON() ([]byte, error) {
j := (time.Time(d)).Format("2006-01-02")
return []byte(j), nil
}
type metadata struct {
Chunks int `json:"c"`
Title string `json:"t"`
Date day `json:"d"`
}
type chunk struct {
Chunk int
Text string
}
type post struct {
ID string
Meta metadata
Chunks []string
}
func (p *post) writeToDNS() error {
var additions []*dns.ResourceRecordSet
additions = append(additions, &dns.ResourceRecordSet{
Name: fmt.Sprintf("_meta.%s.blog.tazj.in.", p.ID),
Type: "TXT",
Ttl: 1200,
Rrdatas: []string{
encodeJSON(p.Meta),
},
})
for i, c := range p.Chunks {
additions = append(additions, &dns.ResourceRecordSet{
Name: fmt.Sprintf("_%v.%s.blog.tazj.in.", i, p.ID),
Type: "TXT",
Ttl: 1200,
Rrdatas: []string{c},
})
}
ctx := context.Background()
dnsSvc, err := dns.NewService(ctx)
if err != nil {
return err
}
change := dns.Change{
Additions: additions,
}
_, 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
s := base64.RawStdEncoding.EncodeToString([]byte(c.Text))
if len(s) >= 255 {
tooLarge = true
}
return s, tooLarge
}
func createPost(id, title, text string, date day) post {
runes := []rune(text)
n := 0
tooLarge := false
var chunks []string
for chunkSize < len(runes) {
c, l := encodeChunk(chunk{
Chunk: n,
Text: string(runes[0:chunkSize:chunkSize]),
})
tooLarge = tooLarge || l
chunks = append(chunks, c)
runes = runes[chunkSize:]
n++
}
if len(runes) > 0 {
c, l := encodeChunk(chunk{
Chunk: n,
Text: string(runes),
})
tooLarge = tooLarge || l
chunks = append(chunks, c)
n++
}
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)")
}
var postDate day
if *date != "" {
t, err := time.Parse("2006-01-02", *date)
if err != nil {
log.Fatalln("Invalid post date", err)
}
postDate = day(t)
} else {
postDate = day(time.Now())
}
t, err := ioutil.ReadFile(*infile)
if err != nil {
log.Fatalln("Failed to read post:", err)
}
post := createPost(*id, *title, string(t), postDate)
log.Println("Writing post to DNS ...")
err = post.writeToDNS()
if err != nil {
log.Fatalln("Failed to write post:", err)
}
log.Println("Successfully wrote entries")
}