~mendelmaleh/bin

ff48826926b3ee03dbd38003d3fb72d6fe75c25c — Mendel E 1 year, 10 months ago
Initial commit
4 files changed, 261 insertions(+), 0 deletions(-)

A .gitignore
A base26/itoa.go
A config.toml
A main.go
A  => .gitignore +2 -0
@@ 1,2 @@
bin
db

A  => base26/itoa.go +87 -0
@@ 1,87 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package base26

// FormatUint returns the string representation of i in base 26.
// The result uses the lower-case letters 'a' to 'z'
func FormatUint(i uint64) string {
	_, s := formatBits(nil, i, false, false)
	return s
}

// FormatInt returns the string representation of i in base 26.
// The result uses the lower-case letters 'a' to 'z'
func FormatInt(i int64) string {
	_, s := formatBits(nil, uint64(i), i < 0, false)
	return s
}

// Itoa is equivalent to FormatInt(int64(i)).
func Itoa(i int) string {
	return FormatInt(int64(i))
}

// AppendInt appends the string form of the integer i,
// as generated by FormatInt, to dst and returns the extended buffer.
func AppendInt(dst []byte, i int64) []byte {
	dst, _ = formatBits(dst, uint64(i), i < 0, true)
	return dst
}

// AppendUint appends the string form of the unsigned integer i,
// as generated by FormatUint, to dst and returns the extended buffer.
func AppendUint(dst []byte, i uint64) []byte {
	dst, _ = formatBits(dst, i, false, true)
	return dst
}

const digits = "abcdefghijklmnopqrstuvwxyz"

// formatBits computes the string representation of u in the given base.
// If neg is set, u is treated as negative int64 value. If append_ is
// set, the string is appended to dst and the resulting byte slice is
// returned as the first result value; otherwise the string is returned
// as the second result value.
//
func formatBits(dst []byte, u uint64, neg, append_ bool) (d []byte, s string) {
	var a [64 + 1]byte // +1 for sign of 64bit value in base 2
	i := len(a)

	if neg {
		u = -u
	}

	// convert bits
	// We use uint values where we can because those will
	// fit into a single register even on a 32bit machine.
	// general case
	b := uint64(26)
	for u >= b {
		i--
		// Avoid using r = a%b in addition to q = a/b
		// since 64bit division and modulo operations
		// are calculated by runtime functions on 32bit machines.
		q := u / b
		a[i] = digits[uint(u-q*b)]
		u = q
	}

	// u < base
	i--
	a[i] = digits[uint(u)]

	// add sign, if any
	if neg {
		i--
		a[i] = '-'
	}

	if append_ {
		d = append(dst, a[i:]...)
		return
	}
	s = string(a[i:])
	return
}

A  => config.toml +4 -0
@@ 1,4 @@
[bin]
db = "db"
addr = ":8080"
pattern = "/"

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

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"math/rand"
	"net/http"
	"net/url"
	"time"
	"unicode"

	"git.sr.ht/~mendelmaleh/bin/base26"
	"github.com/pelletier/go-toml"
	"github.com/prologic/bitcask"
)

// This is for the random id generator, it ensures the id is 6 chars long.
// The limitation is that it won't generate more than 297m (diff) ids.
const (
	min  int = 26 * 26 * 26 * 26 * 26 // 11_881_376  // 26 ** 5
	max  int = min * 26               // 308_915_776 // 26 ** 6
	diff int = max - min              // 297_034_400
)

// Config struct
type Config struct {
	Bin struct {
		DB      string
		Addr    string
		Pattern string
	}
}

func main() {
	// get config
	doc, err := ioutil.ReadFile("config.toml")
	if err != nil {
		log.Panic(err)
	}

	// parse config
	config := Config{}
	err = toml.Unmarshal(doc, &config)
	if err != nil {
		log.Panic(err)
	}

	// setup db
	db, err := bitcask.Open(config.Bin.DB)
	if err != nil {
		log.Fatal(err)
	}

	// setup http server
	http.HandleFunc(config.Bin.Pattern,
		func(w http.ResponseWriter, r *http.Request) {
			Bin(w, r, db)
		},
	)

	// run
	log.Fatal(http.ListenAndServe(config.Bin.Addr, nil))
}

func isASCII(s string) bool {
	for _, r := range s {
		if r > unicode.MaxASCII {
			return false
		}
	}
	return true
}

// Bin is a pastebin service, it requires a bitcask kv store.
func Bin(w http.ResponseWriter, r *http.Request, b *bitcask.Bitcask) {
	id := r.FormValue("id")

	if r.Method == "GET" {
		// no file was requested
		if id == "" {
			fmt.Fprintln(w, "todo: instructions")
			return
		}

		// a file was requested
		buf, err := b.Get([]byte(id))
		if err != nil {
			if err == bitcask.ErrKeyNotFound {
				fmt.Fprintf(w, "no bin with id %s\n", id)
				return
			}

			log.Print(err)
			fmt.Fprintf(w, "error %T when retrieving bin.\n", err)
			return
		}

		w.Write(buf)
		return
	}

	if r.Method == "POST" {
		// if no id was passed
		if id == "" {
			// generate random int
			rand.Seed(time.Now().UnixNano())
			n := rand.Intn(diff) + min
			id = base26.Itoa(n)

			// ensure id is available
			for b.Has([]byte(id)) {
				id = ""
				n = rand.Intn(diff) + min
				id = base26.Itoa(n)
			}
		} else if b.Has([]byte(id)) {
			fmt.Fprintf(w, "id %s is already taken. try again!\n", id)
			return
		} else if len(id) > 32 || !isASCII(id) {
			fmt.Fprintf(w, "id %s is not valid. ensure it's all ASCII and under 32 chars.\n", id)
			return
		}

		// get file
		file, _, err := r.FormFile("file")
		if err != nil {
			log.Println(err)
			fmt.Fprintf(w, "error %T when retrieving file, try again.\n", err)
			return
		}
		defer file.Close()

		// copy file
		var buf bytes.Buffer
		_, err = io.Copy(&buf, file)
		if err != nil {
			log.Println(err)
			fmt.Fprintf(w, "error %T when retrieving file, try again.\n", err)
			return
		}

		// save file
		err = b.Put([]byte(id), buf.Bytes())
		if err != nil {
			log.Println(err)
			fmt.Fprintf(w, "error %T when saving file, try again.\n", err)
			return
		}

		// return id
		u := url.URL{
			Scheme: "http",
			Host:   r.Host,
			Path:   "/", // todo: use config or dynamic
		}

		q := url.Values{}
		q.Set("id", id)

		u.RawQuery = q.Encode()
		fmt.Fprintf(w, "%s\n", u.String())

		return
	}
}