// 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 { 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 time.Time) 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)") } 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") }