mirror of
https://github.com/taigrr/shorturl
synced 2025-01-18 04:03:16 -08:00
Initial Commit
This commit is contained in:
commit
3380bc5e65
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
FROM golang:alpine
|
||||||
|
|
||||||
|
EXPOSE 8000/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["shorturl"]
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --update git && \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
RUN mkdir -p /go/src/shorturl
|
||||||
|
WORKDIR /go/src/shorturl
|
||||||
|
|
||||||
|
COPY . /go/src/shorturl
|
||||||
|
|
||||||
|
RUN go get -v -d
|
||||||
|
RUN go install -v
|
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
Copyright (C) 2017 James Mills
|
||||||
|
|
||||||
|
httpfs is covered by the MIT license::
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person
|
||||||
|
obtaining a copy of this software and associated documentation
|
||||||
|
files (the "Software"), to deal in the Software without restriction,
|
||||||
|
including without limitation the rights to use, copy, modify, merge,
|
||||||
|
publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
13
Makefile
Normal file
13
Makefile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.PHONY: dev build clean
|
||||||
|
|
||||||
|
all: dev
|
||||||
|
|
||||||
|
dev: build
|
||||||
|
./shorturl -bind 127.0.0.1:8000
|
||||||
|
|
||||||
|
build: clean
|
||||||
|
go get ./...
|
||||||
|
go build -o ./shorturl .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf bin shorturl
|
49
README.md
Normal file
49
README.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# shorturl
|
||||||
|
[](https://travis-ci.org/prologic/shorturl)
|
||||||
|
[](https://godoc.org/github.com/prologic/shorturl)
|
||||||
|
[](https://github.com/prologic/shorturl/wiki)
|
||||||
|
[](https://goreportcard.com/report/github.com/prologic/shorturl)
|
||||||
|
[](https://coveralls.io/r/prologic/shorturl)
|
||||||
|
|
||||||
|
shorturl is a web app that allows you to create smart bookmarks, commands and aliases by pointing your web browser's default search engine at a running instance. Similar to bunny1 or yubnub.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Source
|
||||||
|
|
||||||
|
```#!bash
|
||||||
|
$ go install github.com/prologic/shorturl/...
|
||||||
|
```
|
||||||
|
|
||||||
|
### OS X Homebrew
|
||||||
|
|
||||||
|
There is a formula provided that you can tap and install from
|
||||||
|
[prologic/homebrew-shorturl](https://github.com/prologic/homebrew-shorturl):
|
||||||
|
|
||||||
|
```#!bash
|
||||||
|
$ brew tap prologic/shorturl
|
||||||
|
$ brew install shorturl
|
||||||
|
```
|
||||||
|
|
||||||
|
**NB:** This installs the latest released binary; so if you want a more
|
||||||
|
recent unreleased version from master you'll have to clone the repository
|
||||||
|
and build yourself.
|
||||||
|
|
||||||
|
shorturl is still early days so contributions, ideas and expertise are
|
||||||
|
much appreciated and highly welcome!
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Run shorturl:
|
||||||
|
|
||||||
|
```#!bash
|
||||||
|
$ shorturl -bind 127.0.0.1:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Set your browser's default shorturl engine to http://localhost:8000/?q=%s
|
||||||
|
|
||||||
|
Then type `help` to view the main help page, `g foo bar` to perform a [Google](https://google.com) search for "foo bar" or `list` to list all available commands.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
6
config.go
Normal file
6
config.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Config ...
|
||||||
|
type Config struct {
|
||||||
|
FQDN string
|
||||||
|
}
|
21
config_test.go
Normal file
21
config_test.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestZeroConfig(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
cfg := Config{}
|
||||||
|
assert.Equal(cfg.FQDN, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
cfg := Config{FQDN: "bar.com"}
|
||||||
|
assert.Equal(cfg.FQDN, "bar.com")
|
||||||
|
}
|
40
main.go
Normal file
40
main.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
"github.com/namsral/flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *bolt.DB
|
||||||
|
cfg Config
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
config string
|
||||||
|
dbpath string
|
||||||
|
bind string
|
||||||
|
fqdn string
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.StringVar(&config, "config", "", "config file")
|
||||||
|
flag.StringVar(&dbpath, "dbpth", "urls.db", "Database path")
|
||||||
|
flag.StringVar(&bind, "bind", "0.0.0.0:8000", "[int]:<port> to bind to")
|
||||||
|
flag.StringVar(&fqdn, "fqdn", "localhost", "FQDN for public access")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// TODO: Abstract the Config and Handlers better
|
||||||
|
cfg.FQDN = fqdn
|
||||||
|
|
||||||
|
var err error
|
||||||
|
db, err = bolt.Open(dbpath, 0600, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
NewServer(bind, cfg).ListenAndServe()
|
||||||
|
}
|
43
scripts/release.sh
Executable file
43
scripts/release.sh
Executable file
@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo -n "Version to tag: "
|
||||||
|
read TAG
|
||||||
|
|
||||||
|
echo -n "Name of release: "
|
||||||
|
read NAME
|
||||||
|
|
||||||
|
echo -n "Desc of release: "
|
||||||
|
read DESC
|
||||||
|
|
||||||
|
git tag ${TAG}
|
||||||
|
git push --tags
|
||||||
|
|
||||||
|
if [ ! -d ./bin ]; then
|
||||||
|
mkdir bin
|
||||||
|
else
|
||||||
|
rm -rf ./bin/*
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -n "Building binaries ... "
|
||||||
|
|
||||||
|
rice embed-go
|
||||||
|
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o ./bin/shorturl-Linux-x86_64 .
|
||||||
|
GOOS=linux GOARCH=arm64 go build -o ./bin/shorturl-Linux-x86_64 .
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -o ./bin/shorturl-Darwin-x86_64 .
|
||||||
|
GOOS=windows GOARCH=amd64 go build -o ./bin/shorturl-Windows-x86_64.exe .
|
||||||
|
|
||||||
|
echo "DONE"
|
||||||
|
|
||||||
|
echo -n "Uploading binaries ... "
|
||||||
|
|
||||||
|
github-release release \
|
||||||
|
-u prologic -p -r shorturl \
|
||||||
|
-t ${TAG} -n "${NAME}" -d "${DESC}"
|
||||||
|
|
||||||
|
for file in bin/*; do
|
||||||
|
name="$(echo $file | sed -e 's|bin/||g')"
|
||||||
|
github-release upload -u prologic -r shorturl -t ${TAG} -n $name -f $file
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "DONE"
|
101
server.go
Normal file
101
server.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/GeertJohan/go.rice"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"go.iondynamics.net/templice"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server ...
|
||||||
|
type Server struct {
|
||||||
|
bind string
|
||||||
|
config Config
|
||||||
|
templates *templice.Template
|
||||||
|
router *httprouter.Router
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) render(w http.ResponseWriter, tmpl string, data interface{}) {
|
||||||
|
err := s.templates.ExecuteTemplate(w, tmpl+".html", data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexHandler ...
|
||||||
|
func (s *Server) IndexHandler() httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
s.render(w, "index", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortenHandler ...
|
||||||
|
func (s *Server) ShortenHandler() httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Internal Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
url := r.Form.Get("url")
|
||||||
|
u, err := NewURL(url)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Internal Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.render(w, "url", struct{ ID string }{ID: u.ID()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedirectHandler ...
|
||||||
|
func (s *Server) RedirectHandler() httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
id := p.ByName("id")
|
||||||
|
if id == "" {
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u, ok := LookupURL(id)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, u.url, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAndServe ...
|
||||||
|
func (s *Server) ListenAndServe() {
|
||||||
|
log.Fatal(http.ListenAndServe(s.bind, s.router))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) initRoutes() {
|
||||||
|
s.router.GET("/", s.IndexHandler())
|
||||||
|
s.router.POST("/", s.ShortenHandler())
|
||||||
|
s.router.GET("/:id", s.RedirectHandler())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer ...
|
||||||
|
func NewServer(bind string, config Config) *Server {
|
||||||
|
server := &Server{
|
||||||
|
bind: bind,
|
||||||
|
config: config,
|
||||||
|
router: httprouter.New(),
|
||||||
|
templates: templice.New(rice.MustFindBox("templates")),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := server.templates.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("error loading templates: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.initRoutes()
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
14
templates/index.html
Normal file
14
templates/index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>shorturl</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<p>Enter your long URL here:</p>
|
||||||
|
<form method="POST" action="">
|
||||||
|
<input type="text" name="url">
|
||||||
|
<input type="submit">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
10
templates/url.html
Normal file
10
templates/url.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>shorturl</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<p>Your short url is: <a href="/{{.ID}}">{{.ID}}</a></p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
96
urls.go
Normal file
96
urls.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// URL ...
|
||||||
|
type URL struct {
|
||||||
|
id string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID ...
|
||||||
|
func (u URL) ID() string {
|
||||||
|
return u.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL ...
|
||||||
|
func (u URL) URL() string {
|
||||||
|
return u.url
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save ...
|
||||||
|
func (u URL) Save() error {
|
||||||
|
log.Printf("u: %v", u)
|
||||||
|
if u.id == "" || u.url == "" {
|
||||||
|
log.Printf("u is nil :/")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.Update(func(tx *bolt.Tx) error {
|
||||||
|
b, err := tx.CreateBucketIfNotExists([]byte("urls"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("create bucket failed: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = b.Put([]byte(u.id), []byte(u.url))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("put key failed: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupURL ...
|
||||||
|
func LookupURL(id string) (u URL, ok bool) {
|
||||||
|
err := db.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("urls"))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v := b.Get([]byte(id))
|
||||||
|
if v != nil {
|
||||||
|
u = URL{id: id, url: string(v)}
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error looking up url for %s: %s", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewURL ...
|
||||||
|
func NewURL(url string) (u URL, err error) {
|
||||||
|
log.Printf("NewURL: %v", url)
|
||||||
|
var id string
|
||||||
|
|
||||||
|
for {
|
||||||
|
// TODO: Make length (5) configurable
|
||||||
|
id = RandomString(5)
|
||||||
|
_, ok := LookupURL(id)
|
||||||
|
if ok {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u = URL{id: id, url: url}
|
||||||
|
err = u.Save()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
31
urls_test.go
Normal file
31
urls_test.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestZeroURL(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
u := URL{}
|
||||||
|
assert.Equal(u.ID(), "")
|
||||||
|
assert.Equal(u.URL(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURLSaveLookup(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
db, _ = bolt.Open("test.db", 0600, nil)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
URL{id: "asdf", url: "https://localhost"}.Save()
|
||||||
|
|
||||||
|
u, ok := LookupURL("asdf")
|
||||||
|
assert.True(ok)
|
||||||
|
assert.Equal(u.id, "asdf")
|
||||||
|
assert.Equal(u.url, "https://localhost")
|
||||||
|
}
|
19
utils.go
Normal file
19
utils.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomString ...
|
||||||
|
func RandomString(length int) string {
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
rand.Read(bytes)
|
||||||
|
for i, b := range bytes {
|
||||||
|
bytes[i] = alphabet[b%byte(len(alphabet))]
|
||||||
|
}
|
||||||
|
return string(bytes)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user