pasta/cmd/pastad/pastad.go
Felix Niederwanger 9218427a0b
Fix upload not working issue
Fix the issue that the web upload was not working. The issue was coming
from overwriting the multibody reader with r.Body.
2022-08-09 11:22:03 +02:00

814 lines
23 KiB
Go

/*
* pasted - stupid simple paste server
*/
package main
import (
"bufio"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/BurntSushi/toml"
"github.com/akamensky/argparse"
)
type Config struct {
BaseUrl string `toml:"BaseURL"` // Instance base URL
PastaDir string `toml:"PastaDir"` // dir where pasta are stored
BindAddr string `toml:"BindAddress"`
MaxPastaSize int64 `toml:"MaxPastaSize"` // Max bin size in bytes
PastaCharacters int `toml:"PastaCharacters"`
MimeTypesFile string `toml:"MimeTypes"` // Load mime types from this file
DefaultExpire int64 `toml:"Expire"` // Default expire time for a new pasta in seconds
CleanupInterval int `toml:"Cleanup"` // Seconds between cleanup cycles
RequestDelay int64 `toml:"RequestDelay"` // Required delay between requests in milliseconds
}
type ParserConfig struct {
ConfigFile *string
BaseURL *string
PastaDir *string
BindAddr *string
MaxPastaSize *int // parser doesn't support int64
PastaCharacters *int
MimeTypesFile *string
DefaultExpire *int // parser doesn't support int64
CleanupInterval *int
}
var cf Config
var bowl PastaBowl
var mimeExtensions map[string]string
func CreateDefaultConfigfile(filename string) error {
hostname, _ := os.Hostname()
if hostname == "" {
hostname = "localhost"
}
content := []byte(fmt.Sprintf("BaseURL = 'http://%s:8199'\nBindAddress = ':8199'\nPastaDir = 'pastas'\nMaxPastaSize = 5242880 # 5 MiB\nPastaCharacters = 8\nExpire = 2592000 # 1 month\nCleanup = 3600 # cleanup interval in seconds\nRequestDelay = 2000", hostname))
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
if _, err = file.Write(content); err != nil {
return err
}
if err := file.Chmod(0640); err != nil {
return err
}
return file.Close()
}
// SetDefaults sets the default values to a config instance
func (cf *Config) SetDefaults() {
cf.BaseUrl = "http://localhost:8199"
cf.PastaDir = "pastas/"
cf.BindAddr = "127.0.0.1:8199"
cf.MaxPastaSize = 1024 * 1024 * 25 // Default max size: 25 MB
cf.PastaCharacters = 8 // Note: Never use less than 8 characters!
cf.MimeTypesFile = "mime.types"
cf.DefaultExpire = 0
cf.CleanupInterval = 60 * 60 // Default cleanup is once per hour
cf.RequestDelay = 0 // By default not spam protection (Assume we are in safe environment)
}
// getenv reads a given environmental variable and returns it's value if present or defval if not present or empty
func getenv(key string, defval string) string {
val := os.Getenv(key)
if val == "" {
return defval
}
return val
}
// getenv reads a given environmental variable as integer and returns it's value if present or defval if not present or empty
func getenv_i(key string, defval int) int {
val := os.Getenv(key)
if val == "" {
return defval
}
if i32, err := strconv.Atoi(val); err != nil {
return defval
} else {
return i32
}
}
// getenv reads a given environmental variable as integer and returns it's value if present or defval if not present or empty
func getenv_i64(key string, defval int64) int64 {
val := os.Getenv(key)
if val == "" {
return defval
}
if i64, err := strconv.ParseInt(val, 10, 64); err != nil {
return defval
} else {
return i64
}
}
// ReadEnv reads the environmental variables and sets the config accordingly
func (cf *Config) ReadEnv() {
cf.BaseUrl = getenv("PASTA_BASEURL", cf.BaseUrl)
cf.PastaDir = getenv("PASTA_PASTADIR", cf.PastaDir)
cf.BindAddr = getenv("PASTA_BINDADDR", cf.BindAddr)
cf.MaxPastaSize = getenv_i64("PASTA_MAXSIZE", cf.MaxPastaSize)
cf.PastaCharacters = getenv_i("PASTA_CHARACTERS", cf.PastaCharacters)
cf.MimeTypesFile = getenv("PASTA_MIMEFILE", cf.MimeTypesFile)
cf.DefaultExpire = getenv_i64("PASTA_EXPIRE", cf.DefaultExpire)
cf.CleanupInterval = getenv_i("PASTA_CLEANUP", cf.CleanupInterval)
cf.RequestDelay = getenv_i64("PASTA_REQUESTDELAY", cf.RequestDelay)
}
func (pc *ParserConfig) ApplyTo(cf *Config) {
if pc.BaseURL != nil && *pc.BaseURL != "" {
cf.BaseUrl = *pc.BaseURL
}
if pc.PastaDir != nil && *pc.PastaDir != "" {
cf.PastaDir = *pc.PastaDir
}
if pc.BindAddr != nil && *pc.BindAddr != "" {
cf.BindAddr = *pc.BindAddr
}
if pc.MaxPastaSize != nil && *pc.MaxPastaSize > 0 {
cf.MaxPastaSize = int64(*pc.MaxPastaSize)
}
if pc.PastaCharacters != nil && *pc.PastaCharacters > 0 {
cf.PastaCharacters = *pc.PastaCharacters
}
if pc.MimeTypesFile != nil && *pc.MimeTypesFile != "" {
cf.MimeTypesFile = *pc.MimeTypesFile
}
if pc.DefaultExpire != nil && *pc.DefaultExpire > 0 {
cf.DefaultExpire = int64(*pc.DefaultExpire)
}
if pc.CleanupInterval != nil && *pc.CleanupInterval > 0 {
cf.CleanupInterval = *pc.CleanupInterval
}
}
func isAlphaNumeric(c rune) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
}
func containsOnlyAlphaNumeric(input string) bool {
for _, c := range input {
if !isAlphaNumeric(c) {
return false
}
}
return true
}
func removeNonAlphaNumeric(input string) string {
ret := ""
for _, c := range input {
if isAlphaNumeric(c) {
ret += string(c)
}
}
return ret
}
func ExtractPastaId(path string) (string, error) {
var id string
i := strings.LastIndex(path, "/")
if i < 0 {
id = path
} else {
id = path[i+1:]
}
if !containsOnlyAlphaNumeric(id) {
return "", fmt.Errorf("invalid id")
}
return id, nil
}
/* Load MIME types file. MIME types file is a simple text file that describes mime types based on file extenstions.
* The format of the file is
* EXTENSION = MIMETYPE
*/
func loadMimeTypes(filename string) (map[string]string, error) {
ret := make(map[string]string, 0)
file, err := os.OpenFile(filename, os.O_RDONLY, 0400)
if err != nil {
return ret, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || line[0] == '#' {
continue
}
i := strings.Index(line, "=")
if i < 0 {
continue
}
name, value := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:])
if name != "" && value != "" {
ret[name] = value
}
}
return ret, scanner.Err()
}
func takeFirst(arr []string) string {
if len(arr) == 0 {
return ""
}
return arr[0]
}
func SendPasta(pasta Pasta, w http.ResponseWriter) error {
file, err := bowl.GetPastaReader(pasta.Id)
if err != nil {
return err
}
defer file.Close()
w.Header().Set("Content-Length", strconv.FormatInt(pasta.Size, 10))
if pasta.Mime != "" {
w.Header().Set("Content-Type", pasta.Mime)
}
_, err = io.Copy(w, file)
return err
}
func deletePasta(id string, token string, w http.ResponseWriter) {
var pasta Pasta
var err error
if id == "" || token == "" {
goto Invalid
}
pasta, err = bowl.GetPasta(id)
if err != nil {
log.Fatalf("Error getting pasta %s: %s", pasta.Id, err)
goto ServerError
}
if pasta.Id == "" {
goto NotFound
}
if pasta.Token == token {
err = bowl.DeletePasta(pasta.Id)
if err != nil {
log.Fatalf("Error deleting pasta %s: %s", pasta.Id, err)
goto ServerError
}
fmt.Fprintf(w, "OK")
} else {
goto Invalid
}
return
NotFound:
w.WriteHeader(404)
fmt.Fprintf(w, "pasta not found")
return
Invalid:
w.WriteHeader(403)
fmt.Fprintf(w, "Invalid request")
return
ServerError:
w.WriteHeader(500)
fmt.Fprintf(w, "server error")
}
func receive(reader io.Reader, pasta *Pasta) error {
buf := make([]byte, 4096)
file, err := os.OpenFile(pasta.Filename, os.O_RDWR|os.O_APPEND, 0640)
if err != nil {
file.Close()
return err
}
defer file.Close()
pasta.Size = 0
for pasta.Size < cf.MaxPastaSize {
n, err := reader.Read(buf)
if (err == nil || err == io.EOF) && n > 0 {
if _, err = file.Write(buf[:n]); err != nil {
log.Fatalf("Write error while receiving bin: %s", err)
return err
}
pasta.Size += int64(n)
}
if err != nil {
if err == io.EOF {
return nil
}
log.Fatalf("Receive error while receiving bin: %s", err)
return err
}
}
return nil
}
/* try to determine the mime type by file extension. Returns empty string on failure */
func mimeByFilename(filename string) string {
i := strings.LastIndex(filename, ".")
if i < 0 {
return ""
}
extension := filename[i+1:]
if mime, ok := mimeExtensions[extension]; ok {
return mime
}
return ""
}
func receiveMultibody(r *http.Request, pasta *Pasta) (io.ReadCloser, error) {
err := r.ParseMultipartForm(cf.MaxPastaSize)
if err != nil {
return nil, err
}
file, header, err := r.FormFile("file")
if err != nil {
return nil, err
}
filename := header.Filename
// Determine MIME type based on file extension, if present
if filename != "" {
pasta.Mime = mimeByFilename(filename)
}
return file, err
}
/* Parse expire header value. Returns expire value or 0 on error or invalid settings */
func parseExpire(headerValue []string) int64 {
var ret int64
for _, value := range headerValue {
if expire, err := strconv.ParseInt(value, 10, 64); err == nil {
// No negative values allowed
if expire < 0 {
return 0
}
ret = time.Now().Unix() + int64(expire)
}
}
return ret
}
/* isMultipart returns true if the given request is multipart form */
func isMultipart(r *http.Request) bool {
contentType := r.Header.Get("Content-Type")
return contentType == "multipart/form-data" || strings.HasPrefix(contentType, "multipart/form-data;")
}
func ReceivePasta(r *http.Request) (Pasta, error) {
var err error
var reader io.ReadCloser
pasta := Pasta{Id: ""}
// Parse expire if given
if cf.DefaultExpire > 0 {
pasta.ExpireDate = time.Now().Unix() + cf.DefaultExpire
}
if expire := parseExpire(r.Header["Expire"]); expire > 0 {
pasta.ExpireDate = expire
// TODO: Add maximum expiration parameter
}
pasta.Id = removeNonAlphaNumeric(bowl.GenerateRandomBinId(cf.PastaCharacters))
// InsertPasta sets filename
if err = bowl.InsertPasta(&pasta); err != nil {
return pasta, err
}
if isMultipart(r) {
reader, err = receiveMultibody(r, &pasta)
if err != nil {
pasta.Id = ""
return pasta, err
}
} else {
// Check if the input is coming from the POST form
inputs := r.URL.Query()["input"]
if len(inputs) > 0 && inputs[0] == "form" {
// Copy reader, as r.FromValue consumes it's contents
defer r.Body.Close()
reader = r.Body
if content := r.FormValue("content"); content != "" {
reader = io.NopCloser(strings.NewReader(content))
} else {
pasta.Id = "" // Empty pasta
return pasta, nil
}
} else {
reader = r.Body
}
}
defer reader.Close()
if err := receive(reader, &pasta); err != nil {
return pasta, err
}
if pasta.Size >= cf.MaxPastaSize {
log.Println("Max size exceeded while receiving bin")
return pasta, errors.New("Bin size exceeded")
}
if pasta.Size == 0 {
bowl.DeletePasta(pasta.Id)
pasta.Id = ""
pasta.Filename = ""
pasta.Token = ""
pasta.ExpireDate = 0
return pasta, nil
}
return pasta, nil
}
var delays map[string]int64
var delayMutex sync.Mutex
/* Extract the remote IP address of the given remote
* The remote is expected to come from http.Request and contain the IP address plus the port */
func extractRemoteIP(remote string) string {
// Check if IPv6
i := strings.Index(remote, "[")
if i >= 0 {
j := strings.Index(remote, "]")
if j <= i {
return remote
}
return remote[i+1 : j]
}
i = strings.Index(remote, ":")
if i > 0 {
return remote[:i]
}
return remote
}
/* Delay a request for the given remote if required by spam protection */
func delayIfRequired(remote string) {
if cf.RequestDelay == 0 {
return
}
address := extractRemoteIP(remote)
now := time.Now().UnixNano() / 1000000 // Timestamp now in milliseconds. This should be fine until 2262
delayMutex.Lock()
delay, ok := delays[address]
delayMutex.Unlock()
if ok {
delta := cf.RequestDelay - (now - delay)
if delta > 0 {
time.Sleep(time.Duration(delta) * time.Millisecond)
}
}
delays[address] = time.Now().UnixNano() / 1000000 // Fresh timestamp
}
func handlerHead(w http.ResponseWriter, r *http.Request) {
var pasta Pasta
id, err := ExtractPastaId(r.URL.Path)
if err != nil {
goto BadRequest
}
if pasta, err := bowl.GetPasta(id); err != nil {
log.Fatalf("Error getting pasta %s: %s", pasta.Id, err)
goto ServerError
}
if pasta.Id == "" {
goto NotFound
}
w.Header().Set("Content-Length", strconv.FormatInt(pasta.Size, 10))
if pasta.Mime != "" {
w.Header().Set("Content-Type", pasta.Mime)
}
if pasta.ExpireDate > 0 {
w.Header().Set("Expires", time.Unix(pasta.ExpireDate, 0).Format("2006-01-02-15:04:05"))
}
w.WriteHeader(200)
fmt.Fprintf(w, "OK")
return
ServerError:
w.WriteHeader(500)
fmt.Fprintf(w, "server error")
return
NotFound:
w.WriteHeader(404)
fmt.Fprintf(w, "pasta not found")
return
BadRequest:
w.WriteHeader(400)
if err == nil {
fmt.Fprintf(w, "bad request")
} else {
fmt.Fprintf(w, "%s", err)
}
return
}
func handlerPost(w http.ResponseWriter, r *http.Request) {
delayIfRequired(r.RemoteAddr)
pasta, err := ReceivePasta(r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "server error")
log.Printf("Receive error: %s", err)
return
} else {
if pasta.Id == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("empty pasta"))
} else {
log.Printf("Received bin %s (%d bytes) from %s", pasta.Id, pasta.Size, r.RemoteAddr)
w.WriteHeader(http.StatusOK)
url := fmt.Sprintf("%s/%s", cf.BaseUrl, pasta.Id)
// Return format
retFormats := r.URL.Query()["ret"]
retFormat := ""
if len(retFormats) > 0 {
retFormat = retFormats[0]
}
if retFormat == "html" {
// Website as return format
fmt.Fprintf(w, "<!doctype html><html><head><title>pasta</title></head>\n")
fmt.Fprintf(w, "<body>\n")
fmt.Fprintf(w, "<h1>pasta</h1>\n")
deleteLink := fmt.Sprintf("%s/delete?id=%s&token=%s", cf.BaseUrl, pasta.Id, pasta.Token)
fmt.Fprintf(w, "<p>Here is your pasta: <a href=\"%s\">%s</a>.<br/>", url, url)
fmt.Fprintf(w, "<a href=\"%s\">Delete</a> it in case you don't want it anymore.</p>\n", deleteLink)
fmt.Fprintf(w, "<pre>")
if pasta.ExpireDate > 0 {
fmt.Fprintf(w, "Expiration: %s\n", time.Unix(pasta.ExpireDate, 0).Format("2006-01-02-15:04:05"))
}
fmt.Fprintf(w, "Modification token: %s\n</pre>\n", pasta.Token)
fmt.Fprintf(w, "<p>That was fun! Let's <a href=\"%s\">upload another one</a>.</p>\n", cf.BaseUrl)
fmt.Fprintf(w, "</body></html>")
} else if retFormat == "json" {
// Dont use json package, the reply is simple enough to build it on-the-fly
reply := fmt.Sprintf("{\"url\":\"%s\",\"token\":\"%s\", \"expire\":%d}", url, pasta.Token, pasta.ExpireDate)
w.Write([]byte(reply))
} else {
fmt.Fprintf(w, "url: %s\ntoken: %s\n", url, pasta.Token)
}
}
}
}
func handler(w http.ResponseWriter, r *http.Request) {
var err error
if r.Method == http.MethodGet {
// Check if bin ID is given
id, err := ExtractPastaId(r.URL.Path)
if err != nil {
goto BadRequest
}
if id == "" {
handlerIndex(w, r)
} else {
pasta, err := bowl.GetPasta(id)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Storage error")
log.Fatalf("Storage error: %s", err)
return
}
if pasta.Id == "" {
goto NoSuchPasta
} else {
// Delete expired pasta if present
if pasta.Expired() {
if err = bowl.DeletePasta(pasta.Id); err != nil {
log.Fatalf("Cannot deleted expired pasta %s: %s", pasta.Id, err)
}
goto NoSuchPasta
}
if err = SendPasta(pasta, w); err != nil {
log.Printf("Error sending pasta %s: %s", pasta.Id, err)
}
}
}
} else if r.Method == http.MethodPost || r.Method == http.MethodPut {
handlerPost(w, r)
} else if r.Method == http.MethodDelete {
delayIfRequired(r.RemoteAddr)
id, err := ExtractPastaId(r.URL.Path)
if err != nil {
goto BadRequest
}
token := takeFirst(r.URL.Query()["token"])
deletePasta(id, token, w)
} else if r.Method == http.MethodHead {
handlerHead(w, r)
} else {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Unsupported method"))
}
return
NoSuchPasta:
w.WriteHeader(404)
fmt.Fprintf(w, "No pasta\n\nSorry, there is no pasta for this link")
return
BadRequest:
w.WriteHeader(400)
if err == nil {
fmt.Fprintf(w, "bad request")
} else {
fmt.Fprintf(w, "%s", err)
}
return
}
func handlerHealth(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "OK")
}
func handlerRobots(w http.ResponseWriter, r *http.Request) {
// no robots allowed here
fmt.Fprintf(w, "User-agent: *\nDisallow: /\n")
}
// Delete pasta
func handlerDelete(w http.ResponseWriter, r *http.Request) {
delayIfRequired(r.RemoteAddr)
id := takeFirst(r.URL.Query()["id"])
token := takeFirst(r.URL.Query()["token"])
deletePasta(id, token, w)
}
func timeHumanReadable(timestamp int64) string {
if timestamp < 60 {
return fmt.Sprintf("%d s", timestamp)
}
minutes := timestamp / 60
seconds := timestamp - (minutes * 60)
if minutes < 60 {
return fmt.Sprintf("%d:%d min", minutes, seconds)
}
hours := minutes / 60
minutes -= hours * 60
if hours < 24 {
return fmt.Sprintf("%d s", hours)
}
days := hours / 24
hours -= days * 24
if days > 365 {
years := float32(days) / 365.0
return fmt.Sprintf("%.2f years", years)
} else if days > 28 {
weeks := days / 7
if weeks > 4 {
months := days / 30
return fmt.Sprintf("%d months", months)
}
return fmt.Sprintf("%d weeks", weeks)
} else {
return fmt.Sprintf("%d days", days)
}
}
func handlerIndex(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<!doctype html><html><head><title>pasta</title></head>\n")
fmt.Fprintf(w, "<body>\n")
fmt.Fprintf(w, "<h1>pasta</h1>\n")
fmt.Fprintf(w, "<p>Stupid simple paste service written in <code>go</code><br/>\n")
fmt.Fprintf(w, "Checkout our fresh CLI utilities in <a href=\"https://github.com/grisu48/pasta/releases/\" target=\"_BLANK\">releases</a> because you are amazing!</p>\n")
fmt.Fprintf(w, "<h2>Post a new and fresh pasta</h2>\n")
fmt.Fprintf(w, "<p>Just POST your file at the server, e.g. ")
fmt.Fprintf(w, "<code>curl -X POST '%s' --data-binary @FILE</code></p>\n", cf.BaseUrl)
if cf.DefaultExpire > 0 {
fmt.Fprintf(w, "<p>pastas expire by default after %s - Enjoy them while they are fresh!</p>\n", timeHumanReadable(cf.DefaultExpire))
}
fmt.Fprintf(w, "<h3>File upload</h3>")
fmt.Fprintf(w, "<p>Upload your file and make a fresh pasta out of it:</p>")
fmt.Fprintf(w, "<form enctype=\"multipart/form-data\" method=\"post\" action=\"/?ret=html\">\n")
fmt.Fprintf(w, "<input type=\"file\" name=\"file\">\n")
fmt.Fprintf(w, "<input type=\"submit\" value=\"Upload\">\n")
fmt.Fprintf(w, "</form>\n")
fmt.Fprintf(w, "<h3>Text paste</h3>")
fmt.Fprintf(w, "<p>Just paste your contents in the textfield and hit the <tt>pasta</tt> button below</p>\n")
fmt.Fprintf(w, "<form method=\"post\" action=\"/?input=form&ret=html\">\n")
if cf.MaxPastaSize > 0 {
fmt.Fprintf(w, "<textarea name=\"content\" rows=\"10\" cols=\"80\" maxlength=\"%d\"></textarea><br/>\n", cf.MaxPastaSize)
} else {
fmt.Fprintf(w, "<textarea name=\"content\" rows=\"10\" cols=\"80\"></textarea><br/>\n")
}
fmt.Fprintf(w, "<input type=\"submit\" value=\"Pasta!\">\n")
fmt.Fprintf(w, "</form>\n")
fmt.Fprintf(w, "<p>project page: <a href=\"https://github.com/grisu48/pasta\" target=\"_BLANK\">github.com/grisu48/pasta</a></p>\n")
fmt.Fprintf(w, "</body></html>")
}
func cleanupThread() {
// Double check this, because I know that I will screw this up at some point in the main routine :-)
if cf.CleanupInterval == 0 {
return
}
for {
duration := time.Now().Unix()
if err := bowl.RemoveExpired(); err != nil {
log.Fatalf("Error while removing expired pastas: %s", err)
}
if cf.RequestDelay > 0 { // Cleanup of the spam protection addresses only if enabled
delayMutex.Lock()
delays = make(map[string]int64)
delayMutex.Unlock()
}
duration = time.Now().Unix() - duration + int64(cf.CleanupInterval)
if duration > 0 {
time.Sleep(time.Duration(cf.CleanupInterval) * time.Second)
} else {
// Don't spam the system, give it at least some time
time.Sleep(time.Second)
}
}
}
func main() {
cf.SetDefaults()
cf.ReadEnv()
delays = make(map[string]int64)
// Parse program arguments for config
parseCf := ParserConfig{}
parser := argparse.NewParser("pastad", "pasta server")
parseCf.ConfigFile = parser.String("c", "config", &argparse.Options{Default: "", Help: "Set config file"})
parseCf.BaseURL = parser.String("B", "baseurl", &argparse.Options{Help: "Set base URL for instance"})
parseCf.PastaDir = parser.String("d", "dir", &argparse.Options{Help: "Set pasta data directory"})
parseCf.BindAddr = parser.String("b", "bind", &argparse.Options{Help: "Address to bind server to"})
parseCf.MaxPastaSize = parser.Int("s", "size", &argparse.Options{Help: "Maximum allowed size for a pasta"})
parseCf.PastaCharacters = parser.Int("n", "chars", &argparse.Options{Help: "Random characters for new pastas"})
parseCf.MimeTypesFile = parser.String("m", "mime", &argparse.Options{Help: "Define mime types file"})
parseCf.DefaultExpire = parser.Int("e", "expire", &argparse.Options{Help: "Pasta expire in seconds"})
parseCf.CleanupInterval = parser.Int("C", "cleanup", &argparse.Options{Help: "Cleanup interval in seconds"})
if err := parser.Parse(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", parser.Usage(err))
os.Exit(1)
}
log.Println("Starting pasta server ... ")
configFile := *parseCf.ConfigFile
if configFile != "" {
if FileExists(configFile) {
if _, err := toml.DecodeFile(configFile, &cf); err != nil {
fmt.Printf("Error loading configuration file: %s\n", err)
os.Exit(1)
}
} else {
if err := CreateDefaultConfigfile(configFile); err == nil {
fmt.Fprintf(os.Stderr, "Created default config file '%s'\n", configFile)
} else {
fmt.Fprintf(os.Stderr, "Warning: Cannot create default config file '%s': %s\n", configFile, err)
}
}
}
// Program arguments overwrite config file
parseCf.ApplyTo(&cf)
// Sanity check
if cf.PastaCharacters <= 0 {
log.Println("Setting pasta characters to default 8 because it was <= 0")
cf.PastaCharacters = 8
}
if cf.PastaCharacters < 8 {
log.Println("Warning: Using less than 8 pasta characters might not be side-effects free")
}
if cf.PastaDir == "" {
cf.PastaDir = "."
}
bowl.Directory = cf.PastaDir
os.Mkdir(bowl.Directory, os.ModePerm)
// Load MIME types file
if cf.MimeTypesFile == "" {
mimeExtensions = make(map[string]string, 0)
} else {
var err error
mimeExtensions, err = loadMimeTypes(cf.MimeTypesFile)
if err != nil {
log.Printf("Warning: Cannot load mime types file '%s': %s", cf.MimeTypesFile, err)
} else {
log.Printf("Loaded %d mime types", len(mimeExtensions))
}
}
// Start cleanup thread
if cf.CleanupInterval > 0 {
go cleanupThread()
}
// Setup webserver
http.HandleFunc("/", handler)
http.HandleFunc("/health", handlerHealth)
http.HandleFunc("/delete", handlerDelete)
http.HandleFunc("/robots.txt", handlerRobots)
log.Printf("Serving http://%s", cf.BindAddr)
log.Fatal(http.ListenAndServe(cf.BindAddr, nil))
}