diff --git a/_examples/README.md b/_examples/README.md index a57889ba..04fb335a 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -27,7 +27,7 @@ Developers should read the official [documentation](https://godoc.org/gopkg.in/k * [Websockets](examples/websockets/main.go) * [Markdown and Cache](examples/cache-markdown/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! \ No newline at end of file diff --git a/_examples/examples/url-shortener/main.go b/_examples/examples/url-shortener/main.go index fea4291e..c8fba621 100644 --- a/_examples/examples/url-shortener/main.go +++ b/_examples/examples/url-shortener/main.go @@ -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 import ( + "bytes" "html/template" "math/rand" "net/url" - "sync" "time" + "github.com/boltdb/bolt" "gopkg.in/kataras/iris.v6" "gopkg.in/kataras/iris.v6/adaptors/httprouter" "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() { 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( + // print all kind of errors and logs at os.Stdout iris.DevLogger(), + // use the httprouter, you can use adpaotrs/gorillamux if you want httprouter.New(), + // serve the "./templates" directory's "*.html" files with the HTML std view engine. 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) - app.StaticWeb("/static", "./static_files") - - // 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.StaticWeb("/static", "./resources") 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 - // used on http://localhost:8080/url/dsaoj41u321dsa + // used on http://localhost:8080/u/dsaoj41u321dsa execShortURL := func(ctx *iris.Context, key string) { if key == "" { ctx.EmitError(iris.StatusBadRequest) return } - value, found := urls[key] - if !found { + value := db.Get(key) + if value == "" { ctx.SetStatusCode(iris.StatusNotFound) ctx.Writef("Short URL for key: '%s' not found", key) return @@ -61,50 +83,32 @@ func main() { 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")) }) - // for wildcard subdomain (yeah.. cool) http://dsaoj41u321dsa.localhost:8080 - // 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) { + app.Post("/shorten", func(ctx *iris.Context) { data := make(map[string]interface{}, 0) - data["url_count"] = len(urls) - value := ctx.FormValue("url") - if value == "" { + formValue := ctx.FormValue("url") + if formValue == "" { data["form_result"] = "You need to a enter a URL." } else { - urlValue, err := url.ParseRequestURI(value) + key, err := factory.Gen(formValue) if err != nil { - // ctx.JSON(iris.StatusInternalServerError, - // iris.Map{"status": iris.StatusInternalServerError, - // "error": err.Error(), - // "reason": "Invalid URL", - // }) data["form_result"] = "Invalid URL." } else { - key := randomString(12) - // Make sure that the key is unique - for { - if _, exists := urls[key]; !exists { - break - } - key = randomString(8) + if err = db.Set(key, formValue); err != nil { + data["form_result"] = "Internal error while saving the url" + app.Log(iris.DevMode, "while saving url: "+err.Error()) + } else { + ctx.SetStatusCode(iris.StatusOK) + shortenURL := "http://" + app.Config.VHost + "/u/" + key + data["form_result"] = template.HTML("
" + shortenURL + "") } - mu.Lock() - urls[key] = urlValue.String() - mu.Unlock() - ctx.SetStatusCode(iris.StatusOK) - shortenURL := "http://" + app.Config.VHost + "/url/" + key - data["form_result"] = template.HTML("
Here is your short URL: " + shortenURL + "") - } + } } + data["url_count"] = db.Len() 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 ( letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" letterIdxBits = 6 // 6 bits to represent a letter index diff --git a/_examples/examples/url-shortener/static_files/style.css b/_examples/examples/url-shortener/resources/css/style.css similarity index 100% rename from _examples/examples/url-shortener/static_files/style.css rename to _examples/examples/url-shortener/resources/css/style.css diff --git a/_examples/examples/url-shortener/shortener.go b/_examples/examples/url-shortener/shortener.go new file mode 100644 index 00000000..dd6419ba --- /dev/null +++ b/_examples/examples/url-shortener/shortener.go @@ -0,0 +1,4 @@ +package main + +// Version is the current version of the url-shortener package. +const Version = "0.0.1" diff --git a/_examples/examples/url-shortener/templates/index.html b/_examples/examples/url-shortener/templates/index.html index 136f5152..8d4acfca 100644 --- a/_examples/examples/url-shortener/templates/index.html +++ b/_examples/examples/url-shortener/templates/index.html @@ -9,12 +9,13 @@
{{ .url_count }} URLs shortened
+ {{ if isPositive .url_count }} +{{ .url_count }} URLs shortened
+ {{ end }}