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