diff --git a/README.md b/README.md index 570d4da..5ab4a3c 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,14 @@ if exist. The website itself will redirect the request to [pkg.go.dev](https://p There is a reserved ping router for debugging purpose `/x/.ping` which will give you a pong. -### 2. Redirect `golang.design/s/alias` +### 2. Redirect `golang.design/s/alias` and `golang.design/r/randstr` The served alias can be allocated by [golang.design](https://golang.design/) members. The current approach is to use `redir` command on the [golang.design](https://golang.design/) server. Here is the overview of its usage: ``` +$ redir usage: redir [-s] [-f ] [-op -a -l ] options: -a string @@ -42,8 +43,9 @@ options: -s run redir service example: redir -s run the redir service -redir -f ./import.yml import aliases from a file +redir -f ./import.yml import aliases from a file redir -a alias -l link allocate new short link if possible +redir -l link allocate a random alias for the given link if possible redir -op fetch -a alias fetch alias information ``` @@ -52,32 +54,42 @@ The command will talk to the Redis data store and issue a new allocated alias. For instance, the following command: ``` -redir -a changkun -l https://changkun.de +$ redir -a changkun -l https://changkun.de +https://golang.design/s/changkun ``` creates a new alias under [golang.design/s/changkun](https://golang.design/s/changkun). +If the `-a` is not provided, then redir command will generate a random string as an alias, but the link can only be accessed under `/r/alias`. For instance: + +``` +$ redir -l https://changkun.de +https://golang.design/r/qFlKSP +``` + +creates a new alias under [golang.design/r/qFlKSP](https://golang.design/r/qFlKSP). + Import from a YAML file is also possible, for instance: ``` -redir -f import.yml +$ redir -f import.yml ``` The aliases are either imported as a new alias or updated for an existing alias. -Moreover, it is possible to visit [`/s`](https://golang.design/s) directly listing all exist aliases under [golang.design](https://golang.design/). +Moreover, it is possible to visit [`/s`](https://golang.design/s) or [`/r`](https://golang.design/r) directly listing all exist aliases under [golang.design](https://golang.design/). ## Build `Makefile` defines different ways to build the service: ```bash -make # build native binary -make run # assume your local redis is running -make build # build docker images -make up # run via docker-compose -make down # remove compose stuff -make clean # cleanup +$ make # build native binary +$ make run # assume your local redis is running +$ make build # build docker images +$ make up # run via docker-compose +$ make down # remove compose stuff +$ make clean # cleanup ``` ## Troubleshooting diff --git a/config.go b/config.go index 50943fa..b85b638 100644 --- a/config.go +++ b/config.go @@ -31,6 +31,10 @@ type config struct { S struct { Prefix string `yaml:"prefix"` } `yaml:"s"` + R struct { + Length int `yaml:"length"` + Prefix string `yaml:"prefix"` + } `yaml:"r"` X struct { Prefix string `yaml:"prefix"` VCS string `yaml:"vcs"` diff --git a/config.yml b/config.yml index cd5875d..971986e 100644 --- a/config.yml +++ b/config.yml @@ -12,6 +12,9 @@ backup_dir: ./data/backup log: "golang.design/redir: " s: prefix: /s/ +r: + length: 6 + prefix: /r/ x: prefix: /x/ vcs: git diff --git a/data/container.yml b/data/container.yml index 7b27774..1207a0f 100644 --- a/data/container.yml +++ b/data/container.yml @@ -3,7 +3,7 @@ # by a MIT license that can be found in the LICENSE file. --- -title: The golang.design Initiative +title: "The golang.design Initiative" host: https://golang.design addr: :8080 store: redis://redis:6379/9 @@ -12,18 +12,13 @@ backup_dir: ./data/backup log: "golang.design/redir: " s: prefix: /s/ +r: + length: 6 + prefix: /r/ x: prefix: /x/ vcs: git import_path: golang.design/x/* repo_path: https://github.com/golang-design/* godoc_host: https://pkg.go.dev/ -google_analytics: | - - - +google_analytics: UA-80889616-4 diff --git a/db.go b/db.go index a258c0d..e63ea49 100644 --- a/db.go +++ b/db.go @@ -23,10 +23,18 @@ var ( errExistedAlias = errors.New("alias is existed") ) +type aliasKind int + +const ( + kindShort aliasKind = iota + kindRandom +) + // arecord indicates an alias record that stores an short alias // in data store with statistics regarding its UVs and PVs. type arecord struct { Alias string `json:"alias"` + Kind aliasKind `json:"kind"` URL string `json:"url"` UV uint64 `json:"uv"` PV uint64 `json:"pv"` @@ -69,9 +77,9 @@ func (s *store) Close() (err error) { } // StoreAlias stores a given short alias with the given link if not exists -func (s *store) StoreAlias(ctx context.Context, a, l string) (err error) { +func (s *store) StoreAlias(ctx context.Context, a, l string, kind aliasKind) (err error) { b, err := json.Marshal(&arecord{ - URL: l, Alias: a, PV: 0, UV: 0, + URL: l, Kind: kind, Alias: a, PV: 0, UV: 0, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), }) diff --git a/db_test.go b/db_test.go index a58fc06..7d84909 100644 --- a/db_test.go +++ b/db_test.go @@ -20,7 +20,7 @@ func prepare(ctx context.Context, t *testing.T) *store { t.Fatalf("cannot connect to data store") } - err = s.StoreAlias(ctx, kalias, "link") + err = s.StoreAlias(ctx, kalias, "link", kindShort) if err != nil { t.Fatalf("cannot store alias to data store, err: %v\n", err) } diff --git a/handler.go b/handler.go index 3bed301..bd122fa 100644 --- a/handler.go +++ b/handler.go @@ -72,7 +72,8 @@ func (s *server) registerHandler() { }) // short redirector - http.HandleFunc(conf.S.Prefix, s.sHandler) + http.HandleFunc(conf.S.Prefix, s.shortHandler(kindShort)) + http.HandleFunc(conf.R.Prefix, s.shortHandler(kindRandom)) // repo redirector http.Handle(conf.X.Prefix, s.xHandler(conf.X.VCS, conf.X.ImportPath, conf.X.RepoPath)) } diff --git a/import.yml b/import.yml index a48f2e7..47503d1 100644 --- a/import.yml +++ b/import.yml @@ -1,2 +1,5 @@ --- -changkun: https://changkun.de/ \ No newline at end of file +short: + changkun: https://changkun.de +random: + - https://to.what.ever \ No newline at end of file diff --git a/public/stats.html b/public/stats.html index 8233db3..c2ffbf4 100644 --- a/public/stats.html +++ b/public/stats.html @@ -1,7 +1,14 @@ - {{ .GoogleAnalytics }} + + + {{.Title}} @@ -67,7 +74,7 @@
An alias request redirector {{range .Records}} {{ .PV }}/{{ .UV }} - {{ $.Host }}/s/{{ .Alias }} + {{ $.Host }}{{ $.Prefix }}{{ .Alias }} {{end}} diff --git a/public/x.html b/public/x.html index e941a6e..b471b11 100644 --- a/public/x.html +++ b/public/x.html @@ -14,7 +14,7 @@

{{.ImportRoot}}{{.Suffix}}

\ No newline at end of file diff --git a/redir.go b/redir.go index cc3fc03..2da0442 100644 --- a/redir.go +++ b/redir.go @@ -33,6 +33,7 @@ examples: redir -s run the redir service redir -f ./import.yml import aliases from a file redir -a alias -l link allocate new short link if possible +redir -l link allocate a random alias for the given link if possible redir -op fetch -a alias fetch alias information `) os.Exit(2) @@ -77,12 +78,12 @@ func runCmd() { } switch o := op(*operate); o { - case opCreate, opUpdate: - if *alias == "" || *link == "" { + case opCreate: + if *link == "" { flag.Usage() return } - case opDelete, opFetch: + case opUpdate, opDelete, opFetch: if *alias == "" { flag.Usage() return diff --git a/short.go b/short.go index fe7a204..164228c 100644 --- a/short.go +++ b/short.go @@ -10,7 +10,6 @@ import ( "encoding/json" "errors" "fmt" - "html/template" "io/ioutil" "log" "net" @@ -46,25 +45,43 @@ func (o op) valid() bool { } } +type importf struct { + Short map[string]string `yaml:"short"` + Random []string `yaml:"random"` +} + func shortFile(fname string) { b, err := ioutil.ReadFile(fname) if err != nil { log.Fatalf("cannot read import file, err: %v\n", err) } - d := map[string]string{} + var d importf err = yaml.Unmarshal(b, &d) if err != nil { log.Fatalf("cannot unmarshal the imported file, err: %v\n", err) } ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() - for alias, link := range d { + for alias, link := range d.Short { err = shortCmd(ctx, opUpdate, alias, link) if err != nil { err = shortCmd(ctx, opCreate, alias, link) if err != nil { - log.Fatalf("cannot import alias %v, err: %v\n", alias, err) + log.Printf("cannot import alias %v, err: %v\n", alias, err) + } + } + } + for _, link := range d.Random { + err = shortCmd(ctx, opUpdate, "", link) + if err != nil { + for i := 0; i < 10; i++ { // try 10x maximum + err = shortCmd(ctx, opCreate, "", link) + if err != nil { + log.Printf("cannot create alias %v, err: %v\n", alias, err) + continue + } + break } } } @@ -88,11 +105,30 @@ func shortCmd(ctx context.Context, operate op, alias, link string) (err error) { switch operate { case opCreate: - err = s.StoreAlias(ctx, alias, link) + kind := kindShort + if alias == "" { + // This might conflict with existing ones, it should be fine + // at the moment, the user of redir can always the command twice. + if conf.R.Length <= 0 { + conf.R.Length = 6 + } + alias = RandomString(conf.R.Length) + kind = kindRandom + } + err = s.StoreAlias(ctx, alias, link, kind) if err != nil { return } - log.Printf("alias %v has been created.\n", alias) + log.Printf("alias %v has been created:\n", alias) + + var prefix string + switch kind { + case kindShort: + prefix = conf.S.Prefix + case kindRandom: + prefix = conf.R.Prefix + } + fmt.Printf("%s%s%s\n", conf.Host, prefix, alias) case opUpdate: err = s.UpdateAlias(ctx, alias, link) if err != nil { @@ -116,47 +152,57 @@ func shortCmd(ctx context.Context, operate op, alias, link string) (err error) { return } -// sHandler redirects the current request to a known link if the alias -// is found in the redir store. -func (s *server) sHandler(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() +// shortHandler redirects the current request to a known link if the alias is +// found in the redir store. +func (s *server) shortHandler(kind aliasKind) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() - var err error - defer func() { - if err != nil { - // Just tell the user we could not find the record rather than - // throw 50x. The server should be able to identify the issue. - log.Printf("stats err: %v\n", err) - // Use 307 redirect to 404 page - http.Redirect(w, r, "/404.html", http.StatusTemporaryRedirect) + var err error + defer func() { + if err != nil { + // Just tell the user we could not find the record rather than + // throw 50x. The server should be able to identify the issue. + log.Printf("stats err: %v\n", err) + // Use 307 redirect to 404 page + http.Redirect(w, r, "/404.html", http.StatusTemporaryRedirect) + } + }() + + // statistic page + var prefix string + switch kind { + case kindShort: + prefix = conf.S.Prefix + case kindRandom: + prefix = conf.R.Prefix } - }() - // statistic page - alias := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, conf.S.Prefix), "/") - if alias == "" { - err = s.stats(ctx, w) - return - } + alias := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, prefix), "/") + if alias == "" { + err = s.stats(ctx, kind, w) + return + } - // figure out redirect location - url, ok := s.cache.Get(alias) - if !ok { - url, err = s.checkdb(ctx, alias) - if err != nil { - url, err = s.checkvcs(ctx, alias) + // figure out redirect location + url, ok := s.cache.Get(alias) + if !ok { + url, err = s.checkdb(ctx, alias) if err != nil { - return + url, err = s.checkvcs(ctx, alias) + if err != nil { + return + } } + s.cache.Put(alias, url) } - s.cache.Put(alias, url) - } - // redirect the user immediate, but run pv/uv count in background - http.Redirect(w, r, url, http.StatusTemporaryRedirect) + // redirect the user immediate, but run pv/uv count in background + http.Redirect(w, r, url, http.StatusTemporaryRedirect) - // count visit in another goroutine so it won't block the redirect. - go func() { s.visitCh <- visit{s.readIP(r), alias} }() + // count visit in another goroutine so it won't block the redirect. + go func() { s.visitCh <- visit{s.readIP(r), alias} }() + } } // checkdb checks whether the given alias is exsited in the redir database, @@ -200,7 +246,7 @@ func (s *server) checkvcs(ctx context.Context, alias string) (string, error) { } // store such a try path - err = s.db.StoreAlias(ctx, alias, tryPath) + err = s.db.StoreAlias(ctx, alias, tryPath, kindShort) if err != nil { if errors.Is(err, errExistedAlias) { return s.checkdb(ctx, alias) @@ -239,33 +285,49 @@ func (s *server) readIP(r *http.Request) string { type arecords struct { Title string Host string + Prefix string Records []arecord - GoogleAnalytics template.HTML + GoogleAnalytics string } -func (s *server) stats(ctx context.Context, w http.ResponseWriter) (retErr error) { +func (s *server) stats(ctx context.Context, kind aliasKind, w http.ResponseWriter) (retErr error) { aliases, retErr := s.db.Keys(ctx, prefixalias+"*") if retErr != nil { return } + var prefix string + switch kind { + case kindShort: + prefix = conf.S.Prefix + case kindRandom: + prefix = conf.R.Prefix + } + ars := arecords{ Title: conf.Title, Host: conf.Host, - Records: make([]arecord, len(aliases)), - GoogleAnalytics: template.HTML(conf.GoogleAnalytics), + Prefix: prefix, + Records: []arecord{}, + GoogleAnalytics: conf.GoogleAnalytics, } - for i, a := range aliases { + for _, a := range aliases { raw, err := s.db.Fetch(ctx, a) if err != nil { retErr = err return } - err = json.Unmarshal(StringToBytes(raw), &ars.Records[i]) + var record arecord + err = json.Unmarshal(StringToBytes(raw), &record) if err != nil { retErr = err return } + if record.Kind != kind { + continue + } + + ars.Records = append(ars.Records, record) } sort.Slice(ars.Records, func(i, j int) bool { diff --git a/utils.go b/utils.go index a9b8f44..36cade3 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,12 @@ +// Copyright 2020 The golang.design Initiative Authors. +// All rights reserved. Use of this source code is governed +// by a MIT license that can be found in the LICENSE file. + package main import ( + "math/rand" + "time" "unsafe" ) @@ -16,3 +22,18 @@ func StringToBytes(s string) []byte { Cap int }{s, len(s)})) } + +const alphanum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +var r = rand.New(rand.NewSource(time.Now().UnixNano())) + +// RandomString generates a random string +func RandomString(n int) string { + var str string + length := len(alphanum) + for i := 0; i < n; i++ { + a := alphanum[r.Intn(len(alphanum))%length] + str += string(a) + } + return str +} diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..0321a42 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,22 @@ +// Copyright 2020 The golang.design Initiative Authors. +// All rights reserved. Use of this source code is governed +// by a MIT license that can be found in the LICENSE file. + +package main + +import ( + "strings" + "testing" +) + +func TestRandomString(t *testing.T) { + s1 := RandomString(12) + s2 := RandomString(12) + if len(s1) != 12 || len(s2) != 12 { + t.Fatalf("want 12 chars, got: %v, %v", len(s1), len(s2)) + } + if strings.Compare(s1, s2) == 0 { + t.Fatalf("want two different string, got: %v, %v", s1, s2) + } + t.Log(s1, s2) +} diff --git a/x.go b/x.go index c38ec7e..7d92d8d 100644 --- a/x.go +++ b/x.go @@ -7,7 +7,6 @@ package main import ( "bytes" "fmt" - "html/template" "net/http" "strings" ) @@ -17,7 +16,7 @@ type x struct { VCS string VCSRoot string Suffix string - GoogleAnalytics template.HTML + GoogleAnalytics string } // xHandler redirect returns an HTTP handler that redirects requests for @@ -71,7 +70,7 @@ func (s *server) xHandler(vcs, importPath, repoPath string) http.Handler { VCS: vcs, VCSRoot: repoRoot, Suffix: suffix, - GoogleAnalytics: template.HTML(conf.GoogleAnalytics), + GoogleAnalytics: conf.GoogleAnalytics, } var buf bytes.Buffer err := xTmpl.Execute(&buf, d)