Move all the code to a lib folder
This commit is contained in:
parent
e963d00014
commit
9d1f1ad5c0
31 changed files with 123 additions and 98 deletions
234
lib/database/books.go
Normal file
234
lib/database/books.go
Normal file
|
@ -0,0 +1,234 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
log "github.com/cihub/seelog"
|
||||
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
const (
|
||||
books_coll = "books"
|
||||
)
|
||||
|
||||
type Book struct {
|
||||
Id string
|
||||
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
|
||||
FileSize int
|
||||
Cover bool
|
||||
Active bool
|
||||
BadQuality int `bad_quality`
|
||||
BadQualityReporters []string `bad_quality_reporters`
|
||||
}
|
||||
|
||||
type history struct {
|
||||
Date time.Time
|
||||
Changes bson.M
|
||||
}
|
||||
|
||||
func indexBooks(coll *mgo.Collection) {
|
||||
indexes := []mgo.Index{
|
||||
{
|
||||
Key: []string{"id"},
|
||||
Unique: true,
|
||||
Background: true,
|
||||
},
|
||||
{
|
||||
Key: []string{"active", "-_id"},
|
||||
Background: true,
|
||||
},
|
||||
{
|
||||
Key: []string{"active", "-bad_quality", "-_id"},
|
||||
Background: true,
|
||||
},
|
||||
{
|
||||
Key: []string{"$text:title", "$text:author", "$text:contributor",
|
||||
"$text:publisher", "$text:subject", "$text:description"},
|
||||
Weights: map[string]int{"title": 20, "author": 20, "contributor": 15,
|
||||
"publisher": 15, "subject": 10, "description": 5},
|
||||
|
||||
LanguageOverride: "_lang",
|
||||
Background: true,
|
||||
},
|
||||
}
|
||||
for _, k := range []string{"lang", "title", "author", "subject"} {
|
||||
idx := mgo.Index{
|
||||
Key: []string{"active", k, "-_id"},
|
||||
Background: true,
|
||||
}
|
||||
indexes = append(indexes, idx)
|
||||
}
|
||||
|
||||
for _, idx := range indexes {
|
||||
err := coll.EnsureIndex(idx)
|
||||
if err != nil {
|
||||
log.Error("Error indexing books: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addBook(coll *mgo.Collection, book map[string]interface{}) error {
|
||||
book["_lang"] = metadataLang(book)
|
||||
return coll.Insert(book)
|
||||
}
|
||||
|
||||
func getBooks(coll *mgo.Collection, query string, length int, start int) (books []Book, num int, err error) {
|
||||
return _getBooks(coll, buildQuery(query), length, start)
|
||||
}
|
||||
|
||||
func getNewBooks(coll *mgo.Collection, length int, start int) (books []Book, num int, err error) {
|
||||
return _getBooks(coll, bson.M{"$nor": []bson.M{{"active": true}}}, length, start)
|
||||
}
|
||||
|
||||
func getBooksIter(coll *mgo.Collection) Iter {
|
||||
return coll.Find(bson.M{}).Iter()
|
||||
}
|
||||
|
||||
func _getBooks(coll *mgo.Collection, query bson.M, length int, start int) (books []Book, num int, err error) {
|
||||
q := getBookQuery(coll, query)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
func getBookQuery(coll *mgo.Collection, query bson.M) *mgo.Query {
|
||||
sort := []string{"$textScore:score"}
|
||||
if _, present := query["bad_quality"]; present {
|
||||
sort = append(sort, "-bad_quality")
|
||||
}
|
||||
sort = append(sort, "-_id")
|
||||
|
||||
return coll.Find(query).Select(bson.M{"score": bson.M{"$meta": "textScore"}}).Sort(sort...)
|
||||
}
|
||||
|
||||
func getBookId(coll *mgo.Collection, id string) (Book, error) {
|
||||
var book Book
|
||||
err := coll.Find(bson.M{"id": id}).One(&book)
|
||||
return book, err
|
||||
}
|
||||
|
||||
func deleteBook(coll *mgo.Collection, id string) error {
|
||||
return coll.Remove(bson.M{"id": id})
|
||||
}
|
||||
|
||||
func updateBook(coll *mgo.Collection, id string, data map[string]interface{}) error {
|
||||
var book map[string]interface{}
|
||||
record := history{time.Now(), bson.M{}}
|
||||
|
||||
err := coll.Find(bson.M{"id": id}).One(&book)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, _ := range data {
|
||||
record.Changes[k] = book[k]
|
||||
if k == "lang" {
|
||||
data["_lang"] = metadataLang(data)
|
||||
}
|
||||
}
|
||||
|
||||
return coll.Update(bson.M{"id": id}, bson.M{"$set": data, "$push": bson.M{"history": record}})
|
||||
}
|
||||
|
||||
func flagBadQuality(coll *mgo.Collection, id string, user string) error {
|
||||
b, err := getBookId(coll, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, reporter := range b.BadQualityReporters {
|
||||
if reporter == user {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return coll.Update(
|
||||
bson.M{"id": id},
|
||||
bson.M{
|
||||
"$inc": bson.M{"bad_quality": 1},
|
||||
"$addToSet": bson.M{"bad_quality_reporters": user},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func activeBook(coll *mgo.Collection, id string) error {
|
||||
data := map[string]interface{}{"active": true}
|
||||
return coll.Update(bson.M{"id": id}, bson.M{"$set": data})
|
||||
}
|
||||
|
||||
func isBookActive(coll *mgo.Collection, id string) bool {
|
||||
var book Book
|
||||
err := coll.Find(bson.M{"id": id}).One(&book)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return book.Active
|
||||
}
|
||||
|
||||
func buildQuery(q string) bson.M {
|
||||
text := ""
|
||||
query := bson.M{"active": true}
|
||||
words := strings.Split(q, " ")
|
||||
for _, w := range words {
|
||||
tag := strings.SplitN(w, ":", 2)
|
||||
if len(tag) > 1 {
|
||||
if tag[0] == "flag" {
|
||||
query[tag[1]] = bson.M{"$gt": 0}
|
||||
} else {
|
||||
query[tag[0]] = bson.RegEx{tag[1], "i"} //FIXME: this should be a list
|
||||
}
|
||||
} else {
|
||||
if len(text) != 0 {
|
||||
text += " "
|
||||
}
|
||||
text += w
|
||||
}
|
||||
}
|
||||
if len(text) > 0 {
|
||||
query["$text"] = bson.M{"$search": text}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
func metadataLang(book map[string]interface{}) string {
|
||||
text_search_langs := map[string]bool{
|
||||
"da": true, "nl": true, "en": true, "fi": true, "fr": true, "de": true,
|
||||
"hu": true, "it": true, "nb": true, "pt": true, "ro": true, "ru": true,
|
||||
"es": true, "sv": true, "tr": true}
|
||||
|
||||
lang, ok := book["lang"].([]string)
|
||||
if !ok || len(lang) == 0 || len(lang[0]) < 2 {
|
||||
return "none"
|
||||
}
|
||||
lcode := strings.ToLower(lang[0][0:2])
|
||||
if text_search_langs[lcode] {
|
||||
return lcode
|
||||
}
|
||||
return "none"
|
||||
}
|
105
lib/database/books_test.go
Normal file
105
lib/database/books_test.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package database
|
||||
|
||||
import "testing"
|
||||
|
||||
var book = map[string]interface{}{
|
||||
"title": "some title",
|
||||
"author": []string{"Alice", "Bob"},
|
||||
"id": "r_m-IOzzIbA6QK5w",
|
||||
}
|
||||
|
||||
func TestAddBook(t *testing.T) {
|
||||
db := Init(test_host, test_coll)
|
||||
defer db.del()
|
||||
|
||||
tAddBook(t, db)
|
||||
|
||||
books, num, err := db.GetNewBooks(1, 0)
|
||||
if err != nil {
|
||||
t.Fatal("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.Error("Book title don't match : '", books[0].Title, "' <=> '", book["title"], "'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveBook(t *testing.T) {
|
||||
db := Init(test_host, test_coll)
|
||||
defer db.del()
|
||||
|
||||
tAddBook(t, db)
|
||||
books, _, _ := db.GetNewBooks(1, 0)
|
||||
id := books[0].Id
|
||||
|
||||
err := db.ActiveBook(id)
|
||||
if err != nil {
|
||||
t.Fatal("db.ActiveBook(", id, ") return an error: ", err)
|
||||
}
|
||||
|
||||
b, err := db.GetBookId(id)
|
||||
if err != nil {
|
||||
t.Fatal("db.GetBookId(", id, ") return an error: ", err)
|
||||
}
|
||||
if b.Author[0] != books[0].Author[0] {
|
||||
t.Error("Book author don't match : '", b.Author, "' <=> '", book["author"], "'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlag(t *testing.T) {
|
||||
db := Init(test_host, test_coll)
|
||||
defer db.del()
|
||||
|
||||
tAddBook(t, db)
|
||||
id, _ := book["id"].(string)
|
||||
db.ActiveBook(id)
|
||||
id2 := "tfgrBvd2ps_K4iYt"
|
||||
b2 := book
|
||||
b2["id"] = id2
|
||||
err := db.AddBook(b2)
|
||||
if err != nil {
|
||||
t.Error("db.AddBook(", book, ") return an error:", err)
|
||||
}
|
||||
db.ActiveBook(id2)
|
||||
id3 := "tfgrBvd2ps_K4iY2"
|
||||
b3 := book
|
||||
b3["id"] = id3
|
||||
err = db.AddBook(b3)
|
||||
if err != nil {
|
||||
t.Error("db.AddBook(", book, ") return an error:", err)
|
||||
}
|
||||
db.ActiveBook(id3)
|
||||
|
||||
db.FlagBadQuality(id, "1")
|
||||
db.FlagBadQuality(id, "2")
|
||||
db.FlagBadQuality(id3, "1")
|
||||
|
||||
b, _ := db.GetBookId(id)
|
||||
if b.BadQuality != 2 {
|
||||
t.Error("The bad quality flag was not increased")
|
||||
}
|
||||
b, _ = db.GetBookId(id3)
|
||||
if b.BadQuality != 1 {
|
||||
t.Error("The bad quality flag was not increased")
|
||||
}
|
||||
|
||||
books, _, _ := db.GetBooks("flag:bad_quality", 2, 0)
|
||||
if len(books) != 2 {
|
||||
t.Fatal("Not the right number of results to the flag search:", len(books))
|
||||
}
|
||||
if books[0].Id != id {
|
||||
t.Error("Search for flag bad_quality is not sort right")
|
||||
}
|
||||
}
|
||||
|
||||
func tAddBook(t *testing.T, db *DB) {
|
||||
err := db.AddBook(book)
|
||||
if err != nil {
|
||||
t.Error("db.AddBook(", book, ") return an error:", err)
|
||||
}
|
||||
}
|
268
lib/database/database.go
Normal file
268
lib/database/database.go
Normal file
|
@ -0,0 +1,268 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
log "github.com/cihub/seelog"
|
||||
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
const (
|
||||
visited_coll = "visited"
|
||||
downloaded_coll = "downloaded"
|
||||
tags_coll = "tags"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
session *mgo.Session
|
||||
name string
|
||||
}
|
||||
|
||||
type Iter interface {
|
||||
Close() error
|
||||
Next(interface{}) bool
|
||||
}
|
||||
|
||||
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
|
||||
db.initIndexes()
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *DB) initIndexes() {
|
||||
dbCopy := db.session.Copy()
|
||||
booksColl := dbCopy.DB(db.name).C(books_coll)
|
||||
go indexBooks(booksColl)
|
||||
statsColl := dbCopy.DB(db.name).C(stats_coll)
|
||||
go indexStats(statsColl)
|
||||
newsColl := dbCopy.DB(db.name).C(news_coll)
|
||||
go indexNews(newsColl)
|
||||
}
|
||||
|
||||
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 map[string]interface{}) error {
|
||||
booksColl := db.session.DB(db.name).C(books_coll)
|
||||
return addBook(booksColl, book)
|
||||
}
|
||||
|
||||
func (db *DB) GetBooks(query string, 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) GetBooksIter() Iter {
|
||||
booksColl := db.session.DB(db.name).C(books_coll)
|
||||
return getBooksIter(booksColl)
|
||||
}
|
||||
|
||||
func (db *DB) GetNewBooks(length int, start int) (books []Book, num int, err error) {
|
||||
booksColl := db.session.DB(db.name).C(books_coll)
|
||||
return getNewBooks(booksColl, length, start)
|
||||
}
|
||||
|
||||
func (db *DB) GetBookId(id string) (Book, error) {
|
||||
booksColl := db.session.DB(db.name).C(books_coll)
|
||||
return getBookId(booksColl, id)
|
||||
}
|
||||
|
||||
func (db *DB) DeleteBook(id string) error {
|
||||
booksColl := db.session.DB(db.name).C(books_coll)
|
||||
return deleteBook(booksColl, id)
|
||||
}
|
||||
|
||||
func (db *DB) UpdateBook(id string, data map[string]interface{}) error {
|
||||
booksColl := db.session.DB(db.name).C(books_coll)
|
||||
return updateBook(booksColl, id, data)
|
||||
}
|
||||
|
||||
func (db *DB) FlagBadQuality(id string, user string) error {
|
||||
booksColl := db.session.DB(db.name).C(books_coll)
|
||||
return flagBadQuality(booksColl, id, user)
|
||||
}
|
||||
|
||||
func (db *DB) ActiveBook(id string) error {
|
||||
booksColl := db.session.DB(db.name).C(books_coll)
|
||||
return activeBook(booksColl, id)
|
||||
}
|
||||
|
||||
func (db *DB) IsBookActive(id string) bool {
|
||||
booksColl := db.session.DB(db.name).C(books_coll)
|
||||
return isBookActive(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) 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
lib/database/database_test.go
Normal file
40
lib/database/database_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
49
lib/database/news.go
Normal file
49
lib/database/news.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
log "github.com/cihub/seelog"
|
||||
|
||||
"time"
|
||||
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
const (
|
||||
news_coll = "news"
|
||||
)
|
||||
|
||||
type News struct {
|
||||
Date time.Time
|
||||
Text string
|
||||
}
|
||||
|
||||
func indexNews(coll *mgo.Collection) {
|
||||
idx := mgo.Index{
|
||||
Key: []string{"-date"},
|
||||
Background: true,
|
||||
}
|
||||
err := coll.EnsureIndex(idx)
|
||||
if err != nil {
|
||||
log.Error("Error indexing news: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
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
lib/database/news_test.go
Normal file
26
lib/database/news_test.go
Normal 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, "'")
|
||||
}
|
||||
}
|
253
lib/database/stats.go
Normal file
253
lib/database/stats.go
Normal file
|
@ -0,0 +1,253 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
log "github.com/cihub/seelog"
|
||||
|
||||
"time"
|
||||
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
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 indexStats(coll *mgo.Collection) {
|
||||
indexes := []mgo.Index{
|
||||
{
|
||||
Key: []string{"section"},
|
||||
Background: true,
|
||||
},
|
||||
{
|
||||
Key: []string{"-date", "section"},
|
||||
Background: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, idx := range indexes {
|
||||
err := coll.EnsureIndex(idx)
|
||||
if err != nil {
|
||||
log.Error("Error indexing stats: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
137
lib/database/users.go
Normal file
137
lib/database/users.go
Normal file
|
@ -0,0 +1,137 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
log "github.com/cihub/seelog"
|
||||
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
|
||||
"golang.org/x/crypto/scrypt"
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/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
|
||||
Salt []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, user.Salt, err = hashPass(pass)
|
||||
if err != nil {
|
||||
log.Error("Error hashing password: ", err)
|
||||
return errors.New("An error happen storing the password")
|
||||
}
|
||||
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
|
||||
}
|
||||
return validatePass(pass, u.user)
|
||||
}
|
||||
|
||||
func (u User) Role() string {
|
||||
return u.user.Role
|
||||
}
|
||||
|
||||
func (u *User) SetPassword(pass string) error {
|
||||
if u.err != nil {
|
||||
return u.err
|
||||
}
|
||||
hash, salt, err := hashPass(pass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return u.coll.Update(bson.M{"user": u.user.User}, bson.M{"$set": bson.M{"pass": hash, "salt": salt}})
|
||||
}
|
||||
|
||||
func hashPass(pass string) (hash []byte, salt []byte, err error) {
|
||||
salt, err = genSalt()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hash, err = calculateHash(pass, salt)
|
||||
return
|
||||
}
|
||||
|
||||
func genSalt() ([]byte, error) {
|
||||
const (
|
||||
saltLen = 64
|
||||
)
|
||||
|
||||
b := make([]byte, saltLen)
|
||||
_, err := rand.Read(b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
func validatePass(pass string, user db_user) bool {
|
||||
hash, err := calculateHash(pass, user.Salt)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Compare(user.Pass, hash) == 0
|
||||
}
|
||||
|
||||
func calculateHash(pass string, salt []byte) ([]byte, error) {
|
||||
const (
|
||||
N = 16384
|
||||
r = 8
|
||||
p = 1
|
||||
keyLen = 32
|
||||
)
|
||||
|
||||
bpass := []byte(pass)
|
||||
return scrypt.Key(bpass, salt, N, r, p, keyLen)
|
||||
}
|
43
lib/database/users_test.go
Normal file
43
lib/database/users_test.go
Normal 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)
|
||||
}
|
||||
}
|
Reference in a new issue