about summary refs log blame commit diff
path: root/tools/blog_cli/main.go
blob: 9175b2b66f866db1e6004e614d8a7554c9d04c6f (plain) (tree)



















































                                                                                        

                                                             





                                                                  








                                                                              








                                          
                                     


















                                                                     
                                                                 
 
                          


                               
                          









                                                              







                                                                    
                   


                           






                                             
                   

















































                                                                 
// 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")
}