├── .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 | klein logo 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 | --------------------------------------------------------------------------------