mirror of
https://github.com/kataras/iris.git
synced 2025-03-14 08:16:28 +01:00
URL Shortener using Iris and BoltDB example
Former-commit-id: 059f5fcd6c1df8df11ed2594ee3c1b1ccc13c7b0
This commit is contained in:
parent
ac3abca684
commit
549c8eba10
|
@ -27,7 +27,7 @@ Developers should read the official [documentation](https://godoc.org/gopkg.in/k
|
||||||
* [Websockets](examples/websockets/main.go)
|
* [Websockets](examples/websockets/main.go)
|
||||||
* [Markdown and Cache](examples/cache-markdown/main.go)
|
* [Markdown and Cache](examples/cache-markdown/main.go)
|
||||||
* [Online Visitors](examples/online-visitors/main.go)
|
* [Online Visitors](examples/online-visitors/main.go)
|
||||||
* [URL Shortener](examples/url-shortener/main.go)
|
* [URL Shortener using BoltDB](examples/url-shortener/main.go)
|
||||||
|
|
||||||
|
|
||||||
> Take look at the [community examples](https://github.com/iris-contrib/examples) too!
|
> Take look at the [community examples](https://github.com/iris-contrib/examples) too!
|
|
@ -1,59 +1,81 @@
|
||||||
|
// Package main shows how you can create a simple URL SHortener using only Iris and BoltDB.
|
||||||
|
//
|
||||||
|
// $ go get github.com/boltdb/bolt/...
|
||||||
|
// $ go run main.go
|
||||||
|
// $ start http://localhost:8080
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"html/template"
|
"html/template"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
"gopkg.in/kataras/iris.v6"
|
"gopkg.in/kataras/iris.v6"
|
||||||
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
|
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
|
||||||
"gopkg.in/kataras/iris.v6/adaptors/view"
|
"gopkg.in/kataras/iris.v6/adaptors/view"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// a custom Iris event policy, which will run when server interruped (i.e control+C)
|
||||||
|
// receives a func() error, most of packages are compatible with that on their Close/Shutdown/Cancel funcs.
|
||||||
|
func releaser(r func() error) iris.EventPolicy {
|
||||||
|
return iris.EventPolicy{
|
||||||
|
Interrupted: func(app *iris.Framework) {
|
||||||
|
if err := r(); err != nil {
|
||||||
|
app.Log(iris.ProdMode, "error while releasing resources: "+err.Error())
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
|
||||||
|
// assign a variable to the DB so we can use its features later
|
||||||
|
db := NewDB("shortener.db")
|
||||||
|
factory := NewFactory(DefaultGenerator, db)
|
||||||
|
|
||||||
app.Adapt(
|
app.Adapt(
|
||||||
|
// print all kind of errors and logs at os.Stdout
|
||||||
iris.DevLogger(),
|
iris.DevLogger(),
|
||||||
|
// use the httprouter, you can use adpaotrs/gorillamux if you want
|
||||||
httprouter.New(),
|
httprouter.New(),
|
||||||
|
// serve the "./templates" directory's "*.html" files with the HTML std view engine.
|
||||||
view.HTML("./templates", ".html").Reload(true),
|
view.HTML("./templates", ".html").Reload(true),
|
||||||
|
// `db.Close` is a `func() error` so it can be a `releaser` too.
|
||||||
|
// Wrap the db.Close with the releaser in order to be released when app exits or control+C
|
||||||
|
// You probably never saw that before, clever pattern which I am able to use only with Iris :)
|
||||||
|
releaser(db.Close),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// template funcs
|
||||||
|
//
|
||||||
|
// look ./templates/index.html#L16
|
||||||
|
app.Adapt(iris.TemplateFuncsPolicy{"isPositive": func(n int) bool {
|
||||||
|
if n > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}})
|
||||||
|
|
||||||
// Serve static files (css)
|
// Serve static files (css)
|
||||||
app.StaticWeb("/static", "./static_files")
|
app.StaticWeb("/static", "./resources")
|
||||||
|
|
||||||
// other solution for both static files and urls in the root path(not recommended but it's here if you ever need it):
|
|
||||||
// fileserver := iris.StaticHandler("./static_files", false, false)
|
|
||||||
// app.Get("/*path", func(ctx *iris.Context) {
|
|
||||||
// fileserver.Serve(ctx)
|
|
||||||
// if ctx.StatusCode() >= 200 && ctx.StatusCode() < 300 {
|
|
||||||
// // then the file found and served correctly.
|
|
||||||
// } else {
|
|
||||||
// // otherwise check for urls....
|
|
||||||
// execShortURL(ctx, ctx.Param("path"))
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
var mu sync.Mutex
|
|
||||||
var urls = map[string]string{
|
|
||||||
"iris": "http://support.iris-go.com",
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Get("/", func(ctx *iris.Context) {
|
app.Get("/", func(ctx *iris.Context) {
|
||||||
ctx.Render("index.html", iris.Map{"url_count": len(urls)})
|
ctx.MustRender("index.html", iris.Map{"url_count": db.Len()})
|
||||||
})
|
})
|
||||||
|
|
||||||
// find and execute a short url by its key
|
// find and execute a short url by its key
|
||||||
// used on http://localhost:8080/url/dsaoj41u321dsa
|
// used on http://localhost:8080/u/dsaoj41u321dsa
|
||||||
execShortURL := func(ctx *iris.Context, key string) {
|
execShortURL := func(ctx *iris.Context, key string) {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
ctx.EmitError(iris.StatusBadRequest)
|
ctx.EmitError(iris.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
value, found := urls[key]
|
value := db.Get(key)
|
||||||
if !found {
|
if value == "" {
|
||||||
ctx.SetStatusCode(iris.StatusNotFound)
|
ctx.SetStatusCode(iris.StatusNotFound)
|
||||||
ctx.Writef("Short URL for key: '%s' not found", key)
|
ctx.Writef("Short URL for key: '%s' not found", key)
|
||||||
return
|
return
|
||||||
|
@ -61,50 +83,32 @@ func main() {
|
||||||
|
|
||||||
ctx.Redirect(value, iris.StatusTemporaryRedirect)
|
ctx.Redirect(value, iris.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
app.Get("/url/:shortkey", func(ctx *iris.Context) {
|
app.Get("/u/:shortkey", func(ctx *iris.Context) {
|
||||||
execShortURL(ctx, ctx.Param("shortkey"))
|
execShortURL(ctx, ctx.Param("shortkey"))
|
||||||
})
|
})
|
||||||
|
|
||||||
// for wildcard subdomain (yeah.. cool) http://dsaoj41u321dsa.localhost:8080
|
app.Post("/shorten", func(ctx *iris.Context) {
|
||||||
// Note:
|
|
||||||
// if you want subdomains (chrome doesn't works on localhost, so you have to define other hostname on app.Listen)
|
|
||||||
// app.Party("*.", func(ctx *iris.Context) {
|
|
||||||
// execShortURL(ctx, ctx.Subdomain())
|
|
||||||
// })
|
|
||||||
|
|
||||||
app.Post("/url/shorten", func(ctx *iris.Context) {
|
|
||||||
data := make(map[string]interface{}, 0)
|
data := make(map[string]interface{}, 0)
|
||||||
data["url_count"] = len(urls)
|
formValue := ctx.FormValue("url")
|
||||||
value := ctx.FormValue("url")
|
if formValue == "" {
|
||||||
if value == "" {
|
|
||||||
data["form_result"] = "You need to a enter a URL."
|
data["form_result"] = "You need to a enter a URL."
|
||||||
} else {
|
} else {
|
||||||
urlValue, err := url.ParseRequestURI(value)
|
key, err := factory.Gen(formValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// ctx.JSON(iris.StatusInternalServerError,
|
|
||||||
// iris.Map{"status": iris.StatusInternalServerError,
|
|
||||||
// "error": err.Error(),
|
|
||||||
// "reason": "Invalid URL",
|
|
||||||
// })
|
|
||||||
data["form_result"] = "Invalid URL."
|
data["form_result"] = "Invalid URL."
|
||||||
} else {
|
} else {
|
||||||
key := randomString(12)
|
if err = db.Set(key, formValue); err != nil {
|
||||||
// Make sure that the key is unique
|
data["form_result"] = "Internal error while saving the url"
|
||||||
for {
|
app.Log(iris.DevMode, "while saving url: "+err.Error())
|
||||||
if _, exists := urls[key]; !exists {
|
} else {
|
||||||
break
|
ctx.SetStatusCode(iris.StatusOK)
|
||||||
}
|
shortenURL := "http://" + app.Config.VHost + "/u/" + key
|
||||||
key = randomString(8)
|
data["form_result"] = template.HTML("<pre><a target='_new' href='" + shortenURL + "'>" + shortenURL + " </a></pre>")
|
||||||
}
|
}
|
||||||
mu.Lock()
|
|
||||||
urls[key] = urlValue.String()
|
|
||||||
mu.Unlock()
|
|
||||||
ctx.SetStatusCode(iris.StatusOK)
|
|
||||||
shortenURL := "http://" + app.Config.VHost + "/url/" + key
|
|
||||||
data["form_result"] = template.HTML("<pre>Here is your short URL: <a href='" + shortenURL + "'>" + shortenURL + " </a></pre>")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
data["url_count"] = db.Len()
|
||||||
ctx.Render("index.html", data)
|
ctx.Render("index.html", data)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -113,10 +117,178 @@ func main() {
|
||||||
|
|
||||||
// +------------------------------------------------------------+
|
// +------------------------------------------------------------+
|
||||||
// | |
|
// | |
|
||||||
// | Random String |
|
// | Store |
|
||||||
// | |
|
// | |
|
||||||
// +------------------------------------------------------------+
|
// +------------------------------------------------------------+
|
||||||
|
|
||||||
|
// Panic panics, change it if you don't want to panic on critical INITIALIZE-ONLY-ERRORS
|
||||||
|
var Panic = func(v interface{}) {
|
||||||
|
panic(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store is the store interface for urls.
|
||||||
|
// Note: no Del functionality.
|
||||||
|
type Store interface {
|
||||||
|
Set(key string, value string) error // error if something went wrong
|
||||||
|
Get(key string) string // empty value if not found
|
||||||
|
Len() int // should return the number of all the records/tables/buckets
|
||||||
|
Close() error // release the store or ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
tableURLs = []byte("urls")
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB representation of a Store.
|
||||||
|
// Only one table/bucket which contains the urls, so it's not a fully Database,
|
||||||
|
// it works only with single bucket because that all we need.
|
||||||
|
type DB struct {
|
||||||
|
db *bolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Store = &DB{}
|
||||||
|
|
||||||
|
// openDatabase open a new database connection
|
||||||
|
// and returns its instance.
|
||||||
|
func openDatabase(stumb string) *bolt.DB {
|
||||||
|
// Open the data(base) file in the current working directory.
|
||||||
|
// It will be created if it doesn't exist.
|
||||||
|
db, err := bolt.Open(stumb, 0600, nil)
|
||||||
|
if err != nil {
|
||||||
|
Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the buckets here
|
||||||
|
var tables = [...][]byte{
|
||||||
|
tableURLs,
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Update(func(tx *bolt.Tx) (err error) {
|
||||||
|
for _, table := range tables {
|
||||||
|
_, err = tx.CreateBucketIfNotExists(table)
|
||||||
|
if err != nil {
|
||||||
|
Panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDB returns a new DB instance, its connection is opened.
|
||||||
|
// DB implements the Store.
|
||||||
|
func NewDB(stumb string) *DB {
|
||||||
|
return &DB{
|
||||||
|
db: openDatabase(stumb),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets a shorten url and its key
|
||||||
|
// Note: Caller is responsible to generate a key.
|
||||||
|
func (d *DB) Set(key string, value string) error {
|
||||||
|
d.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket(tableURLs)
|
||||||
|
// Generate ID for the url
|
||||||
|
// Note: we could use that instead of a random string key
|
||||||
|
// but we want to simulate a real-world url shortener
|
||||||
|
// so we skip that.
|
||||||
|
// id, _ := b.NextSequence()
|
||||||
|
return b.Put([]byte(key), []byte(value))
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a url by its key.
|
||||||
|
//
|
||||||
|
// Returns an empty string if not found.
|
||||||
|
func (d *DB) Get(key string) (value string) {
|
||||||
|
keyb := []byte(key)
|
||||||
|
d.db.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket(tableURLs)
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
if bytes.Equal(keyb, k) {
|
||||||
|
value = string(v)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns all the "shorted" urls length
|
||||||
|
func (d *DB) Len() (num int) {
|
||||||
|
d.db.View(func(tx *bolt.Tx) error {
|
||||||
|
|
||||||
|
// Assume bucket exists and has keys
|
||||||
|
b := tx.Bucket(tableURLs)
|
||||||
|
|
||||||
|
b.ForEach(func([]byte, []byte) error {
|
||||||
|
num++
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the data(base) connection
|
||||||
|
func (d *DB) Close() error {
|
||||||
|
return d.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// +------------------------------------------------------------+
|
||||||
|
// | |
|
||||||
|
// | Factory |
|
||||||
|
// | |
|
||||||
|
// +------------------------------------------------------------+
|
||||||
|
|
||||||
|
// Generator the type to generate keys(short urls) based on 'n'
|
||||||
|
type Generator func(n int) string
|
||||||
|
|
||||||
|
// DefaultGenerator is the defautl url generator (the simple randomString)
|
||||||
|
var DefaultGenerator = randomString
|
||||||
|
|
||||||
|
// Factory is responsible to generate keys(short urls)
|
||||||
|
type Factory struct {
|
||||||
|
store Store
|
||||||
|
generator Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFactory receives a generator and a store and returns a new url Factory.
|
||||||
|
func NewFactory(generator Generator, store Store) *Factory {
|
||||||
|
return &Factory{
|
||||||
|
store: store,
|
||||||
|
generator: generator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gen generates the key.
|
||||||
|
func (f *Factory) Gen(uri string) (key string, err error) {
|
||||||
|
// we don't return the parsed url because #hash are converted to uri-compatible
|
||||||
|
// and we don't want to encode/decode all the time, there is no need for that,
|
||||||
|
// we save the url as the user expects if the uri validation passed.
|
||||||
|
_, err = url.ParseRequestURI(uri)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
key = f.generator(len(uri))
|
||||||
|
// Make sure that the key is unique
|
||||||
|
for {
|
||||||
|
if v := f.store.Get(key); v == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
key = f.generator((len(uri) / 2) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||||
|
|
4
_examples/examples/url-shortener/shortener.go
Normal file
4
_examples/examples/url-shortener/shortener.go
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// Version is the current version of the url-shortener package.
|
||||||
|
const Version = "0.0.1"
|
|
@ -9,12 +9,13 @@
|
||||||
<body>
|
<body>
|
||||||
<h2>Golang URL Shortener</h2>
|
<h2>Golang URL Shortener</h2>
|
||||||
<h3>{{ .form_result}}</h3>
|
<h3>{{ .form_result}}</h3>
|
||||||
<form action="/url/shorten" method="POST">
|
<form action="/shorten" method="POST">
|
||||||
<input type="text" name="url" style="width: 35em;" />
|
<input type="text" name="url" style="width: 35em;" />
|
||||||
<input type="submit" value="Shorten!" />
|
<input type="submit" value="Shorten!" />
|
||||||
</form>
|
</form>
|
||||||
|
{{ if isPositive .url_count }}
|
||||||
<p>{{ .url_count }} URLs shortened</p>
|
<p>{{ .url_count }} URLs shortened</p>
|
||||||
|
{{ end }}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
Reference in New Issue
Block a user