Refactor database

This commit is contained in:
Las Zenow 2014-06-29 19:41:29 -05:00
parent 533f8241c2
commit 59eaa4e2aa
24 changed files with 712 additions and 436 deletions

77
database/books.go Normal file
View file

@ -0,0 +1,77 @@
package database
import (
"labix.org/v2/mgo"
"labix.org/v2/mgo/bson"
)
const (
books_coll = "books"
)
type Book struct {
Id string `bson:"_id"`
Title string
Author []string
Contributor string
Publisher string
Description string
Subject []string
Date string
Lang []string
Isbn string
Type string
Format string
Source string
Relation string
Coverage string
Rights string
Meta string
File bson.ObjectId
FileSize int
Cover bson.ObjectId
CoverSmall bson.ObjectId
Active bool
Keywords []string
}
func addBook(coll *mgo.Collection, book interface{}) error {
return coll.Insert(book)
}
func getBooks(coll *mgo.Collection, query bson.M, length int, start int) (books []Book, num int, err error) {
q := coll.Find(query).Sort("-_id")
num, err = q.Count()
if err != nil {
return
}
if start != 0 {
q = q.Skip(start)
}
if length != 0 {
q = q.Limit(length)
}
err = q.All(&books)
for i, b := range books {
books[i].Id = bson.ObjectId(b.Id).Hex()
}
return
}
func deleteBook(coll *mgo.Collection, id bson.ObjectId) error {
return coll.Remove(bson.M{"_id": id})
}
func updateBook(coll *mgo.Collection, id bson.ObjectId, data interface{}) error {
return coll.Update(bson.M{"_id": id}, bson.M{"$set": data})
}
func bookActive(coll *mgo.Collection, id bson.ObjectId) bool {
var book Book
err := coll.Find(bson.M{"_id": id}).One(&book)
if err != nil {
return false
}
return book.Active
}

40
database/books_test.go Normal file
View file

@ -0,0 +1,40 @@
package database
import "testing"
import (
"labix.org/v2/mgo/bson"
)
var book = map[string]interface{}{
"title": "some title",
"author": []string{"Alice", "Bob"},
}
func TestAddBook(t *testing.T) {
db := Init(test_host, test_coll)
defer db.del()
tAddBook(t, db)
books, num, err := db.GetBooks(bson.M{}, 1, 0)
if err != nil {
t.Fatalf("db.GetBooks() return an error: ", err)
}
if num < 1 {
t.Fatalf("db.GetBooks() didn't find any result.")
}
if len(books) < 1 {
t.Fatalf("db.GetBooks() didn't return any result.")
}
if books[0].Title != book["title"] {
t.Errorf("Book title don't match : '", books[0].Title, "' <=> '", book["title"], "'")
}
}
func tAddBook(t *testing.T, db *DB) {
err := db.AddBook(book)
if err != nil {
t.Errorf("db.AddBook(", book, ") return an error: ", err)
}
}

236
database/database.go Normal file
View file

@ -0,0 +1,236 @@
package database
import log "github.com/cihub/seelog"
import (
"errors"
"labix.org/v2/mgo"
"labix.org/v2/mgo/bson"
"os"
)
const (
visited_coll = "visited"
downloaded_coll = "downloaded"
tags_coll = "tags"
)
type DB struct {
session *mgo.Session
name string
}
func Init(host string, name string) *DB {
var err error
db := new(DB)
db.session, err = mgo.Dial(host)
if err != nil {
log.Critical(err)
os.Exit(1)
}
db.name = name
return db
}
func (db *DB) Close() {
db.session.Close()
}
func (db *DB) Copy() *DB {
dbCopy := new(DB)
dbCopy.session = db.session.Copy()
dbCopy.name = db.name
return dbCopy
}
func (db *DB) AddBook(book interface{}) error {
booksColl := db.session.DB(db.name).C(books_coll)
return addBook(booksColl, book)
}
// FIXME: don't export bson data
func (db *DB) GetBooks(query bson.M, length int, start int) (books []Book, num int, err error) {
booksColl := db.session.DB(db.name).C(books_coll)
return getBooks(booksColl, query, length, start)
}
func (db *DB) DeleteBook(id bson.ObjectId) error {
booksColl := db.session.DB(db.name).C(books_coll)
return deleteBook(booksColl, id)
}
func (db *DB) UpdateBook(id bson.ObjectId, data interface{}) error {
booksColl := db.session.DB(db.name).C(books_coll)
return updateBook(booksColl, id, data)
}
func (db *DB) GetNewBooks(length int, start int) (books []Book, num int, err error) {
booksColl := db.session.DB(db.name).C(books_coll)
return getBooks(booksColl, bson.M{"$nor": []bson.M{{"active": true}}}, length, start)
}
func (db *DB) BookActive(id bson.ObjectId) bool {
booksColl := db.session.DB(db.name).C(books_coll)
return bookActive(booksColl, id)
}
func (db *DB) User(name string) *User {
userColl := db.session.DB(db.name).C(user_coll)
return getUser(userColl, name)
}
func (db *DB) AddUser(name string, pass string) error {
userColl := db.session.DB(db.name).C(user_coll)
return addUser(userColl, name, pass)
}
func (db *DB) AddNews(text string) error {
newsColl := db.session.DB(db.name).C(news_coll)
return addNews(newsColl, text)
}
func (db *DB) GetNews(num int, days int) (news []News, err error) {
newsColl := db.session.DB(db.name).C(news_coll)
return getNews(newsColl, num, days)
}
// TODO: split code in files
func (db *DB) AddStats(stats interface{}) error {
statsColl := db.session.DB(db.name).C(stats_coll)
return statsColl.Insert(stats)
}
/* Get the most visited books
*/
func (db *DB) GetVisitedBooks() (books []Book, err error) {
visitedColl := db.session.DB(db.name).C(visited_coll)
bookId, err := GetBooksVisited(visitedColl)
if err != nil {
return nil, err
}
books = make([]Book, len(bookId))
for i, id := range bookId {
booksColl := db.session.DB(db.name).C(books_coll)
booksColl.Find(bson.M{"_id": id}).One(&books[i])
books[i].Id = bson.ObjectId(books[i].Id).Hex()
}
return
}
func (db *DB) UpdateMostVisited() error {
var u dbUpdate
u.src = db.session.DB(db.name).C(stats_coll)
u.dst = db.session.DB(db.name).C(visited_coll)
return u.UpdateMostBooks("book")
}
/* Get the most downloaded books
*/
func (db *DB) GetDownloadedBooks() (books []Book, err error) {
downloadedColl := db.session.DB(db.name).C(downloaded_coll)
bookId, err := GetBooksVisited(downloadedColl)
if err != nil {
return nil, err
}
books = make([]Book, len(bookId))
for i, id := range bookId {
booksColl := db.session.DB(db.name).C(books_coll)
booksColl.Find(bson.M{"_id": id}).One(&books[i])
books[i].Id = bson.ObjectId(books[i].Id).Hex()
}
return
}
func (db *DB) UpdateDownloadedBooks() error {
var u dbUpdate
u.src = db.session.DB(db.name).C(stats_coll)
u.dst = db.session.DB(db.name).C(downloaded_coll)
return u.UpdateMostBooks("download")
}
func (db *DB) GetFS(prefix string) *mgo.GridFS {
return db.session.DB(db.name).GridFS(prefix)
}
func (db *DB) GetTags() ([]string, error) {
tagsColl := db.session.DB(db.name).C(tags_coll)
return GetTags(tagsColl)
}
func (db *DB) UpdateTags() error {
var u dbUpdate
u.src = db.session.DB(db.name).C(books_coll)
u.dst = db.session.DB(db.name).C(tags_coll)
return u.UpdateTags()
}
func (db *DB) GetVisits(visitType VisitType) ([]Visits, error) {
var coll *mgo.Collection
switch visitType {
case Hourly_visits:
coll = db.session.DB(db.name).C(hourly_visits_coll)
case Daily_visits:
coll = db.session.DB(db.name).C(daily_visits_coll)
case Monthly_visits:
coll = db.session.DB(db.name).C(monthly_visits_coll)
case Hourly_downloads:
coll = db.session.DB(db.name).C(hourly_downloads_coll)
case Daily_downloads:
coll = db.session.DB(db.name).C(daily_downloads_coll)
case Monthly_downloads:
coll = db.session.DB(db.name).C(monthly_downloads_coll)
default:
return nil, errors.New("Not valid VisitType")
}
return GetVisits(coll)
}
func (db *DB) UpdateHourVisits() error {
var u dbUpdate
u.src = db.session.DB(db.name).C(stats_coll)
u.dst = db.session.DB(db.name).C(hourly_visits_coll)
return u.UpdateHourVisits(false)
}
func (db *DB) UpdateDayVisits() error {
var u dbUpdate
u.src = db.session.DB(db.name).C(stats_coll)
u.dst = db.session.DB(db.name).C(daily_visits_coll)
return u.UpdateDayVisits(false)
}
func (db *DB) UpdateMonthVisits() error {
var u dbUpdate
u.src = db.session.DB(db.name).C(stats_coll)
u.dst = db.session.DB(db.name).C(monthly_visits_coll)
return u.UpdateMonthVisits(false)
}
func (db *DB) UpdateHourDownloads() error {
var u dbUpdate
u.src = db.session.DB(db.name).C(stats_coll)
u.dst = db.session.DB(db.name).C(hourly_downloads_coll)
return u.UpdateHourVisits(true)
}
func (db *DB) UpdateDayDownloads() error {
var u dbUpdate
u.src = db.session.DB(db.name).C(stats_coll)
u.dst = db.session.DB(db.name).C(daily_downloads_coll)
return u.UpdateDayVisits(true)
}
func (db *DB) UpdateMonthDownloads() error {
var u dbUpdate
u.src = db.session.DB(db.name).C(stats_coll)
u.dst = db.session.DB(db.name).C(monthly_downloads_coll)
return u.UpdateMonthVisits(true)
}
// function defined for the tests
func (db *DB) del() {
defer db.Close()
db.session.DB(db.name).DropDatabase()
}

40
database/database_test.go Normal file
View file

@ -0,0 +1,40 @@
package database
import "testing"
const (
test_coll = "test_trantor"
test_host = "127.0.0.1"
)
func TestInit(t *testing.T) {
db := Init(test_host, test_coll)
defer db.Close()
}
func TestCopy(t *testing.T) {
db := Init(test_host, test_coll)
defer db.del()
db2 := db.Copy()
if db.name != db2.name {
t.Errorf("Names don't match")
}
names1, err := db.session.DatabaseNames()
if err != nil {
t.Errorf("Error on db1: ", err)
}
names2, err := db2.session.DatabaseNames()
if err != nil {
t.Errorf("Error on db1: ", err)
}
if len(names1) != len(names2) {
t.Errorf("len(names) don't match")
}
for i, _ := range names1 {
if names1[i] != names2[i] {
t.Errorf("Names don't match")
}
}
}

35
database/news.go Normal file
View file

@ -0,0 +1,35 @@
package database
import (
"labix.org/v2/mgo"
"labix.org/v2/mgo/bson"
"time"
)
const (
news_coll = "news"
)
type News struct {
Date time.Time
Text string
}
func addNews(coll *mgo.Collection, text string) error {
var news News
news.Text = text
news.Date = time.Now()
return coll.Insert(news)
}
func getNews(coll *mgo.Collection, num int, days int) (news []News, err error) {
query := bson.M{}
if days != 0 {
duration := time.Duration(-24*days) * time.Hour
date := time.Now().Add(duration)
query = bson.M{"date": bson.M{"$gt": date}}
}
q := coll.Find(query).Sort("-date").Limit(num)
err = q.All(&news)
return
}

26
database/news_test.go Normal file
View file

@ -0,0 +1,26 @@
package database
import "testing"
func TestNews(t *testing.T) {
const text = "Some news text"
db := Init(test_host, test_coll)
defer db.del()
err := db.AddNews(text)
if err != nil {
t.Errorf("db.News(", text, ") return an error: ", err)
}
news, err := db.GetNews(1, 1)
if err != nil {
t.Fatalf("db.GetNews() return an error: ", err)
}
if len(news) < 1 {
t.Fatalf("No news found.")
}
if news[0].Text != text {
t.Errorf("News text don't match : '", news[0].Text, "' <=> '", text, "'")
}
}

230
database/stats.go Normal file
View file

@ -0,0 +1,230 @@
package database
import (
"labix.org/v2/mgo"
"labix.org/v2/mgo/bson"
"time"
)
const (
stats_coll = "statistics"
hourly_visits_coll = "visits.hourly"
daily_visits_coll = "visits.daily"
monthly_visits_coll = "visits.monthly"
hourly_downloads_coll = "downloads.hourly"
daily_downloads_coll = "downloads.daily"
monthly_downloads_coll = "downloads.monthly"
// FIXME: this should return to the config.go
TAGS_DISPLAY = 50
BOOKS_FRONT_PAGE = 6
)
type dbUpdate struct {
src *mgo.Collection
dst *mgo.Collection
}
type VisitType int
const (
Hourly_visits = iota
Daily_visits
Monthly_visits
Hourly_downloads
Daily_downloads
Monthly_downloads
)
type Visits struct {
Date time.Time "date"
Count int "count"
}
func GetTags(tagsColl *mgo.Collection) ([]string, error) {
var result []struct {
Tag string "_id"
}
err := tagsColl.Find(nil).Sort("-count").All(&result)
if err != nil {
return nil, err
}
tags := make([]string, len(result))
for i, r := range result {
tags[i] = r.Tag
}
return tags, nil
}
func GetBooksVisited(visitedColl *mgo.Collection) ([]bson.ObjectId, error) {
var result []struct {
Book bson.ObjectId "_id"
}
err := visitedColl.Find(nil).Sort("-count").All(&result)
if err != nil {
return nil, err
}
books := make([]bson.ObjectId, len(result))
for i, r := range result {
books[i] = r.Book
}
return books, nil
}
func GetVisits(visitsColl *mgo.Collection) ([]Visits, error) {
var result []Visits
err := visitsColl.Find(nil).All(&result)
return result, err
}
func (u *dbUpdate) UpdateTags() error {
var tags []struct {
Tag string "_id"
Count int "count"
}
err := u.src.Pipe([]bson.M{
{"$project": bson.M{"subject": 1}},
{"$unwind": "$subject"},
{"$group": bson.M{"_id": "$subject", "count": bson.M{"$sum": 1}}},
{"$sort": bson.M{"count": -1}},
{"$limit": TAGS_DISPLAY},
}).All(&tags)
if err != nil {
return err
}
u.dst.DropCollection()
for _, tag := range tags {
err = u.dst.Insert(tag)
if err != nil {
return err
}
}
return nil
}
func (u *dbUpdate) UpdateMostBooks(section string) error {
const numDays = 30
start := time.Now().UTC().Add(-numDays * 24 * time.Hour)
var books []struct {
Book string "_id"
Count int "count"
}
err := u.src.Pipe([]bson.M{
{"$match": bson.M{"date": bson.M{"$gt": start}, "section": section}},
{"$project": bson.M{"id": 1}},
{"$group": bson.M{"_id": "$id", "count": bson.M{"$sum": 1}}},
{"$sort": bson.M{"count": -1}},
{"$limit": BOOKS_FRONT_PAGE},
}).All(&books)
if err != nil {
return err
}
u.dst.DropCollection()
for _, book := range books {
err = u.dst.Insert(book)
if err != nil {
return err
}
}
return nil
}
func (u *dbUpdate) UpdateHourVisits(isDownloads bool) error {
const numDays = 2
spanStore := numDays * 24 * time.Hour
return u.updateVisits(hourInc, spanStore, isDownloads)
}
func (u *dbUpdate) UpdateDayVisits(isDownloads bool) error {
const numDays = 30
spanStore := numDays * 24 * time.Hour
return u.updateVisits(dayInc, spanStore, isDownloads)
}
func (u *dbUpdate) UpdateMonthVisits(isDownloads bool) error {
const numDays = 365
spanStore := numDays * 24 * time.Hour
return u.updateVisits(monthInc, spanStore, isDownloads)
}
func hourInc(date time.Time) time.Time {
const span = time.Hour
return date.Add(span).Truncate(span)
}
func dayInc(date time.Time) time.Time {
const span = 24 * time.Hour
return date.Add(span).Truncate(span)
}
func monthInc(date time.Time) time.Time {
const span = 24 * time.Hour
return date.AddDate(0, 1, 1-date.Day()).Truncate(span)
}
func (u *dbUpdate) updateVisits(incTime func(time.Time) time.Time, spanStore time.Duration, isDownloads bool) error {
start := u.calculateStart(spanStore)
for start.Before(time.Now().UTC()) {
stop := incTime(start)
var count int
var err error
if isDownloads {
count, err = u.countDownloads(start, stop)
} else {
count = u.countVisits(start, stop)
}
if err != nil {
return err
}
err = u.dst.Insert(bson.M{"date": start, "count": count})
if err != nil {
return err
}
start = stop
}
_, err := u.dst.RemoveAll(bson.M{"date": bson.M{"$lt": time.Now().UTC().Add(-spanStore)}})
return err
}
func (u *dbUpdate) calculateStart(spanStore time.Duration) time.Time {
var date struct {
Id bson.ObjectId `bson:"_id"`
Date time.Time `bson:"date"`
}
err := u.dst.Find(bson.M{}).Sort("-date").One(&date)
if err == nil {
u.dst.RemoveId(date.Id)
return date.Date
}
return time.Now().UTC().Add(-spanStore).Truncate(time.Hour)
}
func (u *dbUpdate) countVisits(start time.Time, stop time.Time) int {
var result struct {
Count int "count"
}
err := u.src.Pipe([]bson.M{
{"$match": bson.M{"date": bson.M{"$gte": start, "$lt": stop}}},
{"$group": bson.M{"_id": "$session"}},
{"$group": bson.M{"_id": 1, "count": bson.M{"$sum": 1}}},
}).One(&result)
if err != nil {
return 0
}
return result.Count
}
func (u *dbUpdate) countDownloads(start time.Time, stop time.Time) (int, error) {
query := bson.M{"date": bson.M{"$gte": start, "$lt": stop}, "section": "download"}
return u.src.Find(query).Count()
}

96
database/users.go Normal file
View file

@ -0,0 +1,96 @@
package database
import log "github.com/cihub/seelog"
import (
"bytes"
"crypto/md5"
"errors"
"labix.org/v2/mgo"
"labix.org/v2/mgo/bson"
)
const (
user_coll = "users"
pass_salt = "ImperialLibSalt"
)
type User struct {
user db_user
err error
coll *mgo.Collection
}
type db_user struct {
User string
Pass []byte
Role string
}
func getUser(coll *mgo.Collection, name string) *User {
u := new(User)
if !validUserName(name) {
u.err = errors.New("Invalid username")
return u
}
u.coll = coll
err := u.coll.Find(bson.M{"user": name}).One(&u.user)
if err != nil {
log.Warn("Error on database checking user ", name, ": ", err)
u.err = errors.New("User not found")
return u
}
return u
}
func addUser(coll *mgo.Collection, name string, pass string) error {
if !validUserName(name) {
return errors.New("Invalid user name")
}
num, err := coll.Find(bson.M{"user": name}).Count()
if err != nil {
log.Error("Error on database checking user ", name, ": ", err)
return errors.New("An error happen on the database")
}
if num != 0 {
return errors.New("User name already exist")
}
var user db_user
user.Pass = md5Pass(pass)
user.User = name
user.Role = ""
return coll.Insert(user)
}
func validUserName(name string) bool {
return name != ""
}
func (u User) Valid(pass string) bool {
if u.err != nil {
return false
}
hash := md5Pass(pass)
return bytes.Compare(u.user.Pass, hash) == 0
}
func (u User) Role() string {
return u.user.Role
}
func (u *User) SetPassword(pass string) error {
if u.err != nil {
return u.err
}
hash := md5Pass(pass)
return u.coll.Update(bson.M{"user": u.user.User}, bson.M{"$set": bson.M{"pass": hash}})
}
// FIXME: use a proper salting algorithm
func md5Pass(pass string) []byte {
h := md5.New()
hash := h.Sum(([]byte)(pass_salt + pass))
return hash
}

43
database/users_test.go Normal file
View file

@ -0,0 +1,43 @@
package database
import "testing"
const (
name, pass = "user", "mypass"
)
func TestUserEmpty(t *testing.T) {
db := Init(test_host, test_coll)
defer db.del()
if db.User("").Valid("") {
t.Errorf("user.Valid() with an empty password return true")
}
}
func TestAddUser(t *testing.T) {
db := Init(test_host, test_coll)
defer db.del()
tAddUser(t, db)
if !db.User(name).Valid(pass) {
t.Errorf("user.Valid() return false for a valid user")
}
}
func TestEmptyUsername(t *testing.T) {
db := Init(test_host, test_coll)
defer db.del()
tAddUser(t, db)
if db.User("").Valid(pass) {
t.Errorf("user.Valid() return true for an invalid user")
}
}
func tAddUser(t *testing.T, db *DB) {
err := db.AddUser(name, pass)
if err != nil {
t.Errorf("db.Adduser(", name, ", ", pass, ") return an error: ", err)
}
}