Added basic pages
This commit is contained in:
		
							
								
								
									
										
											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 (
 | 
			
		||||
	"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())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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])
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user