diff --git a/assets/favicon.ico b/assets/favicon.ico
new file mode 100644
index 0000000..7ab6346
Binary files /dev/null and b/assets/favicon.ico differ
diff --git a/assets/index.html b/assets/index.html
new file mode 100644
index 0000000..9217d6c
--- /dev/null
+++ b/assets/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+ qurl.org
+
+
+
+
+
+
+
diff --git a/assets/qurl.css b/assets/qurl.css
new file mode 100644
index 0000000..a94463b
--- /dev/null
+++ b/assets/qurl.css
@@ -0,0 +1,17 @@
+* { box-sizing: border-box; font-family: Tahoma, Arial, san-serif; font-size: 16px; }
+body { background-color: #fff; padding: 0; margin: 32px 16px; }
+form { margin-top: 16px; white-space: nowrap; }
+a { color: #3b8bba; text-decoration: none; }
+a:hover { text-decoration: underline; }
+input:focus { outline: 0; }
+#c { border: 1px solid #eee; background-color: #fafafa; color: #757575; margin: auto; border-radius: 3px; text-align: justify; padding: 8px 16px; }
+#u, #s { border: 1px solid #ccc; display: block; padding: 4px 8px; border-radius: 2px; margin: auto; }
+#u { width: 100%; box-shadow: inset 0 1px 3px 0 #ddd; }
+#u:focus { border-color: #129fea; }
+#s { margin-top: 8px; color: #fff; background-color: #0078e7; border-color: transparent; }
+#s:hover { background-image: linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1)); cursor: hover; }
+@media screen and (min-width: 640px) {
+ #c { max-width: 600px; }
+ #u, #s { display: inline-block; margin: 0; }
+ #u { width: 490px; display: inline-block; }
+}
diff --git a/main.go b/main.go
index c86b99a..7299ecc 100644
--- a/main.go
+++ b/main.go
@@ -3,27 +3,65 @@ package main
import (
"flag"
"fmt"
+ "net"
+ "net/http"
"os"
"qurl/storage"
+ "qurl/static"
+ "runtime"
)
+//go:generate bindata -m Assets -r assets -p static -o static/assets.go assets
+
+
func main() {
dburl := flag.String("u", "bolt:./qurl.db", "url to database")
+ lsaddr := flag.String("l", "127.0.0.1:8080", "listen address/port")
jsonfile := flag.String("j", "", "path to json to load into database")
+ maxpro := flag.Int("m", runtime.NumCPU()+2,
+ "maximum number of threads to use")
flag.Parse()
+ if *maxpro < 3 {
+ fmt.Fprintf(os.Stderr, "Thread limit too low: %d (min 3)\n", *maxpro)
+ return
+ }
+
+ // Limit max processes
+ runtime.GOMAXPROCS(*maxpro)
+
+ // Open storage backend
stor, err := storage.NewStorage(*dburl)
if err != nil {
- fmt.Fprintf(os.Stderr, "Database connection error: %s", err.Error())
+ fmt.Fprintf(os.Stderr, "Database connection error: %s\n", err.Error())
return
}
defer stor.Shutdown()
+ // Load data if asked
if *jsonfile != "" {
err := loadjson(stor, *jsonfile)
if err != nil {
- fmt.Fprintf(os.Stderr, "Load error: %s", err.Error())
+ fmt.Fprintf(os.Stderr, "%s\n", err.Error())
return
}
}
+
+ // Open listener port
+ listen, err := net.Listen("tcp", *lsaddr)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Listen error: %s\n", err.Error())
+ return
+ }
+
+ mux := http.NewServeMux()
+ mux.Handle("/index.html", &static.StaticContent{Content: "index.html"})
+ mux.Handle("/favicon.ico", &static.StaticContent{Content: "favicon.ico"})
+ mux.Handle("/qurl.css", &static.StaticContent{Content: "qurl.css"})
+
+ fmt.Fprintf(os.Stdout, "qurl listening .. \n")
+ err = http.Serve(listen, mux)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Serve error: %s\n", err.Error())
+ }
}
diff --git a/static/static.go b/static/static.go
new file mode 100644
index 0000000..cc0d326
--- /dev/null
+++ b/static/static.go
@@ -0,0 +1,104 @@
+package static
+
+import (
+ "bytes"
+ "compress/gzip"
+ "crypto/md5"
+ "fmt"
+ "mime"
+ "path"
+ "net/http"
+ "strings"
+)
+
+type StaticContent struct {
+ ContentType string
+ Content string
+ ETag string
+ GZIPContent []byte
+ GZIPETag string
+ DisableGZIP bool
+}
+
+func (ctx *StaticContent) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // By default, don't use gzip
+ useGZIP := false
+
+ // If gzip isn't explicitly disabled, test for it
+ if !ctx.DisableGZIP {
+ useGZIP = strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
+ }
+
+ // If gzip is enabled, and there's no gzip etag,
+ // generate the gzip'd content plus the etag
+ if useGZIP && len(ctx.GZIPETag) == 0 {
+ var buf bytes.Buffer
+
+ gz, _ := gzip.NewWriterLevel(&buf, gzip.BestCompression)
+ defer gz.Close()
+
+ if _, err := gz.Write(Assets[ctx.Content]); err != nil {
+ http.Error(w, fmt.Sprintf("GZIP write error: %s", err.Error()),
+ http.StatusInternalServerError)
+ return
+ }
+
+ if err := gz.Flush(); err != nil {
+ http.Error(w, fmt.Sprintf("GZIP flush error: %s", err.Error()),
+ http.StatusInternalServerError)
+ return
+ }
+
+ // Check if GZIP actually resulted in a smaller file
+ if buf.Len() < len(Assets[ctx.Content]) {
+ ctx.GZIPContent = buf.Bytes()
+ ctx.GZIPETag = fmt.Sprintf("%x", md5.Sum(Assets[ctx.Content]))
+ } else {
+ // If gzip turns out to be ineffective, disable it
+ ctx.DisableGZIP = true
+ useGZIP = false
+ }
+ }
+
+ var localETag string
+ if useGZIP {
+ localETag = ctx.GZIPETag
+ } else {
+ // Generate an ETag for content if necessary
+ if ctx.ETag == "" {
+ ctx.ETag = fmt.Sprintf("%x", md5.Sum(Assets[ctx.Content]))
+ }
+ localETag = ctx.ETag
+ }
+
+ // Check the etag, maybe we don't need to send content
+ remoteETag := r.Header.Get("If-None-Match")
+ if localETag == remoteETag {
+ w.WriteHeader(http.StatusNotModified)
+ return
+ }
+ w.Header().Set("ETag", localETag)
+
+ // Check the content type, if we don't already
+ // have one, make one
+ if ctx.ContentType == "" {
+ ext := path.Ext(ctx.Content)
+ ctx.ContentType = mime.TypeByExtension(ext)
+ // Fallback to default mime type
+ if ctx.ContentType == "" {
+ ctx.ContentType = "application/octet-stream"
+ }
+ }
+
+ w.Header().Set("Content-Type", ctx.ContentType)
+
+ // Finally, write our content
+ if useGZIP {
+ w.Header().Set("Content-Encoding", "gzip")
+ w.Header().Set("Content-Length", fmt.Sprintf("%d", len(ctx.GZIPContent)))
+ w.Write(ctx.GZIPContent)
+ } else {
+ w.Header().Set("Content-Length", fmt.Sprintf("%d", len(Assets[ctx.Content])))
+ w.Write(Assets[ctx.Content])
+ }
+}