Move all the code to a lib folder

This commit is contained in:
Las Zenow 2016-05-02 21:36:49 -04:00
parent e963d00014
commit 9d1f1ad5c0
31 changed files with 123 additions and 98 deletions

210
lib/admin.go Normal file
View file

@ -0,0 +1,210 @@
package trantor
import (
log "github.com/cihub/seelog"
"net/http"
"strconv"
"strings"
"github.com/gorilla/mux"
"gitlab.com/trantor/trantor/lib/database"
)
func deleteHandler(h handler) {
if !h.sess.IsAdmin() {
notFound(h)
return
}
var titles []string
var isNew bool
ids := strings.Split(mux.Vars(h.r)["ids"], "/")
for _, id := range ids {
if id == "" {
continue
}
book, err := h.db.GetBookId(id)
if err != nil {
h.sess.Notify("Book not found!", "The book with id '"+id+"' is not there", "error")
continue
}
h.store.Delete(id)
h.db.DeleteBook(id)
if !book.Active {
isNew = true
}
titles = append(titles, book.Title)
}
if titles != nil {
h.sess.Notify("Removed books!", "The books "+strings.Join(titles, ", ")+" are completly removed", "success")
}
h.sess.Save(h.w, h.r)
if isNew {
http.Redirect(h.w, h.r, "/new/", http.StatusFound)
} else {
http.Redirect(h.w, h.r, "/", http.StatusFound)
}
}
func editHandler(h handler) {
id := mux.Vars(h.r)["id"]
if !h.sess.IsAdmin() {
notFound(h)
return
}
book, err := h.db.GetBookId(id)
if err != nil {
notFound(h)
return
}
var data bookData
data.Book = book
data.S = GetStatus(h)
data.S.Title = book.Title + " by " + book.Author[0] + " -- Edit -- " + data.S.Title
loadTemplate(h, "edit", data)
}
func cleanEmptyStr(s []string) []string {
var res []string
for _, v := range s {
if v != "" {
res = append(res, v)
}
}
return res
}
func saveHandler(h handler) {
id := mux.Vars(h.r)["id"]
if !h.sess.IsAdmin() {
notFound(h)
return
}
title := h.r.FormValue("title")
publisher := h.r.FormValue("publisher")
date := h.r.FormValue("date")
description := h.r.FormValue("description")
author := cleanEmptyStr(h.r.Form["author"])
subject := cleanEmptyStr(h.r.Form["subject"])
lang := cleanEmptyStr(h.r.Form["lang"])
book := map[string]interface{}{"title": title,
"publisher": publisher,
"date": date,
"description": description,
"author": author,
"subject": subject,
"lang": lang}
err := h.db.UpdateBook(id, book)
if err != nil {
log.Error("Updating book: ", err)
notFound(h)
return
}
h.sess.Notify("Book Modified!", "", "success")
h.sess.Save(h.w, h.r)
if h.db.IsBookActive(id) {
http.Redirect(h.w, h.r, "/book/"+id, http.StatusFound)
} else {
http.Redirect(h.w, h.r, "/new/", http.StatusFound)
}
}
type newBook struct {
TitleFound int
AuthorFound int
B database.Book
}
type newData struct {
S Status
Found int
Books []newBook
Page int
Next string
Prev string
}
func newHandler(h handler) {
if !h.sess.IsAdmin() {
notFound(h)
return
}
err := h.r.ParseForm()
if err != nil {
http.Error(h.w, err.Error(), http.StatusInternalServerError)
return
}
page := 0
if len(h.r.Form["p"]) != 0 {
page, err = strconv.Atoi(h.r.Form["p"][0])
if err != nil {
page = 0
}
}
res, num, _ := h.db.GetNewBooks(NEW_ITEMS_PAGE, page*NEW_ITEMS_PAGE)
var data newData
data.S = GetStatus(h)
data.S.Title = "New books -- " + data.S.Title
data.Found = num
if num-NEW_ITEMS_PAGE*page < NEW_ITEMS_PAGE {
data.Books = make([]newBook, num-NEW_ITEMS_PAGE*page)
} else {
data.Books = make([]newBook, NEW_ITEMS_PAGE)
}
for i, b := range res {
data.Books[i].B = b
_, data.Books[i].TitleFound, _ = h.db.GetBooks("title:"+b.Title, 1, 0)
_, data.Books[i].AuthorFound, _ = h.db.GetBooks("author:"+strings.Join(b.Author, " author:"), 1, 0)
}
data.Page = page + 1
if num > (page+1)*NEW_ITEMS_PAGE {
data.Next = "/new/?p=" + strconv.Itoa(page+1)
}
if page > 0 {
data.Prev = "/new/?p=" + strconv.Itoa(page-1)
}
loadTemplate(h, "new", data)
}
func storeHandler(h handler) {
if !h.sess.IsAdmin() {
notFound(h)
return
}
var titles []string
ids := strings.Split(mux.Vars(h.r)["ids"], "/")
for _, id := range ids {
if id == "" {
continue
}
book, err := h.db.GetBookId(id)
if err != nil {
h.sess.Notify("Book not found!", "The book with id '"+id+"' is not there", "error")
continue
}
if err != nil {
h.sess.Notify("An error ocurred!", err.Error(), "error")
log.Error("Error getting book for storing '", book.Title, "': ", err.Error())
continue
}
err = h.db.ActiveBook(id)
if err != nil {
h.sess.Notify("An error ocurred!", err.Error(), "error")
log.Error("Error storing book '", book.Title, "': ", err.Error())
continue
}
titles = append(titles, book.Title)
}
if titles != nil {
h.sess.Notify("Store books!", "The books '"+strings.Join(titles, ", ")+"' are stored for public download", "success")
}
h.sess.Save(h.w, h.r)
http.Redirect(h.w, h.r, "/new/", http.StatusFound)
}

44
lib/config.go Normal file
View file

@ -0,0 +1,44 @@
package trantor
const (
HOST_URL = "xfmro77i3lixucja.onion"
META_COLL = "meta"
EPUB_FILE = "book.epub"
COVER_FILE = "cover.jpg"
COVER_SMALL_FILE = "coverSmall.jpg"
MINUTES_UPDATE_TAGS = 11
MINUTES_UPDATE_VISITED = 41
MINUTES_UPDATE_DOWNLOADED = 47
MINUTES_UPDATE_HOURLY_V = 31
MINUTES_UPDATE_DAILY_V = 60*12 + 7
MINUTES_UPDATE_MONTHLY_V = 60*24 + 11
MINUTES_UPDATE_HOURLY_D = 29
MINUTES_UPDATE_DAILY_D = 60*12 + 13
MINUTES_UPDATE_MONTHLY_D = 60*24 + 17
MINUTES_UPDATE_LOGGER = 5
BOOKS_FRONT_PAGE = 6
SEARCH_ITEMS_PAGE = 20
NEW_ITEMS_PAGE = 50
NUM_NEWS = 10
DAYS_NEWS_INDEXPAGE = 15
CACHE_MAX_AGE = 1800
TEMPLATE_PATH = "templates/"
CSS_PATH = "css/"
JS_PATH = "js/"
IMG_PATH = "img/"
ROBOTS_PATH = "robots.txt"
DESCRIPTION_PATH = "description.json"
OPENSEARCH_PATH = "opensearch.xml"
KEY_PATH = "key.asc"
LOGGER_CONFIG = "logger.xml"
IMG_WIDTH_BIG = 300
IMG_WIDTH_SMALL = 60
IMG_QUALITY = 80
CHAN_SIZE = 100
)

190
lib/cover.go Normal file
View file

@ -0,0 +1,190 @@
package trantor
import (
_ "image/gif"
_ "image/jpeg"
_ "image/png"
log "github.com/cihub/seelog"
"bytes"
"image"
"image/jpeg"
"io"
"io/ioutil"
"regexp"
"strings"
"github.com/gorilla/mux"
"github.com/meskio/epubgo"
"github.com/nfnt/resize"
"gitlab.com/trantor/trantor/lib/storage"
)
func coverHandler(h handler) {
vars := mux.Vars(h.r)
book, err := h.db.GetBookId(vars["id"])
if err != nil {
notFound(h)
return
}
if !book.Active {
if !h.sess.IsAdmin() {
notFound(h)
return
}
}
file := COVER_FILE
if vars["size"] == "small" {
file = COVER_SMALL_FILE
}
f, err := h.store.Get(book.Id, file)
if err != nil {
log.Error("Error while opening image: ", err)
notFound(h)
return
}
defer f.Close()
headers := h.w.Header()
headers["Content-Type"] = []string{"image/jpeg"}
_, err = io.Copy(h.w, f)
if err != nil {
log.Error("Error while copying image: ", err)
notFound(h)
return
}
}
func GetCover(e *epubgo.Epub, id string, store *storage.Store) bool {
if coverFromMetadata(e, id, store) {
return true
}
if searchCommonCoverNames(e, id, store) {
return true
}
/* search for img on the text */
exp, _ := regexp.Compile("<.*ima?g.*[(src)(href)]=[\"']([^\"']*(\\.[^\\.\"']*))[\"']")
it, errNext := e.Spine()
for errNext == nil {
file, err := it.Open()
if err != nil {
break
}
defer file.Close()
txt, err := ioutil.ReadAll(file)
if err != nil {
break
}
res := exp.FindSubmatch(txt)
if res != nil {
href := string(res[1])
urlPart := strings.Split(it.URL(), "/")
url := strings.Join(urlPart[:len(urlPart)-1], "/")
if href[:3] == "../" {
href = href[3:]
url = strings.Join(urlPart[:len(urlPart)-2], "/")
}
href = strings.Replace(href, "%20", " ", -1)
href = strings.Replace(href, "%27", "'", -1)
href = strings.Replace(href, "%28", "(", -1)
href = strings.Replace(href, "%29", ")", -1)
if url == "" {
url = href
} else {
url = url + "/" + href
}
img, err := e.OpenFile(url)
if err == nil {
defer img.Close()
return storeImg(img, id, store)
}
}
errNext = it.Next()
}
return false
}
func coverFromMetadata(e *epubgo.Epub, id string, store *storage.Store) bool {
metaList, _ := e.MetadataAttr("meta")
for _, meta := range metaList {
if meta["name"] == "cover" {
img, err := e.OpenFileId(meta["content"])
if err == nil {
defer img.Close()
return storeImg(img, id, store)
}
}
}
return false
}
func searchCommonCoverNames(e *epubgo.Epub, id string, store *storage.Store) bool {
for _, p := range []string{"cover.jpg", "Images/cover.jpg", "images/cover.jpg", "cover.jpeg", "cover1.jpg", "cover1.jpeg"} {
img, err := e.OpenFile(p)
if err == nil {
defer img.Close()
return storeImg(img, id, store)
}
}
return false
}
func storeImg(img io.Reader, id string, store *storage.Store) bool {
/* open the files */
fBig, err := store.Create(id, COVER_FILE)
if err != nil {
log.Error("Error creating cover ", id, ": ", err.Error())
return false
}
defer fBig.Close()
fSmall, err := store.Create(id, COVER_SMALL_FILE)
if err != nil {
log.Error("Error creating small cover ", id, ": ", err.Error())
return false
}
defer fSmall.Close()
/* resize img */
var img2 bytes.Buffer
img1 := io.TeeReader(img, &img2)
jpgOptions := jpeg.Options{IMG_QUALITY}
imgResized, err := resizeImg(img1, IMG_WIDTH_BIG)
if err != nil {
log.Error("Error resizing big image: ", err.Error())
return false
}
err = jpeg.Encode(fBig, imgResized, &jpgOptions)
if err != nil {
log.Error("Error encoding big image: ", err.Error())
return false
}
imgSmallResized, err := resizeImg(&img2, IMG_WIDTH_SMALL)
if err != nil {
log.Error("Error resizing small image: ", err.Error())
return false
}
err = jpeg.Encode(fSmall, imgSmallResized, &jpgOptions)
if err != nil {
log.Error("Error encoding small image: ", err.Error())
return false
}
return true
}
func resizeImg(imgReader io.Reader, width uint) (image.Image, error) {
img, _, err := image.Decode(imgReader)
if err != nil {
return nil, err
}
return resize.Resize(width, 0, img, resize.NearestNeighbor), nil
}

234
lib/database/books.go Normal file
View 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
View 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
View 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()
}

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")
}
}
}

49
lib/database/news.go Normal file
View 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
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, "'")
}
}

253
lib/database/stats.go Normal file
View 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
View 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)
}

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)
}
}

67
lib/news.go Normal file
View file

@ -0,0 +1,67 @@
package trantor
import (
"net/http"
"gitlab.com/trantor/trantor/lib/database"
)
type newsData struct {
S Status
News []newsEntry
}
type newsEntry struct {
Date string
Text string
}
func newsHandler(h handler) {
err := h.r.ParseForm()
if err != nil {
http.Error(h.w, err.Error(), http.StatusInternalServerError)
return
}
var data newsData
data.S = GetStatus(h)
data.S.Title = "News -- " + data.S.Title
data.S.News = true
data.News = getNews(NUM_NEWS, 0, h.db)
loadTemplate(h, "news", data)
}
func editNewsHandler(h handler) {
if !h.sess.IsAdmin() {
notFound(h)
return
}
var data statusData
data.S = GetStatus(h)
data.S.Title = "Edit news -- " + data.S.Title
data.S.News = true
loadTemplate(h, "edit_news", data)
}
func postNewsHandler(h handler) {
if !h.sess.IsAdmin() {
notFound(h)
return
}
text := h.r.FormValue("text")
h.db.AddNews(text)
http.Redirect(h.w, h.r, "/news/", http.StatusFound)
}
func getNews(num int, days int, db *database.DB) []newsEntry {
dbnews, _ := db.GetNews(num, days)
news := make([]newsEntry, len(dbnews))
for i, n := range dbnews {
news[i].Text = n.Text
news[i].Date = n.Date.Format("Jan 2, 2006")
}
return news
}

81
lib/parser/isbn.go Normal file
View file

@ -0,0 +1,81 @@
package parser
import (
"strings"
"unicode"
)
func ISBN(orig string) string {
isbn := getISBN(orig)
if len(isbn) != 13 && len(isbn) != 10 {
return ""
}
if !validChecksum(isbn) {
return ""
}
return toISBN13(isbn)
}
func getISBN(src string) string {
isbn := strings.ToUpper(src)
isNotNumber := func(r rune) bool {
return !unicode.IsNumber(r)
}
isNotNumberOrX := func(r rune) bool {
return !unicode.IsNumber(r) && r != 'X'
}
isbn = strings.TrimLeftFunc(isbn, isNotNumber)
isbn = strings.TrimRightFunc(isbn, isNotNumberOrX)
isbn = strings.Replace(isbn, "-", "", -1)
isbn = strings.Replace(isbn, " ", "", -1)
if len(isbn) > 13 {
isbn = isbn[:13]
}
return isbn
}
func validChecksum(isbn string) bool {
if len(isbn) == 10 {
return rune(isbn[9]) == checkDigit10(isbn)
}
return rune(isbn[12]) == checkDigit13(isbn)
}
func toISBN13(isbn string) string {
if len(isbn) == 13 {
return isbn
}
isbn = "978" + isbn
return isbn[:12] + string(checkDigit13(isbn))
}
func checkDigit10(isbn string) rune {
acc := 0
for i, r := range isbn[:9] {
acc += (10 - i) * int(r-'0')
}
check := (11 - (acc % 11)) % 11
if check == 10 {
return 'X'
}
return rune(check + '0')
}
func checkDigit13(isbn string) rune {
acc := 0
for i, r := range isbn[:12] {
n := int(r - '0')
if i%2 == 1 {
n = 3 * n
}
acc += n
}
check := (10 - (acc % 10)) % 10
return rune(check + '0')
}

26
lib/parser/isbn_test.go Normal file
View file

@ -0,0 +1,26 @@
package parser
import "testing"
func TestISBN(t *testing.T) {
isbn_test := [][]string{
[]string{"", ""},
[]string{"978074341", ""},
[]string{"9780743412395", ""},
[]string{"9780743412391", "9780743412391"},
[]string{"0-688-12189-6", "9780688121891"},
[]string{"033026155X", "9780330261555"},
[]string{"033026155x", "9780330261555"},
[]string{"0307756432", "9780307756435"},
[]string{"urn:isbn:978-3-8387-0337-4:", "9783838703374"},
[]string{"EPUB9788865971468-113465", "9788865971468"},
}
for _, isbn := range isbn_test {
src := isbn[0]
dst := isbn[1]
if res := ISBN(src); res != dst {
t.Error("ISBN parse failed: ", src, " => ", res, " (expected ", dst, ")")
}
}
}

66
lib/parser/language.go Normal file
View file

@ -0,0 +1,66 @@
package parser
import (
"io/ioutil"
"strings"
"github.com/meskio/epubgo"
"github.com/rainycape/cld2"
)
func GuessLang(epub *epubgo.Epub, orig_langs []string) []string {
spine, err := epub.Spine()
if err != nil {
return orig_langs
}
var err_spine error
err_spine = nil
langs := []string{}
for err_spine == nil {
html, err := spine.Open()
err_spine = spine.Next()
if err != nil {
continue
}
defer html.Close()
buff, err := ioutil.ReadAll(html)
if err != nil {
continue
}
langs = append(langs, cld2.Detect(string(buff)))
}
lang := commonLang(langs)
if lang != "un" && differentLang(lang, orig_langs) {
return []string{lang}
}
return orig_langs
}
func commonLang(langs []string) string {
count := map[string]int{}
for _, l := range langs {
count[l]++
}
lang := "un"
maxcount := 0
for l, c := range count {
if c > maxcount && l != "un" {
lang = l
maxcount = c
}
}
return lang
}
func differentLang(lang string, orig_langs []string) bool {
orig_lang := "un"
if len(orig_langs) > 0 && len(orig_langs) >= 2 {
orig_lang = strings.ToLower(orig_langs[0][0:2])
}
return orig_lang != lang
}

103
lib/parser/parser.go Normal file
View file

@ -0,0 +1,103 @@
package parser
import (
"regexp"
"strings"
"github.com/meskio/epubgo"
)
type MetaData map[string]interface{}
func EpubMetadata(epub *epubgo.Epub) MetaData {
metadata := MetaData{}
for _, m := range epub.MetadataFields() {
data, err := epub.Metadata(m)
if err != nil {
continue
}
switch m {
case "creator":
metadata["author"] = parseAuthr(data)
case "description":
metadata[m] = parseDescription(data)
case "subject":
metadata[m] = parseSubject(data)
case "date":
metadata[m] = parseDate(data)
case "language":
metadata["lang"] = GuessLang(epub, data)
case "title", "contributor", "publisher":
metadata[m] = cleanStr(strings.Join(data, ", "))
case "identifier":
attr, _ := epub.MetadataAttr(m)
for i, d := range data {
if attr[i]["scheme"] == "ISBN" {
isbn := ISBN(d)
if isbn != "" {
metadata["isbn"] = isbn
}
}
}
default:
metadata[m] = strings.Join(data, ", ")
}
}
return metadata
}
func cleanStr(str string) string {
str = strings.Replace(str, "&#39;", "'", -1)
exp, _ := regexp.Compile("&[^;]*;")
str = exp.ReplaceAllString(str, "")
exp, _ = regexp.Compile("[ ,]*$")
str = exp.ReplaceAllString(str, "")
return str
}
func parseAuthr(creator []string) []string {
exp1, _ := regexp.Compile("^(.*\\( *([^\\)]*) *\\))*$")
exp2, _ := regexp.Compile("^[^:]*: *(.*)$")
res := make([]string, len(creator))
for i, s := range creator {
auth := exp1.FindStringSubmatch(s)
if auth != nil {
res[i] = cleanStr(strings.Join(auth[2:], ", "))
} else {
auth := exp2.FindStringSubmatch(s)
if auth != nil {
res[i] = cleanStr(auth[1])
} else {
res[i] = cleanStr(s)
}
}
}
return res
}
func parseDescription(description []string) string {
str := cleanStr(strings.Join(description, "\n"))
str = strings.Replace(str, "</p>", "\n", -1)
exp, _ := regexp.Compile("<[^>]*>")
str = exp.ReplaceAllString(str, "")
str = strings.Replace(str, "&amp;", "&", -1)
str = strings.Replace(str, "&lt;", "<", -1)
str = strings.Replace(str, "&gt;", ">", -1)
str = strings.Replace(str, "\\n", "\n", -1)
return str
}
func parseSubject(subject []string) []string {
var res []string
for _, s := range subject {
res = append(res, strings.Split(s, " / ")...)
}
return res
}
func parseDate(date []string) string {
if len(date) == 0 {
return ""
}
return strings.Replace(date[0], "Unspecified: ", "", -1)
}

259
lib/reader.go Normal file
View file

@ -0,0 +1,259 @@
package trantor
import (
log "github.com/cihub/seelog"
"io"
"net/http"
"strconv"
"strings"
"github.com/gorilla/mux"
"github.com/meskio/epubgo"
"gitlab.com/trantor/trantor/lib/database"
)
type chapter struct {
Label string
Link string
Depth int
Active bool
In []bool // one level in depth
Out []bool // one level out depth
}
type readData struct {
S Status
Book database.Book
Content string
Chapters []chapter
Next string
Prev string
Back string
}
func cleanHtml(html string) string {
str := strings.Split(html, "<body")
if len(str) < 2 {
return html
}
str = strings.SplitN(str[1], ">", 2)
if len(str) < 2 {
return str[0]
}
return "<div " + str[0] + ">" + strings.Split(str[1], "</body>")[0] + "</div>"
}
func genLink(id string, base string, link string) string {
return base + id + "/" + link
}
func cleanLink(link string) string {
for i := 0; i < len(link); i++ {
if link[i] == '%' {
c, _ := strconv.ParseInt(link[i+1:i+3], 16, 0)
link = link[:i] + string(c) + link[i+3:]
}
}
return link
}
func getNextPrev(e *epubgo.Epub, file string, id string, base string) (string, string) {
spine, err := e.Spine()
if err != nil {
return "", ""
}
prev := ""
next := ""
for err == nil {
if cleanLink(spine.URL()) == file {
break
}
prev = spine.URL()
err = spine.Next()
}
if err != nil {
return "", ""
}
if prev != "" {
prev = genLink(id, base, prev)
}
if spine.Next() == nil {
next = genLink(id, base, spine.URL())
}
return next, prev
}
func getChapters(e *epubgo.Epub, file string, id string, base string) []chapter {
nav, err := e.Navigation()
if err != nil {
return nil
}
chapters := listChapters(nav, 0)
for i, c := range chapters {
chapters[i].Link = genLink(id, base, c.Link)
if cleanLink(c.Link) == file {
chapters[i].Active = true
}
}
return chapters
}
func listChapters(nav *epubgo.NavigationIterator, depth int) []chapter {
var chapters []chapter
var err error = nil
for err == nil {
var c chapter
c.Label = nav.Title()
c.Link = nav.URL()
c.Depth = depth
chapters = append(chapters, c)
if nav.HasChildren() {
nav.In()
children := listChapters(nav, depth+1)
chapters = append(chapters, children...)
nav.Out()
}
err = nav.Next()
}
chapters[0].In = append(chapters[0].In, true)
chapters[len(chapters)-1].Out = append(chapters[len(chapters)-1].Out, true)
return chapters
}
func readStartHandler(h handler) {
id := mux.Vars(h.r)["id"]
e, _ := openReadEpub(h)
if e == nil {
log.Warn("Open epub returns an empty file")
notFound(h)
return
}
defer e.Close()
it, err := e.Spine()
if err != nil {
log.Warn("No spine in the epub")
notFound(h)
return
}
http.Redirect(h.w, h.r, "/read/"+id+"/"+it.URL(), http.StatusTemporaryRedirect)
}
func readHandler(h handler) {
id := mux.Vars(h.r)["id"]
file := mux.Vars(h.r)["file"]
e, book := openReadEpub(h)
if e == nil {
notFound(h)
return
}
defer e.Close()
var data readData
data.S = GetStatus(h)
data.S.Title = book.Title + " by " + book.Author[0] + " -- Read -- " + data.S.Title
data.Book = book
if !book.Active {
data.Back = "/new/"
} else {
data.Back = "/book/" + id
}
data.Next, data.Prev = getNextPrev(e, file, id, "/read/")
data.Chapters = getChapters(e, file, id, "/read/")
data.Content = genLink(id, "/content/", file)
loadTemplate(h, "read", data)
}
func openReadEpub(h handler) (*epubgo.Epub, database.Book) {
var book database.Book
id := mux.Vars(h.r)["id"]
if id == "" {
return nil, book
}
book, err := h.db.GetBookId(id)
if err != nil {
return nil, book
}
if !book.Active {
if !h.sess.IsAdmin() {
return nil, book
}
}
f, err := h.store.Get(book.Id, EPUB_FILE)
if err != nil {
return nil, book
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return nil, book
}
e, err := epubgo.Load(f, info.Size())
if err != nil {
return nil, book
}
return e, book
}
func contentHandler(h handler) {
vars := mux.Vars(h.r)
id := vars["id"]
file := vars["file"]
if file == "" || id == "" {
notFound(h)
return
}
err := openEpubFile(h, id, file)
if err != nil {
notFound(h)
return
}
}
func openEpubFile(h handler, id string, file string) error {
book, err := h.db.GetBookId(id)
if err != nil {
return err
}
if !book.Active {
if !h.sess.IsAdmin() {
return err
}
}
f, err := h.store.Get(id, EPUB_FILE)
if err != nil {
return err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return err
}
e, err := epubgo.Load(f, info.Size())
if err != nil {
return err
}
defer e.Close()
html, err := e.OpenFile(file)
if err != nil {
return err
}
defer html.Close()
io.Copy(h.w, html)
return nil
}

64
lib/search.go Normal file
View file

@ -0,0 +1,64 @@
package trantor
import (
"net/http"
"strconv"
"strings"
"gitlab.com/trantor/trantor/lib/database"
)
type searchData struct {
S Status
Found int
Books []database.Book
ItemsPage int
Page int
Next string
Prev string
}
func searchHandler(h handler) {
err := h.r.ParseForm()
if err != nil {
http.Error(h.w, err.Error(), http.StatusInternalServerError)
return
}
req := strings.Join(h.r.Form["q"], " ")
page := 0
if len(h.r.Form["p"]) != 0 {
page, err = strconv.Atoi(h.r.Form["p"][0])
if err != nil {
page = 0
}
}
items_page := itemsPage(h.r)
res, num, _ := h.db.GetBooks(req, items_page, page*items_page)
var data searchData
data.S = GetStatus(h)
data.S.Title = req + " -- Search -- " + data.S.Title
data.S.Search = req
data.Books = res
data.ItemsPage = items_page
data.Found = num
data.Page = page + 1
if num > (page+1)*items_page {
data.Next = "/search/?q=" + req + "&p=" + strconv.Itoa(page+1) + "&num=" + strconv.Itoa(items_page)
}
if page > 0 {
data.Prev = "/search/?q=" + req + "&p=" + strconv.Itoa(page-1) + "&num=" + strconv.Itoa(items_page)
}
loadTemplate(h, "search", data)
}
func itemsPage(r *http.Request) int {
if len(r.Form["num"]) > 0 {
items_page, err := strconv.Atoi(r.Form["num"][0])
if err == nil {
return items_page
}
}
return SEARCH_ITEMS_PAGE
}

83
lib/session.go Normal file
View file

@ -0,0 +1,83 @@
package trantor
import (
"encoding/hex"
"net/http"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"gitlab.com/trantor/trantor/lib/database"
)
var sesStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64))
type Notification struct {
Title string
Msg string
Type string /* error, info or success */
}
type Session struct {
User string
Role string
S *sessions.Session
}
func GetSession(r *http.Request, db *database.DB) (s *Session) {
s = new(Session)
var err error
s.S, err = sesStore.Get(r, "session")
if err == nil && !s.S.IsNew {
s.User, _ = s.S.Values["user"].(string)
s.Role = db.User(s.User).Role()
}
if s.S.IsNew {
s.S.Values["id"] = hex.EncodeToString(securecookie.GenerateRandomKey(16))
}
return
}
func (s *Session) GetNotif() []Notification {
session := s.S
msgs := session.Flashes("nMsg")
titles := session.Flashes("nTitle")
tpes := session.Flashes("nType")
notif := make([]Notification, len(msgs))
for i, m := range msgs {
msg, _ := m.(string)
title, _ := titles[i].(string)
tpe, _ := tpes[i].(string)
notif[i] = Notification{title, msg, tpe}
}
return notif
}
func (s *Session) LogIn(user string) {
s.User = user
s.S.Values["user"] = user
}
func (s *Session) LogOut() {
s.S.Values["user"] = ""
}
func (s *Session) Notify(title, msg, tpe string) {
s.S.AddFlash(msg, "nMsg")
s.S.AddFlash(title, "nTitle")
s.S.AddFlash(tpe, "nType")
}
func (s *Session) Save(w http.ResponseWriter, r *http.Request) {
sesStore.Save(r, w, s.S)
}
func (s *Session) Id() string {
id, _ := s.S.Values["id"].(string)
return id
}
func (s *Session) IsAdmin() bool {
return s.Role == "admin"
}

199
lib/stats.go Normal file
View file

@ -0,0 +1,199 @@
package trantor
import (
log "github.com/cihub/seelog"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"gitlab.com/trantor/trantor/lib/database"
"gitlab.com/trantor/trantor/lib/storage"
)
const (
stats_version = 2
)
type handler struct {
w http.ResponseWriter
r *http.Request
sess *Session
db *database.DB
store *storage.Store
}
type StatsGatherer struct {
db *database.DB
store *storage.Store
channel chan statsRequest
}
func InitStats(database *database.DB, store *storage.Store) *StatsGatherer {
sg := new(StatsGatherer)
sg.channel = make(chan statsRequest, CHAN_SIZE)
sg.db = database
sg.store = store
go sg.worker()
return sg
}
func (sg StatsGatherer) Gather(function func(handler)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
log.Info("Query ", r.Method, " ", r.RequestURI)
var h handler
h.store = sg.store
h.db = sg.db.Copy()
defer h.db.Close()
h.w = w
h.r = r
h.sess = GetSession(r, h.db)
function(h)
sg.channel <- statsRequest{time.Now(), mux.Vars(r), h.sess, r}
}
}
type statsRequest struct {
date time.Time
vars map[string]string
sess *Session
r *http.Request
}
func (sg StatsGatherer) worker() {
db := sg.db.Copy()
defer db.Close()
for req := range sg.channel {
stats := make(map[string]interface{})
appendFiles(req.r, stats)
appendMuxVars(req.vars, stats)
appendUrl(req.r, stats)
appendSession(req.sess, stats)
stats["version"] = stats_version
stats["method"] = req.r.Method
stats["date"] = req.date
db.AddStats(stats)
}
}
func statsHandler(h handler) {
var data statsData
data.S = GetStatus(h)
data.S.Title = "Stats -- " + data.S.Title
data.S.Stats = true
data.HVisits = getVisits(hourlyLabel, h.db, database.Hourly_visits)
data.DVisits = getVisits(dailyLabel, h.db, database.Daily_visits)
data.MVisits = getVisits(monthlyLabel, h.db, database.Monthly_visits)
data.HDownloads = getVisits(hourlyLabel, h.db, database.Hourly_downloads)
data.DDownloads = getVisits(dailyLabel, h.db, database.Daily_downloads)
data.MDownloads = getVisits(monthlyLabel, h.db, database.Monthly_downloads)
loadTemplate(h, "stats", data)
}
type statsData struct {
S Status
HVisits []visitData
DVisits []visitData
MVisits []visitData
HDownloads []visitData
DDownloads []visitData
MDownloads []visitData
}
type visitData struct {
Label string
Count int
}
func hourlyLabel(date time.Time) string {
return strconv.Itoa(date.Hour() + 1)
}
func dailyLabel(date time.Time) string {
return strconv.Itoa(date.Day())
}
func monthlyLabel(date time.Time) string {
return date.Month().String()
}
func getVisits(funcLabel func(time.Time) string, db *database.DB, visitType database.VisitType) []visitData {
var visits []visitData
visit, err := db.GetVisits(visitType)
if err != nil {
log.Warn("GetVisits error (", visitType, "): ", err)
}
for _, v := range visit {
var elem visitData
elem.Label = funcLabel(v.Date.UTC())
elem.Count = v.Count
visits = append(visits, elem)
}
return visits
}
func appendFiles(r *http.Request, stats map[string]interface{}) {
if r.Method == "POST" && r.MultipartForm != nil {
files := r.MultipartForm.File
for key := range files {
list := make([]string, len(files[key]))
for i, f := range files[key] {
list[i] = f.Filename
}
stats[key] = list
}
}
}
func appendMuxVars(vars map[string]string, stats map[string]interface{}) {
for key, value := range vars {
switch {
case key == "id":
stats["id"] = value
case key == "ids":
var objectIds []string
ids := strings.Split(value, "/")
for _, id := range ids {
objectIds = append(objectIds, id)
}
if len(objectIds) > 0 {
stats["ids"] = objectIds
stats["id"] = objectIds[0]
}
default:
stats[key] = value
}
}
}
func appendUrl(r *http.Request, stats map[string]interface{}) {
for key, value := range r.URL.Query() {
stats[key] = value
}
stats["host"] = r.Host
stats["path"] = r.URL.Path
pattern := strings.Split(r.URL.Path, "/")
if len(pattern) > 1 && pattern[1] != "" {
stats["section"] = pattern[1]
} else {
stats["section"] = "/"
}
}
func appendSession(sess *Session, stats map[string]interface{}) {
stats["session"] = sess.Id()
if sess.User != "" {
stats["user"] = sess.User
}
}

42
lib/storage/dir.go Normal file
View file

@ -0,0 +1,42 @@
package storage
import (
p "path"
"os"
)
const (
dir_depth = 2
encodeURL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
)
func mkstore(path string) error {
return _mkstore(path, dir_depth)
}
func _mkstore(path string, depth int) error {
err := os.MkdirAll(path, os.ModePerm)
if err != nil || depth == 0 {
return err
}
for _, l := range encodeURL {
next_path := p.Join(path, string(l))
err = _mkstore(next_path, depth-1)
if err != nil {
return err
}
}
return nil
}
func idPath(storePath string, id string) string {
path := storePath
for i := 0; i < dir_depth; i++ {
dir := string(id[i])
path = p.Join(path, dir)
}
path = p.Join(path, id)
return path
}

63
lib/storage/storage.go Normal file
View file

@ -0,0 +1,63 @@
package storage
import (
p "path"
"io"
"os"
)
type Store struct {
path string
}
type File interface {
io.ReadCloser
io.ReaderAt
Stat() (fi os.FileInfo, err error)
}
func Init(path string) (*Store, error) {
st := new(Store)
st.path = path
_, err := os.Stat(path)
if err != nil {
err = mkstore(st.path)
}
return st, err
}
func (st *Store) Create(id string, name string) (io.WriteCloser, error) {
path := idPath(st.path, id)
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
return nil, err
}
return os.Create(p.Join(path, name))
}
func (st *Store) Store(id string, file io.Reader, name string) (size int64, err error) {
dest, err := st.Create(id, name)
if err != nil {
return 0, err
}
defer dest.Close()
return io.Copy(dest, file)
}
func (st *Store) Get(id string, name string) (File, error) {
path := idPath(st.path, id)
return os.Open(p.Join(path, name))
}
func (st *Store) Delete(id string) error {
path := idPath(st.path, id)
return os.RemoveAll(path)
}
func (st *Store) del() {
os.RemoveAll(st.path)
}

113
lib/storage/storage_test.go Normal file
View file

@ -0,0 +1,113 @@
package storage
import "testing"
import (
"bytes"
"io"
"io/ioutil"
"os"
"strings"
)
const (
test_path = "/tmp/store"
test_book = `
HARI SELDON- born in the 11,988th year of the Galactic Era; died
12,069. The dates are more commonly given in terms of the current
Foundational Era as - 79 to the year 1 F.E. Born to middle-class
parents on Helicon, Arcturus sector (where his father, in a legend of
doubtful authenticity, was a tobacco grower in the hydroponic plants
of the planet), he early showed amazing ability in mathematics.
Anecdotes concerning his ability are innumerable, and some are
contradictory. At the age of two, he is said to have `
test_id = "1234567890abcdef"
)
func TestInit(t *testing.T) {
st, err := Init(test_path)
if err != nil {
t.Fatal("An error ocurred initializing the store =>", err)
}
defer st.del()
info, err := os.Stat(test_path)
if err != nil {
t.Fatal("An error ocurred =>", err)
}
if !info.Mode().IsDir() {
t.Errorf(test_path, " is not dir.")
}
info, err = os.Stat(test_path + "/a/M")
if err != nil {
t.Fatal("An error ocurred =>", err)
}
if !info.Mode().IsDir() {
t.Errorf(test_path, " is not dir.")
}
}
func TestStore(t *testing.T) {
st, err := Init(test_path)
defer st.del()
_, err = st.Store(test_id, strings.NewReader(test_book), "epub")
if err != nil {
t.Fatal("An error ocurred storing the book =>", err)
}
book, err := st.Get(test_id, "epub")
if err != nil {
t.Fatal("An error ocurred getting the book =>", err)
}
content, err := ioutil.ReadAll(book)
if err != nil {
t.Fatal("An error ocurred reading the book =>", err)
}
if !bytes.Equal(content, []byte(test_book)) {
t.Error("Not the same content")
}
}
func TestCreate(t *testing.T) {
st, err := Init(test_path)
defer st.del()
f, err := st.Create(test_id, "img")
if err != nil {
t.Fatal("An error ocurred storing the book =>", err)
}
io.Copy(f, strings.NewReader(test_book))
img, err := st.Get(test_id, "img")
if err != nil {
t.Fatal("An error ocurred getting the book =>", err)
}
content, err := ioutil.ReadAll(img)
if err != nil {
t.Fatal("An error ocurred reading the book =>", err)
}
if !bytes.Equal(content, []byte(test_book)) {
t.Error("Not the same content")
}
}
func TestDelete(t *testing.T) {
st, err := Init(test_path)
defer st.del()
_, err = st.Store(test_id, strings.NewReader(test_book), "epub")
if err != nil {
t.Fatal("An error ocurred storing the book =>", err)
}
err = st.Delete(test_id)
if err != nil {
t.Fatal("An error ocurred deleteing id =>", err)
}
_, err = st.Get(test_id, "epub")
if err == nil {
t.Fatal("Retrieve book without error.")
}
}

36
lib/tasker.go Normal file
View file

@ -0,0 +1,36 @@
package trantor
import (
log "github.com/cihub/seelog"
"time"
"gitlab.com/trantor/trantor/lib/database"
)
func InitTasks(db *database.DB) {
periodicTask(UpdateLogger, MINUTES_UPDATE_LOGGER*time.Minute)
periodicTask(db.UpdateTags, MINUTES_UPDATE_TAGS*time.Minute)
periodicTask(db.UpdateMostVisited, MINUTES_UPDATE_VISITED*time.Minute)
periodicTask(db.UpdateDownloadedBooks, MINUTES_UPDATE_DOWNLOADED*time.Minute)
periodicTask(db.UpdateHourVisits, MINUTES_UPDATE_HOURLY_V*time.Minute)
periodicTask(db.UpdateDayVisits, MINUTES_UPDATE_DAILY_V*time.Minute)
periodicTask(db.UpdateMonthVisits, MINUTES_UPDATE_MONTHLY_V*time.Minute)
periodicTask(db.UpdateHourDownloads, MINUTES_UPDATE_HOURLY_D*time.Minute)
periodicTask(db.UpdateDayDownloads, MINUTES_UPDATE_DAILY_D*time.Minute)
periodicTask(db.UpdateMonthDownloads, MINUTES_UPDATE_MONTHLY_D*time.Minute)
}
func periodicTask(task func() error, periodicity time.Duration) {
go tasker(task, periodicity)
}
func tasker(task func() error, periodicity time.Duration) {
for true {
time.Sleep(periodicity)
err := task()
if err != nil {
log.Error("Task error: ", err)
}
}
}

211
lib/template.go Normal file
View file

@ -0,0 +1,211 @@
package trantor
import (
txt_tmpl "text/template"
log "github.com/cihub/seelog"
"encoding/json"
"errors"
"html/template"
"net/http"
"time"
"gitlab.com/trantor/trantor/lib/database"
)
type Status struct {
BaseURL string
FullURL string
Title string
Search string
User string
IsAdmin bool
Notif []Notification
Updated string
Home bool
About bool
News bool
Upload bool
Stats bool
Help bool
Dasboard bool
}
func GetStatus(h handler) Status {
var s Status
s.BaseURL = "http://" + HOST_URL
s.FullURL = s.BaseURL + h.r.RequestURI
s.Title = "Imperial Library of Trantor"
s.User = h.sess.User
s.IsAdmin = h.sess.IsAdmin()
s.Notif = h.sess.GetNotif()
s.Updated = time.Now().UTC().Format("2006-01-02T15:04:05Z")
h.sess.Save(h.w, h.r)
return s
}
var tmpl_html = template.Must(template.ParseFiles(
TEMPLATE_PATH+"header.html",
TEMPLATE_PATH+"footer.html",
TEMPLATE_PATH+"404.html",
TEMPLATE_PATH+"index.html",
TEMPLATE_PATH+"about.html",
TEMPLATE_PATH+"news.html",
TEMPLATE_PATH+"edit_news.html",
TEMPLATE_PATH+"book.html",
TEMPLATE_PATH+"search.html",
TEMPLATE_PATH+"upload.html",
TEMPLATE_PATH+"login.html",
TEMPLATE_PATH+"new.html",
TEMPLATE_PATH+"read.html",
TEMPLATE_PATH+"edit.html",
TEMPLATE_PATH+"dashboard.html",
TEMPLATE_PATH+"settings.html",
TEMPLATE_PATH+"stats.html",
TEMPLATE_PATH+"help.html",
))
var tmpl_rss = txt_tmpl.Must(txt_tmpl.ParseFiles(
TEMPLATE_PATH+"search.rss",
TEMPLATE_PATH+"news.rss",
))
var tmpl_opds = txt_tmpl.Must(txt_tmpl.ParseFiles(
TEMPLATE_PATH+"index.opds",
TEMPLATE_PATH+"search.opds",
))
func loadTemplate(h handler, tmpl string, data interface{}) {
var err error
fmt := h.r.FormValue("fmt")
switch fmt {
case "rss":
err = tmpl_rss.ExecuteTemplate(h.w, tmpl+".rss", data)
case "opds":
err = tmpl_opds.ExecuteTemplate(h.w, tmpl+".opds", data)
case "json":
err = loadJson(h.w, tmpl, data)
default:
err = tmpl_html.ExecuteTemplate(h.w, tmpl+".html", data)
}
if err != nil {
tmpl_html.ExecuteTemplate(h.w, "404.html", data)
log.Warn("An error ocurred loading the template ", tmpl, ".", fmt, ": ", err)
}
}
func loadJson(w http.ResponseWriter, tmpl string, data interface{}) error {
var res []byte
var err error
switch tmpl {
case "index":
res, err = indexJson(data)
case "book":
res, err = bookJson(data)
case "news":
res, err = newsJson(data)
case "search":
res, err = searchJson(data)
}
if err != nil {
return err
}
_, err = w.Write(res)
return err
}
func indexJson(data interface{}) ([]byte, error) {
index, ok := data.(indexData)
if !ok {
return nil, errors.New("Data is not valid")
}
books := make([]map[string]interface{}, len(index.Books))
for i, book := range index.Books {
books[i] = bookJsonRaw(book)
}
news := newsJsonRaw(index.News)
return json.Marshal(map[string]interface{}{
"title": index.S.Title,
"url": index.S.BaseURL,
"count": index.Count,
"news": news,
"tags": index.Tags,
"last_added": books,
})
}
func bookJson(data interface{}) ([]byte, error) {
book, ok := data.(bookData)
if !ok {
return nil, errors.New("Data is not valid")
}
return json.Marshal(bookJsonRaw(book.Book))
}
func newsJson(data interface{}) ([]byte, error) {
news, ok := data.(newsData)
if !ok {
return nil, errors.New("Data is not valid")
}
return json.Marshal(newsJsonRaw(news.News))
}
func newsJsonRaw(news []newsEntry) []map[string]string {
list := make([]map[string]string, len(news))
for i, n := range news {
list[i] = map[string]string{
"date": n.Date,
"text": n.Text,
}
}
return list
}
func searchJson(data interface{}) ([]byte, error) {
search, ok := data.(searchData)
if !ok {
return nil, errors.New("Data is not valid")
}
books := make([]map[string]interface{}, len(search.Books))
for i, book := range search.Books {
books[i] = bookJsonRaw(book)
}
return json.Marshal(map[string]interface{}{
"found": search.Found,
"page": search.Page - 1,
"items": search.ItemsPage,
"books": books,
})
}
func bookJsonRaw(book database.Book) map[string]interface{} {
cover := ""
coverSmall := ""
if book.Cover {
cover = "/cover/" + book.Id + "/big/" + book.Title + ".jpg"
coverSmall = "/cover/" + book.Id + "/small/" + book.Title + ".jpg"
}
return map[string]interface{}{
"id": book.Id,
"title": book.Title,
"author": book.Author,
"contributor": book.Contributor,
"publisher": book.Publisher,
"description": book.Description,
"subject": book.Subject,
"date": book.Date,
"lang": book.Lang,
"isbn": book.Isbn,
"size": book.FileSize,
"cover": cover,
"cover_small": coverSmall,
"download": "/download/" + book.Id + "/" + book.Title + ".epub",
"read": "/read/" + book.Id,
}
}

225
lib/trantor.go Normal file
View file

@ -0,0 +1,225 @@
package trantor
import (
log "github.com/cihub/seelog"
"fmt"
"io"
"net/http"
"strings"
"github.com/gorilla/mux"
"gitlab.com/trantor/trantor/lib/database"
)
type statusData struct {
S Status
}
func aboutHandler(h handler) {
var data statusData
data.S = GetStatus(h)
data.S.Title = "About -- " + data.S.Title
data.S.About = true
loadTemplate(h, "about", data)
}
func helpHandler(h handler) {
var data statusData
data.S = GetStatus(h)
data.S.Title = "Help -- " + data.S.Title
data.S.Help = true
loadTemplate(h, "help", data)
}
func logoutHandler(h handler) {
h.sess.LogOut()
h.sess.Notify("Log out!", "Bye bye "+h.sess.User, "success")
h.sess.Save(h.w, h.r)
log.Info("User ", h.sess.User, " log out")
http.Redirect(h.w, h.r, "/", http.StatusFound)
}
type bookData struct {
S Status
Book database.Book
Description []string
FlaggedBadQuality bool
}
func bookHandler(h handler) {
id := mux.Vars(h.r)["id"]
var data bookData
data.S = GetStatus(h)
book, err := h.db.GetBookId(id)
if err != nil {
notFound(h)
return
}
data.Book = book
data.S.Title = book.Title + " by " + book.Author[0] + " -- " + data.S.Title
data.Description = strings.Split(data.Book.Description, "\n")
data.FlaggedBadQuality = false
for _, reporter := range book.BadQualityReporters {
if reporter == h.sess.User || reporter == h.sess.Id() {
data.FlaggedBadQuality = true
break
}
}
loadTemplate(h, "book", data)
}
func downloadHandler(h handler) {
id := mux.Vars(h.r)["id"]
book, err := h.db.GetBookId(id)
if err != nil {
notFound(h)
return
}
if !book.Active {
if !h.sess.IsAdmin() {
notFound(h)
return
}
}
f, err := h.store.Get(book.Id, EPUB_FILE)
if err != nil {
notFound(h)
return
}
defer f.Close()
headers := h.w.Header()
headers["Content-Type"] = []string{"application/epub+zip"}
headers["Content-Disposition"] = []string{"attachment; filename=\"" + book.Title + ".epub\""}
io.Copy(h.w, f)
}
func flagHandler(h handler) {
id := mux.Vars(h.r)["id"]
user := h.sess.Id()
if h.sess.User != "" {
user = h.sess.User
}
err := h.db.FlagBadQuality(id, user)
if err != nil {
log.Warn("An error ocurred while flaging ", id, ": ", err)
}
h.sess.Notify("Flagged!", "Book marked as bad quality, thank you", "success")
h.sess.Save(h.w, h.r)
http.Redirect(h.w, h.r, h.r.Referer(), http.StatusFound)
}
type indexData struct {
S Status
Books []database.Book
VisitedBooks []database.Book
DownloadedBooks []database.Book
Count int
Tags []string
News []newsEntry
}
func indexHandler(h handler) {
var data indexData
data.Tags, _ = h.db.GetTags()
data.S = GetStatus(h)
data.S.Home = true
data.Books, data.Count, _ = h.db.GetBooks("", BOOKS_FRONT_PAGE, 0)
data.VisitedBooks, _ = h.db.GetVisitedBooks()
data.DownloadedBooks, _ = h.db.GetDownloadedBooks()
data.News = getNews(1, DAYS_NEWS_INDEXPAGE, h.db)
loadTemplate(h, "index", data)
}
func notFound(h handler) {
var data statusData
data.S = GetStatus(h)
data.S.Title = "Not found --" + data.S.Title
h.w.WriteHeader(http.StatusNotFound)
loadTemplate(h, "404", data)
}
func UpdateLogger() error {
logger, err := log.LoggerFromConfigAsFile(LOGGER_CONFIG)
if err != nil {
return err
}
return log.ReplaceLogger(logger)
}
func InitRouter(db *database.DB, sg *StatsGatherer) {
const id_pattern = "[0-9a-zA-Z\\-\\_]{16}"
r := mux.NewRouter()
var notFoundHandler http.HandlerFunc
notFoundHandler = sg.Gather(notFound)
r.NotFoundHandler = notFoundHandler
r.HandleFunc("/", sg.Gather(indexHandler))
r.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, ROBOTS_PATH) })
r.HandleFunc("/description.json", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, DESCRIPTION_PATH) })
r.HandleFunc("/opensearch.xml", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, OPENSEARCH_PATH) })
r.HandleFunc("/key.asc", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, KEY_PATH) })
r.HandleFunc("/book/{id:"+id_pattern+"}", sg.Gather(bookHandler))
r.HandleFunc("/search/", sg.Gather(searchHandler))
r.HandleFunc("/upload/", sg.Gather(uploadHandler)).Methods("GET")
r.HandleFunc("/upload/", sg.Gather(uploadPostHandler)).Methods("POST")
r.HandleFunc("/read/{id:"+id_pattern+"}", sg.Gather(readStartHandler))
r.HandleFunc("/read/{id:"+id_pattern+"}/{file:.*}", sg.Gather(readHandler))
r.HandleFunc("/content/{id:"+id_pattern+"}/{file:.*}", sg.Gather(contentHandler))
r.HandleFunc("/about/", sg.Gather(aboutHandler))
r.HandleFunc("/help/", sg.Gather(helpHandler))
r.HandleFunc("/download/{id:"+id_pattern+"}/{epub:.*}", sg.Gather(downloadHandler))
r.HandleFunc("/cover/{id:"+id_pattern+"}/{size}/{img:.*}", sg.Gather(coverHandler))
r.HandleFunc("/stats/", sg.Gather(statsHandler))
r.HandleFunc("/flag/bad_quality/{id:"+id_pattern+"}", sg.Gather(flagHandler))
r.HandleFunc("/login/", sg.Gather(loginHandler)).Methods("GET")
r.HandleFunc("/login/", sg.Gather(loginPostHandler)).Methods("POST")
r.HandleFunc("/create_user/", sg.Gather(createUserHandler)).Methods("POST")
r.HandleFunc("/logout/", sg.Gather(logoutHandler))
r.HandleFunc("/dashboard/", sg.Gather(dashboardHandler))
r.HandleFunc("/settings/", sg.Gather(settingsHandler))
r.HandleFunc("/new/", sg.Gather(newHandler))
r.HandleFunc("/save/{id:"+id_pattern+"}", sg.Gather(saveHandler)).Methods("POST")
r.HandleFunc("/edit/{id:"+id_pattern+"}", sg.Gather(editHandler))
r.HandleFunc("/store/{ids:("+id_pattern+"/)+}", sg.Gather(storeHandler))
r.HandleFunc("/delete/{ids:("+id_pattern+"/)+}", sg.Gather(deleteHandler))
r.HandleFunc("/news/", sg.Gather(newsHandler))
r.HandleFunc("/news/edit", sg.Gather(editNewsHandler)).Methods("GET")
r.HandleFunc("/news/edit", sg.Gather(postNewsHandler)).Methods("POST")
r.HandleFunc("/img/{img}", fileServer(IMG_PATH, "/img/"))
r.HandleFunc("/css/{css}", fileServer(CSS_PATH, "/css/"))
r.HandleFunc("/js/{js}", fileServer(JS_PATH, "/js/"))
http.Handle("/", r)
}
func fileServer(path string, prefix string) func(w http.ResponseWriter, r *http.Request) {
// FIXME: is there a cleaner way without handler?
h := http.FileServer(http.Dir(path))
handler := http.StripPrefix(prefix, h)
return func(w http.ResponseWriter, r *http.Request) {
addCacheControlHeader(w, false)
handler.ServeHTTP(w, r)
}
}
func addCacheControlHeader(w http.ResponseWriter, private bool) {
// FIXME: cache of download and cover don't depends on user login
if private {
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, private", CACHE_MAX_AGE))
} else {
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, public", CACHE_MAX_AGE))
}
}

118
lib/upload.go Normal file
View file

@ -0,0 +1,118 @@
package trantor
import (
log "github.com/cihub/seelog"
"bytes"
"crypto/rand"
"encoding/base64"
"io/ioutil"
"mime/multipart"
"github.com/meskio/epubgo"
"gitlab.com/trantor/trantor/lib/database"
"gitlab.com/trantor/trantor/lib/parser"
"gitlab.com/trantor/trantor/lib/storage"
)
func InitUpload(database *database.DB, store *storage.Store) {
uploadChannel = make(chan uploadRequest, CHAN_SIZE)
go uploadWorker(database, store)
}
var uploadChannel chan uploadRequest
type uploadRequest struct {
file multipart.File
filename string
}
func uploadWorker(database *database.DB, store *storage.Store) {
db := database.Copy()
defer db.Close()
for req := range uploadChannel {
processFile(req, db, store)
}
}
func processFile(req uploadRequest, db *database.DB, store *storage.Store) {
defer req.file.Close()
epub, err := openMultipartEpub(req.file)
if err != nil {
log.Warn("Not valid epub uploaded file ", req.filename, ": ", err)
return
}
defer epub.Close()
id := genId()
metadata := parser.EpubMetadata(epub)
metadata["id"] = id
metadata["cover"] = GetCover(epub, id, store)
req.file.Seek(0, 0)
size, err := store.Store(id, req.file, EPUB_FILE)
if err != nil {
log.Error("Error storing book (", id, "): ", err)
return
}
metadata["filesize"] = size
err = db.AddBook(metadata)
if err != nil {
log.Error("Error storing metadata (", id, "): ", err)
return
}
log.Info("File uploaded: ", req.filename)
}
func uploadPostHandler(h handler) {
problem := false
h.r.ParseMultipartForm(20000000)
filesForm := h.r.MultipartForm.File["epub"]
for _, f := range filesForm {
file, err := f.Open()
if err != nil {
log.Error("Can not open uploaded file ", f.Filename, ": ", err)
h.sess.Notify("Upload problem!", "There was a problem with book "+f.Filename, "error")
problem = true
continue
}
uploadChannel <- uploadRequest{file, f.Filename}
}
if !problem {
if len(filesForm) > 0 {
h.sess.Notify("Upload successful!", "Thank you for your contribution", "success")
} else {
h.sess.Notify("Upload problem!", "No books where uploaded.", "error")
}
}
uploadHandler(h)
}
func uploadHandler(h handler) {
var data uploadData
data.S = GetStatus(h)
data.S.Title = "Upload -- " + data.S.Title
data.S.Upload = true
loadTemplate(h, "upload", data)
}
type uploadData struct {
S Status
}
func openMultipartEpub(file multipart.File) (*epubgo.Epub, error) {
buff, _ := ioutil.ReadAll(file)
reader := bytes.NewReader(buff)
return epubgo.Load(reader, int64(len(buff)))
}
func genId() string {
b := make([]byte, 12)
rand.Read(b)
return base64.URLEncoding.EncodeToString(b)
}

92
lib/user.go Normal file
View file

@ -0,0 +1,92 @@
package trantor
import (
log "github.com/cihub/seelog"
"net/http"
)
func loginHandler(h handler) {
if h.sess.User != "" {
http.Redirect(h.w, h.r, "/dashboard/", http.StatusFound)
return
}
var data statusData
data.S = GetStatus(h)
data.S.Title = "Login -- " + data.S.Title
loadTemplate(h, "login", data)
}
func loginPostHandler(h handler) {
user := h.r.FormValue("user")
pass := h.r.FormValue("pass")
if h.db.User(user).Valid(pass) {
log.Info("User ", user, " log in")
h.sess.LogIn(user)
h.sess.Notify("Successful login!", "Welcome "+user, "success")
} else {
log.Warn("User ", user, " bad user or password")
h.sess.Notify("Invalid login!", "user or password invalid", "error")
}
h.sess.Save(h.w, h.r)
http.Redirect(h.w, h.r, h.r.Referer(), http.StatusFound)
}
func createUserHandler(h handler) {
pass := h.r.FormValue("pass")
confirmPass := h.r.FormValue("confirmPass")
if pass != confirmPass {
h.sess.Notify("Registration error!", "Passwords don't match", "error")
} else {
user := h.r.FormValue("user")
err := h.db.AddUser(user, pass)
if err == nil {
h.sess.Notify("Account created!", "Welcome "+user+". Now you can login", "success")
} else {
h.sess.Notify("Registration error!", "There was some database problem, if it keeps happening please inform me", "error")
}
}
h.sess.Save(h.w, h.r)
http.Redirect(h.w, h.r, h.r.Referer(), http.StatusFound)
}
func dashboardHandler(h handler) {
if h.sess.User == "" {
notFound(h)
return
}
var data statusData
data.S = GetStatus(h)
data.S.Title = "Dashboard -- " + data.S.Title
data.S.Dasboard = true
loadTemplate(h, "dashboard", data)
}
func settingsHandler(h handler) {
if h.sess.User == "" {
notFound(h)
return
}
if h.r.Method == "POST" {
current_pass := h.r.FormValue("currpass")
pass1 := h.r.FormValue("password1")
pass2 := h.r.FormValue("password2")
switch {
case !h.db.User(h.sess.User).Valid(current_pass):
h.sess.Notify("Password error!", "The current password given don't match with the user password. Try again", "error")
case pass1 != pass2:
h.sess.Notify("Passwords don't match!", "The new password and the confirmation password don't match. Try again", "error")
default:
h.db.User(h.sess.User).SetPassword(pass1)
h.sess.Notify("Password updated!", "Your new password is correctly set.", "success")
}
h.sess.Save(h.w, h.r)
}
var data statusData
data.S = GetStatus(h)
data.S.Title = "Settings -- " + data.S.Title
loadTemplate(h, "settings", data)
}