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