<commit_before>package main

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"goji.io/middleware"
	"goji.io/pat"
)

func isHashed(dir string) bool {
	return dir == "data"
}

func getRepo(r *http.Request) string {
	if strings.HasPrefix(fmt.Sprintf("%s", middleware.Pattern(r.Context())), "/:repo") {
		return filepath.Join(config.path, pat.Param(r, "repo"))
	}

	return config.path
}

func createDirectories(path string) {
	log.Printf("Creating repository directories in %s\n", path)

	if err := os.MkdirAll(path, 0700); err != nil {
		log.Fatal(err)
	}

	dirs := []string{
		"data",
		"index",
		"keys",
		"locks",
		"snapshots",
		"tmp",
	}

	for _, d := range dirs {
		if err := os.MkdirAll(filepath.Join(path, d), 0700); err != nil {
			log.Fatal(err)
		}
	}

	for i := 0; i < 256; i++ {
		if err := os.MkdirAll(filepath.Join(path, "data", fmt.Sprintf("%02x", i)), 0700); err != nil {
			log.Fatal(err)
		}
	}
}

// AuthHandler wraps h with a http.HandlerFunc that performs basic authentication against the user/passwords pairs
// stored in f and returns the http.HandlerFunc.
func AuthHandler(f *HtpasswdFile, h http.Handler) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if username, password, ok := r.BasicAuth(); !ok || !f.Validate(username, password) {
			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
			return
		}

		h.ServeHTTP(w, r)
	}
}

// CheckConfig checks whether a configuration exists.
func CheckConfig(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("CheckConfig()")
	}
	cfg := filepath.Join(getRepo(r), "config")
	st, err := os.Stat(cfg)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	w.Header().Add("Content-Length", fmt.Sprint(st.Size()))
}

// GetConfig allows for a config to be retrieved.
func GetConfig(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("GetConfig()")
	}
	cfg := filepath.Join(getRepo(r), "config")
	bytes, err := ioutil.ReadFile(cfg)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	w.Write(bytes)
}

// SaveConfig allows for a config to be saved.
func SaveConfig(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("SaveConfig()")
	}
	cfg := filepath.Join(getRepo(r), "config")
	bytes, err := ioutil.ReadAll(r.Body)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
		return
	}
	if err := ioutil.WriteFile(cfg, bytes, 0600); err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	w.Write([]byte("200 ok"))
}

// DeleteConfig removes a config.
func DeleteConfig(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("DeleteConfig()")
	}

	err := os.Remove(filepath.Join(getRepo(r), "config"))
	if err == nil {
		return
	}

	if config.debug {
		log.Print(err)
	}

	if os.IsNotExist(err) {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

// ListBlobs lists all blobs of a given type in an arbitrary order.
func ListBlobs(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("ListBlobs()")
	}
	dir := pat.Param(r, "type")
	path := filepath.Join(getRepo(r), dir)

	items, err := ioutil.ReadDir(path)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	var names []string
	for _, i := range items {
		if isHashed(dir) {
			subpath := filepath.Join(path, i.Name())
			subitems, err := ioutil.ReadDir(subpath)
			if err != nil {
				if config.debug {
					log.Print(err)
				}
				http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
				return
			}
			for _, f := range subitems {
				names = append(names, f.Name())
			}
		} else {
			names = append(names, i.Name())
		}
	}

	data, err := json.Marshal(names)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	w.Write(data)
}

// CheckBlob tests whether a blob exists.
func CheckBlob(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("CheckBlob()")
	}
	dir := pat.Param(r, "type")
	name := pat.Param(r, "name")

	if isHashed(dir) {
		name = filepath.Join(name[:2], name)
	}
	path := filepath.Join(getRepo(r), dir, name)

	st, err := os.Stat(path)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	w.Header().Add("Content-Length", fmt.Sprint(st.Size()))
}

// GetBlob retrieves a blob from the repository.
func GetBlob(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("GetBlob()")
	}
	dir := pat.Param(r, "type")
	name := pat.Param(r, "name")

	if isHashed(dir) {
		name = filepath.Join(name[:2], name)
	}
	path := filepath.Join(getRepo(r), dir, name)

	file, err := os.Open(path)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	http.ServeContent(w, r, "", time.Unix(0, 0), file)
	file.Close()
}

// SaveBlob saves a blob to the repository.
func SaveBlob(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("SaveBlob()")
	}
	repo := getRepo(r)
	dir := pat.Param(r, "type")
	name := pat.Param(r, "name")

	// Legacy code, required for restic client <= v0.3.3
	// TODO: remove sometime later
	if dir == "keys" {
		if _, err := os.Stat("keys"); err != nil && os.IsNotExist(err) {
			createDirectories(repo)
		}
	}

	if isHashed(dir) {
		name = filepath.Join(name[:2], name)
	}
	path := filepath.Join(repo, dir, name)

	tf, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0600)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	if _, err := io.Copy(tf, r.Body); err != nil {
		tf.Close()
		os.Remove(path)
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
		return
	}
	if err := tf.Sync(); err != nil {
		tf.Close()
		os.Remove(path)
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	if err := tf.Close(); err != nil {
		os.Remove(path)
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	w.Write([]byte("200 ok"))
}

// DeleteBlob deletes a blob from the repository.
func DeleteBlob(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("DeleteBlob()")
	}

	dir := pat.Param(r, "type")
	name := pat.Param(r, "name")

	if isHashed(dir) {
		name = filepath.Join(name[:2], name)
	}
	path := filepath.Join(getRepo(r), dir, name)

	err := os.Remove(path)
	if err == nil {
		w.Write([]byte("200 ok"))
		return
	}

	if config.debug {
		log.Print(err)
	}

	if os.IsNotExist(err) {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

// CreateRepo creates repository directories.
func CreateRepo(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("CreateRepo()")
	}

	if r.URL.Query().Get("create") != "true" {
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
		return
	}

	createDirectories(getRepo(r))

	w.Write([]byte("200 ok"))
}
<commit_msg>Handler cleanup<commit_after>package main

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"goji.io/middleware"
	"goji.io/pat"
)

func isHashed(dir string) bool {
	return dir == "data"
}

func getRepo(r *http.Request) string {
	if strings.HasPrefix(fmt.Sprintf("%s", middleware.Pattern(r.Context())), "/:repo") {
		return filepath.Join(config.path, pat.Param(r, "repo"))
	}

	return config.path
}

func createDirectories(path string) {
	log.Printf("Creating repository directories in %s\n", path)

	if err := os.MkdirAll(path, 0700); err != nil {
		log.Fatal(err)
	}

	dirs := []string{
		"data",
		"index",
		"keys",
		"locks",
		"snapshots",
		"tmp",
	}

	for _, d := range dirs {
		if err := os.MkdirAll(filepath.Join(path, d), 0700); err != nil {
			log.Fatal(err)
		}
	}

	for i := 0; i < 256; i++ {
		if err := os.MkdirAll(filepath.Join(path, "data", fmt.Sprintf("%02x", i)), 0700); err != nil {
			log.Fatal(err)
		}
	}
}

// AuthHandler wraps h with a http.HandlerFunc that performs basic authentication against the user/passwords pairs
// stored in f and returns the http.HandlerFunc.
func AuthHandler(f *HtpasswdFile, h http.Handler) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if username, password, ok := r.BasicAuth(); !ok || !f.Validate(username, password) {
			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
			return
		}

		h.ServeHTTP(w, r)
	}
}

// CheckConfig checks whether a configuration exists.
func CheckConfig(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("CheckConfig()")
	}
	cfg := filepath.Join(getRepo(r), "config")
	st, err := os.Stat(cfg)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	w.Header().Add("Content-Length", fmt.Sprint(st.Size()))
}

// GetConfig allows for a config to be retrieved.
func GetConfig(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("GetConfig()")
	}
	cfg := filepath.Join(getRepo(r), "config")
	bytes, err := ioutil.ReadFile(cfg)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	w.Write(bytes)
}

// SaveConfig allows for a config to be saved.
func SaveConfig(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("SaveConfig()")
	}
	cfg := filepath.Join(getRepo(r), "config")
	bytes, err := ioutil.ReadAll(r.Body)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
		return
	}

	if err := ioutil.WriteFile(cfg, bytes, 0600); err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
}

// DeleteConfig removes a config.
func DeleteConfig(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("DeleteConfig()")
	}

	if err := os.Remove(filepath.Join(getRepo(r), "config")); err != nil {
		if config.debug {
			log.Print(err)
		}
		if os.IsNotExist(err) {
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		} else {
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}
		return
	}
}

// ListBlobs lists all blobs of a given type in an arbitrary order.
func ListBlobs(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("ListBlobs()")
	}
	dir := pat.Param(r, "type")
	path := filepath.Join(getRepo(r), dir)

	items, err := ioutil.ReadDir(path)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	var names []string
	for _, i := range items {
		if isHashed(dir) {
			subpath := filepath.Join(path, i.Name())
			subitems, err := ioutil.ReadDir(subpath)
			if err != nil {
				if config.debug {
					log.Print(err)
				}
				http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
				return
			}
			for _, f := range subitems {
				names = append(names, f.Name())
			}
		} else {
			names = append(names, i.Name())
		}
	}

	data, err := json.Marshal(names)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	w.Write(data)
}

// CheckBlob tests whether a blob exists.
func CheckBlob(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("CheckBlob()")
	}
	dir := pat.Param(r, "type")
	name := pat.Param(r, "name")

	if isHashed(dir) {
		name = filepath.Join(name[:2], name)
	}
	path := filepath.Join(getRepo(r), dir, name)

	st, err := os.Stat(path)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	w.Header().Add("Content-Length", fmt.Sprint(st.Size()))
}

// GetBlob retrieves a blob from the repository.
func GetBlob(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("GetBlob()")
	}
	dir := pat.Param(r, "type")
	name := pat.Param(r, "name")

	if isHashed(dir) {
		name = filepath.Join(name[:2], name)
	}
	path := filepath.Join(getRepo(r), dir, name)

	file, err := os.Open(path)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	http.ServeContent(w, r, "", time.Unix(0, 0), file)
	file.Close()
}

// SaveBlob saves a blob to the repository.
func SaveBlob(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("SaveBlob()")
	}
	repo := getRepo(r)
	dir := pat.Param(r, "type")
	name := pat.Param(r, "name")

	// Legacy code, required for restic client <= v0.3.3
	// TODO: remove sometime later
	if dir == "keys" {
		if _, err := os.Stat("keys"); err != nil && os.IsNotExist(err) {
			createDirectories(repo)
		}
	}

	if isHashed(dir) {
		name = filepath.Join(name[:2], name)
	}
	path := filepath.Join(repo, dir, name)

	tf, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0600)
	if err != nil {
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	if _, err := io.Copy(tf, r.Body); err != nil {
		tf.Close()
		os.Remove(path)
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
		return
	}

	if err := tf.Sync(); err != nil {
		tf.Close()
		os.Remove(path)
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	if err := tf.Close(); err != nil {
		os.Remove(path)
		if config.debug {
			log.Print(err)
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
}

// DeleteBlob deletes a blob from the repository.
func DeleteBlob(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("DeleteBlob()")
	}

	dir := pat.Param(r, "type")
	name := pat.Param(r, "name")

	if isHashed(dir) {
		name = filepath.Join(name[:2], name)
	}
	path := filepath.Join(getRepo(r), dir, name)

	if err := os.Remove(path); err != nil {
		if config.debug {
			log.Print(err)
		}
		if os.IsNotExist(err) {
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		} else {
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}
		return
	}
}

// CreateRepo creates repository directories.
func CreateRepo(w http.ResponseWriter, r *http.Request) {
	if config.debug {
		log.Println("CreateRepo()")
	}

	if r.URL.Query().Get("create") != "true" {
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
		return
	}

	createDirectories(getRepo(r))
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"

	"github.com/julienschmidt/httprouter"
)

var validBasketName = regexp.MustCompile(BASKET_NAME)
var indexPage = template.Must(template.New("index").Parse(INDEX_HTML))
var basketPage = template.Must(template.New("basket").Parse(BASKET_HTML))

// writeJson writes JSON content to HTTP response
func writeJson(w http.ResponseWriter, status int, json []byte, err error) {
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	} else {
		w.Header().Set("Content-Type", "application/json; charset=UTF-8")
		w.WriteHeader(status)
		w.Write(json)
	}
}

// parseInt parses integer parameter from HTTP request query
func parseInt(value string, defaultValue int) int {
	if len(value) > 0 {
		i, err := strconv.Atoi(value)
		if err == nil {
			return i
		}
	}

	return defaultValue
}

// getPage retrieves page settings from HTTP request query params
func getPage(values url.Values) (int, int) {
	max := parseInt(values.Get("max"), serverConfig.PageSize)
	skip := parseInt(values.Get("skip"), 0)

	return max, skip
}

// getAndAuthBasket retrieves basket by name from HTTP request path and authorize access to the basket object
func getAndAuthBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (string, Basket) {
	name := ps.ByName("basket")
	basket := basketsDb.Get(name)
	if basket != nil {
		// maybe custom header, e.g. basket_key, basket_token
		token := r.Header.Get("Authorization")
		if basket.Authorize(token) || token == serverConfig.MasterToken {
			return name, basket
		} else {
			w.WriteHeader(http.StatusUnauthorized)
		}
	} else {
		w.WriteHeader(http.StatusNotFound)
	}

	return "", nil
}

// parseBasketConfig parses basket configuration and validate its content
func parseBasketConfig(body []byte, config *BasketConfig) error {
	// parse request
	if err := json.Unmarshal(body, config); err != nil {
		return err
	}

	// validate Capacity
	if config.Capacity < 1 {
		return fmt.Errorf("Capacity should be a positive number, but was %d", config.Capacity)
	}

	if config.Capacity > serverConfig.MaxCapacity {
		return fmt.Errorf("Capacity may not be greater than %d", serverConfig.MaxCapacity)
	}

	// validate URL
	if len(config.ForwardUrl) > 0 {
		if _, err := url.ParseRequestURI(config.ForwardUrl); err != nil {
			return err
		}
	}

	return nil
}

// GetBaskets handles HTTP request to get registered baskets
func GetBaskets(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	values := r.URL.Query()
	query := values.Get("q")
	if len(query) > 0 {
		// Find names
		max, skip := getPage(values)
		json, err := json.Marshal(basketsDb.FindNames(query, max, skip))
		writeJson(w, http.StatusOK, json, err)
	} else {
		// Get basket names page
		json, err := json.Marshal(basketsDb.GetNames(getPage(values)))
		writeJson(w, http.StatusOK, json, err)
	}
}

// GetBasket handles HTTP request to get basket configuration
func GetBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	if _, basket := getAndAuthBasket(w, r, ps); basket != nil {
		json, err := json.Marshal(basket.Config())
		writeJson(w, http.StatusOK, json, err)
	}
}

// CreateBasket handles HTTP request to create a new basket
func CreateBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	name := ps.ByName("basket")
	if name == BASKETS_ROOT || name == WEB_ROOT {
		http.Error(w, "You cannot use system path as basket name: "+name, http.StatusForbidden)
		return
	}
	if !validBasketName.MatchString(name) {
		http.Error(w, "Invalid basket name: "+name+", valid name pattern: "+validBasketName.String(), http.StatusBadRequest)
		return
	}

	log.Printf("[info] creating basket: %s", name)

	// read config (max 2 kB)
	body, err := ioutil.ReadAll(io.LimitReader(r.Body, 2048))
	r.Body.Close()
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// default config
	config := BasketConfig{ForwardUrl: "", Capacity: serverConfig.InitCapacity}
	if len(body) > 0 {
		if err := parseBasketConfig(body, &config); err != nil {
			http.Error(w, err.Error(), 422)
			return
		}
	}

	auth, err := basketsDb.Create(name, config)
	if err != nil {
		http.Error(w, err.Error(), http.StatusConflict)
	} else {
		json, err := json.Marshal(auth)
		writeJson(w, http.StatusCreated, json, err)
	}
}

// UpdateBasket handles HTTP request to update basket configuration
func UpdateBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	if _, basket := getAndAuthBasket(w, r, ps); basket != nil {
		// read config (max 2 kB)
		body, err := ioutil.ReadAll(io.LimitReader(r.Body, 2048))
		r.Body.Close()
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
		} else if len(body) > 0 {
			config := basket.Config()
			if err := parseBasketConfig(body, &config); err != nil {
				http.Error(w, err.Error(), 422)
				return
			}

			basket.Update(config)

			w.WriteHeader(http.StatusNoContent)
		} else {
			w.WriteHeader(http.StatusNotModified)
		}
	}
}

// DeleteBasket handles HTTP request to delete basket
func DeleteBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	if name, basket := getAndAuthBasket(w, r, ps); basket != nil {
		log.Printf("[info] deleting basket: %s", name)

		basketsDb.Delete(name)
		w.WriteHeader(http.StatusNoContent)
	}
}

// GetBasketRequests handles HTTP request to get requests collected by basket
func GetBasketRequests(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	if _, basket := getAndAuthBasket(w, r, ps); basket != nil {
		values := r.URL.Query()
		query := values.Get("q")
		if len(query) > 0 {
			// Find requests
			max, skip := getPage(values)
			json, err := json.Marshal(basket.FindRequests(query, values.Get("in"), max, skip))
			writeJson(w, http.StatusOK, json, err)
		} else {
			// Get requests page
			json, err := json.Marshal(basket.GetRequests(getPage(values)))
			writeJson(w, http.StatusOK, json, err)
		}
	}
}

// ClearBasket handles HTTP request to delete all requests collected by basket
func ClearBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	if _, basket := getAndAuthBasket(w, r, ps); basket != nil {
		basket.Clear()
		w.WriteHeader(http.StatusNoContent)
	}
}

// ForwardToWeb handels HTTP forwarding to /web
func ForwardToWeb(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	http.Redirect(w, r, "/web", 302)
}

// WebIndexPage handles HTTP request to render index page
func WebIndexPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	indexPage.Execute(w, "")
}

// WebBasketPage handles HTTP request to render basket details page
func WebBasketPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	name := ps.ByName("basket")
	basketPage.Execute(w, name)
}

// AcceptBasketRequests accepts and handles HTTP requests passed to different baskets
func AcceptBasketRequests(w http.ResponseWriter, r *http.Request) {
	parts := strings.Split(r.URL.Path, "/")
	name := parts[1]
	basket := basketsDb.Get(name)
	if basket != nil {
		request := basket.Add(r)
		w.WriteHeader(http.StatusOK)

		if len(basket.Config().ForwardUrl) > 0 {
			go request.Forward(basket.Config(), name)
		}
	} else {
		w.WriteHeader(http.StatusNotFound)
	}
}
<commit_msg>use constant value for main page forward<commit_after>package main

import (
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"

	"github.com/julienschmidt/httprouter"
)

var validBasketName = regexp.MustCompile(BASKET_NAME)
var indexPage = template.Must(template.New("index").Parse(INDEX_HTML))
var basketPage = template.Must(template.New("basket").Parse(BASKET_HTML))

// writeJson writes JSON content to HTTP response
func writeJson(w http.ResponseWriter, status int, json []byte, err error) {
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	} else {
		w.Header().Set("Content-Type", "application/json; charset=UTF-8")
		w.WriteHeader(status)
		w.Write(json)
	}
}

// parseInt parses integer parameter from HTTP request query
func parseInt(value string, defaultValue int) int {
	if len(value) > 0 {
		i, err := strconv.Atoi(value)
		if err == nil {
			return i
		}
	}

	return defaultValue
}

// getPage retrieves page settings from HTTP request query params
func getPage(values url.Values) (int, int) {
	max := parseInt(values.Get("max"), serverConfig.PageSize)
	skip := parseInt(values.Get("skip"), 0)

	return max, skip
}

// getAndAuthBasket retrieves basket by name from HTTP request path and authorize access to the basket object
func getAndAuthBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (string, Basket) {
	name := ps.ByName("basket")
	basket := basketsDb.Get(name)
	if basket != nil {
		// maybe custom header, e.g. basket_key, basket_token
		token := r.Header.Get("Authorization")
		if basket.Authorize(token) || token == serverConfig.MasterToken {
			return name, basket
		} else {
			w.WriteHeader(http.StatusUnauthorized)
		}
	} else {
		w.WriteHeader(http.StatusNotFound)
	}

	return "", nil
}

// parseBasketConfig parses basket configuration and validate its content
func parseBasketConfig(body []byte, config *BasketConfig) error {
	// parse request
	if err := json.Unmarshal(body, config); err != nil {
		return err
	}

	// validate Capacity
	if config.Capacity < 1 {
		return fmt.Errorf("Capacity should be a positive number, but was %d", config.Capacity)
	}

	if config.Capacity > serverConfig.MaxCapacity {
		return fmt.Errorf("Capacity may not be greater than %d", serverConfig.MaxCapacity)
	}

	// validate URL
	if len(config.ForwardUrl) > 0 {
		if _, err := url.ParseRequestURI(config.ForwardUrl); err != nil {
			return err
		}
	}

	return nil
}

// GetBaskets handles HTTP request to get registered baskets
func GetBaskets(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	values := r.URL.Query()
	query := values.Get("q")
	if len(query) > 0 {
		// Find names
		max, skip := getPage(values)
		json, err := json.Marshal(basketsDb.FindNames(query, max, skip))
		writeJson(w, http.StatusOK, json, err)
	} else {
		// Get basket names page
		json, err := json.Marshal(basketsDb.GetNames(getPage(values)))
		writeJson(w, http.StatusOK, json, err)
	}
}

// GetBasket handles HTTP request to get basket configuration
func GetBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	if _, basket := getAndAuthBasket(w, r, ps); basket != nil {
		json, err := json.Marshal(basket.Config())
		writeJson(w, http.StatusOK, json, err)
	}
}

// CreateBasket handles HTTP request to create a new basket
func CreateBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	name := ps.ByName("basket")
	if name == BASKETS_ROOT || name == WEB_ROOT {
		http.Error(w, "You cannot use system path as basket name: "+name, http.StatusForbidden)
		return
	}
	if !validBasketName.MatchString(name) {
		http.Error(w, "Invalid basket name: "+name+", valid name pattern: "+validBasketName.String(), http.StatusBadRequest)
		return
	}

	log.Printf("[info] creating basket: %s", name)

	// read config (max 2 kB)
	body, err := ioutil.ReadAll(io.LimitReader(r.Body, 2048))
	r.Body.Close()
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// default config
	config := BasketConfig{ForwardUrl: "", Capacity: serverConfig.InitCapacity}
	if len(body) > 0 {
		if err := parseBasketConfig(body, &config); err != nil {
			http.Error(w, err.Error(), 422)
			return
		}
	}

	auth, err := basketsDb.Create(name, config)
	if err != nil {
		http.Error(w, err.Error(), http.StatusConflict)
	} else {
		json, err := json.Marshal(auth)
		writeJson(w, http.StatusCreated, json, err)
	}
}

// UpdateBasket handles HTTP request to update basket configuration
func UpdateBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	if _, basket := getAndAuthBasket(w, r, ps); basket != nil {
		// read config (max 2 kB)
		body, err := ioutil.ReadAll(io.LimitReader(r.Body, 2048))
		r.Body.Close()
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
		} else if len(body) > 0 {
			config := basket.Config()
			if err := parseBasketConfig(body, &config); err != nil {
				http.Error(w, err.Error(), 422)
				return
			}

			basket.Update(config)

			w.WriteHeader(http.StatusNoContent)
		} else {
			w.WriteHeader(http.StatusNotModified)
		}
	}
}

// DeleteBasket handles HTTP request to delete basket
func DeleteBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	if name, basket := getAndAuthBasket(w, r, ps); basket != nil {
		log.Printf("[info] deleting basket: %s", name)

		basketsDb.Delete(name)
		w.WriteHeader(http.StatusNoContent)
	}
}

// GetBasketRequests handles HTTP request to get requests collected by basket
func GetBasketRequests(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	if _, basket := getAndAuthBasket(w, r, ps); basket != nil {
		values := r.URL.Query()
		query := values.Get("q")
		if len(query) > 0 {
			// Find requests
			max, skip := getPage(values)
			json, err := json.Marshal(basket.FindRequests(query, values.Get("in"), max, skip))
			writeJson(w, http.StatusOK, json, err)
		} else {
			// Get requests page
			json, err := json.Marshal(basket.GetRequests(getPage(values)))
			writeJson(w, http.StatusOK, json, err)
		}
	}
}

// ClearBasket handles HTTP request to delete all requests collected by basket
func ClearBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	if _, basket := getAndAuthBasket(w, r, ps); basket != nil {
		basket.Clear()
		w.WriteHeader(http.StatusNoContent)
	}
}

// ForwardToWeb handels HTTP forwarding to /web
func ForwardToWeb(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	http.Redirect(w, r, "/"+WEB_ROOT, 302)
}

// WebIndexPage handles HTTP request to render index page
func WebIndexPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	indexPage.Execute(w, "")
}

// WebBasketPage handles HTTP request to render basket details page
func WebBasketPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	name := ps.ByName("basket")
	basketPage.Execute(w, name)
}

// AcceptBasketRequests accepts and handles HTTP requests passed to different baskets
func AcceptBasketRequests(w http.ResponseWriter, r *http.Request) {
	parts := strings.Split(r.URL.Path, "/")
	name := parts[1]
	basket := basketsDb.Get(name)
	if basket != nil {
		request := basket.Add(r)
		w.WriteHeader(http.StatusOK)

		if len(basket.Config().ForwardUrl) > 0 {
			go request.Forward(basket.Config(), name)
		}
	} else {
		w.WriteHeader(http.StatusNotFound)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"html/template"
	"net/http"
	"path"
	"strings"
)

var tmpl = make(map[string]*template.Template)

func init() {
	tmpl["gallery"] = template.Must(template.ParseFiles("assets/templates/gallery.html", "assets/templates/base.html"))
}

type Page struct {
	BaseURL string
	JSON    string
	Name    string
	Path    string
	Dirs    []string
	Images  []ImageInfo
}

func galleryStaticHandler(w http.ResponseWriter, r *http.Request, basePath string) {
	// Check path
	cleanPath := path.Clean(path.Join(basePath, r.URL.Path))
	if !strings.HasPrefix(cleanPath, basePath) {
		http.NotFound(w, r)
		return
	}

	// Serve it
	noDirFileServer(http.FileServer(http.Dir(basePath))).ServeHTTP(w, r)
}

// Serve static images for galleries
func ImageHandler(w http.ResponseWriter, r *http.Request) {
	// Check the gallery header
	g := getGallery(r)
	if g == "" {
		http.NotFound(w, r)
		return
	}
	gallery := Config.Gallery[g]

	galleryStaticHandler(w, r, gallery.ImagePath)
}

// Service static thumbnails for galleries
func ThumbHandler(w http.ResponseWriter, r *http.Request) {
	// Check the gallery header
	g := getGallery(r)
	if g == "" {
		http.NotFound(w, r)
		return
	}
	gallery := Config.Gallery[g]

	galleryStaticHandler(w, r, gallery.ThumbPath)
}

func GalleryHandler(w http.ResponseWriter, r *http.Request) {
	// Check the gallery header
	g := getGallery(r)
	if g == "" {
		http.NotFound(w, r)
		return
	}
	gallery := Config.Gallery[g]

	// Check path
	cleanPath := path.Clean(path.Join(gallery.ImagePath, r.URL.Path))
	if !strings.HasPrefix(cleanPath, gallery.ImagePath) {
		http.NotFound(w, r)
		return
	}

	// Check for trailing /
	if !strings.HasSuffix(r.URL.Path, "/") {
		newPath := path.Clean(gallery.BaseURL+r.URL.Path) + "/"
		log.Debug("%s -> %s", r.URL.Path, newPath)
		localRedirect(w, r, newPath)
		return
	}

	// Scan the directory
	dirs, images, err := tn.ScanFolder(gallery, cleanPath)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Render the page
	p := &Page{
		BaseURL: gallery.BaseURL,
		Name:    gallery.Name,
		Path:    r.URL.Path,
		Dirs:    dirs,
		Images:  images,
	}
	renderTemplate(w, "gallery", p)
}

// Render a template
func renderTemplate(w http.ResponseWriter, t string, p *Page) {
	err := tmpl[t].ExecuteTemplate(w, "base", p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// (from net/http/fs.go)
//
// localRedirect gives a Moved Permanently response.
// It does not convert relative paths to absolute paths like Redirect does.
func localRedirect(w http.ResponseWriter, r *http.Request, newPath string) {
	if q := r.URL.RawQuery; q != "" {
		newPath += "?" + q
	}
	w.Header().Set("Location", newPath)
	w.WriteHeader(http.StatusTemporaryRedirect)
}
<commit_msg>Handle invalid path errors properly<commit_after>package main

import (
	"html/template"
	"net/http"
	"os"
	"path"
	"strings"
)

var tmpl = make(map[string]*template.Template)

func init() {
	tmpl["gallery"] = template.Must(template.ParseFiles("assets/templates/gallery.html", "assets/templates/base.html"))
}

type Page struct {
	BaseURL string
	JSON    string
	Name	string
	Path    string
	Dirs    []string
	Images  []ImageInfo
}

func galleryStaticHandler(w http.ResponseWriter, r *http.Request, basePath string) {
	// Check path
	cleanPath := path.Clean(path.Join(basePath, r.URL.Path))
	if !strings.HasPrefix(cleanPath, basePath) {
		http.NotFound(w, r)
		return
	}

	// Serve it
	noDirFileServer(http.FileServer(http.Dir(basePath))).ServeHTTP(w, r)
}

// Serve static images for galleries
func ImageHandler(w http.ResponseWriter, r *http.Request) {
	// Check the gallery header
	g := getGallery(r)
	if g == "" {
		http.NotFound(w, r)
		return
	}
	gallery := Config.Gallery[g]

	galleryStaticHandler(w, r, gallery.ImagePath)
}

// Serve static thumbnails for galleries
func ThumbHandler(w http.ResponseWriter, r *http.Request) {
	// Check the gallery header
	g := getGallery(r)
	if g == "" {
		http.NotFound(w, r)
		return
	}
	gallery := Config.Gallery[g]

	galleryStaticHandler(w, r, gallery.ThumbPath)
}

// Serve a gallery page
func GalleryHandler(w http.ResponseWriter, r *http.Request) {
	// Check the gallery header
	g := getGallery(r)
	if g == "" {
		http.NotFound(w, r)
		return
	}
	gallery := Config.Gallery[g]

	// Check path
	cleanPath := path.Clean(path.Join(gallery.ImagePath, r.URL.Path))
	if !strings.HasPrefix(cleanPath, gallery.ImagePath) {
		http.NotFound(w, r)
		return
	}

	// Check for trailing /
	if !strings.HasSuffix(r.URL.Path, "/") {
		newPath := path.Clean(gallery.BaseURL+r.URL.Path) + "/"
		log.Debug("%s -> %s", r.URL.Path, newPath)
		localRedirect(w, r, newPath)
		return
	}

	// Scan the directory
	dirs, images, err := tn.ScanFolder(gallery, cleanPath)
	if os.IsNotExist(err) {
		http.NotFound(w, r)
		return
	} else if err != nil {
		log.Error("GalleryHandler:", err.Error())
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Render the page
	p := &Page{
		BaseURL: gallery.BaseURL,
		Name:    gallery.Name,
		Path:    r.URL.Path,
		Dirs:    dirs,
		Images:  images,
	}
	renderTemplate(w, "gallery", p)
}

// Render a template
func renderTemplate(w http.ResponseWriter, t string, p *Page) {
	err := tmpl[t].ExecuteTemplate(w, "base", p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// (from net/http/fs.go)
//
// localRedirect gives a Moved Permanently response.
// It does not convert relative paths to absolute paths like Redirect does.
func localRedirect(w http.ResponseWriter, r *http.Request, newPath string) {
	if q := r.URL.RawQuery; q != "" {
		newPath += "?" + q
	}
	w.Header().Set("Location", newPath)
	w.WriteHeader(http.StatusTemporaryRedirect)
}
<|endoftext|>


<commit_before>package telebot

// Default handler prefix.
const Default string = ""

type Context struct {
	Bot *Bot
	Msg *Message
}

type Handler func(Context)

func (b *Bot) Handle(prefix string, handler Handler) {
	b.tree.Insert(prefix, handler)
}

func (b *Bot) Serve(msg *Message) {
	request := msg.Text

	_, value, _ := b.tree.LongestPrefix(request)

	if endpoint, ok := value.(Handler); ok {
		endpoint(Context{b, msg})
	} else {
		panic("telebot: couldn't find a route")
	}
}
<commit_msg>Fixing excessive panicing.<commit_after>package telebot

// Default handler prefix.
const Default string = ""

type Context struct {
	Bot *Bot
	Msg *Message
}

type Handler func(Context)

func (b *Bot) Handle(prefix string, handler Handler) {
	b.tree.Insert(prefix, handler)
}

func (b *Bot) Serve(msg *Message) {
	request := msg.Text

	_, value, _ := b.tree.LongestPrefix(request)

	if endpoint, ok := value.(Handler); ok {
		endpoint(Context{b, msg})
	}
}
<|endoftext|>


<commit_before>/*
Hugocs or Hugo Comment Script is a script that processes POST requests
in a specific format and converts them into JSON files that are saved
to the filesystem. These files can then be processed using Hugo's
readDir and getJSON functions and displayed as comments.

Copyright 2016 Juha Auvinen.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main

import (
	"crypto/md5"
	"crypto/sha256"
	"crypto/sha512"
	"encoding/hex"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

type Comment struct {
	Name        string `json:"name"`
	Email       string `json:"-"`
	EmailHashed string `json:"emailHashed"`
	EmailMd5    string `json:"emailMd5"`
	EmailSha256 string `json:"emailSha256"`
	EmailSha512 string `json:"emailSha512"`
	Website     string `json:"website"`
	AvatarType  string `json:"avatarType"`
	IPAddress   string `json:"ipv4Address"`
	PageID      string `json:"pageId"`
	Body        string `json:"body"`
	Timestamp   string `json:"timestamp"`
}

type Config struct {
	BaseDir     string
	CommentsDir string
	ContentDir  string
	Salt        string
	TouchFile   string
}

type Response struct {
	Message string `json:"message"`
	IsError bool   `json:"isError"`
}

var config = Config{
	BaseDir:     ".",
	CommentsDir: "comments",
	ContentDir:  "content",
	TouchFile:   ".comment"}

// Used for testing
func newComment(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles("new.html")
	t.Execute(w, "")
}

func saveComment(w http.ResponseWriter, r *http.Request) {
	// TODO: Allow other content-types for javascript-disabled clients.
	// Maybe render a HTML template?
	w.Header().Set("Content-Type", "application/json")
	response := Response{
		Message: "",
		IsError: true}

	if r.Method == "POST" {
		r.ParseForm()
		_, err := validateComment(r, config.ContentDir)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			response = Response{
				Message: err.Error(),
				IsError: true}
		} else {
			now := time.Now()
			comment := Comment{
				Name:        r.Form.Get("name"),
				Email:       r.Form.Get("email"),
				EmailHashed: getHash(r.Form.Get("email"), config.Salt),
				EmailMd5:    getMd5(r.Form.Get("email")),
				EmailSha256: getSha256(r.Form.Get("email")),
				EmailSha512: getSha512(r.Form.Get("email")),
				Website:     r.Form.Get("website"),
				AvatarType:  r.Form.Get("avatar_type"),
				IPAddress:   getIPAddress(r),
				PageID:      r.Form.Get("page_id"),
				Body:        processBody(r.Form.Get("body")),
				Timestamp:   now.Format(time.RFC3339)}

			// FIXME: Needs error handling
			jsonData, _ := json.Marshal(comment)
			filename := buildFilename(comment.Name, comment.Body, buildTimestamp(now))
			writePath := filepath.Join(config.CommentsDir, comment.PageID)
			writeCommentToDisk(jsonData, writePath, filename)

			//str := fmt.Sprintf( "%#v", r )

			w.WriteHeader(http.StatusOK)
			response = Response{
				Message: "Thank you for the comment",
				IsError: false}
		}
	} else {
		w.WriteHeader(http.StatusBadRequest)
		response = Response{
			Message: "Must be POST",
			IsError: true}
	}
	// FIXME: Needs error handling
	responseJSON, _ := json.Marshal(response)
	w.Write(responseJSON)
}

// TODO: Check field lengths to prevent too large comment files
// TODO: Error messages should be read from a config file
func validateComment(r *http.Request, contentDir string) (string, error) {
	form := r.Form
	if len(form.Get("last_name")) > 0 {
		return "last_name", errors.New("You appear to be a spammer, or your browser auto-fills this form.")
	}
	if len(form.Get("name")) > 128 || regexp.MustCompile(`(?i)[\w\s\d\-]+`).MatchString(form.Get("name")) != true {
		return "name", errors.New("Name is not valid")
	}
	if len(form.Get("email")) > 128 || regexp.MustCompile(`^[a-z0-9_.\-\+]+@[a-z0-9_.\-\+]+$`).MatchString(form.Get("email")) != true {
		return "email", errors.New("Email address is not valid")
	}
	if len(form.Get("website")) > 128 || form.Get("website") != "" && regexp.MustCompile(`(https?:\/\/)?[a-z0-9\-\.]+`).MatchString(form.Get("website")) != true {
		return "website", errors.New("Website is not valid")
	}
	if len(form.Get("avatar_type")) > 32 || regexp.MustCompile(`[a-z]+`).MatchString(form.Get("avatar_type")) != true {
		return "avatar_type", errors.New("Avatar type is not valid")
	}
	if len(form.Get("page_id")) > 1024 || regexp.MustCompile(`[a-z0-9\-]+(\/[a-z0-9\-]+)*`).MatchString(form.Get("page_id")) != true {
		return "page_id", errors.New("page_id is not valid")
	}
	if len(form.Get("content_type")) > 4 || regexp.MustCompile(`[a-z]+`).MatchString(form.Get("content_type")) != true {
		return "content_type", errors.New("Content type is not valid")
	}
	if len(form.Get("body")) > 8192 || form.Get("body") == "" {
		return "body", errors.New("You forgot to write the actual comment!")
	}
	if !postExists(form.Get("page_id"), form.Get("content_type"), contentDir) {
		return "page_id", errors.New("Specified post does not exist")
	}
	return "", nil
}

func isValidPath(path string) bool {
	if _, err := os.Stat(path); err == nil {
		return true
	}
	//panic(path)
	return false
}

func postExists(pageID string, extension string, contentDir string) bool {
	return isValidPath(filepath.Join(contentDir, pageID+"."+extension))
}

func processBody(body string) string {
	body = strings.Replace(body, `"""`, "%quote%", -1)
	body = template.HTMLEscapeString(body)
	body = strings.Replace(body, "%quote%", ">", -1)
	return body
}

/*
getHash returns a salted hash for email addresses. The idea is that in
case the commenter would like to have an avatar but either doesn't have
an avatar in an existing service or doesn't want to use them because of
weaker hashes, we can use this one to generate the avatar picture. The
email address is still out there, but it requires just a little bit more
effort from someone to dig it out.
*/
func getHash(s string, salt string) string {
	return getSha512(s + salt)
}

/*
getMd5 generates a md5 checksum for given string. Used here to hash the
email address for Gravatar URLs.
*/
func getMd5(s string) string {
	hash := md5.Sum([]byte(s))
	return hex.EncodeToString(hash[:])
}

/*
getSha256 generates a SHA256 checksum for given string. Used here to
hash the email address for Libravatar URLs.
*/
func getSha256(s string) string {
	hash := sha256.Sum256([]byte(s))
	return hex.EncodeToString(hash[:])
}

/*
getSha512 generates a SHA256 checksum for given string. Used here just
in case.
*/
func getSha512(s string) string {
	hash := sha512.Sum512([]byte(s))
	return hex.EncodeToString(hash[:])
}

func getIPAddress(r *http.Request) string {
	if r.RemoteAddr != "" {
		return r.RemoteAddr
	}
	// FIXME: This should probably look at other fields instead
	return "Unknown"
}

func debugJSON(comment Comment) string {
	str := fmt.Sprintf("%#v", comment)
	return str
}

func buildFilename(name string, msg string, timestamp string) string {
	firstWords := getFirstWords(name + " " + msg)
	return timestamp + "-" + firstWords + ".json"
}

func getFirstWords(s string) string {
	re, _ := regexp.Compile(`(?i)[a-z]+`)
	res := re.FindAllStringSubmatch(s, 7)
	parts := make([]string, 0)
	for _, part := range res {
		parts = append(parts, strings.ToLower(part[0]))
	}
	joined := strings.Join(parts, "-")
	if len(joined) > 32 {
		joined = joined[:32]
	}
	return strings.TrimRight(joined, " -")
}

/*
BuildTimestamp builds a timestamp string to be used as part of the
filename for JSON comment files.
*/
func buildTimestamp(now time.Time) string {
	dateString := fmt.Sprintf("%d-%02d-%02d-%02d%02d%02d",
		now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
	return dateString
}

func writeCommentToDisk(JSON []byte, path string, filename string) error {
	dirCreated := false
	// If path doesn't exist, create it
	if !isValidPath(path) {
		os.MkdirAll(path, os.ModePerm)
		dirCreated = true
	}
	fn := filepath.Join(path, filename)
	// Write file to disk
	// FIXME: Needs error handling
	ioutil.WriteFile(fn, JSON, 0600)
	if dirCreated == true && config.TouchFile != "" {
		updateChangeFile(filepath.Join(config.TouchFile))
	}
	return nil
}

func updateChangeFile(touchFile string) {
	// FIXME: Needs error handling
	ioutil.WriteFile(touchFile, []byte("."), 0600)
}

func main() {
	sourceFlag := flag.String("src", ".", "Full path to the sources dir.")
	contentFlag := flag.String("content", "content", "The content directory. Relative to source dir.")
	commentsFlag := flag.String("comments", "comments", "The directory to save comments. Relative to source dir.")
	touchFlag := flag.String("touch", "", "File to update when a new comment directory is created. Relative to source dir. Some watch scripts may require this.")
	addressFlag := flag.String("address", "", "IP address to use for incoming requests. Defaults to any address.")
	portFlag := flag.String("port", "8080", "Port to listen to for incoming requests.")
	pathFlag := flag.String("path", "/comment", "The url path that is used for comment processing.")
	saltFlag := flag.String("salt", "", "Salt used when generating hashes for anonymous email addresses")

	flag.Parse()

	config.BaseDir = *sourceFlag
	config.ContentDir = filepath.Join(*sourceFlag, *contentFlag)
	config.CommentsDir = filepath.Join(*sourceFlag, *commentsFlag)
	if *touchFlag != "" {
		config.TouchFile = filepath.Join(*sourceFlag, *touchFlag)
	} else {
		config.TouchFile = ""
	}
	config.Salt = *saltFlag

	serverAddress := *addressFlag + ":" + *portFlag

	http.HandleFunc(*pathFlag, saveComment)
	http.HandleFunc("/new", newComment)
	http.ListenAndServe(serverAddress, nil)
}
<commit_msg>Remove all other email hashes except md5 and salted md5<commit_after>/*
Hugocs or Hugo Comment Script is a script that processes POST requests
in a specific format and converts them into JSON files that are saved
to the filesystem. These files can then be processed using Hugo's
readDir and getJSON functions and displayed as comments.

Copyright 2016 Juha Auvinen.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main

import (
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

type Comment struct {
	Name           string `json:"name"`
	Email          string `json:"-"`
	EmailMd5       string `json:"emailMd5"`
	EmailMd5Salted string `json:"emailMd5Salted"`
	Website        string `json:"website"`
	AvatarType     string `json:"avatarType"`
	IPAddress      string `json:"ipv4Address"`
	PageID         string `json:"pageId"`
	Body           string `json:"body"`
	Timestamp      string `json:"timestamp"`
}

type Config struct {
	BaseDir     string
	CommentsDir string
	ContentDir  string
	Salt        string
	TouchFile   string
}

type Response struct {
	Message string `json:"message"`
	IsError bool   `json:"isError"`
}

var config = Config{
	BaseDir:     ".",
	CommentsDir: "comments",
	ContentDir:  "content",
	TouchFile:   ".comment"}

// Used for testing
func newComment(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles("new.html")
	t.Execute(w, "")
}

func saveComment(w http.ResponseWriter, r *http.Request) {
	// TODO: Allow other content-types for javascript-disabled clients.
	// Maybe render a HTML template?
	w.Header().Set("Content-Type", "application/json")
	response := Response{
		Message: "",
		IsError: true}

	if r.Method == "POST" {
		r.ParseForm()
		_, err := validateComment(r, config.ContentDir)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			response.Message = err.Error()
		} else {
			now := time.Now()
			comment := Comment{
				Name:           r.Form.Get("name"),
				Email:          r.Form.Get("email"),
				EmailMd5:       getMd5(r.Form.Get("email"), ""),
				EmailMd5Salted: getMd5(r.Form.Get("email"), config.Salt),
				Website:        r.Form.Get("website"),
				AvatarType:     r.Form.Get("avatar_type"),
				IPAddress:      getIPAddress(r),
				PageID:         r.Form.Get("page_id"),
				Body:           processBody(r.Form.Get("body")),
				Timestamp:      now.Format(time.RFC3339)}

			// FIXME: Needs error handling
			jsonData, _ := json.Marshal(comment)
			filename := buildFilename(comment.Name, comment.Body, buildTimestamp(now))
			writePath := filepath.Join(config.CommentsDir, comment.PageID)
			writeCommentToDisk(jsonData, writePath, filename)

			//str := fmt.Sprintf( "%#v", r )

			w.WriteHeader(http.StatusOK)
			response.Message = "Thank you for the comment"
			response.IsError = false
		}
	} else {
		w.WriteHeader(http.StatusBadRequest)
		response.Message = "Must be POST"
	}
	// FIXME: Needs error handling
	responseJSON, _ := json.Marshal(response)
	w.Write(responseJSON)
}

// TODO: Check field lengths to prevent too large comment files
// TODO: Error messages should be read from a config file
func validateComment(r *http.Request, contentDir string) (string, error) {
	form := r.Form
	if len(form.Get("last_name")) > 0 {
		return "last_name", errors.New("You appear to be a spammer, or your browser auto-fills this form.")
	}
	if len(form.Get("name")) > 128 || regexp.MustCompile(`(?i)[\w\s\d\-]+`).MatchString(form.Get("name")) != true {
		return "name", errors.New("Name is not valid")
	}
	if len(form.Get("email")) > 128 || regexp.MustCompile(`^[a-z0-9_.\-\+]+@[a-z0-9_.\-\+]+$`).MatchString(form.Get("email")) != true {
		return "email", errors.New("Email address is not valid")
	}
	if len(form.Get("website")) > 128 || form.Get("website") != "" && regexp.MustCompile(`(https?:\/\/)?[a-z0-9\-\.]+`).MatchString(form.Get("website")) != true {
		return "website", errors.New("Website is not valid")
	}
	if len(form.Get("avatar_type")) > 32 || regexp.MustCompile(`[a-z]+`).MatchString(form.Get("avatar_type")) != true {
		return "avatar_type", errors.New("Avatar type is not valid")
	}
	if len(form.Get("page_id")) > 1024 || regexp.MustCompile(`[a-z0-9\-]+(\/[a-z0-9\-]+)*`).MatchString(form.Get("page_id")) != true {
		return "page_id", errors.New("page_id is not valid")
	}
	if len(form.Get("content_type")) > 4 || regexp.MustCompile(`[a-z]+`).MatchString(form.Get("content_type")) != true {
		return "content_type", errors.New("Content type is not valid")
	}
	if len(form.Get("body")) > 8192 || form.Get("body") == "" {
		return "body", errors.New("You forgot to write the actual comment!")
	}
	if !postExists(form.Get("page_id"), form.Get("content_type"), contentDir) {
		return "page_id", errors.New("Specified post does not exist")
	}
	return "", nil
}

func isValidPath(path string) bool {
	if _, err := os.Stat(path); err == nil {
		return true
	}
	//panic(path)
	return false
}

func postExists(pageID string, extension string, contentDir string) bool {
	return isValidPath(filepath.Join(contentDir, pageID+"."+extension))
}

func processBody(body string) string {
	body = strings.Replace(body, `"""`, "%quote%", -1)
	body = template.HTMLEscapeString(body)
	body = strings.Replace(body, "%quote%", ">", -1)
	return body
}

// getMd5 generates a md5 checksum for given string with optional salt.
// Used here to hash the email address for avatar service URLs.
func getMd5(s string, salt string) string {
	hash := md5.Sum([]byte(s + salt))
	return hex.EncodeToString(hash[:])
}

func getIPAddress(r *http.Request) string {
	if r.RemoteAddr != "" {
		return r.RemoteAddr
	}
	// FIXME: This should probably look at other fields instead
	return "Unknown"
}

func debugJSON(comment Comment) string {
	str := fmt.Sprintf("%#v", comment)
	return str
}

func buildFilename(name string, msg string, timestamp string) string {
	firstWords := getFirstWords(name + " " + msg)
	return timestamp + "-" + firstWords + ".json"
}

func getFirstWords(s string) string {
	re, _ := regexp.Compile(`(?i)[a-z]+`)
	res := re.FindAllStringSubmatch(s, 7)
	parts := make([]string, 0)
	for _, part := range res {
		parts = append(parts, strings.ToLower(part[0]))
	}
	joined := strings.Join(parts, "-")
	if len(joined) > 32 {
		joined = joined[:32]
	}
	return strings.TrimRight(joined, " -")
}

/*
BuildTimestamp builds a timestamp string to be used as part of the
filename for JSON comment files.
*/
func buildTimestamp(now time.Time) string {
	dateString := fmt.Sprintf("%d-%02d-%02d-%02d%02d%02d",
		now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
	return dateString
}

func writeCommentToDisk(JSON []byte, path string, filename string) error {
	dirCreated := false
	// If path doesn't exist, create it
	if !isValidPath(path) {
		os.MkdirAll(path, os.ModePerm)
		dirCreated = true
	}
	fn := filepath.Join(path, filename)
	// Write file to disk
	// FIXME: Needs error handling
	ioutil.WriteFile(fn, JSON, 0600)
	if dirCreated == true && config.TouchFile != "" {
		updateChangeFile(filepath.Join(config.TouchFile))
	}
	return nil
}

func updateChangeFile(touchFile string) {
	// FIXME: Needs error handling
	ioutil.WriteFile(touchFile, []byte("."), 0600)
}

func main() {
	sourceFlag := flag.String("src", ".", "Full path to the sources dir.")
	contentFlag := flag.String("content", "content", "The content directory. Relative to source dir.")
	commentsFlag := flag.String("comments", "comments", "The directory to save comments. Relative to source dir.")
	touchFlag := flag.String("touch", "", "File to update when a new comment directory is created. Relative to source dir. Some watch scripts may require this.")
	addressFlag := flag.String("address", "", "IP address to use for incoming requests. Defaults to any address.")
	portFlag := flag.String("port", "8080", "Port to listen to for incoming requests.")
	pathFlag := flag.String("path", "/comment", "The url path that is used for comment processing.")
	saltFlag := flag.String("salt", "", "Salt used when generating hashes for anonymous email addresses")

	flag.Parse()

	config.BaseDir = *sourceFlag
	config.ContentDir = filepath.Join(*sourceFlag, *contentFlag)
	config.CommentsDir = filepath.Join(*sourceFlag, *commentsFlag)
	if *touchFlag != "" {
		config.TouchFile = filepath.Join(*sourceFlag, *touchFlag)
	} else {
		config.TouchFile = ""
	}
	config.Salt = *saltFlag

	serverAddress := *addressFlag + ":" + *portFlag

	http.HandleFunc(*pathFlag, saveComment)
	http.HandleFunc("/new", newComment)
	http.ListenAndServe(serverAddress, nil)
}
<|endoftext|>


<commit_before>// Copyright 2015-2016, Cyrill @ Schumacher.fm and the CoreStore contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/*
Package i18n supports string translations with variable substitution, CLDR
pluralization, currency, formats, language, regions and timezones.

Localization vs. Internationalization: http://www.w3.org/International/questions/qa-i18n

Decimals

A decimal number, or just decimal, refers to any number written in decimal
notation, although it is more commonly used to refer to numbers that have
a fractional part separated from the integer part with a decimal separator
(e.g. 11.25) 11 is the integer part, dot the decimal separator and 25 the
fractional part.

Number Format

A format like #,##0.00;(#,##0.00) consists of two parts. The first required
part will be used for negative and positive numbers and if there is a second
part after the semi-colon then this format will be solely used for formatting
of negative numbers. More pattern details can be found:
http://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns
Formatting with a format like #,##,##0.00 is currently not implemented as
too rarely used.

Number formatting

To instantiate your custom number formatter:

	nf := i18n.NewNumber(
		i18n.NumberFormat("#,##0.00;(#,##0.00)" [, Symbols{Decimal: ',' ... } ] ),
	)
	nf.FmtNumber(w io.Writer, sign int, intgr, dec int64) (int, error)

Sign can be 1 for positive number and -1 for negative.
intgr is the integer part and dec the decimal aka fractal part of your float.
Roundings will be applied if dec does not fit within the decimals specified in the
format.

There are also short hand methods for FmtInt(w io.Writer, i int) (int, error) and
FmtFloat64(w io.Writer, f float64) (int, error).

For more information read the details in the documentation of the functions and types.

Currency formatting

To instantiate your custom currency formatter:

	cf := i18n.NewCurrency(
		CurrencyISO("3-letter ISO 4217 code"),
		CurrencySign(s []byte),
		CurrencyFormat("#,##0.00 ¤" [, Symbols{Decimal: ',' ... } ] ),
		CurrencyFraction(digits, rounding, cashDigits, cashRounding int)
	)
	cf.FmtCurrency(w io.Writer, sign int, i, dec int64) (int, error)

CurrencyFraction: Digits are important when your currency has a different amount
of decimal places as specified in the format. E.g. Japanese Yen has Digits 0
despite the format is #,##0.00 ¤.

@todo: Rounding refers to the Swedish rounding and are a todo in this i18n package. Use the money.Currency type for Swedish rounding.
@todo: CashDigits and CashRounding are currently not implemented.
@todo: something like https://github.com/maximilien/i18n4go

Currency Format

The currency symbol ¤ specifies where the currency sign will be placed.

https://github.com/theplant/cldr
https://github.com/vube/i18n
*/
package i18n
<commit_msg>i18n: Add more TODOs and ideas<commit_after>// Copyright 2015-2016, Cyrill @ Schumacher.fm and the CoreStore contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/*
Package i18n supports string translations with variable substitution, CLDR
pluralization, currency, formats, language, regions and timezones.

Localization vs. Internationalization: http://www.w3.org/International/questions/qa-i18n

Decimals

A decimal number, or just decimal, refers to any number written in decimal
notation, although it is more commonly used to refer to numbers that have
a fractional part separated from the integer part with a decimal separator
(e.g. 11.25) 11 is the integer part, dot the decimal separator and 25 the
fractional part.

Number Format

A format like #,##0.00;(#,##0.00) consists of two parts. The first required
part will be used for negative and positive numbers and if there is a second
part after the semi-colon then this format will be solely used for formatting
of negative numbers. More pattern details can be found:
http://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns
Formatting with a format like #,##,##0.00 is currently not implemented as
too rarely used.

Number formatting

To instantiate your custom number formatter:

	nf := i18n.NewNumber(
		i18n.NumberFormat("#,##0.00;(#,##0.00)" [, Symbols{Decimal: ',' ... } ] ),
	)
	nf.FmtNumber(w io.Writer, sign int, intgr, dec int64) (int, error)

Sign can be 1 for positive number and -1 for negative.
intgr is the integer part and dec the decimal aka fractal part of your float.
Roundings will be applied if dec does not fit within the decimals specified in the
format.

There are also short hand methods for FmtInt(w io.Writer, i int) (int, error) and
FmtFloat64(w io.Writer, f float64) (int, error).

For more information read the details in the documentation of the functions and types.

Currency formatting

To instantiate your custom currency formatter:

	cf := i18n.NewCurrency(
		CurrencyISO("3-letter ISO 4217 code"),
		CurrencySign(s []byte),
		CurrencyFormat("#,##0.00 ¤" [, Symbols{Decimal: ',' ... } ] ),
		CurrencyFraction(digits, rounding, cashDigits, cashRounding int)
	)
	cf.FmtCurrency(w io.Writer, sign int, i, dec int64) (int, error)

CurrencyFraction: Digits are important when your currency has a different amount
of decimal places as specified in the format. E.g. Japanese Yen has Digits 0
despite the format is #,##0.00 ¤.

TODO: Rounding refers to the Swedish rounding and are a todo in this i18n package. Use the money.Currency type for Swedish rounding.
TODO: CashDigits and CashRounding are currently not implemented.
TODO: something like https://github.com/maximilien/i18n4go
TODO: https://go-macaron.com/docs/middlewares/i18n

Currency Format

The currency symbol ¤ specifies where the currency sign will be placed.

https://github.com/theplant/cldr
https://github.com/vube/i18n
*/
package i18n
<|endoftext|>


<commit_before>// Copyright 2018, OpenCensus Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package stackdriver contains the OpenCensus exporters for
// Stackdriver Monitoring and Tracing.
//
// Please note that the Stackdriver exporter is currently experimental.
//
// Create an exporter:
//
// 	import (
// 		"go.opencensus.io/exporter/stackdriver"
// 		"go.opencensus.io/trace"
// 		"go.opencensus.io/stats"
// 	)
//
// 	exporter, err := stackdriver.NewExporter(stackdriver.Options{ProjectID: "google-project-id"})
// 	if err != nil {
// 		log.Fatal(err)
// 	}
//
// Export to Stackdriver Stats:
//
// 	stats.RegisterExporter(exporter)
//
// Export to Stackdriver Trace:
//
// 	trace.RegisterExporter(exporter)
//
// The package uses Application Default Credentials to authenticate.  See
// https://developers.google.com/identity/protocols/application-default-credentials
//
// Exporter can be used to propagate traces over HTTP requests for Stackdriver Trace.
//
// Example:
//
// 	import (
// 		"go.opencensus.io/plugin/http/httptrace"
// 		"go.opencensus.io/plugin/http/propagation/google"
// 	)
//
//	client := &http.Client {
//		Transport: httptrace.NewTransport(nil, &google.HTTPFormat{}),
//	}
//
// All outgoing requests from client will include a Stackdriver Trace header.
// See the httptrace package for how to handle incoming requests.
package stackdriver

import (
	"time"

	"go.opencensus.io/stats"
	"go.opencensus.io/trace"
	"google.golang.org/api/option"
	monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres"
)

// Options contains options for configuring the exporter.
type Options struct {
	// ProjectID is the identifier of the Stackdriver
	// project the user is uploading the stats data to.
	ProjectID string

	// OnError is the hook to be called when there is
	// an error occurred when uploading the stats data.
	// If no custom hook is set, errors are logged.
	// Optional.
	OnError func(err error)

	// ClientOptions are additional options to be passed
	// to the underlying Stackdriver Monitoring API client.
	// Optional.
	ClientOptions []option.ClientOption

	// BundleDelayThreshold determines the max amount of time
	// the exporter can wait before uploading view data to
	// the backend.
	// Optional.
	BundleDelayThreshold time.Duration

	// BundleCountThreshold determines how many view data events
	// can be buffered before batch uploading them to the backend.
	// Optional.
	BundleCountThreshold int

	// Resource is an optional field that represents the Stackdriver
	// MonitoredResource, a resource that can be used for monitoring.
	// If no custom ResourceDescriptor is set, a default MonitoredResource
	// with type global and no resource labels will be used.
	// Optional.
	Resource *monitoredrespb.MonitoredResource
}

// Exporter is a stats.Exporter and trace.Exporter
// implementation that uploads data to Stackdriver.
type Exporter struct {
	traceExporter *traceExporter
	statsExporter *statsExporter
}

func NewExporter(o Options) (*Exporter, error) {
	se, err := newStatsExporter(o)
	if err != nil {
		return nil, err
	}
	te, err := newTraceExporter(o)
	if err != nil {
		return nil, err
	}
	return &Exporter{
		statsExporter: se,
		traceExporter: te,
	}, nil
}

// ExportView exports to the Stackdriver Monitoring if view data
// has one or more rows.
func (e *Exporter) ExportView(vd *stats.ViewData) {
	e.statsExporter.ExportView(vd)
}

// ExportSpan exports a SpanData to Stackdriver Trace.
func (e *Exporter) ExportSpan(sd *trace.SpanData) {
	e.traceExporter.ExportSpan(sd)
}

// Flush waits for exported data to be uploaded.
//
// This is useful if your program is ending and you do not
// want to lose recent stats or spans.
func (e *Exporter) Flush() {
	e.statsExporter.Flush()
	e.traceExporter.Flush()
}
<commit_msg>Fix some golint warnings. (#387)<commit_after>// Copyright 2018, OpenCensus Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package stackdriver contains the OpenCensus exporters for
// Stackdriver Monitoring and Tracing.
//
// Please note that the Stackdriver exporter is currently experimental.
//
// Create an exporter:
//
// 	import (
// 		"go.opencensus.io/exporter/stackdriver"
// 		"go.opencensus.io/trace"
// 		"go.opencensus.io/stats"
// 	)
//
// 	exporter, err := stackdriver.NewExporter(stackdriver.Options{ProjectID: "google-project-id"})
// 	if err != nil {
// 		log.Fatal(err)
// 	}
//
// Export to Stackdriver Stats:
//
// 	stats.RegisterExporter(exporter)
//
// Export to Stackdriver Trace:
//
// 	trace.RegisterExporter(exporter)
//
// The package uses Application Default Credentials to authenticate.  See
// https://developers.google.com/identity/protocols/application-default-credentials
//
// Exporter can be used to propagate traces over HTTP requests for Stackdriver Trace.
//
// Example:
//
// 	import (
// 		"go.opencensus.io/plugin/http/httptrace"
// 		"go.opencensus.io/plugin/http/propagation/google"
// 	)
//
//	client := &http.Client {
//		Transport: httptrace.NewTransport(nil, &google.HTTPFormat{}),
//	}
//
// All outgoing requests from client will include a Stackdriver Trace header.
// See the httptrace package for how to handle incoming requests.
package stackdriver

import (
	"time"

	"go.opencensus.io/stats"
	"go.opencensus.io/trace"
	"google.golang.org/api/option"
	monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres"
)

// Options contains options for configuring the exporter.
type Options struct {
	// ProjectID is the identifier of the Stackdriver
	// project the user is uploading the stats data to.
	ProjectID string

	// OnError is the hook to be called when there is
	// an error occurred when uploading the stats data.
	// If no custom hook is set, errors are logged.
	// Optional.
	OnError func(err error)

	// ClientOptions are additional options to be passed
	// to the underlying Stackdriver Monitoring API client.
	// Optional.
	ClientOptions []option.ClientOption

	// BundleDelayThreshold determines the max amount of time
	// the exporter can wait before uploading view data to
	// the backend.
	// Optional.
	BundleDelayThreshold time.Duration

	// BundleCountThreshold determines how many view data events
	// can be buffered before batch uploading them to the backend.
	// Optional.
	BundleCountThreshold int

	// Resource is an optional field that represents the Stackdriver
	// MonitoredResource, a resource that can be used for monitoring.
	// If no custom ResourceDescriptor is set, a default MonitoredResource
	// with type global and no resource labels will be used.
	// Optional.
	Resource *monitoredrespb.MonitoredResource
}

// Exporter is a stats.Exporter and trace.Exporter
// implementation that uploads data to Stackdriver.
type Exporter struct {
	traceExporter *traceExporter
	statsExporter *statsExporter
}

// NewExporter creates a new Exporter that implements both stats.Exporter and
// trace.Exporter.
func NewExporter(o Options) (*Exporter, error) {
	se, err := newStatsExporter(o)
	if err != nil {
		return nil, err
	}
	te, err := newTraceExporter(o)
	if err != nil {
		return nil, err
	}
	return &Exporter{
		statsExporter: se,
		traceExporter: te,
	}, nil
}

// ExportView exports to the Stackdriver Monitoring if view data
// has one or more rows.
func (e *Exporter) ExportView(vd *stats.ViewData) {
	e.statsExporter.ExportView(vd)
}

// ExportSpan exports a SpanData to Stackdriver Trace.
func (e *Exporter) ExportSpan(sd *trace.SpanData) {
	e.traceExporter.ExportSpan(sd)
}

// Flush waits for exported data to be uploaded.
//
// This is useful if your program is ending and you do not
// want to lose recent stats or spans.
func (e *Exporter) Flush() {
	e.statsExporter.Flush()
	e.traceExporter.Flush()
}
<|endoftext|>


<commit_before>package dynago

//import "log"

const (
	SELECT_ALL        = "ALL_ATTRIBUTES"
	SELECT_PROJECTED  = "ALL_PROJECTED_ATTRIBUTES"
	SELECT_ATTRIBUTES = "SPECIFIC_ATTRIBUTES"
	SELECT_COUNT      = "COUNT"
)

var (
	RETURN_CONSUMED = map[bool]string{true: "TOTAL", false: "NONE"}
)

type ConsumedCapacityDescription struct {
	CapacityUnits float32
	TableName     string
}

type KeyValue struct {
	Key   AttributeDefinition
	Value interface{}
}

//type ItemValues map[string]AttributeValue

//////////////////////////////////////////////////////////////////////////////
//
// GetItem
//

type GetItemRequest struct {
	TableName              string
	Key                    map[string]AttributeValue
	AttributesToGet        []string
	ConsistentRead         bool
	ReturnConsumedCapacity string
}

type GetItemResult struct {
	ConsumedCapacity ConsumedCapacityDescription

	Item DBItem
}

func (db *DBClient) GetItem(tableName string, hashKey *KeyValue, rangeKey *KeyValue, attributes []string, consistent bool, consumed bool) (map[string]interface{}, float32, error) {

	getReq := GetItemRequest{TableName: tableName, AttributesToGet: attributes, ConsistentRead: consistent, ReturnConsumedCapacity: RETURN_CONSUMED[consumed]}
	getReq.Key = EncodeAttribute(hashKey.Key, hashKey.Value)
	if rangeKey != nil {
		getReq.Key[rangeKey.Key.AttributeName] = EncodeAttributeValue(rangeKey.Key, rangeKey.Value)
	}

	var getRes GetItemResult

	if err := db.Query("GetItem", getReq).Decode(&getRes); err != nil {
		return nil, 0.0, err
	}

	if len(getRes.Item) == 0 {
		return nil, getRes.ConsumedCapacity.CapacityUnits, nil
	}

	return DecodeItem(getRes.Item), getRes.ConsumedCapacity.CapacityUnits, nil
}

//////////////////////////////////////////////////////////////////////////////
//
// Query
//

type QueryRequest struct {
	TableName              string
	AttributesToGet        []string `json:",omitempty"`
	ScanIndexForward       bool
	ExclusiveStartKey      AttributeNameValue   `json:",omitempty"`
	KeyConditions          map[string]Condition `json:",omitempty"`
	IndexName              string               `json:",omitempty"`
	Limit                  int                  `json:",omitempty"`
	Select                 string               `json:",omitempty"`
	ReturnConsumedCapacity string               `json:",omitempty"`

	table *TableInstance
}

type QueryResult struct {
	Items            []DBItem
	ConsumedCapacity ConsumedCapacityDescription
	LastEvaluatedKey AttributeNameValue
	Count            int
	ScannedCount     int
}

func QueryTable(table *TableInstance) *QueryRequest {
	return &QueryRequest{TableName: table.Name, ScanIndexForward: true, KeyConditions: make(map[string]Condition), table: table}
}

func Query(tableName string) *QueryRequest {
	return &QueryRequest{TableName: tableName, ScanIndexForward: true, KeyConditions: make(map[string]Condition)}
}

func (queryReq *QueryRequest) WithAttributes(attributes []string) *QueryRequest {
	queryReq.AttributesToGet = attributes
	return queryReq
}

func (queryReq *QueryRequest) WithStartKey(startKey AttributeNameValue) *QueryRequest {
	queryReq.ExclusiveStartKey = startKey
	return queryReq
}

func (queryReq *QueryRequest) WithIndex(indexName string) *QueryRequest {
	queryReq.IndexName = indexName
	return queryReq
}

func (queryReq *QueryRequest) WithCondition(attrName string, condition Condition) *QueryRequest {
	queryReq.KeyConditions[attrName] = condition
	return queryReq
}

func (queryReq *QueryRequest) WithAttrCondition(cond AttrCondition) *QueryRequest {
	for k, v := range cond {
		queryReq.KeyConditions[k] = v
	}

	return queryReq
}

func (queryReq *QueryRequest) WithLimit(limit int) *QueryRequest {
	queryReq.Limit = limit
	return queryReq
}

func (queryReq *QueryRequest) WithSelect(selectValue string) *QueryRequest {
	queryReq.Select = selectValue
	return queryReq
}

func (queryReq *QueryRequest) WithConsumed(consumed bool) *QueryRequest {
	queryReq.ReturnConsumedCapacity = RETURN_CONSUMED[consumed]
	return queryReq
}

func (queryReq *QueryRequest) Exec(db *DBClient) ([]DBItem, AttributeNameValue, float32, error) {
	if db == nil && queryReq.table != nil {
		db = queryReq.table.DB
	}

	var queryRes QueryResult

	if err := db.Query("Query", queryReq).Decode(&queryRes); err != nil {
		return nil, nil, 0.0, err
	}

	return queryRes.Items, queryRes.LastEvaluatedKey, queryRes.ConsumedCapacity.CapacityUnits, nil
}

//////////////////////////////////////////////////////////////////////////////
//
// Scan
//

type ScanRequest struct {
	TableName              string
	AttributesToGet        []string
	ExclusiveStartKey      AttributeNameValue
	ScanFilter             map[string]Condition
	Limit                  int    `json:",omitempty"`
	Segment                int    `json:",omitempty"`
	TotalSegments          int    `json:",omitempty"`
	Select                 string `json:",omitempty"`
	ReturnConsumedCapacity string `json:",omitempty"`

	table *TableInstance
}

func ScanTable(table *TableInstance) *ScanRequest {
	return &ScanRequest{TableName: table.Name, table: table}
}

func Scan(tableName string) *ScanRequest {
	return &ScanRequest{TableName: tableName}
}

func (scanReq *ScanRequest) WithAttributes(attributes []string) *ScanRequest {
	scanReq.AttributesToGet = attributes
	return scanReq
}

func (scanReq *ScanRequest) WithStartKey(startKey AttributeNameValue) *ScanRequest {
	scanReq.ExclusiveStartKey = startKey
	return scanReq
}

func (scanReq *ScanRequest) WithFilter(attrName string, condition Condition) *ScanRequest {
	if scanReq.ScanFilter == nil {
		scanReq.ScanFilter = map[string]Condition{attrName: condition}
	} else {
		scanReq.ScanFilter[attrName] = condition
	}
	return scanReq
}

func (scanReq *ScanRequest) WithFilters(filters AttrCondition) *ScanRequest {
	scanReq.ScanFilter = filters
	return scanReq
}

func (scanReq *ScanRequest) WithLimit(limit int) *ScanRequest {
	scanReq.Limit = limit
	return scanReq
}

func (scanReq *ScanRequest) WithSegment(segment, totalSegments int) *ScanRequest {
	scanReq.Segment = segment
	scanReq.TotalSegments = totalSegments
	return scanReq
}

func (scanReq *ScanRequest) WithSelect(selectValue string) *ScanRequest {
	scanReq.Select = selectValue
	return scanReq
}

func (scanReq *ScanRequest) WithConsumed(consumed bool) *ScanRequest {
	scanReq.ReturnConsumedCapacity = RETURN_CONSUMED[consumed]
	return scanReq
}

func (scanReq *ScanRequest) Exec(db *DBClient) ([]DBItem, AttributeNameValue, float32, error) {
	var scanRes QueryResult

	if err := db.Query("Scan", scanReq).Decode(&scanRes); err != nil {
		return nil, nil, 0.0, err
	}

	return scanRes.Items, scanRes.LastEvaluatedKey, scanRes.ConsumedCapacity.CapacityUnits, nil
}

func (scanReq *ScanRequest) Count(db *DBClient) (count int, scount int, consumed float32, err error) {
	var scanRes QueryResult

	req := *scanReq
	req.Select = SELECT_COUNT

	for {
		scanRes.LastEvaluatedKey = nil

		if err = db.Query("Scan", &req).Decode(&scanRes); err != nil {
			break
		}

		count += scanRes.Count
		scount += scanRes.ScannedCount
		consumed += scanRes.ConsumedCapacity.CapacityUnits

		if scanRes.LastEvaluatedKey == nil {
			break
		}

		req.ExclusiveStartKey = make(AttributeNameValue)
		for k, v := range scanRes.LastEvaluatedKey {
			req.ExclusiveStartKey[k] = v
		}
	}

	return
}
<commit_msg>query and scan now return map of name/value pairs as Go types. also fixed limit/segment/totalSegments when value is 0 (now they are stored as "string" so that empty string means "omitempty" and 0 means 0.<commit_after>package dynago

import (
	"encoding/json"
	//"log"
	"strconv"
)

const (
	SELECT_ALL        = "ALL_ATTRIBUTES"
	SELECT_PROJECTED  = "ALL_PROJECTED_ATTRIBUTES"
	SELECT_ATTRIBUTES = "SPECIFIC_ATTRIBUTES"
	SELECT_COUNT      = "COUNT"
)

var (
	RETURN_CONSUMED = map[bool]string{true: "TOTAL", false: "NONE"}
)

type ConsumedCapacityDescription struct {
	CapacityUnits float32
	TableName     string
}

type KeyValue struct {
	Key   AttributeDefinition
	Value interface{}
}

// Items are maps of name/value pairs
type Item map[string]interface{}

func (pi *Item) UnmarshalJSON(data []byte) error {
	var dbitem DBItem

	if err := json.Unmarshal(data, &dbitem); err != nil {
		return err
	}

	item := make(Item)

	for k, v := range dbitem {
		item[k] = DecodeValue(v)
	}

	*pi = item
	return nil
}

func AsNumber(v int) json.Number {
	return json.Number(strconv.Itoa(v))
}

//////////////////////////////////////////////////////////////////////////////
//
// GetItem
//

type GetItemRequest struct {
	TableName              string
	Key                    map[string]AttributeValue
	AttributesToGet        []string
	ConsistentRead         bool
	ReturnConsumedCapacity string
}

type GetItemResult struct {
	ConsumedCapacity ConsumedCapacityDescription

	Item DBItem
}

func (db *DBClient) GetItem(tableName string, hashKey *KeyValue, rangeKey *KeyValue, attributes []string, consistent bool, consumed bool) (map[string]interface{}, float32, error) {

	getReq := GetItemRequest{TableName: tableName, AttributesToGet: attributes, ConsistentRead: consistent, ReturnConsumedCapacity: RETURN_CONSUMED[consumed]}
	getReq.Key = EncodeAttribute(hashKey.Key, hashKey.Value)
	if rangeKey != nil {
		getReq.Key[rangeKey.Key.AttributeName] = EncodeAttributeValue(rangeKey.Key, rangeKey.Value)
	}

	var getRes GetItemResult

	if err := db.Query("GetItem", getReq).Decode(&getRes); err != nil {
		return nil, 0.0, err
	}

	if len(getRes.Item) == 0 {
		return nil, getRes.ConsumedCapacity.CapacityUnits, nil
	}

	return DecodeItem(getRes.Item), getRes.ConsumedCapacity.CapacityUnits, nil
}

//////////////////////////////////////////////////////////////////////////////
//
// Query
//

type QueryRequest struct {
	TableName              string
	AttributesToGet        []string `json:",omitempty"`
	ScanIndexForward       bool
	ExclusiveStartKey      AttributeNameValue   `json:",omitempty"`
	KeyConditions          map[string]Condition `json:",omitempty"`
	IndexName              string               `json:",omitempty"`
	Limit                  json.Number          `json:",omitempty"`
	Select                 string               `json:",omitempty"`
	ReturnConsumedCapacity string               `json:",omitempty"`

	table *TableInstance
}

type QueryResult struct {
	Items            []Item
	ConsumedCapacity ConsumedCapacityDescription
	LastEvaluatedKey AttributeNameValue
	Count            int
	ScannedCount     int
}

func QueryTable(table *TableInstance) *QueryRequest {
	return &QueryRequest{TableName: table.Name, ScanIndexForward: true, KeyConditions: make(map[string]Condition), table: table}
}

func Query(tableName string) *QueryRequest {
	return &QueryRequest{TableName: tableName, ScanIndexForward: true, KeyConditions: make(map[string]Condition)}
}

func (queryReq *QueryRequest) WithAttributes(attributes []string) *QueryRequest {
	queryReq.AttributesToGet = attributes
	return queryReq
}

func (queryReq *QueryRequest) WithStartKey(startKey AttributeNameValue) *QueryRequest {
	queryReq.ExclusiveStartKey = startKey
	return queryReq
}

func (queryReq *QueryRequest) WithIndex(indexName string) *QueryRequest {
	queryReq.IndexName = indexName
	return queryReq
}

func (queryReq *QueryRequest) WithCondition(attrName string, condition Condition) *QueryRequest {
	queryReq.KeyConditions[attrName] = condition
	return queryReq
}

func (queryReq *QueryRequest) WithAttrCondition(cond AttrCondition) *QueryRequest {
	for k, v := range cond {
		queryReq.KeyConditions[k] = v
	}

	return queryReq
}

func (queryReq *QueryRequest) WithLimit(limit int) *QueryRequest {
	queryReq.Limit = AsNumber(limit)
	return queryReq
}

func (queryReq *QueryRequest) WithSelect(selectValue string) *QueryRequest {
	queryReq.Select = selectValue
	return queryReq
}

func (queryReq *QueryRequest) WithConsumed(consumed bool) *QueryRequest {
	queryReq.ReturnConsumedCapacity = RETURN_CONSUMED[consumed]
	return queryReq
}

func (queryReq *QueryRequest) Exec(db *DBClient) ([]Item, AttributeNameValue, float32, error) {
	if db == nil && queryReq.table != nil {
		db = queryReq.table.DB
	}

	var queryRes QueryResult

	if err := db.Query("Query", queryReq).Decode(&queryRes); err != nil {
		return nil, nil, 0.0, err
	}

	return queryRes.Items, queryRes.LastEvaluatedKey, queryRes.ConsumedCapacity.CapacityUnits, nil
}

//////////////////////////////////////////////////////////////////////////////
//
// Scan
//

type ScanRequest struct {
	TableName              string
	AttributesToGet        []string
	ExclusiveStartKey      AttributeNameValue
	ScanFilter             map[string]Condition
	Limit                  json.Number `json:",omitempty"`
	Segment                json.Number `json:",omitempty"`
	TotalSegments          json.Number `json:",omitempty"`
	Select                 string      `json:",omitempty"`
	ReturnConsumedCapacity string      `json:",omitempty"`

	table *TableInstance
}

func ScanTable(table *TableInstance) *ScanRequest {
	return &ScanRequest{TableName: table.Name, table: table}
}

func Scan(tableName string) *ScanRequest {
	return &ScanRequest{TableName: tableName}
}

func (scanReq *ScanRequest) WithAttributes(attributes []string) *ScanRequest {
	scanReq.AttributesToGet = attributes
	return scanReq
}

func (scanReq *ScanRequest) WithStartKey(startKey AttributeNameValue) *ScanRequest {
	scanReq.ExclusiveStartKey = startKey
	return scanReq
}

func (scanReq *ScanRequest) WithFilter(attrName string, condition Condition) *ScanRequest {
	if scanReq.ScanFilter == nil {
		scanReq.ScanFilter = map[string]Condition{attrName: condition}
	} else {
		scanReq.ScanFilter[attrName] = condition
	}
	return scanReq
}

func (scanReq *ScanRequest) WithFilters(filters AttrCondition) *ScanRequest {
	scanReq.ScanFilter = filters
	return scanReq
}

func (scanReq *ScanRequest) WithLimit(limit int) *ScanRequest {
	scanReq.Limit = AsNumber(limit)
	return scanReq
}

func (scanReq *ScanRequest) WithSegment(segment, totalSegments int) *ScanRequest {
	scanReq.Segment = AsNumber(segment)
	scanReq.TotalSegments = AsNumber(totalSegments)
	return scanReq
}

func (scanReq *ScanRequest) WithSelect(selectValue string) *ScanRequest {
	scanReq.Select = selectValue
	return scanReq
}

func (scanReq *ScanRequest) WithConsumed(consumed bool) *ScanRequest {
	scanReq.ReturnConsumedCapacity = RETURN_CONSUMED[consumed]
	return scanReq
}

func (scanReq *ScanRequest) Exec(db *DBClient) ([]Item, AttributeNameValue, float32, error) {
	var scanRes QueryResult

	if err := db.Query("Scan", scanReq).Decode(&scanRes); err != nil {
		return nil, nil, 0.0, err
	}

	return scanRes.Items, scanRes.LastEvaluatedKey, scanRes.ConsumedCapacity.CapacityUnits, nil
}

func (scanReq *ScanRequest) Count(db *DBClient) (count int, scount int, consumed float32, err error) {
	var scanRes QueryResult

	req := *scanReq
	req.Select = SELECT_COUNT

	for {
		scanRes.LastEvaluatedKey = nil

		if err = db.Query("Scan", &req).Decode(&scanRes); err != nil {
			break
		}

		count += scanRes.Count
		scount += scanRes.ScannedCount
		consumed += scanRes.ConsumedCapacity.CapacityUnits

		if scanRes.LastEvaluatedKey == nil {
			break
		}

		req.ExclusiveStartKey = make(AttributeNameValue)
		for k, v := range scanRes.LastEvaluatedKey {
			req.ExclusiveStartKey[k] = v
		}
	}

	return
}
<|endoftext|>