1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-20 03:17:04 +00:00

Add notes for the new lead maintainer of the open-source iris project and align with @get-ion/ion by @hiveminded

Former-commit-id: da4f38eb9034daa49446df3ee529423b98f9b331
This commit is contained in:
kataras
2017-07-10 18:32:42 +03:00
parent 2d4c2779a7
commit 9f85b74fc9
344 changed files with 4842 additions and 5174 deletions

View File

@@ -0,0 +1,166 @@
package main
import (
"sync/atomic"
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/websocket"
)
func main() {
// init the web application instance
// app := iris.New()
app := iris.Default()
// load templaes
app.RegisterView(iris.HTML("./templates", ".html").Reload(true))
// setup the websocket server
ws := websocket.New(websocket.Config{})
ws.OnConnection(HandleWebsocketConnection)
app.Get("/my_endpoint", ws.Handler())
app.Any("/iris-ws.js", websocket.ClientHandler())
// register static assets request path and system directory
app.StaticWeb("/js", "./static/assets/js")
h := func(ctx context.Context) {
ctx.ViewData("", page{PageID: "index page"})
ctx.View("index.html")
}
h2 := func(ctx context.Context) {
ctx.ViewData("", page{PageID: "other page"})
ctx.View("other.html")
}
// Open some browser tabs/or windows
// and navigate to
// http://localhost:8080/ and http://localhost:8080/other multiple times.
// Each page has its own online-visitors counter.
app.Get("/", h)
app.Get("/other", h2)
app.Run(iris.Addr(":8080"))
}
type page struct {
PageID string
}
type pageView struct {
source string
count uint64
}
func (v *pageView) increment() {
atomic.AddUint64(&v.count, 1)
}
func (v *pageView) decrement() {
oldCount := v.count
if oldCount > 0 {
atomic.StoreUint64(&v.count, oldCount-1)
}
}
func (v *pageView) getCount() uint64 {
val := atomic.LoadUint64(&v.count)
return val
}
type (
pageViews []pageView
)
func (v *pageViews) Add(source string) {
args := *v
n := len(args)
for i := 0; i < n; i++ {
kv := &args[i]
if kv.source == source {
kv.increment()
return
}
}
c := cap(args)
if c > n {
args = args[:n+1]
kv := &args[n]
kv.source = source
kv.count = 1
*v = args
return
}
kv := pageView{}
kv.source = source
kv.count = 1
*v = append(args, kv)
}
func (v *pageViews) Get(source string) *pageView {
args := *v
n := len(args)
for i := 0; i < n; i++ {
kv := &args[i]
if kv.source == source {
return kv
}
}
return nil
}
func (v *pageViews) Reset() {
*v = (*v)[:0]
}
var v pageViews
// HandleWebsocketConnection handles the online viewers per example(gist source)
func HandleWebsocketConnection(c websocket.Connection) {
c.On("watch", func(pageSource string) {
v.Add(pageSource)
// join the socket to a room linked with the page source
c.Join(pageSource)
viewsCount := v.Get(pageSource).getCount()
if viewsCount == 0 {
viewsCount++ // count should be always > 0 here
}
c.To(pageSource).Emit("watch", viewsCount)
})
c.OnLeave(func(roomName string) {
if roomName != c.ID() { // if the roomName it's not the connection iself
// the roomName here is the source, this is the only room(except the connection's ID room) which we join the users to.
pageV := v.Get(roomName)
if pageV == nil {
return // for any case that this room is not a pageView source
}
// decrement -1 the specific counter for this page source.
pageV.decrement()
// 1. open 30 tabs.
// 2. close the browser.
// 3. re-open the browser
// 4. should be v.getCount() = 1
// in order to achieve the previous flow we should decrement exactly when the user disconnects
// but emit the result a little after, on a goroutine
// getting all connections within this room and emit the online views one by one.
// note:
// we can also add a time.Sleep(2-3 seconds) inside the goroutine at the future if we don't need 'real-time' updates.
go func(currentConnID string) {
for _, conn := range c.Server().GetConnectionsByRoom(roomName) {
if conn.ID() != currentConnID {
conn.Emit("watch", pageV.getCount())
}
}
}(c.ID())
}
})
}

View File

@@ -0,0 +1,21 @@
(function() {
var socket = new Ws("ws://localhost:8080/my_endpoint");
socket.OnConnect(function () {
socket.Emit("watch", PAGE_SOURCE);
});
socket.On("watch", function (onlineViews) {
var text = "1 online view";
if (onlineViews > 1) {
text = onlineViews + " online views";
}
document.getElementById("online_views").innerHTML = text;
});
socket.OnDisconnect(function () {
document.getElementById("online_views").innerHTML = "you've been disconnected";
});
})();

View File

@@ -0,0 +1,43 @@
<html>
<head>
<title>Online visitors example</title>
<style>
body {
margin: 0;
font-family: -apple-system, "San Francisco", "Helvetica Neue", "Noto", "Roboto", "Calibri Light", sans-serif;
color: #212121;
font-size: 1.0em;
line-height: 1.6;
}
.container {
max-width: 750px;
margin: auto;
padding: 15px;
}
#online_views {
font-weight: bold;
font-size: 18px;
}
</style>
</head>
<body>
<div class="container">
<span id="online_views">1 online view</span>
</div>
<script type="text/javascript">
/* take the page source from our passed struct on .Render */
var PAGE_SOURCE = {{ .PageID }}
</script>
<script src="/iris-ws.js"></script>
<script src="/js/visitors.js"></script>
</body>
</html>

View File

@@ -0,0 +1,29 @@
<html>
<head>
<title>Different page, different results</title>
<style>
#online_views {
font-weight: bold;
font-size: 18px;
}
</style>
</head>
<body>
<span id="online_views">1 online view</span>
<script type="text/javascript">
/* take the page source from our passed struct on .Render */
var PAGE_SOURCE = {{ .PageID }}
</script>
<script src="/iris-ws.js"></script>
<script src="/js/visitors.js"></script>
</body>
</html>

View File

@@ -0,0 +1,352 @@
// Package main shows how you can create a simple URL SHortener.
//
// $ go get github.com/boltdb/bolt/...
// $ go run main.go
// $ start http://localhost:8080
package main
import (
"bytes"
"html/template"
"net/url"
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/view"
"github.com/boltdb/bolt"
"github.com/satori/go.uuid"
)
func main() {
// assign a variable to the DB so we can use its features later.
db := NewDB("shortener.db")
// Pass that db to our app, in order to be able to test the whole app with a different database later on.
app := newApp(db)
// release the "db" connection when server goes off.
iris.RegisterOnInterrupt(db.Close)
app.Run(iris.Addr(":8080"))
}
func newApp(db *DB) *iris.Application {
app := iris.Default() // or app := iris.New()
// create our factory, which is the manager for the object creation.
// between our web app and the db.
factory := NewFactory(DefaultGenerator, db)
// serve the "./templates" directory's "*.html" files with the HTML std view engine.
tmpl := view.HTML("./templates", ".html").Reload(true)
// register any template func(s) here.
//
// Look ./templates/index.html#L16
tmpl.AddFunc("IsPositive", func(n int) bool {
if n > 0 {
return true
}
return false
})
app.RegisterView(tmpl)
// Serve static files (css)
app.StaticWeb("/static", "./resources")
indexHandler := func(ctx context.Context) {
ctx.ViewData("URL_COUNT", db.Len())
ctx.View("index.html")
}
app.Get("/", indexHandler)
// find and execute a short url by its key
// used on http://localhost:8080/u/dsaoj41u321dsa
execShortURL := func(ctx context.Context, key string) {
if key == "" {
ctx.StatusCode(iris.StatusBadRequest)
return
}
value := db.Get(key)
if value == "" {
ctx.StatusCode(iris.StatusNotFound)
ctx.Writef("Short URL for key: '%s' not found", key)
return
}
ctx.Redirect(value, iris.StatusTemporaryRedirect)
}
app.Get("/u/{shortkey}", func(ctx context.Context) {
execShortURL(ctx, ctx.Params().Get("shortkey"))
})
app.Post("/shorten", func(ctx context.Context) {
formValue := ctx.FormValue("url")
if formValue == "" {
ctx.ViewData("FORM_RESULT", "You need to a enter a URL")
ctx.StatusCode(iris.StatusLengthRequired)
} else {
key, err := factory.Gen(formValue)
if err != nil {
ctx.ViewData("FORM_RESULT", "Invalid URL")
ctx.StatusCode(iris.StatusBadRequest)
} else {
if err = db.Set(key, formValue); err != nil {
ctx.ViewData("FORM_RESULT", "Internal error while saving the URL")
app.Logger().Infof("while saving URL: " + err.Error())
ctx.StatusCode(iris.StatusInternalServerError)
} else {
ctx.StatusCode(iris.StatusOK)
shortenURL := "http://" + app.ConfigurationReadOnly().GetVHost() + "/u/" + key
ctx.ViewData("FORM_RESULT",
template.HTML("<pre><a target='_new' href='"+shortenURL+"'>"+shortenURL+" </a></pre>"))
}
}
}
indexHandler(ctx) // no redirect, we need the FORM_RESULT.
})
app.Post("/clear_cache", func(ctx context.Context) {
db.Clear()
ctx.Redirect("/")
})
return app
}
// +------------------------------------------------------------+
// | |
// | 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() // 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 {
return d.db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists(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()
if err != nil {
return err
}
k := []byte(key)
valueB := []byte(value)
c := b.Cursor()
found := false
for k, v := c.First(); k != nil; k, v = c.Next() {
if bytes.Equal(valueB, v) {
found = true
break
}
}
// if value already exists don't re-put it.
if found {
return nil
}
return b.Put(k, []byte(value))
})
}
// Clear clears all the database entries for the table urls.
func (d *DB) Clear() error {
return d.db.Update(func(tx *bolt.Tx) error {
return tx.DeleteBucket(tableURLs)
})
}
// 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.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(tableURLs)
if b == nil {
return nil
}
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
}
// GetByValue returns all keys for a specific (original) url value.
func (d *DB) GetByValue(value string) (keys []string) {
valueB := []byte(value)
d.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(tableURLs)
if b == nil {
return nil
}
c := b.Cursor()
// first for the bucket's table "urls"
for k, v := c.First(); k != nil; k, v = c.Next() {
if bytes.Equal(valueB, v) {
keys = append(keys, string(k))
}
}
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)
if b == nil {
return nil
}
b.ForEach(func([]byte, []byte) error {
num++
return nil
})
return nil
})
return
}
// Close shutdowns the data(base) connection.
func (d *DB) Close() {
if err := d.db.Close(); err != nil {
Panic(err)
}
}
// +------------------------------------------------------------+
// | |
// | Factory |
// | |
// +------------------------------------------------------------+
// Generator the type to generate keys(short urls)
type Generator func() string
// DefaultGenerator is the defautl url generator
var DefaultGenerator = func() string {
return uuid.NewV4().String()
}
// 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()
// Make sure that the key is unique
for {
if v := f.store.Get(key); v == "" {
break
}
key = f.generator()
}
return key, nil
}

View File

@@ -0,0 +1,74 @@
package main
import (
"io/ioutil"
"os"
"time"
"testing"
"github.com/kataras/iris/httptest"
)
func TestURLShortener(t *testing.T) {
// temp db file
f, err := ioutil.TempFile("", "shortener")
if err != nil {
t.Fatalf("creating temp file for database failed: %v", err)
}
db := NewDB(f.Name())
app := newApp(db)
e := httptest.New(t, app)
originalURL := "https://google.com"
// save
e.POST("/shorten").
WithFormField("url", originalURL).Expect().
Status(httptest.StatusOK).Body().Contains("<pre><a target='_new' href=")
keys := db.GetByValue(originalURL)
if got := len(keys); got != 1 {
t.Fatalf("expected to have 1 key but saved %d short urls", got)
}
// get
e.GET("/u/" + keys[0]).Expect().
Status(httptest.StatusTemporaryRedirect).Header("Location").Equal(originalURL)
// save the same again, it should add a new key
e.POST("/shorten").
WithFormField("url", originalURL).Expect().
Status(httptest.StatusOK).Body().Contains("<pre><a target='_new' href=")
keys2 := db.GetByValue(originalURL)
if got := len(keys2); got != 1 {
t.Fatalf("expected to have 1 keys even if we save the same original url but saved %d short urls", got)
} // the key is the same, so only the first one matters.
if keys[0] != keys2[0] {
t.Fatalf("expected keys to be equal if the original url is the same, but got %s = %s ", keys[0], keys2[0])
}
// clear db
e.POST("/clear_cache").Expect().Status(httptest.StatusOK)
if got := db.Len(); got != 0 {
t.Fatalf("expected database to have 0 registered objects after /clear_cache but has %d", got)
}
// give it some time to release the db connection
db.Close()
time.Sleep(1 * time.Second)
// close the file
if err := f.Close(); err != nil {
t.Fatalf("unable to close the file: %s", f.Name())
}
// and remove the file
if err := os.Remove(f.Name()); err != nil {
t.Fatalf("unable to remove the file from %s", f.Name())
}
time.Sleep(1 * time.Second)
}

View File

@@ -0,0 +1,3 @@
body{
background-color:silver;
}

View File

@@ -0,0 +1,4 @@
package main
// Version is the current version of the iris url-shortener example.
const Version = "0.0.2"

View File

@@ -0,0 +1,25 @@
<html>
<head>
<meta charset="utf-8">
<title>Golang URL Shortener</title>
<link rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
<h2>Golang URL Shortener</h2>
<h3>{{ .FORM_RESULT}}</h3>
<form action="/shorten" method="POST">
<input type="text" name="url" style="width: 35em;" />
<input type="submit" value="Shorten!" />
</form>
{{ if IsPositive .URL_COUNT }}
<p>{{ .URL_COUNT }} URLs shortened</p>
{{ end }}
<form action="/clear_cache" method="POST">
<input type="submit" value="Clear DB" />
</form>
</body>
</html>