2017-07-11 18:09:08 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
|
2018-10-21 18:20:05 +02:00
|
|
|
bolt "github.com/etcd-io/bbolt"
|
2017-07-11 18:09:08 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// Panic panics, change it if you don't want to panic on critical INITIALIZE-ONLY-ERRORS
|
|
|
|
var Panic = func(v interface{}) {
|
|
|
|
panic(v)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store is the store interface for urls.
|
|
|
|
// Note: no Del functionality.
|
|
|
|
type Store interface {
|
|
|
|
Set(key string, value string) error // error if something went wrong
|
|
|
|
Get(key string) string // empty value if not found
|
|
|
|
Len() int // should return the number of all the records/tables/buckets
|
|
|
|
Close() // release the store or ignore
|
|
|
|
}
|
|
|
|
|
2019-08-17 09:06:20 +02:00
|
|
|
var tableURLs = []byte("urls")
|
2017-07-11 18:09:08 +02:00
|
|
|
|
|
|
|
// DB representation of a Store.
|
|
|
|
// Only one table/bucket which contains the urls, so it's not a fully Database,
|
|
|
|
// it works only with single bucket because that all we need.
|
|
|
|
type DB struct {
|
|
|
|
db *bolt.DB
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ Store = &DB{}
|
|
|
|
|
|
|
|
// openDatabase open a new database connection
|
|
|
|
// and returns its instance.
|
|
|
|
func openDatabase(stumb string) *bolt.DB {
|
|
|
|
// Open the data(base) file in the current working directory.
|
|
|
|
// It will be created if it doesn't exist.
|
|
|
|
db, err := bolt.Open(stumb, 0600, nil)
|
|
|
|
if err != nil {
|
|
|
|
Panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// create the buckets here
|
2019-08-17 09:06:20 +02:00
|
|
|
tables := [...][]byte{
|
2017-07-11 18:09:08 +02:00
|
|
|
tableURLs,
|
|
|
|
}
|
|
|
|
|
|
|
|
db.Update(func(tx *bolt.Tx) (err error) {
|
|
|
|
for _, table := range tables {
|
|
|
|
_, err = tx.CreateBucketIfNotExists(table)
|
|
|
|
if err != nil {
|
|
|
|
Panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
})
|
|
|
|
|
|
|
|
return db
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewDB returns a new DB instance, its connection is opened.
|
|
|
|
// DB implements the Store.
|
|
|
|
func NewDB(stumb string) *DB {
|
|
|
|
return &DB{
|
|
|
|
db: openDatabase(stumb),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set sets a shorten url and its key
|
|
|
|
// Note: Caller is responsible to generate a key.
|
|
|
|
func (d *DB) Set(key string, value string) error {
|
|
|
|
return d.db.Update(func(tx *bolt.Tx) error {
|
|
|
|
b, err := tx.CreateBucketIfNotExists(tableURLs)
|
|
|
|
// Generate ID for the url
|
|
|
|
// Note: we could use that instead of a random string key
|
|
|
|
// but we want to simulate a real-world url shortener
|
|
|
|
// so we skip that.
|
|
|
|
// id, _ := b.NextSequence()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
k := []byte(key)
|
|
|
|
valueB := []byte(value)
|
|
|
|
c := b.Cursor()
|
|
|
|
|
|
|
|
found := false
|
|
|
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
|
|
|
if bytes.Equal(valueB, v) {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// if value already exists don't re-put it.
|
|
|
|
if found {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return b.Put(k, []byte(value))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear clears all the database entries for the table urls.
|
|
|
|
func (d *DB) Clear() error {
|
|
|
|
return d.db.Update(func(tx *bolt.Tx) error {
|
|
|
|
return tx.DeleteBucket(tableURLs)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get returns a url by its key.
|
|
|
|
//
|
|
|
|
// Returns an empty string if not found.
|
|
|
|
func (d *DB) Get(key string) (value string) {
|
|
|
|
keyB := []byte(key)
|
|
|
|
d.db.Update(func(tx *bolt.Tx) error {
|
|
|
|
b := tx.Bucket(tableURLs)
|
|
|
|
if b == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
c := b.Cursor()
|
|
|
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
|
|
|
if bytes.Equal(keyB, k) {
|
|
|
|
value = string(v)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetByValue returns all keys for a specific (original) url value.
|
|
|
|
func (d *DB) GetByValue(value string) (keys []string) {
|
|
|
|
valueB := []byte(value)
|
|
|
|
d.db.Update(func(tx *bolt.Tx) error {
|
|
|
|
b := tx.Bucket(tableURLs)
|
|
|
|
if b == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
c := b.Cursor()
|
|
|
|
// first for the bucket's table "urls"
|
|
|
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
|
|
|
if bytes.Equal(valueB, v) {
|
|
|
|
keys = append(keys, string(k))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Len returns all the "shorted" urls length
|
|
|
|
func (d *DB) Len() (num int) {
|
|
|
|
d.db.View(func(tx *bolt.Tx) error {
|
|
|
|
// Assume bucket exists and has keys
|
|
|
|
b := tx.Bucket(tableURLs)
|
|
|
|
if b == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
b.ForEach(func([]byte, []byte) error {
|
|
|
|
num++
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close shutdowns the data(base) connection.
|
|
|
|
func (d *DB) Close() {
|
|
|
|
if err := d.db.Close(); err != nil {
|
|
|
|
Panic(err)
|
|
|
|
}
|
|
|
|
}
|