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 + + + + +
+ qurl.org is a simple url shortening service, in the same vein as + bit.ly, and + tinyurl.com. + qurl.org is open source, + it's code is freely available + and has an easy to use API. + +
+ + +
+
+ + 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]) + } +}