5 changed files with 185 additions and 2 deletions
-
BINassets/favicon.ico
-
24assets/index.html
-
17assets/qurl.css
-
42main.go
-
104static/static.go
@ -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> |
@ -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; } |
||||
|
} |
@ -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]) |
||||
|
} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue