Added basic pages
This commit is contained in:
parent
208e13d2a8
commit
412c625916
BIN
assets/favicon.ico
Normal file
BIN
assets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 318 B |
24
assets/index.html
Normal file
24
assets/index.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>qurl.org</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<link rel="stylesheet" media="screen" href="qurl.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="c">
|
||||||
|
qurl.org is a simple url shortening service, in the same vein as
|
||||||
|
<a href="https://bit.ly">bit.ly</a>, and
|
||||||
|
<a href="https://tinyurl.com">tinyurl.com</a>.
|
||||||
|
qurl.org is <a href="http://binarythought.com/qurl/LICENSE">open source</a>,
|
||||||
|
it's code is <a href="https://binarythought.com/fossils/qurl/">freely available</a>
|
||||||
|
and has an <a href="api/index.html">easy to use API</a>.
|
||||||
|
|
||||||
|
<form method="post" action="submit.html">
|
||||||
|
<input type="text" id="u" name="url" placeholder="https://qurl.org" />
|
||||||
|
<input type="submit" id="s" value="shorten" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
17
assets/qurl.css
Normal file
17
assets/qurl.css
Normal file
@ -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; }
|
||||||
|
}
|
42
main.go
42
main.go
@ -3,27 +3,65 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"qurl/storage"
|
"qurl/storage"
|
||||||
|
"qurl/static"
|
||||||
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:generate bindata -m Assets -r assets -p static -o static/assets.go assets
|
||||||
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
dburl := flag.String("u", "bolt:./qurl.db", "url to database")
|
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")
|
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()
|
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)
|
stor, err := storage.NewStorage(*dburl)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
defer stor.Shutdown()
|
defer stor.Shutdown()
|
||||||
|
|
||||||
|
// Load data if asked
|
||||||
if *jsonfile != "" {
|
if *jsonfile != "" {
|
||||||
err := loadjson(stor, *jsonfile)
|
err := loadjson(stor, *jsonfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Load error: %s", err.Error())
|
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||||
return
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
104
static/static.go
Normal file
104
static/static.go
Normal file
@ -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])
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user