├── .gitignore
├── .goreleaser.yml
├── 404.html
├── Dockerfile-goreleaser
├── LICENSE
├── README.md
├── alias
├── alphanumeric
│ └── main.go
├── emoji
│ └── main.go
├── main.go
└── memorable
│ ├── main.go
│ └── wordlist.go
├── auth
├── httpbasic
│ └── main.go
├── main.go
├── statickey
│ └── main.go
└── unauthenticated
│ └── main.go
├── cmd
├── main.go
└── version.go
├── go.mod
├── go.sum
├── klein.png
├── main.go
├── server
└── main.go
└── storage
├── bolt
├── main.go
└── main_test.go
├── file
├── main.go
└── main_test.go
├── main.go
├── memory
├── main.go
└── main_test.go
├── postgresql
└── main.go
├── redis
├── main.go
└── main_test.go
├── spaces
└── main.go
├── spacesstateless
└── main.go
└── storagetest
└── main.go
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | klein
3 | urls
4 | bolt.db
5 | .DS_Store
6 | dist/
7 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | builds:
2 | - binary: klein
3 | env:
4 | - CGO_ENABLED=0
5 | ldflags:
6 | - -s -w -X "github.com/kamaln7/klein/cmd.Version={{.Version}}"
7 | goos:
8 | - linux
9 | - darwin
10 | - windows
11 | archive:
12 | replacements:
13 | darwin: Darwin
14 | linux: Linux
15 | windows: Windows
16 | 386: i386
17 | amd64: x86_64
18 | checksum:
19 | name_template: "checksums.txt"
20 | snapshot:
21 | name_template: "{{ .Version }}-next"
22 | changelog:
23 | sort: asc
24 | filters:
25 | exclude:
26 | - "^docs:"
27 | - "^test:"
28 | dockers:
29 | - dockerfile: Dockerfile-goreleaser
30 | extra_files:
31 | - 404.html
32 | image_templates:
33 | - "kamaln7/klein:{{ .Version }}"
34 | - "kamaln7/klein:{{ .Major }}-latest"
35 | - kamaln7/klein:latest
36 |
--------------------------------------------------------------------------------
/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404 not found
7 |
15 |
16 |
17 | 404 not found
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Dockerfile-goreleaser:
--------------------------------------------------------------------------------
1 | FROM drone/ca-certs
2 | COPY 404.html /404.html
3 | COPY klein /
4 | ENV KLEIN_LISTEN 0.0.0.0:5556
5 | ENV KLEIN_TEMPLATE /404.html
6 | EXPOSE 5556/tcp
7 | ENTRYPOINT ["/klein"]
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2017 Kamal Nasser
2 |
3 | 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:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | 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.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | klein is a minimalist URL shortener written in Go. No unnecessary clutter, web UI, features, etc. Just shortening and serving redirections.
6 |
7 | ## Modularity
8 |
9 | klein has three core components that are abstracted into drivers to allow different functionality:
10 |
11 | 1. auth
12 | - Handles authentication, guarding access to shortening links
13 | - Comes with two drivers:
14 | - Unauthenticated—shorten URLs without authentication
15 | - Static Key—require a static key/password
16 | - HTTP Basic—uses HTTP Basic Auth, require a username and password
17 | 2. alias
18 | - Handles generating URL aliases.
19 | - Comes with two drivers:
20 | - Alphanumeric—returns a random alphanumeric string with a configurable length
21 | - Memorable—returns a configurable amount of English words
22 | 3. storage
23 | - Handles storing and reading shortened URLs.
24 | - Comes with four drivers:
25 | - File—stores data as text files in a directory
26 | - Bolt—stores data in a [bolt](https://github.com/boltdb/bolt) database
27 | - Redis—stores data in a [redis](https://redis.io/) database (ensure you configure save)
28 | - Spaces.stateful—stores data as a single file in [DigitalOcean Spaces](https://do.co/spaces)
29 | - Spaces.stateless—stores each URL as an object in [DigitalOcean Spaces](https://do.co/spaces)
30 | - PostgreSQL—stores data in a [PostgreSQL](https://www.postgresql.org) database
31 | - Memory—stores data in a temporary map in memory
32 |
33 | ## Usage
34 |
35 | Once installed and configured, there are two actions that you can do:
36 |
37 | 1. Shorten a URL:
38 | - Send a POST request to `/` with the following two fields:
39 | 1. `url`—the URL to shorten
40 | 2. `key`—if the Static Key auth driver is enabled
41 | 3. `alias`—a custom alias to be used instead of a randomly-generated one
42 | - Example cURL command: `curl -X POST -d 'url=http://github.com/kamaln7/klein' -d 'key=secret_password' -d 'alias=klein_gh' http://localhost:5556/`
43 | - This will create a short URL at `http://localhost:5556/klein_gh` that redirects to `http://github.com/kamaln7/klein`.
44 | 2. Look up a URL/serve a redirect:
45 | - Browse to `http://[path to klein]/[alias]` to access a short URL.
46 |
47 | ## Installation
48 |
49 | ✅ Use the docker image `kamaln7/klein`. The `latest` tag is a good bet. See [the releases page](https://github.com/kamaln7/klein/releases) for version numbers.
50 |
51 | Or grab the latest binary from [the releases page](https://github.com/kamaln7/klein/releases) and drop it in `/usr/local/bin`, `/opt`, or wherever you like.
52 |
53 | ### Configuration
54 |
55 | klein uses CLI options or environment variables for config. For environment variables, each option is prefixed with `klein` and both dots and dashes are replaced with underscores, eg the environment variable for the `storage.spaces.access-key` option is `KLEIN_STORAGE_SPACES_ACCESS_KEY`.
56 |
57 | Running klein without any configuration will use the following default config:
58 |
59 | - Aliases are random 5-character alphanumeric strings
60 | - Listens on 127.0.0.1:5556
61 | - No authentication
62 | - Stores URLs as files in a `urls` directory in the current working directory
63 |
64 | #### Full list of config options
65 |
66 | ```
67 | $ klein --help
68 | klein is a minimalist URL shortener.
69 |
70 | Usage:
71 | klein [flags]
72 |
73 | Flags:
74 | --alias.alphanumeric.alpha use letters in code (default true)
75 | --alias.alphanumeric.length int alphanumeric code length (default 5)
76 | --alias.alphanumeric.num use numbers in code (default true)
77 | --alias.driver string what alias generation to use (alphanumeric, memorable) (default "alphanumeric")
78 | --alias.memorable.length int memorable word count (default 3)
79 | --auth.basic.password string password for HTTP basic auth
80 | --auth.basic.username string username for HTTP basic auth
81 | --auth.driver string what auth backend to use (basic, key, none) (default "none")
82 | --auth.key string upload API key
83 | --error-template string path to error template
84 | -h, --help help for klein
85 | --listen string listen address (default "127.0.0.1:5556")
86 | --root string root redirect
87 | --storage.boltdb.path string path to use for bolt db (default "bolt.db")
88 | --storage.driver string what storage backend to use (file, boltdb, redis, spaces.stateful, sql.pg, memory) (default "file")
89 | --storage.file.path string path to use for file store (default "urls")
90 | --storage.redis.address string address:port of redis instance (default "127.0.0.1:6379")
91 | --storage.redis.auth string password to access redis
92 | --storage.redis.db int db to select within redis
93 | --storage.spaces.access-key string access key for spaces
94 | --storage.spaces.region string region for spaces
95 | --storage.spaces.secret-key string secret key for spaces
96 | --storage.spaces.space string space to use
97 | --storage.spaces.stateful.path string path of the file in spaces (default "klein.json")
98 | --storage.spaces.stateless.cache-duration duration time to cache spaces results in memory. 0 to disable (default 1m0s)
99 | --storage.spaces.stateless.path string path of the directory in spaces to store urls in (default "/klein")
100 | --storage.sql.pg.database string postgresql database (default "klein")
101 | --storage.sql.pg.host string postgresql host (default "localhost")
102 | --storage.sql.pg.password string postgresql password (default "secret")
103 | --storage.sql.pg.port int32 postgresql port (default 5432)
104 | --storage.sql.pg.sslmode string postgresql sslmode (default "prefer")
105 | --storage.sql.pg.table string postgresql table (default "klein")
106 | --storage.sql.pg.user string postgresql user (default "klein")
107 | --url string path to public facing url
108 | ```
109 |
110 | ### Service file
111 |
112 | Here's a Systemd service file that you can use with klein:
113 |
114 | ```
115 | [Unit]
116 | Description=klein
117 | After=network-online.target
118 |
119 | [Service]
120 | Restart=on-failure
121 |
122 | User=klein
123 | Group=klein
124 |
125 | ExecStart=/usr/local/bin/klein
126 |
127 | [Install]
128 | WantedBy=multi-user.target
129 | ```
130 |
131 | Don't forget to add your config to the `ExecStart` line and update `User` and `Group` if necessary. Make sure that klein has permission to write to the URLs directory.
132 |
133 | ## Development
134 |
135 | To manage dependencies, we use [Go modules](https://blog.golang.org/using-go-modules).
136 |
137 | To build the app, run `go build`.
138 | This will produce a binary named `klein`. You can now run the app by running `./klein`
139 |
140 | ### ❤️ Contributors
141 |
142 | - @LukeHandle
143 | - @DMarby
144 |
145 | ## License
146 |
147 | See [./LICENSE](/LICENSE)
148 |
--------------------------------------------------------------------------------
/alias/alphanumeric/main.go:
--------------------------------------------------------------------------------
1 | package alphanumeric
2 |
3 | import (
4 | "errors"
5 | "math/rand"
6 | "time"
7 |
8 | "github.com/kamaln7/klein/alias"
9 | )
10 |
11 | // Provider implements an alias generator
12 | type Provider struct {
13 | Config *Config
14 | runes []rune
15 | }
16 |
17 | // ensure that the storage.Provider interface is implemented
18 | var _ alias.Provider = new(Provider)
19 |
20 | // Config contains the configuration for the file storage
21 | type Config struct {
22 | Length int
23 | Alpha bool
24 | Num bool
25 | randSource *rand.Rand
26 | }
27 |
28 | var alpha = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
29 | var num = []rune("0123456789")
30 |
31 | // New initializes the alias generator and returns a new instance
32 | func New(c *Config) (*Provider, error) {
33 | c.randSource = rand.New(rand.NewSource(time.Now().UnixNano()))
34 |
35 | provider := &Provider{
36 | Config: c,
37 | }
38 | err := provider.Init()
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | return provider, nil
44 | }
45 |
46 | // Init sets up the alphanumeric alias
47 | func (p *Provider) Init() error {
48 | var runes []rune
49 | switch {
50 | case p.Config.Alpha == true:
51 | runes = append(runes, alpha...)
52 | fallthrough
53 | case p.Config.Num == true:
54 | runes = append(runes, num...)
55 | default:
56 | return errors.New("please specify at least alpha or numeric!")
57 | }
58 |
59 | p.runes = runes
60 | return nil
61 | }
62 |
63 | // Generate returns a random alias
64 | func (p *Provider) Generate() string {
65 | b := make([]rune, p.Config.Length)
66 | for i := range b {
67 | b[i] = p.runes[p.Config.randSource.Intn(len(p.runes))]
68 | }
69 |
70 | return string(b)
71 | }
72 |
--------------------------------------------------------------------------------
/alias/emoji/main.go:
--------------------------------------------------------------------------------
1 | package emoji
2 |
3 | import (
4 | "math/rand"
5 | "strings"
6 | "time"
7 |
8 | "github.com/kamaln7/klein/alias"
9 | )
10 |
11 | // Provider implements an alias generator
12 | type Provider struct {
13 | Config *Config
14 | }
15 |
16 | // ensure that the storage.Provider interface is implemented
17 | var _ alias.Provider = new(Provider)
18 |
19 | // Config contains the configuration for the file storage
20 | type Config struct {
21 | Length int
22 | randSource *rand.Rand
23 | }
24 |
25 | var emojis = []string{"👍", "👎", "👽", "👼", "😠", "😧", "😲", "👟", "👶", "👙", "👱", "👱♀️", "😊", "👢", "🙇", "🙇", "👦", "👰", "💼", "👤", "👥", "🤙", "👏", "🌂", "🤡", "😰", "😖", "😕", "👷", "👷♀️", "👫", "👨❤️👨", "💑", "👩❤️👩", "👨❤️💋👨", "💏", "👩❤️💋👩", "🤠", "🤞", "👑", "😢", "😿", "💃", "👯♂️", "👯", "🕶", "😞", "😥", "😵", "👗", "🤤", "👂", "😑", "👁", "👓", "👀", "🤕", "🤒", "👊", "👨👦", "👨👦👦", "👨👧", "👨👧👦", "👨👧👧", "👨👨👦", "👨👨👦👦", "👨👨👧", "👨👨👧👦", "👨👨👧👧", "👪", "👨👩👦👦", "👨👩👧", "👨👩👧👦", "👨👩👧👧", "👩👦", "👩👦👦", "👩👧", "👩👧👦", "👩👧👧", "👩👩👦", "👩👩👦👦", "👩👩👧", "👩👩👧👦", "👩👩👧👧", "😨", "🕵️♀️", "✊", "🤛", "🤜", "😳", "👣", "😦", "☹", "🙍♂️", "🙍", "🖕", "👻", "👧", "😬", "😁", "😀", "💂", "💂♀️", "💇♂️", "💇", "👜", "🤝", "😍", "😻", "👠", "🤗", "😯", "👿", "😇", "👺", "👹", "👖", "😂", "😹", "👘", "💋", "😗", "😽", "😚", "😘", "😙", "😆", "👄", "💄", "🤥", "🕵", "👨", "👨🎨", "👨🚀", "👨🍳", "🕺", "🤦", "👨🏭", "👨🌾", "👨🚒", "👨⚕️", "🤵", "👨⚖️", "👨🔧", "👨💼", "👨✈️", "👨🔬", "🤷♂️", "👨🎤", "👨🎓", "👨🏫", "👨💻", "👲", "👳", "👞", "😷", "💆♂️", "💆", "🤘", "🤑", "🎓", "🤶", "💪", "💅", "🤢", "👔", "🤓", "😐", "🙅♂️", "🙅", "😶", "👃", "👌", "🙆♂️", "🙆", "👴", "👵", "👐", "😮", "😔", "😣", "👇", "👈", "👉", "☝", "👆", "👮", "👮♀️", "💩", "👝", "😾", "🙎♂️", "🙎", "🙏", "🤰", "🤴", "👸", "👛", "😡", "🤚", "✋", "🖐", "🙌", "🙋♂️", "🙋", "☺️", "😌", "⛑", "💍", "🤖", "🤣", "🙄", "🏃", "🏃♀️", "👡", "🎅", "🎒", "😱", "🙀", "🤳", "💀", "😴", "😪", "🙁", "🙂", "😄", "😸", "😃", "😺", "😈", "😏", "😼", "🤧", "😭", "🗣", "😛", "😝", "😜", "😎", "😓", "😅", "🤔", "💁♂️", "💁", "😫", "👅", "🎩", "😤", "👕", "👬", "👭", "😒", "🙃", "✌", "🖖", "🚶", "🚶♀️", "👋", "😩", "😉", "👩", "👩🎨", "👩🚀", "👩🍳", "🤦♀️", "👩🏭", "👩🌾", "👩🚒", "👩⚕️", "👩⚖️", "👩🔧", "👩💼", "👩✈️", "👩🔬", "🤷", "👩🎤", "👩🎓", "👩🏫", "👩💻", "👳♀️", "👚", "👒", "😟", "✍", "😋", "🤐", "💤", "🐜", "🐤", "🎍", "🦇", "🐻", "🐞", "🐦", "🌼", "🐡", "🐗", "💥", "💐", "🐛", "🦋", "🌵", "🐫", "🐱", "🐈", "🌸", "🌰", "🐔", "🐿", "🎄", "☁️", "🌩", "⛈", "🌧", "🌨", "☄", "🐮", "🐄", "🦀", "🌙", "🐊", "💨", "🌳", "🦌", "💫", "🐶", "🐕", "🐬", "🕊", "🐉", "🐲", "🐪", "💧", "🦆", "🦅", "🌾", "🌍", "🌎", "🌏", "🐘", "🌲", "🍂", "🔥", "🌓", "🌛", "🐟", "🌫", "🍀", "🦊", "🐸", "🌕", "🌝", "🐐", "🦍", "🐹", "🐥", "🐣", "🙉", "🌿", "🌺", "🐝", "🐴", "🎃", "🐨", "🌗", "🌜", "🍃", "🐆", "🦁", "🦎", "🍁", "🐒", "🐵", "🐭", "🐁", "🍄", "🌑", "🌚", "🌊", "🐙", "☂", "🦉", "🐂", "🌴", "🐼", "⛅", "🐾", "🐧", "🐷", "🐖", "🐽", "🐩", "🐰", "🐇", "🐎", "🐏", "🐀", "🦏", "🐓", "🌹", "🦂", "🙈", "🌱", "☘", "🦈", "🐑", "🐚", "🦐", "🐌", "🐍", "❄️", "⛄", "☃", "✨", "🙊", "🕷", "🕸", "🦑", "⭐", "🌟", "🌥", "🌦", "🌤", "🌞", "🌻", "☀️", "💦", "🎋", "🐯", "🐅", "🌪", "🐠", "🌷", "🦃", "🐢", "☔", "🦄", "🌘", "🌖", "🐃", "🌒", "🌔", "🐳", "🐋", "🥀", "🌬", "🐺", "⚡", "🍎", "🥑", "🍼", "🥓", "🥖", "🍌", "🍺", "🍻", "🍱", "🎂", "🍞", "🌯", "🍰", "🍬", "🥕", "🍾", "🧀", "🍒", "🍫", "🥂", "🍸", "☕", "🍪", "🌽", "🥐", "🥒", "🍛", "🍮", "🍡", "🍩", "🥚", "🍆", "🍥", "🍴", "🍳", "🍤", "🍟", "🍇", "🍏", "🥗", "🍔", "🍯", "🌶", "🌭", "🍨", "🍦", "🥝", "🍋", "🍭", "🍖", "🍈", "🥛", "🍢", "🥞", "🍑", "🥜", "🍐", "🍍", "🍕", "🍽", "🍿", "🥔", "🍗", "🍜", "🍚", "🍙", "🍘", "🍶", "🥘", "🍧", "🍝", "🥄", "🍲", "🍓", "🥙", "🍣", "🍠", "🌮", "🍊", "🍵", "🍅", "🍹", "🥃", "🍉", "🍷", "🥇", "🥈", "🥉", "🎱", "🎨", "🏸", "⚾", "🏀", "⛹", "⛹️♀️", "🛀", "🚴", "🚴♀️", "🏹", "🎳", "🥊", "🕴", "🎪", "🎬", "🏏", "🎯", "🥁", "🏑", "🎣", "🏈", "🎲", "🥅", "⛳", "🏌", "🏌️♀️", "🎸", "🎧", "🏇", "🏒", "⛸", "🤸♂️", "🤹♂️", "🤾♂️", "🤽♂️", "🥋", "🎖", "🏅", "🤼♂️", "🎤", "🚵", "🚵♀️", "🎹", "🎼", "🎭", "🤺", "🏓", "🎗", "🏵", "🚣", "🚣♀️", "🏉", "🎽", "🎷", "🎿", "⛷", "🎰", "🏂", "⚽", "👾", "🏄", "🏄♀️", "🏊", "🏊♀️", "🎾", "🎫", "🎟", "🏆", "🎺", "🎮", "🎻", "🏐", "🏋", "🏋️♀️", "🤸♀️", "🤹♀️", "🤾♀️", "🤽♀️", "🤼♀️", "🚡", "✈️", "🚑", "⚓", "🚛", "🛰", "🏦", "🏖", "🚲", "🚙", "🌉", "🏗", "🚅", "🚄", "🚌", "🚏", "🏕", "🛶", "🎠", "🏁", "⛪", "🌇", "🌆", "🏙", "🏛", "🚧", "🏪", "🏬", "🏚", "🏜", "🏝", "🏰", "🏤", "🏭", "🎡", "⛴", "🚒", "🎆", "🛬", "🛫", "🌁", "⛲", "⛽", "🚁", "🏥", "🏨", "🏠", "🏡", "🏘", "🗾", "🏯", "🕋", "🛴", "🚈", "🏩", "🚇", "🌌", "🚐", "🚝", "🕌", "🛥", "🛵", "🏍", "🛣", "🗻", "⛰", "🚠", "🚞", "🏔", "🏞", "🌃", "🏢", "🚘", "🚍", "🚔", "🚖", "🛳", "🚓", "🏣", "🏎", "🚃", "🛤", "🌈", "🚗", "🎑", "🚀", "🎢", "🚨", "⛵", "🏫", "💺", "⛩", "🚢", "🛩", "🎇", "🚤", "🏟", "🌠", "🚉", "🗽", "🚂", "🌅", "🌄", "🚟", "🕍", "🚕", "⛺", "🗼", "🚜", "🚥", "🚋", "🚆", "🚊", "🚎", "🚚", "🚦", "🌋", "💒", "⏰", "⚗", "🏺", "⚖", "🎈", "🗳", "📊", "💈", "🛁", "🔋", "🛏", "🛎", "🏴", "✒️", "📘", "💣", "🔖", "📑", "📚", "💡", "📆", "📲", "📷", "📸", "🕯", "🗃", "📇", "🗂", "💿", "⛓", "📉", "📈", "🗜", "📋", "📕", "🔐", "⚰", "💻", "🖱", "🎊", "🎛", "🛋", "🖍", "💳", "🎌", "⚔", "🔮", "🗡", "📅", "🖥", "💵", "🎎", "🚪", "📀", "📧", "🔌", "✉️", "📩", "💶", "📠", "🗄", "📁", "📽", "🎞", "🎏", "🔦", "💾", "🖋", "🖼", "⚱", "⚙", "💎", "🎁", "📗", "🔫", "🔨", "⚒", "🛠", "🔪", "🕳", "⌛", "⏳", "📥", "📨", "📱", "🏮", "🕹", "🔑", "⌨", "🏷", "📒", "🎚", "🔗", "🔒", "🔏", "💌", "🔍", "🔎", "📫", "📪", "📬", "📭", "🕰", "📝", "🔬", "💽", "💸", "💰", "🎥", "🗿", "📰", "🗞", "📓", "📔", "🔩", "🛢", "🗝", "📖", "📂", "📙", "📤", "📦", "📄", "📃", "📟", "🖌", "📎", "🖇", "⛱", "🖊", "✏️", "☎️", "⛏", "💊", "📯", "📮", "💷", "📿", "🖨", "📌", "📻", "🏳️🌈", "🎀", "📍", "📡", "✂️", "📜", "🛡", "🛍", "🛒", "🚿", "☠", "🛌", "🚬", "🗓", "🗒", "⏱", "📏", "🎙", "💉", "🎉", "📞", "🔭", "🌡", "⏲", "🚽", "🖲", "🚩", "📐", "📺", "🔓", "📼", "📹", "🗑", "⌚", "🏳", "🎐", "🗺", "🔧", "💴", "💯", "🔢", "🅰️", "🆎", "🔤", "🔡", "🉑", "💢", "♒", "♈", "◀️", "⏬", "⏫", "⬇️", "🔽", "▶️", "⤵️", "⤴️", "⬅️", "↙️", "↘️", "➡️", "↪️", "⬆️", "↕️", "🔼", "↖️", "↗️", "🔃", "🔄", "*⃣", "🏧", "⚛", "🅱️", "🚼", "🔙", "🛄", "☑️", "‼️", "🔰", "🔔", "☣", "⚫", "🖤", "🃏", "⬛", "◾", "◼️", "▪️", "🔲", "💙", "💔", "♋", "🔠", "♑", "💹", "🚸", "🎦", "🆑", "🕐", "🕙", "🕥", "🕚", "🕦", "🕛", "🕧", "🕜", "🕑", "🕝", "🕒", "🕞", "🕓", "🕟", "🕔", "🕠", "🕕", "🕡", "🕖", "🕢", "🕗", "🕣", "🕘", "🕤", "♣️", "㊗️", "🆒", "©️", "💘", "➰", "💱", "🛃", "🌀", "💠", "♦️", "🚯", "8️⃣", "✴️", "✳️", "🔚", "❗", "⏩", "5️⃣", "⚜", "🎴", "4️⃣", "🆓", "♊", "💝", "🌐", "💚", "❕", "❔", "#️⃣", "❤️", "💟", "💓", "💗", "♥️", "✔️", "➗", "💲", "❣", "➖", "✖️", "➕", "🔆", "♨️", "🆔", "🉐", "ℹ️", "⁉️", "🔟", "🈁", "🔵", "🔷", "🔶", "✝", "🛅", "↔️", "↩️", "♌", "♎", "➿", "🔊", "📢", "🔅", "Ⓜ️", "🀄", "📣", "🕎", "🚹", "📴", "🎵", "🔇", "📛", "❎", "🆕", "⏭", "🆖", "9️⃣", "🔕", "🚳", "⛔", "🚫", "📵", "🚷", "🚭", "🚱", "🎶", "⭕", "🅾️", "🆗", "🕉", "🔛", "1️⃣", "⛎", "☦", "🅿️", "〽️", "🛂", "⏸", "☮", "♓", "🛐", "⏯", "🚰", "⏮", "💜", "🚮", "❓", "🔘", "☢", "⏺", "♻️", "🔴", "®️", "🔁", "🔂", "🚻", "💞", "⏪", "🗯", "🈂️", "♐", "♏", "㊙️", "7️⃣", "📶", "6️⃣", "🔯", "🔹", "🔸", "🔺", "🔻", "🔜", "🆘", "🔉", "♠️", "❇️", "💖", "🔈", "💬", "☪", "✡", "⏹", "🛑", "🔣", "♉", "💭", "3️⃣", "™️", "🔝", "🔱", "🔀", "2️⃣", "💕", "🈹", "🈴", "🈺", "🈯", "🈷️", "🈶", "🈵", "🈚", "🈸", "🈲", "🈳", "🔞", "🆙", "📳", "♍", "🆚", "⚠️", "〰️", "🚾", "☸", "♿", "✅", "⚪", "💮", "⬜", "◽", "◻️", "▫️", "🔳", "🚺", "❌", "💛", "☯", "0️⃣", "🇦🇫", "🇦🇽", "🇦🇱", "🇩🇿", "🇦🇸", "🇦🇩", "🇦🇴", "🇦🇮", "🇦🇶", "🇦🇬", "🇦🇷", "🇦🇲", "🇦🇼", "🇦🇺", "🇦🇹", "🇦🇿", "🇧🇸", "🇧🇭", "🇧🇩", "🇧🇧", "🇧🇾", "🇧🇪", "🇧🇿", "🇧🇯", "🇧🇲", "🇧🇹", "🇧🇴", "🇧🇦", "🇧🇼", "🇧🇷", "🇮🇴", "🇻🇬", "🇧🇳", "🇧🇬", "🇧🇫", "🇧🇮", "🇰🇭", "🇨🇲", "🇨🇦", "🇮🇨", "🇨🇻", "🇧🇶", "🇰🇾", "🇨🇫", "🇹🇩", "🇨🇱", "🇨🇽", "🇨🇳", "🇨🇨", "🇨🇴", "🇰🇲", "🇨🇬", "🇨🇩", "🇨🇰", "🇨🇷", "🇨🇮", "🇭🇷", "🇨🇺", "🇨🇼", "🇨🇾", "🇨🇿", "🇩🇪", "🇩🇰", "🇩🇯", "🇩🇲", "🇩🇴", "🇪🇨", "🇪🇬", "🇸🇻", "🇬🇶", "🇪🇷", "🇪🇸", "🇪🇪", "🇪🇹", "🇪🇺", "🇫🇰", "🇫🇴", "🇫🇯", "🇫🇮", "🇫🇷", "🇬🇫", "🇵🇫", "🇹🇫", "🇬🇦", "🇬🇲", "🇬🇪", "🇬🇭", "🇬🇮", "🇬🇷", "🇬🇱", "🇬🇩", "🇬🇵", "🇬🇺", "🇬🇹", "🇬🇬", "🇬🇳", "🇬🇼", "🇬🇾", "🇭🇹", "🇭🇳", "🇭🇰", "🇭🇺", "🇮🇸", "🇮🇳", "🇮🇩", "🇮🇷", "🇮🇶", "🇮🇪", "🇮🇲", "🇮🇱", "🇮🇹", "🇯🇲", "🇯🇪", "🇯🇴", "🇯🇵", "🇰🇿", "🇰🇪", "🇰🇮", "🇽🇰", "🇰🇷", "🇰🇼", "🇰🇬", "🇱🇦", "🇱🇻", "🇱🇧", "🇱🇸", "🇱🇷", "🇱🇾", "🇱🇮", "🇱🇹", "🇱🇺", "🇲🇴", "🇲🇰", "🇲🇬", "🇲🇼", "🇲🇾", "🇲🇻", "🇲🇱", "🇲🇹", "🇲🇭", "🇲🇶", "🇲🇷", "🇲🇺", "🇾🇹", "🇲🇽", "🇫🇲", "🇲🇩", "🇲🇨", "🇲🇳", "🇲🇪", "🇲🇸", "🇲🇦", "🇲🇿", "🇲🇲", "🇳🇦", "🇳🇷", "🇳🇵", "🇳🇱", "🇳🇨", "🇳🇿", "🇳🇮", "🇳🇪", "🇳🇬", "🇳🇺", "🇳🇫", "🇰🇵", "🇲🇵", "🇳🇴", "🇴🇲", "🇵🇰", "🇵🇼", "🇵🇸", "🇵🇦", "🇵🇬", "🇵🇾", "🇵🇪", "🇵🇭", "🇵🇳", "🇵🇱", "🇵🇹", "🇵🇷", "🇶🇦", "🇷🇪", "🇷🇴", "🇷🇺", "🇷🇼", "🇼🇸", "🇸🇲", "🇸🇹", "🇸🇦", "🇸🇳", "🇷🇸", "🇸🇨", "🇸🇱", "🇸🇬", "🇸🇽", "🇸🇰", "🇸🇮", "🇸🇧", "🇸🇴", "🇿🇦", "🇬🇸", "🇸🇸", "🇱🇰", "🇧🇱", "🇸🇭", "🇰🇳", "🇱🇨", "🇵🇲", "🇻🇨", "🇸🇩", "🇸🇷", "🇸🇿", "🇸🇪", "🇨🇭", "🇸🇾", "🇹🇼", "🇹🇯", "🇹🇿", "🇹🇭", "🇹🇱", "🇹🇬", "🇹🇰", "🇹🇴", "🇹🇷", "🇹🇹", "🇹🇳", "🇹🇲", "🇹🇨", "🇹🇻", "🇺🇬", "🇬🇧", "🇺🇦", "🇦🇪", "🇺🇾", "🇺🇸", "🇻🇮", "🇺🇿", "🇻🇺", "🇻🇦", "🇻🇪", "🇻🇳", "🇼🇫", "🇪🇭", "🇾🇪", "🇿🇲", "🇿🇼"}
26 |
27 | // New initializes the alias generator and returns a new instance
28 | func New(c *Config) *Provider {
29 | c.randSource = rand.New(rand.NewSource(time.Now().UnixNano()))
30 |
31 | provider := &Provider{
32 | Config: c,
33 | }
34 |
35 | return provider
36 | }
37 |
38 | // Generate returns a random alias
39 | func (p *Provider) Generate() string {
40 | var (
41 | b strings.Builder
42 | n = len(emojis)
43 | )
44 |
45 | for i := 0; i < p.Config.Length; i++ {
46 | b.WriteString(emojis[p.Config.randSource.Intn(n)])
47 | }
48 |
49 | return b.String()
50 | }
51 |
--------------------------------------------------------------------------------
/alias/main.go:
--------------------------------------------------------------------------------
1 | package alias
2 |
3 | // A Provider implements all the necessary functions for an alias generator
4 | type Provider interface {
5 | Generate() string
6 | }
7 |
--------------------------------------------------------------------------------
/alias/memorable/main.go:
--------------------------------------------------------------------------------
1 | package memorable
2 |
3 | import (
4 | "math/rand"
5 | "strings"
6 | "time"
7 |
8 | "github.com/kamaln7/klein/alias"
9 | )
10 |
11 | // Provider implements an alias generator
12 | type Provider struct {
13 | Config *Config
14 | }
15 |
16 | // ensure that the storage.Provider interface is implemented
17 | var _ alias.Provider = new(Provider)
18 |
19 | // Config contains the configuration for the file storage
20 | type Config struct {
21 | Length int
22 | randSource *rand.Rand
23 | }
24 |
25 | // New initializes the alias generator and returns a new instance
26 | func New(c *Config) *Provider {
27 | c.randSource = rand.New(rand.NewSource(time.Now().UnixNano()))
28 |
29 | return &Provider{
30 | Config: c,
31 | }
32 | }
33 |
34 | // Generate returns a random alias
35 | func (p *Provider) Generate() string {
36 | var (
37 | output = ""
38 | length = len(wordlist)
39 | )
40 |
41 | for i := 0; i < p.Config.Length; i++ {
42 | output += strings.Title(wordlist[p.Config.randSource.Intn(length)])
43 | }
44 |
45 | return output
46 | }
47 |
--------------------------------------------------------------------------------
/alias/memorable/wordlist.go:
--------------------------------------------------------------------------------
1 | package memorable
2 |
3 | var wordlist = []string{
4 | "a",
5 | "ability",
6 | "able",
7 | "about",
8 | "above",
9 | "accept",
10 | "according",
11 | "account",
12 | "across",
13 | "act",
14 | "action",
15 | "activity",
16 | "actually",
17 | "add",
18 | "address",
19 | "administration",
20 | "admit",
21 | "adult",
22 | "affect",
23 | "after",
24 | "again",
25 | "against",
26 | "age",
27 | "agency",
28 | "agent",
29 | "ago",
30 | "agree",
31 | "agreement",
32 | "ahead",
33 | "air",
34 | "all",
35 | "allow",
36 | "almost",
37 | "alone",
38 | "along",
39 | "already",
40 | "also",
41 | "although",
42 | "always",
43 | "American",
44 | "among",
45 | "amount",
46 | "analysis",
47 | "and",
48 | "animal",
49 | "another",
50 | "answer",
51 | "any",
52 | "anyone",
53 | "anything",
54 | "appear",
55 | "apply",
56 | "approach",
57 | "area",
58 | "argue",
59 | "arm",
60 | "around",
61 | "arrive",
62 | "art",
63 | "article",
64 | "artist",
65 | "as",
66 | "ask",
67 | "assume",
68 | "at",
69 | "attack",
70 | "attention",
71 | "attorney",
72 | "audience",
73 | "author",
74 | "authority",
75 | "available",
76 | "avoid",
77 | "away",
78 | "baby",
79 | "back",
80 | "bad",
81 | "bag",
82 | "ball",
83 | "bank",
84 | "bar",
85 | "base",
86 | "be",
87 | "beat",
88 | "beautiful",
89 | "because",
90 | "become",
91 | "bed",
92 | "before",
93 | "begin",
94 | "behavior",
95 | "behind",
96 | "believe",
97 | "benefit",
98 | "best",
99 | "better",
100 | "between",
101 | "beyond",
102 | "big",
103 | "bill",
104 | "billion",
105 | "bit",
106 | "black",
107 | "blood",
108 | "blue",
109 | "board",
110 | "body",
111 | "book",
112 | "born",
113 | "both",
114 | "box",
115 | "boy",
116 | "break",
117 | "bring",
118 | "brother",
119 | "budget",
120 | "build",
121 | "building",
122 | "business",
123 | "but",
124 | "buy",
125 | "by",
126 | "call",
127 | "camera",
128 | "campaign",
129 | "can",
130 | "cancer",
131 | "candidate",
132 | "capital",
133 | "car",
134 | "card",
135 | "care",
136 | "career",
137 | "carry",
138 | "case",
139 | "catch",
140 | "cause",
141 | "cell",
142 | "center",
143 | "central",
144 | "century",
145 | "certain",
146 | "certainly",
147 | "chair",
148 | "challenge",
149 | "chance",
150 | "change",
151 | "character",
152 | "charge",
153 | "check",
154 | "child",
155 | "choice",
156 | "choose",
157 | "church",
158 | "citizen",
159 | "city",
160 | "civil",
161 | "claim",
162 | "class",
163 | "clear",
164 | "clearly",
165 | "close",
166 | "coach",
167 | "cold",
168 | "collection",
169 | "college",
170 | "color",
171 | "come",
172 | "commercial",
173 | "common",
174 | "community",
175 | "company",
176 | "compare",
177 | "computer",
178 | "concern",
179 | "condition",
180 | "conference",
181 | "Congress",
182 | "consider",
183 | "consumer",
184 | "contain",
185 | "continue",
186 | "control",
187 | "cost",
188 | "could",
189 | "country",
190 | "couple",
191 | "course",
192 | "court",
193 | "cover",
194 | "create",
195 | "crime",
196 | "cultural",
197 | "culture",
198 | "cup",
199 | "current",
200 | "customer",
201 | "cut",
202 | "dark",
203 | "data",
204 | "daughter",
205 | "day",
206 | "dead",
207 | "deal",
208 | "death",
209 | "debate",
210 | "decade",
211 | "decide",
212 | "decision",
213 | "deep",
214 | "defense",
215 | "degree",
216 | "Democrat",
217 | "democratic",
218 | "describe",
219 | "design",
220 | "despite",
221 | "detail",
222 | "determine",
223 | "develop",
224 | "development",
225 | "die",
226 | "difference",
227 | "different",
228 | "difficult",
229 | "dinner",
230 | "direction",
231 | "director",
232 | "discover",
233 | "discuss",
234 | "discussion",
235 | "disease",
236 | "do",
237 | "doctor",
238 | "dog",
239 | "door",
240 | "down",
241 | "draw",
242 | "dream",
243 | "drive",
244 | "drop",
245 | "drug",
246 | "during",
247 | "each",
248 | "early",
249 | "east",
250 | "easy",
251 | "eat",
252 | "economic",
253 | "economy",
254 | "edge",
255 | "education",
256 | "effect",
257 | "effort",
258 | "eight",
259 | "either",
260 | "election",
261 | "else",
262 | "employee",
263 | "end",
264 | "energy",
265 | "enjoy",
266 | "enough",
267 | "enter",
268 | "entire",
269 | "environment",
270 | "environmental",
271 | "especially",
272 | "establish",
273 | "even",
274 | "evening",
275 | "event",
276 | "ever",
277 | "every",
278 | "everybody",
279 | "everyone",
280 | "everything",
281 | "evidence",
282 | "exactly",
283 | "example",
284 | "executive",
285 | "exist",
286 | "expect",
287 | "experience",
288 | "expert",
289 | "explain",
290 | "eye",
291 | "face",
292 | "fact",
293 | "factor",
294 | "fail",
295 | "fall",
296 | "family",
297 | "far",
298 | "fast",
299 | "father",
300 | "fear",
301 | "federal",
302 | "feel",
303 | "feeling",
304 | "few",
305 | "field",
306 | "fight",
307 | "figure",
308 | "fill",
309 | "film",
310 | "final",
311 | "finally",
312 | "financial",
313 | "find",
314 | "fine",
315 | "finger",
316 | "finish",
317 | "fire",
318 | "firm",
319 | "first",
320 | "fish",
321 | "five",
322 | "floor",
323 | "fly",
324 | "focus",
325 | "follow",
326 | "food",
327 | "foot",
328 | "for",
329 | "force",
330 | "foreign",
331 | "forget",
332 | "form",
333 | "former",
334 | "forward",
335 | "four",
336 | "free",
337 | "friend",
338 | "from",
339 | "front",
340 | "full",
341 | "fund",
342 | "future",
343 | "game",
344 | "garden",
345 | "gas",
346 | "general",
347 | "generation",
348 | "get",
349 | "girl",
350 | "give",
351 | "glass",
352 | "go",
353 | "goal",
354 | "good",
355 | "government",
356 | "great",
357 | "green",
358 | "ground",
359 | "group",
360 | "grow",
361 | "growth",
362 | "guess",
363 | "gun",
364 | "guy",
365 | "hair",
366 | "half",
367 | "hand",
368 | "hang",
369 | "happen",
370 | "happy",
371 | "hard",
372 | "have",
373 | "he",
374 | "head",
375 | "health",
376 | "hear",
377 | "heart",
378 | "heat",
379 | "heavy",
380 | "help",
381 | "her",
382 | "here",
383 | "herself",
384 | "high",
385 | "him",
386 | "himself",
387 | "his",
388 | "history",
389 | "hit",
390 | "hold",
391 | "home",
392 | "hope",
393 | "hospital",
394 | "hot",
395 | "hotel",
396 | "hour",
397 | "house",
398 | "how",
399 | "however",
400 | "huge",
401 | "human",
402 | "hundred",
403 | "husband",
404 | "I",
405 | "idea",
406 | "identify",
407 | "if",
408 | "image",
409 | "imagine",
410 | "impact",
411 | "important",
412 | "improve",
413 | "in",
414 | "include",
415 | "including",
416 | "increase",
417 | "indeed",
418 | "indicate",
419 | "individual",
420 | "industry",
421 | "information",
422 | "inside",
423 | "instead",
424 | "institution",
425 | "interest",
426 | "interesting",
427 | "international",
428 | "interview",
429 | "into",
430 | "investment",
431 | "involve",
432 | "issue",
433 | "it",
434 | "item",
435 | "its",
436 | "itself",
437 | "job",
438 | "join",
439 | "just",
440 | "keep",
441 | "key",
442 | "kid",
443 | "kill",
444 | "kind",
445 | "kitchen",
446 | "know",
447 | "knowledge",
448 | "land",
449 | "language",
450 | "large",
451 | "last",
452 | "late",
453 | "later",
454 | "laugh",
455 | "law",
456 | "lawyer",
457 | "lay",
458 | "lead",
459 | "leader",
460 | "learn",
461 | "least",
462 | "leave",
463 | "left",
464 | "leg",
465 | "legal",
466 | "less",
467 | "let",
468 | "letter",
469 | "level",
470 | "lie",
471 | "life",
472 | "light",
473 | "like",
474 | "likely",
475 | "line",
476 | "list",
477 | "listen",
478 | "little",
479 | "live",
480 | "local",
481 | "long",
482 | "look",
483 | "lose",
484 | "loss",
485 | "lot",
486 | "love",
487 | "low",
488 | "machine",
489 | "magazine",
490 | "main",
491 | "maintain",
492 | "major",
493 | "majority",
494 | "make",
495 | "man",
496 | "manage",
497 | "management",
498 | "manager",
499 | "many",
500 | "market",
501 | "marriage",
502 | "material",
503 | "matter",
504 | "may",
505 | "maybe",
506 | "me",
507 | "mean",
508 | "measure",
509 | "media",
510 | "medical",
511 | "meet",
512 | "meeting",
513 | "member",
514 | "memory",
515 | "mention",
516 | "message",
517 | "method",
518 | "middle",
519 | "might",
520 | "military",
521 | "million",
522 | "mind",
523 | "minute",
524 | "miss",
525 | "mission",
526 | "model",
527 | "modern",
528 | "moment",
529 | "money",
530 | "month",
531 | "more",
532 | "morning",
533 | "most",
534 | "mother",
535 | "mouth",
536 | "move",
537 | "movement",
538 | "movie",
539 | "Mr",
540 | "Mrs",
541 | "much",
542 | "music",
543 | "must",
544 | "my",
545 | "myself",
546 | "name",
547 | "nation",
548 | "national",
549 | "natural",
550 | "nature",
551 | "near",
552 | "nearly",
553 | "necessary",
554 | "need",
555 | "network",
556 | "never",
557 | "new",
558 | "news",
559 | "newspaper",
560 | "next",
561 | "nice",
562 | "night",
563 | "no",
564 | "none",
565 | "nor",
566 | "north",
567 | "not",
568 | "note",
569 | "nothing",
570 | "notice",
571 | "now",
572 | "n't",
573 | "number",
574 | "occur",
575 | "of",
576 | "off",
577 | "offer",
578 | "office",
579 | "officer",
580 | "official",
581 | "often",
582 | "oh",
583 | "oil",
584 | "ok",
585 | "old",
586 | "on",
587 | "once",
588 | "one",
589 | "only",
590 | "onto",
591 | "open",
592 | "operation",
593 | "opportunity",
594 | "option",
595 | "or",
596 | "order",
597 | "organization",
598 | "other",
599 | "others",
600 | "our",
601 | "out",
602 | "outside",
603 | "over",
604 | "own",
605 | "owner",
606 | "page",
607 | "pain",
608 | "painting",
609 | "paper",
610 | "parent",
611 | "part",
612 | "participant",
613 | "particular",
614 | "particularly",
615 | "partner",
616 | "party",
617 | "pass",
618 | "past",
619 | "patient",
620 | "pattern",
621 | "pay",
622 | "peace",
623 | "people",
624 | "per",
625 | "perform",
626 | "performance",
627 | "perhaps",
628 | "period",
629 | "person",
630 | "personal",
631 | "phone",
632 | "physical",
633 | "pick",
634 | "picture",
635 | "piece",
636 | "place",
637 | "plan",
638 | "plant",
639 | "play",
640 | "player",
641 | "PM",
642 | "point",
643 | "police",
644 | "policy",
645 | "political",
646 | "politics",
647 | "poor",
648 | "popular",
649 | "population",
650 | "position",
651 | "positive",
652 | "possible",
653 | "power",
654 | "practice",
655 | "prepare",
656 | "present",
657 | "president",
658 | "pressure",
659 | "pretty",
660 | "prevent",
661 | "price",
662 | "private",
663 | "probably",
664 | "problem",
665 | "process",
666 | "produce",
667 | "product",
668 | "production",
669 | "professional",
670 | "professor",
671 | "program",
672 | "project",
673 | "property",
674 | "protect",
675 | "prove",
676 | "provide",
677 | "public",
678 | "pull",
679 | "purpose",
680 | "push",
681 | "put",
682 | "quality",
683 | "question",
684 | "quickly",
685 | "quite",
686 | "race",
687 | "radio",
688 | "raise",
689 | "range",
690 | "rate",
691 | "rather",
692 | "reach",
693 | "read",
694 | "ready",
695 | "real",
696 | "reality",
697 | "realize",
698 | "really",
699 | "reason",
700 | "receive",
701 | "recent",
702 | "recently",
703 | "recognize",
704 | "record",
705 | "red",
706 | "reduce",
707 | "reflect",
708 | "region",
709 | "relate",
710 | "relationship",
711 | "religious",
712 | "remain",
713 | "remember",
714 | "remove",
715 | "report",
716 | "represent",
717 | "Republican",
718 | "require",
719 | "research",
720 | "resource",
721 | "respond",
722 | "response",
723 | "responsibility",
724 | "rest",
725 | "result",
726 | "return",
727 | "reveal",
728 | "rich",
729 | "right",
730 | "rise",
731 | "risk",
732 | "road",
733 | "rock",
734 | "role",
735 | "room",
736 | "rule",
737 | "run",
738 | "safe",
739 | "same",
740 | "save",
741 | "say",
742 | "scene",
743 | "school",
744 | "science",
745 | "scientist",
746 | "score",
747 | "sea",
748 | "season",
749 | "seat",
750 | "second",
751 | "section",
752 | "security",
753 | "see",
754 | "seek",
755 | "seem",
756 | "sell",
757 | "send",
758 | "senior",
759 | "sense",
760 | "series",
761 | "serious",
762 | "serve",
763 | "service",
764 | "set",
765 | "seven",
766 | "several",
767 | "sex",
768 | "sexual",
769 | "shake",
770 | "share",
771 | "she",
772 | "shoot",
773 | "short",
774 | "shot",
775 | "should",
776 | "shoulder",
777 | "show",
778 | "side",
779 | "sign",
780 | "significant",
781 | "similar",
782 | "simple",
783 | "simply",
784 | "since",
785 | "sing",
786 | "single",
787 | "sister",
788 | "sit",
789 | "site",
790 | "situation",
791 | "six",
792 | "size",
793 | "skill",
794 | "skin",
795 | "small",
796 | "smile",
797 | "so",
798 | "social",
799 | "society",
800 | "soldier",
801 | "some",
802 | "somebody",
803 | "someone",
804 | "something",
805 | "sometimes",
806 | "son",
807 | "song",
808 | "soon",
809 | "sort",
810 | "sound",
811 | "source",
812 | "south",
813 | "southern",
814 | "space",
815 | "speak",
816 | "special",
817 | "specific",
818 | "speech",
819 | "spend",
820 | "sport",
821 | "spring",
822 | "staff",
823 | "stage",
824 | "stand",
825 | "standard",
826 | "star",
827 | "start",
828 | "state",
829 | "statement",
830 | "station",
831 | "stay",
832 | "step",
833 | "still",
834 | "stock",
835 | "stop",
836 | "store",
837 | "story",
838 | "strategy",
839 | "street",
840 | "strong",
841 | "structure",
842 | "student",
843 | "study",
844 | "stuff",
845 | "style",
846 | "subject",
847 | "success",
848 | "successful",
849 | "such",
850 | "suddenly",
851 | "suffer",
852 | "suggest",
853 | "summer",
854 | "support",
855 | "sure",
856 | "surface",
857 | "system",
858 | "table",
859 | "take",
860 | "talk",
861 | "task",
862 | "tax",
863 | "teach",
864 | "teacher",
865 | "team",
866 | "technology",
867 | "television",
868 | "tell",
869 | "ten",
870 | "tend",
871 | "term",
872 | "test",
873 | "than",
874 | "thank",
875 | "that",
876 | "the",
877 | "their",
878 | "them",
879 | "themselves",
880 | "then",
881 | "theory",
882 | "there",
883 | "these",
884 | "they",
885 | "thing",
886 | "think",
887 | "third",
888 | "this",
889 | "those",
890 | "though",
891 | "thought",
892 | "thousand",
893 | "threat",
894 | "three",
895 | "through",
896 | "throughout",
897 | "throw",
898 | "thus",
899 | "time",
900 | "to",
901 | "today",
902 | "together",
903 | "tonight",
904 | "too",
905 | "top",
906 | "total",
907 | "tough",
908 | "toward",
909 | "town",
910 | "trade",
911 | "traditional",
912 | "training",
913 | "travel",
914 | "treat",
915 | "treatment",
916 | "tree",
917 | "trial",
918 | "trip",
919 | "trouble",
920 | "true",
921 | "truth",
922 | "try",
923 | "turn",
924 | "TV",
925 | "two",
926 | "type",
927 | "under",
928 | "understand",
929 | "unit",
930 | "until",
931 | "up",
932 | "upon",
933 | "us",
934 | "use",
935 | "usually",
936 | "value",
937 | "various",
938 | "very",
939 | "victim",
940 | "view",
941 | "violence",
942 | "visit",
943 | "voice",
944 | "vote",
945 | "wait",
946 | "walk",
947 | "wall",
948 | "want",
949 | "war",
950 | "watch",
951 | "water",
952 | "way",
953 | "we",
954 | "weapon",
955 | "wear",
956 | "week",
957 | "weight",
958 | "well",
959 | "west",
960 | "western",
961 | "what",
962 | "whatever",
963 | "when",
964 | "where",
965 | "whether",
966 | "which",
967 | "while",
968 | "white",
969 | "who",
970 | "whole",
971 | "whom",
972 | "whose",
973 | "why",
974 | "wide",
975 | "wife",
976 | "will",
977 | "win",
978 | "wind",
979 | "window",
980 | "wish",
981 | "with",
982 | "within",
983 | "without",
984 | "woman",
985 | "wonder",
986 | "word",
987 | "work",
988 | "worker",
989 | "world",
990 | "worry",
991 | "would",
992 | "write",
993 | "writer",
994 | "wrong",
995 | "yard",
996 | "yeah",
997 | "year",
998 | "yes",
999 | "yet",
1000 | "you",
1001 | "young",
1002 | "your",
1003 | "yourself",
1004 | }
1005 |
--------------------------------------------------------------------------------
/auth/httpbasic/main.go:
--------------------------------------------------------------------------------
1 | package httpbasic
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/kamaln7/klein/auth"
7 | )
8 |
9 | // Provider implements an alias generator
10 | type Provider struct {
11 | Config *Config
12 | }
13 |
14 | // Config contains the config
15 | type Config struct {
16 | Username, Password string
17 | }
18 |
19 | // ensure that the auth.Provider interface is implemented
20 | var _ auth.Provider = new(Provider)
21 |
22 | // New initializes the auth provider and returns a new instance
23 | func New(c *Config) *Provider {
24 | return &Provider{
25 | Config: c,
26 | }
27 | }
28 |
29 | // Authenticate makes sure the right credentials are passed
30 | func (p *Provider) Authenticate(w http.ResponseWriter, r *http.Request) (bool, error) {
31 | if p.Config.Username == "" || p.Config.Password == "" {
32 | return false, nil
33 | }
34 |
35 | w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
36 |
37 | username, password, authOK := r.BasicAuth()
38 | if authOK == false {
39 | return false, nil
40 | }
41 |
42 | if username != p.Config.Username || password != p.Config.Password {
43 | return false, nil
44 | }
45 |
46 | return true, nil
47 | }
48 |
--------------------------------------------------------------------------------
/auth/main.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | // A Provider implements all the necessary functions for an authentication system
8 | type Provider interface {
9 | Authenticate(w http.ResponseWriter, r *http.Request) (bool, error)
10 | }
11 |
--------------------------------------------------------------------------------
/auth/statickey/main.go:
--------------------------------------------------------------------------------
1 | package statickey
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/kamaln7/klein/auth"
7 | )
8 |
9 | // Provider implements an alias generator
10 | type Provider struct {
11 | Config *Config
12 | }
13 |
14 | // Config contains the config
15 | type Config struct {
16 | Key string
17 | }
18 |
19 | // ensure that the storage.Provider interface is implemented
20 | var _ auth.Provider = new(Provider)
21 |
22 | // New initializes the alias generator and returns a new instance
23 | func New(c *Config) *Provider {
24 | return &Provider{
25 | Config: c,
26 | }
27 | }
28 |
29 | // Authenticate makes sure the right key is passed
30 | func (p *Provider) Authenticate(w http.ResponseWriter, r *http.Request) (bool, error) {
31 | key := r.FormValue("key")
32 |
33 | if key == "" || key != p.Config.Key {
34 | return false, nil
35 | }
36 |
37 | return true, nil
38 | }
39 |
--------------------------------------------------------------------------------
/auth/unauthenticated/main.go:
--------------------------------------------------------------------------------
1 | package unauthenticated
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/kamaln7/klein/auth"
7 | )
8 |
9 | // Provider implements an alias generator
10 | type Provider struct{}
11 |
12 | // ensure that the storage.Provider interface is implemented
13 | var _ auth.Provider = new(Provider)
14 |
15 | // New initializes the alias generator and returns a new instance
16 | func New() *Provider {
17 | return &Provider{}
18 | }
19 |
20 | // Authenticate lets everyone go through
21 | func (p *Provider) Authenticate(w http.ResponseWriter, r *http.Request) (bool, error) {
22 | return true, nil
23 | }
24 |
--------------------------------------------------------------------------------
/cmd/main.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "os"
8 | "strings"
9 | "time"
10 |
11 | "github.com/kamaln7/klein/alias"
12 | "github.com/kamaln7/klein/alias/alphanumeric"
13 | "github.com/kamaln7/klein/alias/emoji"
14 | "github.com/kamaln7/klein/alias/memorable"
15 | "github.com/kamaln7/klein/auth"
16 | "github.com/kamaln7/klein/auth/httpbasic"
17 | "github.com/kamaln7/klein/auth/statickey"
18 | "github.com/kamaln7/klein/auth/unauthenticated"
19 | "github.com/kamaln7/klein/server"
20 | "github.com/kamaln7/klein/storage"
21 | "github.com/kamaln7/klein/storage/bolt"
22 | "github.com/kamaln7/klein/storage/file"
23 | "github.com/kamaln7/klein/storage/memory"
24 | "github.com/kamaln7/klein/storage/postgresql"
25 | "github.com/kamaln7/klein/storage/redis"
26 | "github.com/kamaln7/klein/storage/spaces"
27 | "github.com/kamaln7/klein/storage/spacesstateless"
28 | "github.com/spf13/cobra"
29 | "github.com/spf13/viper"
30 | )
31 |
32 | var rootCmd = &cobra.Command{
33 | Use: "klein",
34 | Short: "klein is a minimalist URL shortener.",
35 | Long: "klein is a minimalist URL shortener.",
36 | Run: func(cmd *cobra.Command, args []string) {
37 | logger := log.New(os.Stdout, "[klein] ", log.Ldate|log.Ltime)
38 |
39 | // 404
40 | notFoundHTML := []byte("404 not found")
41 | notFoundPath := viper.GetString("error-template")
42 | if notFoundPath != "" {
43 | var err error
44 | notFoundHTML, err = ioutil.ReadFile(notFoundPath)
45 | if err != nil {
46 | logger.Fatal(err)
47 | return
48 | }
49 | }
50 |
51 | // auth
52 | var authProvider auth.Provider
53 | switch viper.GetString("auth.driver") {
54 | case "none":
55 | authProvider = unauthenticated.New()
56 | case "basic":
57 | username := viper.GetString("auth.basic.username")
58 | password := viper.GetString("auth.basic.password")
59 | if username == "" || password == "" {
60 | logger.Fatalf("You need to provide a username and password in order to use basic auth")
61 | }
62 |
63 | authProvider = httpbasic.New(&httpbasic.Config{
64 | Username: username,
65 | Password: password,
66 | })
67 | case "key":
68 | key := viper.GetString("auth.key")
69 | if key == "" {
70 | logger.Fatalf("You need to provide an auth key in order to use key auth")
71 | }
72 | authProvider = statickey.New(&statickey.Config{
73 | Key: key,
74 | })
75 | default:
76 | logger.Fatal("invalid auth driver")
77 | }
78 |
79 | // storage
80 | var storageProvider storage.Provider
81 | switch viper.GetString("storage.driver") {
82 | case "file":
83 | storageProvider = file.New(&file.Config{
84 | Path: viper.GetString("storage.file.path"),
85 | })
86 | case "boltdb":
87 | var err error
88 | storageProvider, err = bolt.New(&bolt.Config{
89 | Path: viper.GetString("storage.boltdb.path"),
90 | })
91 |
92 | if err != nil {
93 | logger.Fatalf("could not open bolt database: %s\n", err.Error())
94 | }
95 | case "redis":
96 | var err error
97 | storageProvider, err = redis.New(&redis.Config{
98 | Address: viper.GetString("storage.redis.address"),
99 | Auth: viper.GetString("storage.redis.auth"),
100 | DB: viper.GetInt("storage.redis.db"),
101 | })
102 |
103 | if err != nil {
104 | logger.Fatalf("could not open redis database: %s\n", err.Error())
105 | }
106 | case "spaces.stateful":
107 | accessKey := viper.GetString("storage.spaces.access_key")
108 | secretKey := viper.GetString("storage.spaces.secret_key")
109 | region := viper.GetString("storage.spaces.region")
110 | space := viper.GetString("storage.spaces.space")
111 |
112 | if accessKey == "" || secretKey == "" || region == "" || space == "" {
113 | logger.Fatalf("You need to provide an access key, secret key, region and space to use the spaces storage backend")
114 | }
115 |
116 | var err error
117 | storageProvider, err = spaces.New(&spaces.Config{
118 | AccessKey: accessKey,
119 | SecretKey: secretKey,
120 | Region: region,
121 | Space: space,
122 | Path: viper.GetString("storage.spaces.stateful.path"),
123 | })
124 |
125 | if err != nil {
126 | logger.Fatalf("could not connect to spaces: %s\n", err.Error())
127 | }
128 | case "spaces.stateless":
129 | accessKey := viper.GetString("storage.spaces.access_key")
130 | secretKey := viper.GetString("storage.spaces.secret_key")
131 | region := viper.GetString("storage.spaces.region")
132 | space := viper.GetString("storage.spaces.space")
133 |
134 | if accessKey == "" || secretKey == "" || region == "" || space == "" {
135 | logger.Fatalf("You need to provide an access key, secret key, region and space to use the spaces stateless storage backend")
136 | }
137 |
138 | var err error
139 | storageProvider, err = spacesstateless.New(&spacesstateless.Config{
140 | AccessKey: accessKey,
141 | SecretKey: secretKey,
142 | Region: region,
143 | Space: space,
144 | Path: viper.GetString("storage.spaces.stateless.path"),
145 | CacheDuration: viper.GetDuration("storage.spaces.stateless.cache-duration"),
146 | })
147 |
148 | if err != nil {
149 | logger.Fatalf("could not connect to spaces: %s\n", err.Error())
150 | }
151 | case "sql.pg":
152 | var err error
153 | storageProvider, err = postgresql.New(&postgresql.Config{
154 | Host: viper.GetString("storage.sql.pg.host"),
155 | Port: viper.GetInt32("storage.sql.pg.port"),
156 | User: viper.GetString("storage.sql.pg.user"),
157 | Password: viper.GetString("storage.sql.pg.password"),
158 | Database: viper.GetString("storage.sql.pg.database"),
159 | Table: viper.GetString("storage.sql.pg.table"),
160 | SSLMode: viper.GetString("storage.sql.pg.sslmode"),
161 | })
162 |
163 | if err != nil {
164 | logger.Fatalf("could not connect to postgresql: %s\n", err.Error())
165 | }
166 | case "memory":
167 | storageProvider = memory.New(&memory.Config{})
168 | default:
169 | logger.Fatal("invalid storage driver")
170 | }
171 |
172 | // alias
173 | var aliasProvider alias.Provider
174 | switch viper.GetString("alias.driver") {
175 | case "alphanumeric":
176 | var err error
177 | aliasProvider, err = alphanumeric.New(&alphanumeric.Config{
178 | Length: viper.GetInt("alias.alphanumeric.length"),
179 | Alpha: viper.GetBool("alias.alphanumeric.alpha"),
180 | Num: viper.GetBool("alias.alphanumeric.num"),
181 | })
182 |
183 | if err != nil {
184 | logger.Fatalf("could not select alphanumeric alias: %s\n", err.Error())
185 | }
186 | case "emoji":
187 | aliasProvider = emoji.New(&emoji.Config{
188 | Length: viper.GetInt("alias.emoji.length"),
189 | })
190 | case "memorable":
191 | aliasProvider = memorable.New(&memorable.Config{
192 | Length: viper.GetInt("alias.memorable.length"),
193 | })
194 | default:
195 | logger.Fatal("invalid alias driver")
196 | }
197 |
198 | // url
199 | publicURL := viper.GetString("url")
200 | if publicURL == "" {
201 | publicURL = fmt.Sprintf("http://%s/", viper.GetString("listen"))
202 | }
203 |
204 | // klein
205 | k := server.New(&server.Config{
206 | Alias: aliasProvider,
207 | Auth: authProvider,
208 | Storage: storageProvider,
209 | Log: logger,
210 |
211 | ListenAddr: viper.GetString("listen"),
212 | RootURL: viper.GetString("root"),
213 | PublicURL: publicURL,
214 | NotFoundHTML: notFoundHTML,
215 | })
216 |
217 | k.Serve()
218 | },
219 | }
220 |
221 | func init() {
222 | cobra.OnInitialize(initConfig)
223 |
224 | // General options
225 | rootCmd.PersistentFlags().String("error-template", "", "path to error template")
226 | rootCmd.PersistentFlags().String("url", "", "path to public facing url")
227 | rootCmd.PersistentFlags().String("listen", "127.0.0.1:5556", "listen address")
228 | rootCmd.PersistentFlags().String("root", "", "root redirect")
229 |
230 | // Alias options
231 | rootCmd.PersistentFlags().String("alias.driver", "alphanumeric", "what alias generation to use (alphanumeric, emoji, memorable)")
232 |
233 | rootCmd.PersistentFlags().Int("alias.alphanumeric.length", 5, "alphanumeric code length")
234 | rootCmd.PersistentFlags().Bool("alias.alphanumeric.alpha", true, "use letters in code")
235 | rootCmd.PersistentFlags().Bool("alias.alphanumeric.num", true, "use numbers in code")
236 |
237 | rootCmd.PersistentFlags().Int("alias.emoji.length", 6, "emoji count")
238 |
239 | rootCmd.PersistentFlags().Int("alias.memorable.length", 3, "memorable word count")
240 |
241 | // Auth options
242 | rootCmd.PersistentFlags().String("auth.driver", "none", "what auth backend to use (basic, key, none)")
243 |
244 | rootCmd.PersistentFlags().String("auth.key", "", "upload API key")
245 |
246 | rootCmd.PersistentFlags().String("auth.basic.username", "", "username for HTTP basic auth")
247 | rootCmd.PersistentFlags().String("auth.basic.password", "", "password for HTTP basic auth")
248 |
249 | // Storage options
250 | rootCmd.PersistentFlags().String("storage.driver", "file", "what storage backend to use (file, boltdb, redis, spaces.stateful, sql.pg, memory)")
251 |
252 | rootCmd.PersistentFlags().String("storage.file.path", "urls", "path to use for file store")
253 |
254 | rootCmd.PersistentFlags().String("storage.boltdb.path", "bolt.db", "path to use for bolt db")
255 |
256 | rootCmd.PersistentFlags().String("storage.redis.address", "127.0.0.1:6379", "address:port of redis instance")
257 | rootCmd.PersistentFlags().String("storage.redis.auth", "", "password to access redis")
258 | rootCmd.PersistentFlags().Int("storage.redis.db", 0, "db to select within redis")
259 |
260 | rootCmd.PersistentFlags().String("storage.spaces.access-key", "", "access key for spaces")
261 | rootCmd.PersistentFlags().String("storage.spaces.secret-key", "", "secret key for spaces")
262 | rootCmd.PersistentFlags().String("storage.spaces.region", "", "region for spaces")
263 | rootCmd.PersistentFlags().String("storage.spaces.space", "", "space to use")
264 |
265 | rootCmd.PersistentFlags().String("storage.spaces.stateful.path", "klein.json", "path of the file in spaces")
266 |
267 | rootCmd.PersistentFlags().String("storage.spaces.stateless.path", "/klein", "path of the directory in spaces to store urls in")
268 | rootCmd.PersistentFlags().Duration("storage.spaces.stateless.cache-duration", time.Minute, "time to cache spaces results in memory. 0 to disable")
269 |
270 | rootCmd.PersistentFlags().String("storage.sql.pg.host", "localhost", "postgresql host")
271 | rootCmd.PersistentFlags().Int32("storage.sql.pg.port", 5432, "postgresql port")
272 | rootCmd.PersistentFlags().String("storage.sql.pg.user", "klein", "postgresql user")
273 | rootCmd.PersistentFlags().String("storage.sql.pg.password", "secret", "postgresql password")
274 | rootCmd.PersistentFlags().String("storage.sql.pg.database", "klein", "postgresql database")
275 | rootCmd.PersistentFlags().String("storage.sql.pg.table", "klein", "postgresql table")
276 | rootCmd.PersistentFlags().String("storage.sql.pg.sslmode", "prefer", "postgresql sslmode")
277 |
278 | viper.BindPFlags(rootCmd.PersistentFlags())
279 | }
280 |
281 | func initConfig() {
282 | viper.SetEnvPrefix("klein")
283 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
284 | viper.AutomaticEnv()
285 | }
286 |
287 | // Execute executes the root command
288 | func Execute() {
289 | if err := rootCmd.Execute(); err != nil {
290 | fmt.Println(err)
291 | os.Exit(1)
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | func init() {
10 | rootCmd.AddCommand(versionCmd)
11 | }
12 |
13 | // Version is the klein package version
14 | // to be filled in at compile time using ldflags
15 | var Version = "-dev"
16 |
17 | var versionCmd = &cobra.Command{
18 | Use: "version",
19 | Short: "print the version number of klein",
20 | Long: "print the version number of klein",
21 | Run: func(cmd *cobra.Command, args []string) {
22 | fmt.Printf("klein v%s\n", Version)
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/kamaln7/klein
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/BurntSushi/toml v0.3.1 // indirect
7 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect
8 | github.com/alicebob/miniredis v2.5.0+incompatible
9 | github.com/aws/aws-sdk-go v1.17.2
10 | github.com/boltdb/bolt v1.3.1
11 | github.com/cockroachdb/apd v1.1.0 // indirect
12 | github.com/gomodule/redigo v2.0.0+incompatible // indirect
13 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
14 | github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
15 | github.com/jackc/pgx v3.3.0+incompatible
16 | github.com/jmoiron/sqlx v1.2.0
17 | github.com/mediocregopher/radix.v2 v0.0.0-20181115013041-b67df6e626f9
18 | github.com/patrickmn/go-cache v2.1.0+incompatible
19 | github.com/pkg/errors v0.8.1 // indirect
20 | github.com/satori/go.uuid v1.2.0 // indirect
21 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
22 | github.com/spf13/afero v1.2.1 // indirect
23 | github.com/spf13/cobra v0.0.3
24 | github.com/spf13/viper v1.3.1
25 | github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036 // indirect
26 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect
27 | google.golang.org/appengine v1.6.1 // indirect
28 | )
29 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U=
4 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
5 | github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI=
6 | github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
7 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
8 | github.com/aws/aws-sdk-go v1.17.2 h1:92HvIn2MROLHcidibvnzy7D0iHCygmonkNQKACbAvuA=
9 | github.com/aws/aws-sdk-go v1.17.2/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
10 | github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
11 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
12 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
13 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
14 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
15 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
16 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
17 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
18 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
19 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
23 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
24 | github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
25 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
26 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
27 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
28 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
29 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
30 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
31 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
32 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
33 | github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
34 | github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
35 | github.com/jackc/pgx v3.3.0+incompatible h1:Wa90/+qsITBAPkAZjiByeIGHFcj3Ztu+VzrrIpHjL90=
36 | github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
37 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
38 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
39 | github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
40 | github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
41 | github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
42 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
43 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
44 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
45 | github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
46 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
47 | github.com/mediocregopher/radix.v2 v0.0.0-20181115013041-b67df6e626f9 h1:ViNuGS149jgnttqhc6XQNPwdupEMBXqCx9wtlW7P3sA=
48 | github.com/mediocregopher/radix.v2 v0.0.0-20181115013041-b67df6e626f9/go.mod h1:fLRUbhbSd5Px2yKUaGYYPltlyxi1guJz1vCmo1RQL50=
49 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
50 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
51 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
52 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
53 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
54 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
55 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
56 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
57 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
58 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
59 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
60 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
61 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
62 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
63 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
64 | github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M=
65 | github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
66 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
67 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
68 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
69 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
70 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
71 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
72 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
73 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
74 | github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38=
75 | github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
76 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
77 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
78 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
79 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
80 | github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036 h1:1b6PAtenNyhsmo/NKXVe34h7JEZKva1YB/ne7K7mqKM=
81 | github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
82 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
83 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
84 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
85 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
86 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
87 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
88 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
89 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
90 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
91 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
92 | golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
93 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
94 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
95 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c h1:+EXw7AwNOKzPFXMZ1yNjO40aWCh3PIquJB2fYlv9wcs=
96 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
97 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
98 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
99 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
100 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
101 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
102 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
103 | google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
104 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
105 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
106 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
107 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
108 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
109 |
--------------------------------------------------------------------------------
/klein.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamaln7/klein/aaf57fa8c321547ecbd81091bda61aaa6d8825e0/klein.png
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/kamaln7/klein/cmd"
4 |
5 | func main() {
6 | cmd.Execute()
7 | }
8 |
--------------------------------------------------------------------------------
/server/main.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "strings"
7 |
8 | "github.com/kamaln7/klein/alias"
9 | "github.com/kamaln7/klein/auth"
10 | "github.com/kamaln7/klein/storage"
11 | )
12 |
13 | // Klein is a URL shortener
14 | type Klein struct {
15 | Config *Config
16 | mux *http.ServeMux
17 | }
18 |
19 | // Config contains the necessary configuration to run the URL shortener
20 | type Config struct {
21 | Alias alias.Provider
22 | Auth auth.Provider
23 | Storage storage.Provider
24 | Log *log.Logger
25 | NotFoundHTML []byte
26 |
27 | ListenAddr, PublicURL, RootURL string
28 | }
29 |
30 | // New returns a new Klein instance
31 | func New(c *Config) *Klein {
32 | c.PublicURL = strings.TrimRight(c.PublicURL, "/") + "/"
33 |
34 | return &Klein{
35 | Config: c,
36 | }
37 | }
38 |
39 | // Serve starts Klein's HTTP server
40 | func (b *Klein) Serve() {
41 | b.mux = http.NewServeMux()
42 | b.mux.HandleFunc("/", b.httpHandler)
43 |
44 | b.Config.Log.Printf("listening on %s\n", b.Config.ListenAddr)
45 | if err := http.ListenAndServe(b.Config.ListenAddr, b.mux); err != nil {
46 | b.Config.Log.Fatal(err)
47 | }
48 | }
49 |
50 | func (b *Klein) httpHandler(w http.ResponseWriter, r *http.Request) {
51 | path := r.URL.Path
52 |
53 | // root redirect & upload handlers
54 | if path == "/" {
55 | switch r.Method {
56 | case "GET":
57 | if b.Config.RootURL != "" {
58 | http.Redirect(w, r, b.Config.RootURL, 302)
59 | } else {
60 | b.notFound(w, r)
61 | }
62 | case "POST":
63 | b.create(w, r)
64 | }
65 |
66 | return
67 | }
68 |
69 | b.redirect(w, r, path[1:])
70 | }
71 |
72 | func (b *Klein) redirect(w http.ResponseWriter, r *http.Request, alias string) {
73 | url, err := b.Config.Storage.Get(alias)
74 |
75 | switch err {
76 | case nil:
77 | case storage.ErrNotFound:
78 | b.notFound(w, r)
79 | return
80 | default:
81 | w.WriteHeader(http.StatusInternalServerError)
82 | w.Write([]byte("error"))
83 | return
84 | }
85 |
86 | http.Redirect(w, r, url, 302)
87 | }
88 |
89 | func (b *Klein) create(w http.ResponseWriter, r *http.Request) {
90 | var (
91 | err error
92 | url = r.FormValue("url")
93 | )
94 |
95 | // authenticate
96 | authed, err := b.Config.Auth.Authenticate(w, r)
97 |
98 | if err != nil {
99 | w.WriteHeader(http.StatusInternalServerError)
100 | w.Write([]byte("error"))
101 | return
102 | }
103 | if !authed {
104 | w.WriteHeader(http.StatusForbidden)
105 | w.Write([]byte("unauthenticated"))
106 | return
107 | }
108 |
109 | // validate input
110 | if url == "" {
111 | w.WriteHeader(http.StatusBadRequest)
112 | w.Write([]byte("you need to pass a url"))
113 | return
114 | }
115 |
116 | // set an alias
117 | alias := r.FormValue("alias")
118 | if alias == "" {
119 | exists := true
120 | for exists {
121 | alias = b.Config.Alias.Generate()
122 | exists, err = b.Config.Storage.Exists(alias)
123 |
124 | if err != nil {
125 | w.WriteHeader(http.StatusInternalServerError)
126 | w.Write([]byte("error"))
127 | return
128 | }
129 | }
130 | } else {
131 | exists, err := b.Config.Storage.Exists(alias)
132 | if err != nil {
133 | w.WriteHeader(http.StatusInternalServerError)
134 | w.Write([]byte("error"))
135 | return
136 | }
137 |
138 | if exists {
139 | w.WriteHeader(http.StatusBadRequest)
140 | w.Write([]byte("code already exists"))
141 | return
142 | }
143 | }
144 |
145 | // store the URL
146 | err = b.Config.Storage.Store(url, alias)
147 | if err != nil {
148 | w.WriteHeader(http.StatusInternalServerError)
149 | return
150 | }
151 |
152 | w.WriteHeader(http.StatusCreated)
153 | w.Write([]byte(b.Config.PublicURL + alias))
154 | }
155 |
156 | func (b *Klein) notFound(w http.ResponseWriter, r *http.Request) {
157 | w.WriteHeader(http.StatusNotFound)
158 | w.Write(b.Config.NotFoundHTML)
159 | }
160 |
--------------------------------------------------------------------------------
/storage/bolt/main.go:
--------------------------------------------------------------------------------
1 | package bolt
2 |
3 | import (
4 | "bytes"
5 | "time"
6 |
7 | "github.com/boltdb/bolt"
8 | "github.com/kamaln7/klein/storage"
9 | )
10 |
11 | // Provider implements a file-based storage system
12 | type Provider struct {
13 | Config *Config
14 | db *bolt.DB
15 | }
16 |
17 | // Config contains the configuration for the file storage
18 | type Config struct {
19 | Path string
20 | }
21 |
22 | // ensure that the storage.Provider interface is implemented
23 | var _ storage.Provider = new(Provider)
24 |
25 | // New returns a new Provider instance
26 | func New(c *Config) (*Provider, error) {
27 | provider := &Provider{
28 | Config: c,
29 | }
30 | err := provider.Init()
31 | if err != nil {
32 | return nil, err
33 | }
34 |
35 | return provider, nil
36 | }
37 |
38 | // Init sets up the BoltDB database
39 | func (p *Provider) Init() error {
40 | db, err := bolt.Open(p.Config.Path, 0600, &bolt.Options{Timeout: 1 * time.Second})
41 | if err != nil {
42 | return err
43 | }
44 |
45 | err = db.Update(func(tx *bolt.Tx) error {
46 | _, err := tx.CreateBucketIfNotExists([]byte("klein"))
47 |
48 | return err
49 | })
50 | if err != nil {
51 | return err
52 | }
53 |
54 | p.db = db
55 | return nil
56 | }
57 |
58 | // Get attempts to find a URL by its alias and returns its original URL
59 | func (p *Provider) Get(alias string) (string, error) {
60 | var url []byte
61 |
62 | err := p.db.View(func(tx *bolt.Tx) error {
63 | b := tx.Bucket([]byte("klein"))
64 | url = bytes.TrimSpace(b.Get([]byte(alias)))
65 | if url == nil {
66 | return storage.ErrNotFound
67 | }
68 |
69 | return nil
70 | })
71 |
72 | if err != nil {
73 | return "", err
74 | }
75 |
76 | return string(url), nil
77 | }
78 |
79 | // Exists checks if there is a URL with the requested alias
80 | func (p *Provider) Exists(alias string) (bool, error) {
81 | _, err := p.Get(alias)
82 |
83 | if err == storage.ErrNotFound {
84 | return false, nil
85 | }
86 |
87 | return true, err
88 | }
89 |
90 | // Store creates a new short URL
91 | func (p *Provider) Store(url, alias string) error {
92 | exists, err := p.Exists(alias)
93 | if err != nil {
94 | return err
95 | }
96 | if exists {
97 | return storage.ErrAlreadyExists
98 | }
99 |
100 | err = p.db.Update(func(tx *bolt.Tx) error {
101 | b := tx.Bucket([]byte("klein"))
102 | err := b.Put([]byte(alias), bytes.TrimSpace([]byte(url)))
103 |
104 | return err
105 | })
106 |
107 | return err
108 | }
109 |
--------------------------------------------------------------------------------
/storage/bolt/main_test.go:
--------------------------------------------------------------------------------
1 | package bolt
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "testing"
7 |
8 | "github.com/kamaln7/klein/storage/storagetest"
9 | )
10 |
11 | func TestProvider(t *testing.T) {
12 | file, err := ioutil.TempFile("", "klein")
13 | if err != nil {
14 | t.Errorf("couldn't create temporary test file: %v\n", err)
15 | }
16 | defer os.Remove(file.Name())
17 |
18 | p, err := New(&Config{
19 | Path: file.Name(),
20 | })
21 | if err != nil {
22 | t.Errorf("couldn't init bolt driver: %v\n", err)
23 | }
24 |
25 | storagetest.RunBasicTests(p, t)
26 | }
27 |
--------------------------------------------------------------------------------
/storage/file/main.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "os"
7 | "path"
8 | "path/filepath"
9 | "sync"
10 |
11 | "github.com/kamaln7/klein/storage"
12 | )
13 |
14 | // Provider implements a file-based storage system
15 | type Provider struct {
16 | Config *Config
17 | mutex sync.RWMutex
18 | }
19 |
20 | // Config contains the configuration for the file storage
21 | type Config struct {
22 | Path string
23 | }
24 |
25 | // ensure that the storage.Provider interface is implemented
26 | var _ storage.Provider = new(Provider)
27 |
28 | // New returns a new Provider instance
29 | func New(c *Config) *Provider {
30 | return &Provider{
31 | Config: c,
32 | }
33 | }
34 |
35 | // Get attempts to find a URL by its alias and returns its original URL
36 | func (p *Provider) Get(alias string) (string, error) {
37 | alias = path.Base(alias)
38 |
39 | p.mutex.RLock()
40 | url, err := ioutil.ReadFile(filepath.Join(p.Config.Path, alias))
41 | p.mutex.RUnlock()
42 | if err != nil {
43 | return "", storage.ErrNotFound
44 | }
45 |
46 | return string(bytes.TrimSpace(url)), nil
47 | }
48 |
49 | // Exists checks if there is a URL with the requested alias
50 | func (p *Provider) Exists(alias string) (bool, error) {
51 | p.mutex.RLock()
52 | defer p.mutex.RUnlock()
53 |
54 | _, err := os.Stat(filepath.Join(p.Config.Path, path.Base(alias)))
55 | return !os.IsNotExist(err), nil
56 | }
57 |
58 | // Store creates a new short URL
59 | func (p *Provider) Store(url, alias string) error {
60 | exists, _ := p.Exists(alias)
61 | if exists {
62 | return storage.ErrAlreadyExists
63 | }
64 |
65 | p.mutex.Lock()
66 | err := ioutil.WriteFile(filepath.Join(p.Config.Path, alias), bytes.TrimSpace([]byte(url)), 0644)
67 | p.mutex.Unlock()
68 |
69 | if err != nil {
70 | return err
71 | }
72 |
73 | return nil
74 | }
75 |
--------------------------------------------------------------------------------
/storage/file/main_test.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "testing"
7 |
8 | "github.com/kamaln7/klein/storage/storagetest"
9 | )
10 |
11 | func TestProvider(t *testing.T) {
12 | dir, err := ioutil.TempDir("", "klein")
13 | if err != nil {
14 | t.Errorf("couldn't create temporary test dir: %v\n", err)
15 | }
16 | defer os.RemoveAll(dir)
17 |
18 | p := New(&Config{
19 | Path: dir,
20 | })
21 |
22 | storagetest.RunBasicTests(p, t)
23 | }
24 |
--------------------------------------------------------------------------------
/storage/main.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | // A Provider implements all the necessary functions for a storage backend for URLs
8 | type Provider interface {
9 | Get(alias string) (string, error)
10 | Exists(alias string) (bool, error)
11 | Store(url, alias string) error
12 | }
13 |
14 | // Errors
15 | var (
16 | ErrNotFound = errors.New("URL does not exist")
17 | ErrAlreadyExists = errors.New("Alias already exists")
18 | )
19 |
--------------------------------------------------------------------------------
/storage/memory/main.go:
--------------------------------------------------------------------------------
1 | package memory
2 |
3 | import (
4 | "github.com/kamaln7/klein/storage"
5 | )
6 |
7 | // Provider implements a temporary in-memory storage
8 | type Provider struct {
9 | Config *Config
10 |
11 | urls map[string]string
12 | }
13 |
14 | // Config contains the configuration for the in-memory storage
15 | type Config struct {
16 | }
17 |
18 | // ensure that the storage.Provider interface is implemented
19 | var _ storage.Provider = new(Provider)
20 |
21 | // New returns a new Provider instance
22 | func New(c *Config) *Provider {
23 | return &Provider{
24 | Config: c,
25 | urls: make(map[string]string),
26 | }
27 | }
28 |
29 | // Get attempts to find a URL by its alias and returns its original URL
30 | func (p *Provider) Get(alias string) (string, error) {
31 | url, found := p.urls[alias]
32 | if !found {
33 | return "", storage.ErrNotFound
34 | }
35 |
36 | return url, nil
37 | }
38 |
39 | // Exists checks if there is a URL with the requested alias
40 | func (p *Provider) Exists(alias string) (bool, error) {
41 | _, found := p.urls[alias]
42 |
43 | return found, nil
44 | }
45 |
46 | // Store creates a new short URL
47 | func (p *Provider) Store(url, alias string) error {
48 | _, found := p.urls[alias]
49 | if found {
50 | return storage.ErrAlreadyExists
51 | }
52 |
53 | p.urls[alias] = url
54 | return nil
55 | }
56 |
--------------------------------------------------------------------------------
/storage/memory/main_test.go:
--------------------------------------------------------------------------------
1 | package memory
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/kamaln7/klein/storage/storagetest"
7 | )
8 |
9 | func TestProvider(t *testing.T) {
10 | p := New(&Config{})
11 |
12 | storagetest.RunBasicTests(p, t)
13 | }
14 |
--------------------------------------------------------------------------------
/storage/postgresql/main.go:
--------------------------------------------------------------------------------
1 | package postgresql
2 |
3 | import (
4 | "crypto/tls"
5 | "database/sql"
6 | "errors"
7 | "fmt"
8 |
9 | "github.com/jackc/pgx"
10 | pgxstdlib "github.com/jackc/pgx/stdlib"
11 | "github.com/jmoiron/sqlx"
12 | "github.com/kamaln7/klein/storage"
13 | )
14 |
15 | // Provider implements a PostgreSQL-based storage system
16 | type Provider struct {
17 | Config *Config
18 |
19 | db *sqlx.DB
20 | }
21 |
22 | // Config contains the configuration for the PostgreSQL server and database
23 | type Config struct {
24 | Host, User, Password, Database, Table, SSLMode string
25 | Port int32
26 | }
27 |
28 | // ensure that the storage.Provider interface is implemented
29 | var _ storage.Provider = new(Provider)
30 |
31 | // database url type
32 | type url struct {
33 | ID int
34 | URL, Alias string
35 | }
36 |
37 | // New returns a new Provider instance
38 | func New(c *Config) (*Provider, error) {
39 | provider := &Provider{
40 | Config: c,
41 | }
42 |
43 | err := provider.Init()
44 | if err != nil {
45 | return nil, err
46 | }
47 |
48 | return provider, nil
49 | }
50 |
51 | // Init sets up the PostgreSQL database connection and creates the table if needed
52 | func (p *Provider) Init() error {
53 | cc := pgx.ConnConfig{
54 | Host: p.Config.Host,
55 | Port: uint16(p.Config.Port),
56 | User: p.Config.User,
57 | Password: p.Config.Password,
58 | Database: p.Config.Database,
59 | }
60 |
61 | // Copied from https://github.com/jackc/pgx/blob/f25025a5801f9c925f4a7ffea5636bf53755c67e/conn.go#L976-L997
62 | switch p.Config.SSLMode {
63 | case "disable":
64 | cc.UseFallbackTLS = false
65 | cc.TLSConfig = nil
66 | cc.FallbackTLSConfig = nil
67 | case "allow":
68 | cc.UseFallbackTLS = true
69 | cc.FallbackTLSConfig = &tls.Config{InsecureSkipVerify: true}
70 | case "prefer":
71 | cc.TLSConfig = &tls.Config{InsecureSkipVerify: true}
72 | cc.UseFallbackTLS = true
73 | cc.FallbackTLSConfig = nil
74 | case "require":
75 | cc.TLSConfig = &tls.Config{InsecureSkipVerify: true}
76 | case "verify-ca", "verify-full":
77 | cc.TLSConfig = &tls.Config{
78 | ServerName: cc.Host,
79 | }
80 | default:
81 | return errors.New("sslmode is invalid")
82 | }
83 |
84 | db := pgxstdlib.OpenDB(cc)
85 | p.db = sqlx.NewDb(db, "pgx")
86 |
87 | // create table if it doesn't already exist
88 | if err := p.createTable(); err != nil {
89 | return err
90 | }
91 |
92 | return nil
93 | }
94 |
95 | func (p *Provider) fillInTableName(query string) string {
96 | return fmt.Sprintf(query, p.Config.Table)
97 | }
98 |
99 | func (p *Provider) createTable() error {
100 | q := p.fillInTableName(`
101 | create table if not exists %s (
102 | id serial,
103 | alias text unique not null,
104 | url text not null,
105 | primary key( id )
106 | )`)
107 |
108 | _, err := p.db.Exec(q)
109 | return err
110 | }
111 |
112 | // Get attempts to find a URL by its alias and returns its original URL
113 | func (p *Provider) Get(alias string) (string, error) {
114 | u := &url{}
115 |
116 | q := p.fillInTableName("select * from %s where alias = $1")
117 | err := p.db.Get(u, q, alias)
118 |
119 | if err != nil {
120 | if err == sql.ErrNoRows {
121 | return "", storage.ErrNotFound
122 | }
123 |
124 | return "", err
125 | }
126 |
127 | return u.URL, nil
128 | }
129 |
130 | // Exists checks if there is a URL with the requested alias
131 | func (p *Provider) Exists(alias string) (bool, error) {
132 | _, err := p.Get(alias)
133 |
134 | if err == storage.ErrNotFound {
135 | return false, nil
136 | }
137 |
138 | return true, err
139 | }
140 |
141 | // Store creates a new short URL
142 | func (p *Provider) Store(url, alias string) error {
143 | q := p.fillInTableName("insert into %s (url, alias) values ($1, $2)")
144 | _, err := p.db.Exec(q, url, alias)
145 |
146 | if err, ok := err.(*pgx.PgError); ok && err.Code == "23505" {
147 | return storage.ErrAlreadyExists
148 | }
149 |
150 | return err
151 | }
152 |
--------------------------------------------------------------------------------
/storage/redis/main.go:
--------------------------------------------------------------------------------
1 | package redis
2 |
3 | import (
4 | "github.com/kamaln7/klein/storage"
5 | "github.com/mediocregopher/radix.v2/pool"
6 | "github.com/mediocregopher/radix.v2/redis"
7 | )
8 |
9 | // Provider implements a redis-based storage system
10 | type Provider struct {
11 | Config *Config
12 | pool *pool.Pool
13 | }
14 |
15 | // Config contains the configuration for the redis storage
16 | type Config struct {
17 | Address string
18 | Auth string
19 | DB int
20 | }
21 |
22 | // ensure that the storage.Provider interface is implemented
23 | var _ storage.Provider = new(Provider)
24 |
25 | // New returns a new Provider instance
26 | func New(c *Config) (*Provider, error) {
27 | provider := &Provider{
28 | Config: c,
29 | }
30 | err := provider.Init()
31 | if err != nil {
32 | return nil, err
33 | }
34 |
35 | return provider, nil
36 | }
37 |
38 | // Init sets up the Redis pool
39 | func (p *Provider) Init() error {
40 | df := func(network, addr string) (*redis.Client, error) {
41 | client, err := redis.Dial(network, addr)
42 | if err != nil {
43 | return nil, err
44 | }
45 |
46 | if p.Config.Auth != "" {
47 | if err = client.Cmd("AUTH", p.Config.Auth).Err; err != nil {
48 | client.Close()
49 | return nil, err
50 | }
51 | }
52 |
53 | if err = client.Cmd("SELECT", p.Config.DB).Err; err != nil {
54 | client.Close()
55 | return nil, err
56 | }
57 |
58 | return client, nil
59 | }
60 |
61 | pool, err := pool.NewCustom("tcp", p.Config.Address, 10, df)
62 | if err != nil {
63 | return err
64 | }
65 |
66 | p.pool = pool
67 | return nil
68 | }
69 |
70 | // Get attempts to find a URL by its alias and returns its original URL
71 | func (p *Provider) Get(alias string) (string, error) {
72 | r := p.pool.Cmd("GET", alias)
73 | if r.Err != nil {
74 | return "", r.Err
75 | }
76 |
77 | url, _ := r.Str()
78 | if url == "" {
79 | return "", storage.ErrNotFound
80 | }
81 |
82 | return url, nil
83 | }
84 |
85 | // Exists checks if there is a URL with the requested alias
86 | func (p *Provider) Exists(alias string) (bool, error) {
87 | r, err := p.pool.Cmd("EXISTS", alias).Int()
88 | if err != nil {
89 | return false, err
90 | } else if r == 1 {
91 | return true, nil
92 | }
93 |
94 | return false, nil
95 | }
96 |
97 | // Store creates a new short URL
98 | func (p *Provider) Store(url, alias string) error {
99 | exists, err := p.Exists(alias)
100 | if err != nil {
101 | return err
102 | }
103 | if exists {
104 | return storage.ErrAlreadyExists
105 | }
106 |
107 | r := p.pool.Cmd("SET", alias, url)
108 | return r.Err
109 | }
110 |
--------------------------------------------------------------------------------
/storage/redis/main_test.go:
--------------------------------------------------------------------------------
1 | package redis
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/alicebob/miniredis"
7 | "github.com/kamaln7/klein/storage/storagetest"
8 | )
9 |
10 | func TestProvider(t *testing.T) {
11 | redisPassword := "secret-password"
12 |
13 | redisServer, err := miniredis.Run()
14 | if err != nil {
15 | t.Errorf("couldn't start redis client: %v\n", err)
16 | }
17 | redisServer.RequireAuth(redisPassword)
18 | defer redisServer.Close()
19 |
20 | p, err := New(&Config{
21 | Address: redisServer.Addr(),
22 | DB: 5,
23 | Auth: redisPassword,
24 | })
25 | if err != nil {
26 | t.Errorf("couldn't connect to redis server: %v\n", err)
27 | }
28 |
29 | storagetest.RunBasicTests(p, t)
30 | }
31 |
--------------------------------------------------------------------------------
/storage/spaces/main.go:
--------------------------------------------------------------------------------
1 | package spaces
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "sync"
8 |
9 | "github.com/aws/aws-sdk-go/aws"
10 | "github.com/aws/aws-sdk-go/aws/awserr"
11 | "github.com/aws/aws-sdk-go/aws/credentials"
12 | "github.com/aws/aws-sdk-go/aws/session"
13 | "github.com/aws/aws-sdk-go/service/s3"
14 | "github.com/kamaln7/klein/storage"
15 | )
16 |
17 | // Provider implements an in memory storage that persists on DigitalOcean Spaces
18 | type Provider struct {
19 | Config *Config
20 | Spaces *s3.S3
21 | URLs map[string]string
22 | mutex sync.RWMutex
23 | }
24 |
25 | // Config contains the configuration for the file storage
26 | type Config struct {
27 | AccessKey string
28 | SecretKey string
29 | Region string
30 | Space string
31 | Path string
32 | }
33 |
34 | // ensure that the storage.Provider interface is implemented
35 | var _ storage.Provider = new(Provider)
36 |
37 | // New returns a new Provider instance
38 | func New(c *Config) (*Provider, error) {
39 | spacesSession := session.New(&aws.Config{
40 | Credentials: credentials.NewStaticCredentials(c.AccessKey, c.SecretKey, ""),
41 | Endpoint: aws.String(fmt.Sprintf("https://%s.digitaloceanspaces.com", c.Region)),
42 | Region: aws.String("us-east-1"), // Needs to be us-east-1, or it'll fail.
43 | })
44 |
45 | spaces := s3.New(spacesSession)
46 |
47 | object := s3.GetObjectInput{
48 | Bucket: aws.String(c.Space),
49 | Key: aws.String(c.Path),
50 | }
51 |
52 | urls := make(map[string]string)
53 |
54 | output, err := spaces.GetObject(&object)
55 | if err != nil {
56 | if aerr, ok := err.(awserr.Error); ok && aerr.Code() == s3.ErrCodeNoSuchKey {
57 | return &Provider{
58 | Spaces: spaces,
59 | Config: c,
60 | URLs: urls,
61 | }, nil
62 | }
63 |
64 | return nil, err
65 | }
66 |
67 | buf := new(bytes.Buffer)
68 | buf.ReadFrom(output.Body)
69 |
70 | err = json.Unmarshal(buf.Bytes(), &urls)
71 | if err != nil {
72 | return nil, err
73 | }
74 |
75 | return &Provider{
76 | Spaces: spaces,
77 | Config: c,
78 | URLs: urls,
79 | }, nil
80 | }
81 |
82 | // Get attempts to find a URL by its alias and returns its original URL
83 | func (p *Provider) Get(alias string) (string, error) {
84 | if url, exists := p.URLs[alias]; exists {
85 | return url, nil
86 | }
87 |
88 | return "", storage.ErrNotFound
89 | }
90 |
91 | // Exists checks if there is a URL with the requested alias
92 | func (p *Provider) Exists(alias string) (bool, error) {
93 | p.mutex.RLock()
94 | defer p.mutex.RUnlock()
95 |
96 | _, exists := p.URLs[alias]
97 | return exists, nil
98 | }
99 |
100 | // Store creates a new short URL
101 | func (p *Provider) Store(url, alias string) error {
102 | exists, _ := p.Exists(alias)
103 | if exists {
104 | return storage.ErrAlreadyExists
105 | }
106 |
107 | p.mutex.Lock()
108 | defer p.mutex.Unlock()
109 |
110 | p.URLs[alias] = url
111 |
112 | body, err := json.Marshal(p.URLs)
113 | if err != nil {
114 | return err
115 | }
116 |
117 | object := s3.PutObjectInput{
118 | Body: bytes.NewReader(body),
119 | Bucket: aws.String(p.Config.Space),
120 | Key: aws.String(p.Config.Path),
121 | }
122 | _, err = p.Spaces.PutObject(&object)
123 |
124 | if err != nil {
125 | return err
126 | }
127 |
128 | return nil
129 | }
130 |
--------------------------------------------------------------------------------
/storage/spacesstateless/main.go:
--------------------------------------------------------------------------------
1 | package spacesstateless
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "log"
7 | "strings"
8 | "time"
9 |
10 | "github.com/aws/aws-sdk-go/aws"
11 | "github.com/aws/aws-sdk-go/aws/awserr"
12 | "github.com/aws/aws-sdk-go/aws/credentials"
13 | "github.com/aws/aws-sdk-go/aws/session"
14 | "github.com/aws/aws-sdk-go/service/s3"
15 | "github.com/kamaln7/klein/storage"
16 | cache "github.com/patrickmn/go-cache"
17 | )
18 |
19 | // Provider implements an in memory storage that persists on DigitalOcean Spaces
20 | type Provider struct {
21 | Config *Config
22 |
23 | spaces *s3.S3
24 | cache *cache.Cache
25 | }
26 |
27 | // Config contains the configuration for the file storage
28 | type Config struct {
29 | AccessKey string
30 | SecretKey string
31 | Region string
32 | Space string
33 | Path string
34 |
35 | CacheDuration time.Duration
36 | }
37 |
38 | // ensure that the storage.Provider interface is implemented
39 | var _ storage.Provider = new(Provider)
40 |
41 | // New returns a new Provider instance
42 | func New(c *Config) (*Provider, error) {
43 | spacesSession := session.New(&aws.Config{
44 | Credentials: credentials.NewStaticCredentials(c.AccessKey, c.SecretKey, ""),
45 | Endpoint: aws.String(fmt.Sprintf("https://%s.digitaloceanspaces.com", c.Region)),
46 | Region: aws.String("us-east-1"), // Needs to be us-east-1, or it'll fail.
47 | })
48 | spaces := s3.New(spacesSession)
49 |
50 | p := &Provider{
51 | Config: c,
52 |
53 | spaces: spaces,
54 | }
55 |
56 | if c.CacheDuration != 0 {
57 | p.cache = cache.New(c.CacheDuration, c.CacheDuration/2)
58 | }
59 |
60 | return p, nil
61 | }
62 |
63 | func (p *Provider) aliasFullPath(alias string) string {
64 | prefix := ""
65 | if p.Config.Path != "" {
66 | prefix = fmt.Sprintf("%s/", strings.TrimSuffix(p.Config.Path, "/"))
67 | }
68 |
69 | return fmt.Sprintf("%s%s", prefix, alias)
70 | }
71 |
72 | func (p *Provider) getFromSpaces(alias string) (string, error) {
73 | output, err := p.spaces.GetObject(&s3.GetObjectInput{
74 | Bucket: aws.String(p.Config.Space),
75 | Key: aws.String(p.aliasFullPath(alias)),
76 | })
77 |
78 | if err != nil {
79 | if aerr, ok := err.(awserr.Error); ok {
80 | switch aerr.Code() {
81 | case s3.ErrCodeNoSuchKey:
82 | return "", storage.ErrNotFound
83 | case "InvalidAccessKeyId":
84 | log.Printf("storage/spaces-stateless: invalid access key, could not access spaces")
85 | return "", aerr
86 | default:
87 | return "", aerr
88 | }
89 | }
90 |
91 | return "", err
92 | }
93 |
94 | buf := new(bytes.Buffer)
95 | buf.ReadFrom(output.Body)
96 |
97 | return buf.String(), nil
98 | }
99 |
100 | // Get attempts to find a URL by its alias and returns its original URL
101 | func (p *Provider) Get(alias string) (string, error) {
102 | if p.cache == nil {
103 | return p.getFromSpaces(alias)
104 | }
105 |
106 | cachedURL, isCached := p.cache.Get(alias)
107 | if isCached {
108 | return cachedURL.(string), nil
109 | }
110 |
111 | url, err := p.getFromSpaces(alias)
112 | if err != nil {
113 | return "", err
114 | }
115 |
116 | p.cache.Set(alias, url, cache.DefaultExpiration)
117 | return url, nil
118 | }
119 |
120 | // Exists checks if there is a URL with the requested alias
121 | func (p *Provider) Exists(alias string) (bool, error) {
122 | _, err := p.Get(alias)
123 |
124 | if err == storage.ErrNotFound {
125 | return false, nil
126 | }
127 |
128 | return true, err
129 | }
130 |
131 | // Store creates a new short URL
132 | func (p *Provider) Store(url, alias string) error {
133 | exists, err := p.Exists(alias)
134 | if err != nil {
135 | return err
136 | }
137 |
138 | if exists {
139 | return storage.ErrAlreadyExists
140 | }
141 |
142 | object := s3.PutObjectInput{
143 | Body: strings.NewReader(url),
144 | Bucket: aws.String(p.Config.Space),
145 | Key: aws.String(p.aliasFullPath(alias)),
146 | }
147 |
148 | _, err = p.spaces.PutObject(&object)
149 | if err != nil {
150 | return err
151 | }
152 |
153 | if p.cache != nil {
154 | p.cache.Set(alias, url, cache.DefaultExpiration)
155 | }
156 | return nil
157 | }
158 |
--------------------------------------------------------------------------------
/storage/storagetest/main.go:
--------------------------------------------------------------------------------
1 | package storagetest
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/kamaln7/klein/storage"
7 | )
8 |
9 | // RunBasicTests run a basic test suite that should work on all storage providers
10 | func RunBasicTests(p storage.Provider, t *testing.T) {
11 | var err error
12 |
13 | url := "http://example.com"
14 | alias := "example"
15 |
16 | t.Run("store new url", func(t *testing.T) {
17 | err = p.Store(url, alias)
18 | if err != nil {
19 | t.Error("couldn't store a new URL")
20 | }
21 | })
22 |
23 | t.Run("check existance of alias", func(t *testing.T) {
24 | exists, err := p.Exists(alias)
25 | if err != nil {
26 | t.Error(err)
27 | }
28 |
29 | if !exists {
30 | t.Error("expected alias exists got otherwise")
31 | }
32 | })
33 |
34 | t.Run("attempt to overwrite existing alias", func(t *testing.T) {
35 | err = p.Store(url, alias)
36 | if err != storage.ErrAlreadyExists {
37 | t.Error("couldn't handle storing a new URL with an existing alias properly")
38 | }
39 | })
40 |
41 | t.Run("look up existing alias", func(t *testing.T) {
42 | storedURL, err := p.Get(alias)
43 | if err != nil {
44 | t.Error("couldn't look up an existing alias")
45 | }
46 | if storedURL != url {
47 | t.Error("got a wrong url when looking up an alias")
48 | }
49 | })
50 |
51 | t.Run("look up nonexistant alias", func(t *testing.T) {
52 | // look up inexistent alias
53 | _, err = p.Get("1234567890")
54 | if err != storage.ErrNotFound {
55 | t.Error("didn't get the correct error looking up inexistent alias")
56 | }
57 | })
58 | }
59 |
--------------------------------------------------------------------------------