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
210
lib/admin.go
Normal file
210
lib/admin.go
Normal 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
44
lib/config.go
Normal 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
190
lib/cover.go
Normal 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
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)
|
||||
}
|
||||
}
|
67
lib/news.go
Normal file
67
lib/news.go
Normal 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
81
lib/parser/isbn.go
Normal 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
26
lib/parser/isbn_test.go
Normal 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
66
lib/parser/language.go
Normal 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
103
lib/parser/parser.go
Normal 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, "'", "'", -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, "&", "&", -1)
|
||||
str = strings.Replace(str, "<", "<", -1)
|
||||
str = strings.Replace(str, ">", ">", -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
259
lib/reader.go
Normal 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
64
lib/search.go
Normal 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
83
lib/session.go
Normal 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
199
lib/stats.go
Normal 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
42
lib/storage/dir.go
Normal 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
63
lib/storage/storage.go
Normal 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
113
lib/storage/storage_test.go
Normal 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
36
lib/tasker.go
Normal 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
211
lib/template.go
Normal 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
225
lib/trantor.go
Normal 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
118
lib/upload.go
Normal 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
92
lib/user.go
Normal 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)
|
||||
}
|
Reference in a new issue