~zacbrown/api.zacbrown.org-app

cc8ca9b51aebb94d90e277b499dad350145a237b — Zac Brown 11 months ago a81dc13
Implement basic dictionary resource at /dictionary.
5 files changed, 189 insertions(+), 0 deletions(-)

A config.go
A dictionary.go
M go.mod
A go.sum
M main.go
A config.go => config.go +43 -0
@@ 0,0 1,43 @@
package main

import (
	"log"
	"os"

	"github.com/BurntSushi/toml"
)

type ConfigFile struct {
	AwsConfig awsConfig
}

func loadConfiguration(filePath string) ConfigFile {

	var config ConfigFile
	if _, err := toml.DecodeFile(filePath, &config); err != nil {
		log.Fatal(err)
	}

	return config
}

type awsConfig struct {
	IamAccessKeyId     string
	IamAccessKeySecret string
	Region             string
}

func (c *awsConfig) setAwsEnvironmentVariables() {
	if err := os.Setenv("AWS_ACCESS_KEY_ID", c.IamAccessKeyId); err != nil {
		log.Fatal(err)
	}
	if err := os.Setenv("AWS_SECRET_ACCESS_KEY", c.IamAccessKeySecret); err != nil {
		log.Fatal(err)
	}
	if err := os.Setenv("AWS_DEFAULT_REGION", c.Region); err != nil {
		log.Fatal(err)
	}
	if err := os.Setenv("AWS_REGION", c.Region); err != nil {
		log.Fatal(err)
	}
}

A dictionary.go => dictionary.go +112 -0
@@ 0,0 1,112 @@
package main

import (
	"bytes"
	"compress/bzip2"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
)

func downloadFromS3(filePath string, bucket string, key string) {
	file, err := os.Create(filePath)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	sess := session.Must(session.NewSession())
	downloader := s3manager.NewDownloader(sess)

	_, err = downloader.Download(file,
		&s3.GetObjectInput{
			Bucket: aws.String(bucket),
			Key:    aws.String(key),
		})

	if err != nil {
		log.Fatal(err)
	}

	log.Printf("Successfully fetched '%s' item from '%s' bucket in S3. "+
		"Content written to '%s'\n", key, bucket, filePath)
}

type DictionaryHandler struct {
	dictionary map[string]string
}

func NewDictionaryHandler() *DictionaryHandler {
	dictHandler := new(DictionaryHandler)
	dictHandler.loadDictionary()
	return dictHandler
}

func (d *DictionaryHandler) loadDictionary() {
	const dictionaryFileName string = "dictionary.json.bz2"
	const dictionaryFileS3Bucket string = "pub-static-files"
	const dictionaryFileS3ObjectKey string = "dictionary.json.bz2"

	if _, err := os.Stat(dictionaryFileName); os.IsNotExist(err) {
		log.Printf("Didn't find %s file locally, fetching from S3.\n", dictionaryFileName)
		downloadFromS3(dictionaryFileName, dictionaryFileS3Bucket, dictionaryFileS3ObjectKey)
	} else {
		log.Printf("Found the %s file locally. Using that.\n", dictionaryFileName)
	}

	file, err := os.Open(dictionaryFileName)
	if err != nil {
		log.Fatal(err)
	}

	decompressedStream := bzip2.NewReader(file)
	buf := new(bytes.Buffer)
	if _, err := buf.ReadFrom(decompressedStream); err != nil {
		log.Fatal(err)
	}

	if err := json.Unmarshal([]byte(buf.String()), &d.dictionary); err != nil {
		log.Fatal(err)
	}
}

func (d *DictionaryHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	if request.Method == "GET" {
		lookupWord := request.URL.Path[len("/dictionary/"):]
		lookupWord = strings.ToLower(lookupWord)

		if lookupWord == "" {
			// They just asked for /dictionary so let's print some help.
			formatted := fmt.Sprintf("Hello! You've reached the /dictionary resource. " +
				"You can get a definition by adding the word you're looking for to the end of the URL.\n\n" +
				"For example: /dictionary/banana")
			writer.Write([]byte(formatted))
		} else {
			if found, ok := d.dictionary[lookupWord]; ok {
				// They asked for a valid word. e.g. /dictionary/potato
				formatted := fmt.Sprintf("Hello! You've just requested the definition of '%s'.\n\n'%s' "+
					" is defined as: %s\n", lookupWord, lookupWord, found)
				writer.Write([]byte(formatted))
				return
			} else {
				// They asked for a word that we didn't know. e.g. /dictionary/taters
				message := fmt.Sprintf("Hello! You've just requested the definition of '%s'. "+
					"Unfortunately, I don't know that word :(.", lookupWord)
				writer.Write([]byte(message))
			}
		}
	} else {
		message := fmt.Sprintf("Hello! You've made a '%s' request to the '%s' resource. "+
			"Unfortunately, that REST method is not supported at this time.", request.Method, request.URL.Path)
		writer.Write([]byte(message))
	}

}

M go.mod => go.mod +5 -0
@@ 1,3 1,8 @@
module git.sr.ht/~zacbrown/api.zacbrown.org

go 1.13

require (
	github.com/BurntSushi/toml v0.3.1
	github.com/aws/aws-sdk-go v1.25.43
)

A go.sum => go.sum +6 -0
@@ 0,0 1,6 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/aws/aws-sdk-go v1.25.43 h1:R5YqHQFIulYVfgRySz9hvBRTWBjudISa+r0C8XQ1ufg=
github.com/aws/aws-sdk-go v1.25.43/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=

M main.go => main.go +23 -0
@@ 4,12 4,35 @@ import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func getConfigFilePathFromProgramArgs() string {
	if len(os.Args) >= 2 {
		path := os.Args[1]
		if _, err := os.Stat(path); os.IsExist(err) {
			return path
		}
	}

	panic("Not enough arguments provided. Panic time.")
}

func main() {
	configFilePath := os.Args[1]
	config := loadConfiguration(configFilePath)
	log.Printf("Loaded config file from path '%s'.\n", configFilePath)

	config.AwsConfig.setAwsEnvironmentVariables()
	log.Println("Setup AWS variables.")

	http.HandleFunc("/", rootFunc)
	log.Println("Setup root '/' handler.")

	dictionaryHandler := NewDictionaryHandler()
	http.HandleFunc("/dictionary/", dictionaryHandler.ServeHTTP)
	log.Println("Setup dictionary '/dictionary' handler.")

	log.Println("Starting server on port 80...")
	log.Fatal(http.ListenAndServe(":80", nil))
}