├── .gitignore ├── LICENSE ├── README ├── admin.go ├── config.go ├── cover.go ├── css ├── FredokaOne.ttf ├── PTSerif.ttf ├── bootstrap-responsive.min.css ├── bootstrap.min.css └── custom.css ├── database ├── books.go ├── books_test.go ├── database.go ├── database_test.go ├── news.go ├── news_test.go ├── stats.go ├── users.go └── users_test.go ├── description.json ├── img ├── bright_squares.png ├── favicon.ico ├── feed.png ├── glyphicons-halflings-white.png └── glyphicons-halflings.png ├── js ├── Chart.min.js ├── bootstrap.min.js └── jquery.js ├── language.go ├── language_develop.go ├── logger.xml ├── news.go ├── opensearch.xml ├── reader.go ├── robots.txt ├── search.go ├── session.go ├── stats.go ├── storage ├── dir.go ├── storage.go └── storage_test.go ├── tasker.go ├── template.go ├── templates ├── 404.html ├── about.html ├── book.html ├── dashboard.html ├── edit.html ├── edit_news.html ├── footer.html ├── header.html ├── help.html ├── index.html ├── index.opds ├── login.html ├── new.html ├── news.html ├── news.rss ├── read.html ├── search.html ├── search.opds ├── search.rss ├── settings.html ├── stats.html └── upload.html ├── trantor.go ├── upload.go └── user.go /.gitignore: -------------------------------------------------------------------------------- 1 | trantor.git 2 | /store/ 3 | tools/adduser/adduser 4 | tools/update/update 5 | tools/togridfs/togridfs 6 | tools/getISBNnDesc/getISBNnDesc 7 | tools/coverNew/coverNew 8 | tools/addsize/addsize 9 | tools/importer/importer 10 | tools/keywords/keywords 11 | tools/store/store 12 | tags 13 | .*.swp 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Imperial Library of Trantor 2 | 3 | The Imperial Library of Trantor (also known as Galactic Library) is a repository management system of ebooks on ePub format. 4 | 5 | You can check out the main development branch from Gitorious at: 6 | 7 | https://gitorious.org/trantor/ 8 | 9 | (We still in pre-beta fase) 10 | 11 | == Dependences == 12 | 13 | In order to run Trantor, you need to install the following packages: 14 | 15 | * Go language 16 | * Mongodb (>= 2.6) 17 | * Bazaar 18 | * Git 19 | 20 | Under Debian Wheezy you can simply run: 21 | 22 | # aptitude install golang-go git mercurial bzr mongodb 23 | 24 | Yo also need to install go dependences: 25 | 26 | # go get gopkg.in/mgo.v2 gopkg.in/mgo.v2/bson github.com/gorilla/sessions \ 27 | github.com/gorilla/securecookie github.com/gorilla/mux \ 28 | github.com/nfnt/resize github.com/cihub/seelog \ 29 | code.google.com/p/go.crypto/scrypt \ 30 | github.com/rainycape/cld2 31 | 32 | == Installation == 33 | === For admins ("for developers" below) === 34 | 35 | Now you can install Trantor itself: 36 | 37 | # go get -tags prod git.gitorious.org/trantor/trantor.git 38 | 39 | You can run trantor in /srv/www/trantor i.e. For this: 40 | 41 | # mkdir -p /srv/www/trantor 42 | 43 | # cd /srv/www/trantor 44 | 45 | # ln -s /usr/lib/go/src/pkg/git.gitorious.org/trantor/trantor.git/templates/ templates 46 | # ln -s /usr/lib/go/src/pkg/git.gitorious.org/trantor/trantor.git/css/ css 47 | # ln -s /usr/lib/go/src/pkg/git.gitorious.org/trantor/trantor.git/js/ js 48 | # ln -s /usr/lib/go/src/pkg/git.gitorious.org/trantor/trantor.git/img/ img 49 | 50 | Now you can run it: 51 | # /usr/lib/go/bin/trantor.git 52 | 53 | Go to your browser to: http://localhost:8080 54 | 55 | === For developers === 56 | 57 | Login to gitorius: https://gitorious.org/login 58 | and clone your own Trantor: https://gitorious.org/trantor/trantor/clone 59 | 60 | In your shell 61 | $ git clone git://gitorious.org/~yourname/trantor/yournames-trantor.git 62 | $ cd yournames-trantor 63 | 64 | You can edit config.go if you want to change the port and other configuration, by default is 8080 65 | 66 | Now you can compile Trantor: 67 | $ go build -tags prod 68 | (remove '-tags prod' for a faster compilation without language guessing) 69 | 70 | Now you can run it: 71 | $ ./yourname-trantor 72 | 73 | Go to your browser to: http://localhost:8080 74 | 75 | == Bugs == 76 | 77 | Please, report bugs to zenow@tormail.org 78 | 79 | == Patches == 80 | Make your enhacements and sent it by git: 81 | 82 | $ git commit -m "comment" 83 | $ git remote set-url --push origin git@gitorious.org:~yournames/trantor/alfinals-trantor.git 84 | $ git push origin master 85 | $ git push 86 | 87 | Go to "merge-requests" 88 | https://gitorious.org/trantor/yournames-trantor/merge_requests/new 89 | 90 | 91 | == Rights == 92 | 93 | All the matterial of this project is under WTFPL as described on the LICENSE 94 | file with the exception of: 95 | - css/bootstrap.min.css css/bootstra-responsive.min.css js/bootstrap.min.js 96 | img/glyphicons-halflings-white.png img/glyphicons-halflings.png 97 | From the bootstrap framework: http://twitter.github.com/bootstrap/ 98 | - js/jquery.js 99 | From jquery library: http://jquery.com/ 100 | - js/Chart.min.js 101 | From chart.js library: http://www.chartjs.org/ 102 | - img/bright_squares.png 103 | From subtlepatterns: http://subtlepatterns.com/bright-squares/ 104 | - css/FredokaOne.ttf css/PTSerif.ttf 105 | From Google Web Fonts: http://www.google.com/webfonts 106 | -------------------------------------------------------------------------------- /admin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/cihub/seelog" 5 | 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | 10 | "git.gitorious.org/trantor/trantor.git/database" 11 | "github.com/gorilla/mux" 12 | ) 13 | 14 | func deleteHandler(h handler) { 15 | if !h.sess.IsAdmin() { 16 | notFound(h) 17 | return 18 | } 19 | 20 | var titles []string 21 | var isNew bool 22 | ids := strings.Split(mux.Vars(h.r)["ids"], "/") 23 | for _, id := range ids { 24 | if id == "" { 25 | continue 26 | } 27 | book, err := h.db.GetBookId(id) 28 | if err != nil { 29 | h.sess.Notify("Book not found!", "The book with id '"+id+"' is not there", "error") 30 | continue 31 | } 32 | h.store.Delete(id) 33 | h.db.DeleteBook(id) 34 | 35 | if !book.Active { 36 | isNew = true 37 | } 38 | titles = append(titles, book.Title) 39 | } 40 | if titles != nil { 41 | h.sess.Notify("Removed books!", "The books "+strings.Join(titles, ", ")+" are completly removed", "success") 42 | } 43 | h.sess.Save(h.w, h.r) 44 | if isNew { 45 | http.Redirect(h.w, h.r, "/new/", http.StatusFound) 46 | } else { 47 | http.Redirect(h.w, h.r, "/", http.StatusFound) 48 | } 49 | } 50 | 51 | func editHandler(h handler) { 52 | id := mux.Vars(h.r)["id"] 53 | if !h.sess.IsAdmin() { 54 | notFound(h) 55 | return 56 | } 57 | book, err := h.db.GetBookId(id) 58 | if err != nil { 59 | notFound(h) 60 | return 61 | } 62 | 63 | var data bookData 64 | data.Book = book 65 | data.S = GetStatus(h) 66 | loadTemplate(h, "edit", data) 67 | } 68 | 69 | func cleanEmptyStr(s []string) []string { 70 | var res []string 71 | for _, v := range s { 72 | if v != "" { 73 | res = append(res, v) 74 | } 75 | } 76 | return res 77 | } 78 | 79 | func saveHandler(h handler) { 80 | id := mux.Vars(h.r)["id"] 81 | if !h.sess.IsAdmin() { 82 | notFound(h) 83 | return 84 | } 85 | 86 | title := h.r.FormValue("title") 87 | publisher := h.r.FormValue("publisher") 88 | date := h.r.FormValue("date") 89 | description := h.r.FormValue("description") 90 | author := cleanEmptyStr(h.r.Form["author"]) 91 | subject := cleanEmptyStr(h.r.Form["subject"]) 92 | lang := cleanEmptyStr(h.r.Form["lang"]) 93 | book := map[string]interface{}{"title": title, 94 | "publisher": publisher, 95 | "date": date, 96 | "description": description, 97 | "author": author, 98 | "subject": subject, 99 | "lang": lang} 100 | err := h.db.UpdateBook(id, book) 101 | if err != nil { 102 | log.Error("Updating book: ", err) 103 | notFound(h) 104 | return 105 | } 106 | 107 | h.sess.Notify("Book Modified!", "", "success") 108 | h.sess.Save(h.w, h.r) 109 | if h.db.IsBookActive(id) { 110 | http.Redirect(h.w, h.r, "/book/"+id, http.StatusFound) 111 | } else { 112 | http.Redirect(h.w, h.r, "/new/", http.StatusFound) 113 | } 114 | } 115 | 116 | type newBook struct { 117 | TitleFound int 118 | AuthorFound int 119 | B database.Book 120 | } 121 | type newData struct { 122 | S Status 123 | Found int 124 | Books []newBook 125 | Page int 126 | Next string 127 | Prev string 128 | } 129 | 130 | func newHandler(h handler) { 131 | if !h.sess.IsAdmin() { 132 | notFound(h) 133 | return 134 | } 135 | 136 | err := h.r.ParseForm() 137 | if err != nil { 138 | http.Error(h.w, err.Error(), http.StatusInternalServerError) 139 | return 140 | } 141 | page := 0 142 | if len(h.r.Form["p"]) != 0 { 143 | page, err = strconv.Atoi(h.r.Form["p"][0]) 144 | if err != nil { 145 | page = 0 146 | } 147 | } 148 | res, num, _ := h.db.GetNewBooks(NEW_ITEMS_PAGE, page*NEW_ITEMS_PAGE) 149 | 150 | var data newData 151 | data.S = GetStatus(h) 152 | data.Found = num 153 | if num-NEW_ITEMS_PAGE*page < NEW_ITEMS_PAGE { 154 | data.Books = make([]newBook, num-NEW_ITEMS_PAGE*page) 155 | } else { 156 | data.Books = make([]newBook, NEW_ITEMS_PAGE) 157 | } 158 | for i, b := range res { 159 | data.Books[i].B = b 160 | _, data.Books[i].TitleFound, _ = h.db.GetBooks("title:"+b.Title, 1, 0) 161 | _, data.Books[i].AuthorFound, _ = h.db.GetBooks("author:"+strings.Join(b.Author, " author:"), 1, 0) 162 | } 163 | data.Page = page + 1 164 | if num > (page+1)*NEW_ITEMS_PAGE { 165 | data.Next = "/new/?p=" + strconv.Itoa(page+1) 166 | } 167 | if page > 0 { 168 | data.Prev = "/new/?p=" + strconv.Itoa(page-1) 169 | } 170 | loadTemplate(h, "new", data) 171 | } 172 | 173 | func storeHandler(h handler) { 174 | if !h.sess.IsAdmin() { 175 | notFound(h) 176 | return 177 | } 178 | 179 | var titles []string 180 | ids := strings.Split(mux.Vars(h.r)["ids"], "/") 181 | for _, id := range ids { 182 | if id == "" { 183 | continue 184 | } 185 | book, err := h.db.GetBookId(id) 186 | if err != nil { 187 | h.sess.Notify("Book not found!", "The book with id '"+id+"' is not there", "error") 188 | continue 189 | } 190 | if err != nil { 191 | h.sess.Notify("An error ocurred!", err.Error(), "error") 192 | log.Error("Error getting book for storing '", book.Title, "': ", err.Error()) 193 | continue 194 | } 195 | err = h.db.ActiveBook(id) 196 | if err != nil { 197 | h.sess.Notify("An error ocurred!", err.Error(), "error") 198 | log.Error("Error storing book '", book.Title, "': ", err.Error()) 199 | continue 200 | } 201 | titles = append(titles, book.Title) 202 | } 203 | if titles != nil { 204 | h.sess.Notify("Store books!", "The books '"+strings.Join(titles, ", ")+"' are stored for public download", "success") 205 | } 206 | h.sess.Save(h.w, h.r) 207 | http.Redirect(h.w, h.r, "/new/", http.StatusFound) 208 | } 209 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | PORT = "8080" 5 | 6 | DB_IP = "127.0.0.1" 7 | DB_NAME = "trantor" 8 | META_COLL = "meta" 9 | 10 | EPUB_FILE = "book.epub" 11 | COVER_FILE = "cover.jpg" 12 | COVER_SMALL_FILE = "coverSmall.jpg" 13 | 14 | MINUTES_UPDATE_TAGS = 11 15 | MINUTES_UPDATE_VISITED = 41 16 | MINUTES_UPDATE_DOWNLOADED = 47 17 | MINUTES_UPDATE_HOURLY_V = 31 18 | MINUTES_UPDATE_DAILY_V = 60*12 + 7 19 | MINUTES_UPDATE_MONTHLY_V = 60*24 + 11 20 | MINUTES_UPDATE_HOURLY_D = 29 21 | MINUTES_UPDATE_DAILY_D = 60*12 + 13 22 | MINUTES_UPDATE_MONTHLY_D = 60*24 + 17 23 | MINUTES_UPDATE_LOGGER = 5 24 | BOOKS_FRONT_PAGE = 6 25 | SEARCH_ITEMS_PAGE = 20 26 | NEW_ITEMS_PAGE = 50 27 | NUM_NEWS = 10 28 | DAYS_NEWS_INDEXPAGE = 15 29 | 30 | STORE_PATH = "store/" 31 | TEMPLATE_PATH = "templates/" 32 | CSS_PATH = "css/" 33 | JS_PATH = "js/" 34 | IMG_PATH = "img/" 35 | ROBOTS_PATH = "robots.txt" 36 | DESCRIPTION_PATH = "description.json" 37 | OPENSEARCH_PATH = "opensearch.xml" 38 | LOGGER_CONFIG = "logger.xml" 39 | 40 | IMG_WIDTH_BIG = 300 41 | IMG_WIDTH_SMALL = 60 42 | IMG_QUALITY = 80 43 | 44 | CHAN_SIZE = 100 45 | ) 46 | -------------------------------------------------------------------------------- /cover.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "image/gif" 5 | _ "image/jpeg" 6 | _ "image/png" 7 | 8 | log "github.com/cihub/seelog" 9 | 10 | "bytes" 11 | "image" 12 | "image/jpeg" 13 | "io" 14 | "io/ioutil" 15 | "regexp" 16 | "strings" 17 | 18 | "git.gitorious.org/go-pkg/epubgo.git" 19 | "git.gitorious.org/trantor/trantor.git/storage" 20 | "github.com/gorilla/mux" 21 | "github.com/nfnt/resize" 22 | ) 23 | 24 | func coverHandler(h handler) { 25 | vars := mux.Vars(h.r) 26 | book, err := h.db.GetBookId(vars["id"]) 27 | if err != nil { 28 | notFound(h) 29 | return 30 | } 31 | 32 | if !book.Active { 33 | if !h.sess.IsAdmin() { 34 | notFound(h) 35 | return 36 | } 37 | } 38 | 39 | file := COVER_FILE 40 | if vars["size"] == "small" { 41 | file = COVER_SMALL_FILE 42 | } 43 | f, err := h.store.Get(book.Id, file) 44 | if err != nil { 45 | log.Error("Error while opening image: ", err) 46 | notFound(h) 47 | return 48 | } 49 | defer f.Close() 50 | 51 | headers := h.w.Header() 52 | headers["Content-Type"] = []string{"image/jpeg"} 53 | 54 | _, err = io.Copy(h.w, f) 55 | if err != nil { 56 | log.Error("Error while copying image: ", err) 57 | notFound(h) 58 | return 59 | } 60 | } 61 | 62 | func GetCover(e *epubgo.Epub, id string, store *storage.Store) bool { 63 | if coverFromMetadata(e, id, store) { 64 | return true 65 | } 66 | 67 | if searchCommonCoverNames(e, id, store) { 68 | return true 69 | } 70 | 71 | /* search for img on the text */ 72 | exp, _ := regexp.Compile("<.*ima?g.*[(src)(href)]=[\"']([^\"']*(\\.[^\\.\"']*))[\"']") 73 | it, errNext := e.Spine() 74 | for errNext == nil { 75 | file, err := it.Open() 76 | if err != nil { 77 | break 78 | } 79 | defer file.Close() 80 | 81 | txt, err := ioutil.ReadAll(file) 82 | if err != nil { 83 | break 84 | } 85 | res := exp.FindSubmatch(txt) 86 | if res != nil { 87 | href := string(res[1]) 88 | urlPart := strings.Split(it.URL(), "/") 89 | url := strings.Join(urlPart[:len(urlPart)-1], "/") 90 | if href[:3] == "../" { 91 | href = href[3:] 92 | url = strings.Join(urlPart[:len(urlPart)-2], "/") 93 | } 94 | href = strings.Replace(href, "%20", " ", -1) 95 | href = strings.Replace(href, "%27", "'", -1) 96 | href = strings.Replace(href, "%28", "(", -1) 97 | href = strings.Replace(href, "%29", ")", -1) 98 | if url == "" { 99 | url = href 100 | } else { 101 | url = url + "/" + href 102 | } 103 | 104 | img, err := e.OpenFile(url) 105 | if err == nil { 106 | defer img.Close() 107 | return storeImg(img, id, store) 108 | } 109 | } 110 | errNext = it.Next() 111 | } 112 | return false 113 | } 114 | 115 | func coverFromMetadata(e *epubgo.Epub, id string, store *storage.Store) bool { 116 | metaList, _ := e.MetadataAttr("meta") 117 | for _, meta := range metaList { 118 | if meta["name"] == "cover" { 119 | img, err := e.OpenFileId(meta["content"]) 120 | if err == nil { 121 | defer img.Close() 122 | return storeImg(img, id, store) 123 | } 124 | } 125 | } 126 | return false 127 | } 128 | 129 | func searchCommonCoverNames(e *epubgo.Epub, id string, store *storage.Store) bool { 130 | for _, p := range []string{"cover.jpg", "Images/cover.jpg", "images/cover.jpg", "cover.jpeg", "cover1.jpg", "cover1.jpeg"} { 131 | img, err := e.OpenFile(p) 132 | if err == nil { 133 | defer img.Close() 134 | return storeImg(img, id, store) 135 | } 136 | } 137 | return false 138 | } 139 | 140 | func storeImg(img io.Reader, id string, store *storage.Store) bool { 141 | /* open the files */ 142 | fBig, err := store.Create(id, COVER_FILE) 143 | if err != nil { 144 | log.Error("Error creating cover ", id, ": ", err.Error()) 145 | return false 146 | } 147 | defer fBig.Close() 148 | 149 | fSmall, err := store.Create(id, COVER_SMALL_FILE) 150 | if err != nil { 151 | log.Error("Error creating small cover ", id, ": ", err.Error()) 152 | return false 153 | } 154 | defer fSmall.Close() 155 | 156 | /* resize img */ 157 | var img2 bytes.Buffer 158 | img1 := io.TeeReader(img, &img2) 159 | jpgOptions := jpeg.Options{IMG_QUALITY} 160 | imgResized, err := resizeImg(img1, IMG_WIDTH_BIG) 161 | if err != nil { 162 | log.Error("Error resizing big image: ", err.Error()) 163 | return false 164 | } 165 | err = jpeg.Encode(fBig, imgResized, &jpgOptions) 166 | if err != nil { 167 | log.Error("Error encoding big image: ", err.Error()) 168 | return false 169 | } 170 | imgSmallResized, err := resizeImg(&img2, IMG_WIDTH_SMALL) 171 | if err != nil { 172 | log.Error("Error resizing small image: ", err.Error()) 173 | return false 174 | } 175 | err = jpeg.Encode(fSmall, imgSmallResized, &jpgOptions) 176 | if err != nil { 177 | log.Error("Error encoding small image: ", err.Error()) 178 | return false 179 | } 180 | return true 181 | } 182 | 183 | func resizeImg(imgReader io.Reader, width uint) (image.Image, error) { 184 | img, _, err := image.Decode(imgReader) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | return resize.Resize(width, 0, img, resize.NearestNeighbor), nil 190 | } 191 | -------------------------------------------------------------------------------- /css/FredokaOne.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trantor-library/trantor/efcc9cdf8ef7fae24ca6384429d76884e57e78d6/css/FredokaOne.ttf -------------------------------------------------------------------------------- /css/PTSerif.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trantor-library/trantor/efcc9cdf8ef7fae24ca6384429d76884e57e78d6/css/PTSerif.ttf -------------------------------------------------------------------------------- /css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.2.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */@-ms-viewport{width:device-width}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /css/custom.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Fredoka One'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Fredoka One'), local('FredokaOne-Regular'), url(/css/FredokaOne.ttf) format('truetype'); 6 | } 7 | @font-face { 8 | font-family: 'PT Serif'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('PT Serif'), local('PTSerif-Regular'), url(/css/PTSerif.ttf) format('truetype'); 12 | } 13 | h1, h2, h3, h4, h5, h6 { 14 | font-family: 'Fredoka One', cursive; 15 | } 16 | p, div { 17 | font-family: 'PT Serif', serif; 18 | } 19 | 20 | body { 21 | background: url(/img/bright_squares.png) repeat 0 0; 22 | } 23 | .centered { 24 | text-align:center; 25 | } 26 | .down { 27 | vertical-align:text-bottom; 28 | } 29 | -------------------------------------------------------------------------------- /database/books.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | log "github.com/cihub/seelog" 5 | 6 | "strings" 7 | "time" 8 | 9 | "gopkg.in/mgo.v2" 10 | "gopkg.in/mgo.v2/bson" 11 | ) 12 | 13 | const ( 14 | books_coll = "books" 15 | ) 16 | 17 | type Book struct { 18 | Id string 19 | Title string 20 | Author []string 21 | Contributor string 22 | Publisher string 23 | Description string 24 | Subject []string 25 | Date string 26 | Lang []string 27 | Isbn string 28 | Type string 29 | Format string 30 | Source string 31 | Relation string 32 | Coverage string 33 | Rights string 34 | Meta string 35 | FileSize int 36 | Cover bool 37 | Active bool 38 | BadQuality int `bad_quality` 39 | BadQualityReporters []string `bad_quality_reporters` 40 | } 41 | 42 | type history struct { 43 | Date time.Time 44 | Changes bson.M 45 | } 46 | 47 | func indexBooks(coll *mgo.Collection) { 48 | indexes := []mgo.Index{ 49 | { 50 | Key: []string{"id"}, 51 | Unique: true, 52 | Background: true, 53 | }, 54 | { 55 | Key: []string{"active", "-_id"}, 56 | Background: true, 57 | }, 58 | { 59 | Key: []string{"active", "-bad_quality", "-_id"}, 60 | Background: true, 61 | }, 62 | // TODO: there is no weights in mgo 63 | } 64 | for _, k := range []string{"lang", "title", "author", "subject"} { 65 | idx := mgo.Index{ 66 | Key: []string{"active", k, "-_id"}, 67 | Background: true, 68 | } 69 | indexes = append(indexes, idx) 70 | } 71 | 72 | for _, idx := range indexes { 73 | err := coll.EnsureIndex(idx) 74 | if err != nil { 75 | log.Error("Error indexing books: ", err) 76 | } 77 | } 78 | } 79 | 80 | func addBook(coll *mgo.Collection, book map[string]interface{}) error { 81 | book["_lang"] = metadataLang(book) 82 | return coll.Insert(book) 83 | } 84 | 85 | func getBooks(coll *mgo.Collection, query string, length int, start int) (books []Book, num int, err error) { 86 | return _getBooks(coll, buildQuery(query), length, start) 87 | } 88 | 89 | func getNewBooks(coll *mgo.Collection, length int, start int) (books []Book, num int, err error) { 90 | return _getBooks(coll, bson.M{"$nor": []bson.M{{"active": true}}}, length, start) 91 | } 92 | 93 | func _getBooks(coll *mgo.Collection, query bson.M, length int, start int) (books []Book, num int, err error) { 94 | sort := []string{"$textScore:score"} 95 | if _, present := query["bad_quality"]; present { 96 | sort = append(sort, "-bad_quality") 97 | } 98 | sort = append(sort, "-_id") 99 | 100 | q := coll.Find(query).Select(bson.M{"score": bson.M{"$meta": "textScore"}}).Sort(sort...) 101 | num, err = q.Count() 102 | if err != nil { 103 | return 104 | } 105 | if start != 0 { 106 | q = q.Skip(start) 107 | } 108 | if length != 0 { 109 | q = q.Limit(length) 110 | } 111 | 112 | err = q.All(&books) 113 | return 114 | } 115 | 116 | func getBookId(coll *mgo.Collection, id string) (Book, error) { 117 | var book Book 118 | err := coll.Find(bson.M{"id": id}).One(&book) 119 | return book, err 120 | } 121 | 122 | func deleteBook(coll *mgo.Collection, id string) error { 123 | return coll.Remove(bson.M{"id": id}) 124 | } 125 | 126 | func updateBook(coll *mgo.Collection, id string, data map[string]interface{}) error { 127 | var book map[string]interface{} 128 | record := history{time.Now(), bson.M{}} 129 | 130 | err := coll.Find(bson.M{"id": id}).One(&book) 131 | if err != nil { 132 | return err 133 | } 134 | for k, _ := range data { 135 | record.Changes[k] = book[k] 136 | if k == "lang" { 137 | if lang := metadataLang(data); lang != "" { 138 | data["_lang"] = lang 139 | } 140 | } 141 | } 142 | 143 | return coll.Update(bson.M{"id": id}, bson.M{"$set": data, "$push": bson.M{"history": record}}) 144 | } 145 | 146 | func flagBadQuality(coll *mgo.Collection, id string, user string) error { 147 | b, err := getBookId(coll, id) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | for _, reporter := range b.BadQualityReporters { 153 | if reporter == user { 154 | return nil 155 | } 156 | } 157 | return coll.Update( 158 | bson.M{"id": id}, 159 | bson.M{ 160 | "$inc": bson.M{"bad_quality": 1}, 161 | "$addToSet": bson.M{"bad_quality_reporters": user}, 162 | }, 163 | ) 164 | } 165 | 166 | func activeBook(coll *mgo.Collection, id string) error { 167 | data := map[string]interface{}{"active": true} 168 | return coll.Update(bson.M{"id": id}, bson.M{"$set": data}) 169 | } 170 | 171 | func isBookActive(coll *mgo.Collection, id string) bool { 172 | var book Book 173 | err := coll.Find(bson.M{"id": id}).One(&book) 174 | if err != nil { 175 | return false 176 | } 177 | return book.Active 178 | } 179 | 180 | func buildQuery(q string) bson.M { 181 | text := "" 182 | query := bson.M{"active": true} 183 | words := strings.Split(q, " ") 184 | for _, w := range words { 185 | tag := strings.SplitN(w, ":", 2) 186 | if len(tag) > 1 { 187 | if tag[0] == "flag" { 188 | query[tag[1]] = bson.M{"$gt": 0} 189 | } else { 190 | query[tag[0]] = bson.RegEx{tag[1], "i"} //FIXME: this should be a list 191 | } 192 | } else { 193 | if len(text) != 0 { 194 | text += " " 195 | } 196 | text += w 197 | } 198 | } 199 | if len(text) > 0 { 200 | query["$text"] = bson.M{"$search": text} 201 | } 202 | return query 203 | } 204 | 205 | func metadataLang(book map[string]interface{}) string { 206 | lang, ok := book["lang"].([]string) 207 | if !ok || len(lang) == 0 || len(lang[0]) < 2 { 208 | return "" 209 | } 210 | return strings.ToLower(lang[0][0:2]) 211 | } 212 | -------------------------------------------------------------------------------- /database/books_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "testing" 4 | 5 | var book = map[string]interface{}{ 6 | "title": "some title", 7 | "author": []string{"Alice", "Bob"}, 8 | "id": "r_m-IOzzIbA6QK5w", 9 | } 10 | 11 | func TestAddBook(t *testing.T) { 12 | db := Init(test_host, test_coll) 13 | defer db.del() 14 | 15 | tAddBook(t, db) 16 | 17 | books, num, err := db.GetNewBooks(1, 0) 18 | if err != nil { 19 | t.Fatal("db.GetBooks() return an error: ", err) 20 | } 21 | if num < 1 { 22 | t.Fatalf("db.GetBooks() didn't find any result.") 23 | } 24 | if len(books) < 1 { 25 | t.Fatalf("db.GetBooks() didn't return any result.") 26 | } 27 | if books[0].Title != book["title"] { 28 | t.Error("Book title don't match : '", books[0].Title, "' <=> '", book["title"], "'") 29 | } 30 | } 31 | 32 | func TestActiveBook(t *testing.T) { 33 | db := Init(test_host, test_coll) 34 | defer db.del() 35 | 36 | tAddBook(t, db) 37 | books, _, _ := db.GetNewBooks(1, 0) 38 | id := books[0].Id 39 | 40 | err := db.ActiveBook(id) 41 | if err != nil { 42 | t.Fatal("db.ActiveBook(", id, ") return an error: ", err) 43 | } 44 | 45 | b, err := db.GetBookId(id) 46 | if err != nil { 47 | t.Fatal("db.GetBookId(", id, ") return an error: ", err) 48 | } 49 | if b.Author[0] != books[0].Author[0] { 50 | t.Error("Book author don't match : '", b.Author, "' <=> '", book["author"], "'") 51 | } 52 | } 53 | 54 | func TestFlag(t *testing.T) { 55 | db := Init(test_host, test_coll) 56 | defer db.del() 57 | 58 | tAddBook(t, db) 59 | id, _ := book["id"].(string) 60 | db.ActiveBook(id) 61 | id2 := "tfgrBvd2ps_K4iYt" 62 | b2 := book 63 | b2["id"] = id2 64 | err := db.AddBook(b2) 65 | if err != nil { 66 | t.Error("db.AddBook(", book, ") return an error:", err) 67 | } 68 | db.ActiveBook(id2) 69 | id3 := "tfgrBvd2ps_K4iY2" 70 | b3 := book 71 | b3["id"] = id3 72 | err = db.AddBook(b3) 73 | if err != nil { 74 | t.Error("db.AddBook(", book, ") return an error:", err) 75 | } 76 | db.ActiveBook(id3) 77 | 78 | db.FlagBadQuality(id, "1") 79 | db.FlagBadQuality(id, "2") 80 | db.FlagBadQuality(id3, "1") 81 | 82 | b, _ := db.GetBookId(id) 83 | if b.BadQuality != 2 { 84 | t.Error("The bad quality flag was not increased") 85 | } 86 | b, _ = db.GetBookId(id3) 87 | if b.BadQuality != 1 { 88 | t.Error("The bad quality flag was not increased") 89 | } 90 | 91 | books, _, _ := db.GetBooks("flag:bad_quality", 2, 0) 92 | if len(books) != 2 { 93 | t.Fatal("Not the right number of results to the flag search:", len(books)) 94 | } 95 | if books[0].Id != id { 96 | t.Error("Search for flag bad_quality is not sort right") 97 | } 98 | } 99 | 100 | func tAddBook(t *testing.T, db *DB) { 101 | err := db.AddBook(book) 102 | if err != nil { 103 | t.Error("db.AddBook(", book, ") return an error:", err) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | log "github.com/cihub/seelog" 5 | 6 | "errors" 7 | "os" 8 | 9 | "gopkg.in/mgo.v2" 10 | "gopkg.in/mgo.v2/bson" 11 | ) 12 | 13 | const ( 14 | visited_coll = "visited" 15 | downloaded_coll = "downloaded" 16 | tags_coll = "tags" 17 | ) 18 | 19 | type DB struct { 20 | session *mgo.Session 21 | name string 22 | } 23 | 24 | func Init(host string, name string) *DB { 25 | var err error 26 | db := new(DB) 27 | db.session, err = mgo.Dial(host) 28 | if err != nil { 29 | log.Critical(err) 30 | os.Exit(1) 31 | } 32 | db.name = name 33 | db.initIndexes() 34 | return db 35 | } 36 | 37 | func (db *DB) initIndexes() { 38 | dbCopy := db.session.Copy() 39 | booksColl := dbCopy.DB(db.name).C(books_coll) 40 | go indexBooks(booksColl) 41 | statsColl := dbCopy.DB(db.name).C(stats_coll) 42 | go indexStats(statsColl) 43 | newsColl := dbCopy.DB(db.name).C(news_coll) 44 | go indexNews(newsColl) 45 | } 46 | 47 | func (db *DB) Close() { 48 | db.session.Close() 49 | } 50 | 51 | func (db *DB) Copy() *DB { 52 | dbCopy := new(DB) 53 | dbCopy.session = db.session.Copy() 54 | dbCopy.name = db.name 55 | return dbCopy 56 | } 57 | 58 | func (db *DB) AddBook(book map[string]interface{}) error { 59 | booksColl := db.session.DB(db.name).C(books_coll) 60 | return addBook(booksColl, book) 61 | } 62 | 63 | func (db *DB) GetBooks(query string, length int, start int) (books []Book, num int, err error) { 64 | booksColl := db.session.DB(db.name).C(books_coll) 65 | return getBooks(booksColl, query, length, start) 66 | } 67 | 68 | func (db *DB) GetNewBooks(length int, start int) (books []Book, num int, err error) { 69 | booksColl := db.session.DB(db.name).C(books_coll) 70 | return getNewBooks(booksColl, length, start) 71 | } 72 | 73 | func (db *DB) GetBookId(id string) (Book, error) { 74 | booksColl := db.session.DB(db.name).C(books_coll) 75 | return getBookId(booksColl, id) 76 | } 77 | 78 | func (db *DB) DeleteBook(id string) error { 79 | booksColl := db.session.DB(db.name).C(books_coll) 80 | return deleteBook(booksColl, id) 81 | } 82 | 83 | func (db *DB) UpdateBook(id string, data map[string]interface{}) error { 84 | booksColl := db.session.DB(db.name).C(books_coll) 85 | return updateBook(booksColl, id, data) 86 | } 87 | 88 | func (db *DB) FlagBadQuality(id string, user string) error { 89 | booksColl := db.session.DB(db.name).C(books_coll) 90 | return flagBadQuality(booksColl, id, user) 91 | } 92 | 93 | func (db *DB) ActiveBook(id string) error { 94 | booksColl := db.session.DB(db.name).C(books_coll) 95 | return activeBook(booksColl, id) 96 | } 97 | 98 | func (db *DB) IsBookActive(id string) bool { 99 | booksColl := db.session.DB(db.name).C(books_coll) 100 | return isBookActive(booksColl, id) 101 | } 102 | 103 | func (db *DB) User(name string) *User { 104 | userColl := db.session.DB(db.name).C(user_coll) 105 | return getUser(userColl, name) 106 | } 107 | 108 | func (db *DB) AddUser(name string, pass string) error { 109 | userColl := db.session.DB(db.name).C(user_coll) 110 | return addUser(userColl, name, pass) 111 | } 112 | 113 | func (db *DB) AddNews(text string) error { 114 | newsColl := db.session.DB(db.name).C(news_coll) 115 | return addNews(newsColl, text) 116 | } 117 | 118 | func (db *DB) GetNews(num int, days int) (news []News, err error) { 119 | newsColl := db.session.DB(db.name).C(news_coll) 120 | return getNews(newsColl, num, days) 121 | } 122 | 123 | // TODO: split code in files 124 | func (db *DB) AddStats(stats interface{}) error { 125 | statsColl := db.session.DB(db.name).C(stats_coll) 126 | return statsColl.Insert(stats) 127 | } 128 | 129 | /* Get the most visited books 130 | */ 131 | func (db *DB) GetVisitedBooks() (books []Book, err error) { 132 | visitedColl := db.session.DB(db.name).C(visited_coll) 133 | bookId, err := GetBooksVisited(visitedColl) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | books = make([]Book, len(bookId)) 139 | for i, id := range bookId { 140 | booksColl := db.session.DB(db.name).C(books_coll) 141 | booksColl.Find(bson.M{"_id": id}).One(&books[i]) 142 | books[i].Id = bson.ObjectId(books[i].Id).Hex() 143 | } 144 | return 145 | } 146 | 147 | func (db *DB) UpdateMostVisited() error { 148 | var u dbUpdate 149 | u.src = db.session.DB(db.name).C(stats_coll) 150 | u.dst = db.session.DB(db.name).C(visited_coll) 151 | return u.UpdateMostBooks("book") 152 | } 153 | 154 | /* Get the most downloaded books 155 | */ 156 | func (db *DB) GetDownloadedBooks() (books []Book, err error) { 157 | downloadedColl := db.session.DB(db.name).C(downloaded_coll) 158 | bookId, err := GetBooksVisited(downloadedColl) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | books = make([]Book, len(bookId)) 164 | for i, id := range bookId { 165 | booksColl := db.session.DB(db.name).C(books_coll) 166 | booksColl.Find(bson.M{"_id": id}).One(&books[i]) 167 | books[i].Id = bson.ObjectId(books[i].Id).Hex() 168 | } 169 | return 170 | } 171 | 172 | func (db *DB) UpdateDownloadedBooks() error { 173 | var u dbUpdate 174 | u.src = db.session.DB(db.name).C(stats_coll) 175 | u.dst = db.session.DB(db.name).C(downloaded_coll) 176 | return u.UpdateMostBooks("download") 177 | } 178 | 179 | func (db *DB) GetTags() ([]string, error) { 180 | tagsColl := db.session.DB(db.name).C(tags_coll) 181 | return GetTags(tagsColl) 182 | } 183 | 184 | func (db *DB) UpdateTags() error { 185 | var u dbUpdate 186 | u.src = db.session.DB(db.name).C(books_coll) 187 | u.dst = db.session.DB(db.name).C(tags_coll) 188 | return u.UpdateTags() 189 | } 190 | 191 | func (db *DB) GetVisits(visitType VisitType) ([]Visits, error) { 192 | var coll *mgo.Collection 193 | switch visitType { 194 | case Hourly_visits: 195 | coll = db.session.DB(db.name).C(hourly_visits_coll) 196 | case Daily_visits: 197 | coll = db.session.DB(db.name).C(daily_visits_coll) 198 | case Monthly_visits: 199 | coll = db.session.DB(db.name).C(monthly_visits_coll) 200 | case Hourly_downloads: 201 | coll = db.session.DB(db.name).C(hourly_downloads_coll) 202 | case Daily_downloads: 203 | coll = db.session.DB(db.name).C(daily_downloads_coll) 204 | case Monthly_downloads: 205 | coll = db.session.DB(db.name).C(monthly_downloads_coll) 206 | default: 207 | return nil, errors.New("Not valid VisitType") 208 | } 209 | return GetVisits(coll) 210 | } 211 | 212 | func (db *DB) UpdateHourVisits() error { 213 | var u dbUpdate 214 | u.src = db.session.DB(db.name).C(stats_coll) 215 | u.dst = db.session.DB(db.name).C(hourly_visits_coll) 216 | return u.UpdateHourVisits(false) 217 | } 218 | 219 | func (db *DB) UpdateDayVisits() error { 220 | var u dbUpdate 221 | u.src = db.session.DB(db.name).C(stats_coll) 222 | u.dst = db.session.DB(db.name).C(daily_visits_coll) 223 | return u.UpdateDayVisits(false) 224 | } 225 | 226 | func (db *DB) UpdateMonthVisits() error { 227 | var u dbUpdate 228 | u.src = db.session.DB(db.name).C(stats_coll) 229 | u.dst = db.session.DB(db.name).C(monthly_visits_coll) 230 | return u.UpdateMonthVisits(false) 231 | } 232 | 233 | func (db *DB) UpdateHourDownloads() error { 234 | var u dbUpdate 235 | u.src = db.session.DB(db.name).C(stats_coll) 236 | u.dst = db.session.DB(db.name).C(hourly_downloads_coll) 237 | return u.UpdateHourVisits(true) 238 | } 239 | 240 | func (db *DB) UpdateDayDownloads() error { 241 | var u dbUpdate 242 | u.src = db.session.DB(db.name).C(stats_coll) 243 | u.dst = db.session.DB(db.name).C(daily_downloads_coll) 244 | return u.UpdateDayVisits(true) 245 | } 246 | 247 | func (db *DB) UpdateMonthDownloads() error { 248 | var u dbUpdate 249 | u.src = db.session.DB(db.name).C(stats_coll) 250 | u.dst = db.session.DB(db.name).C(monthly_downloads_coll) 251 | return u.UpdateMonthVisits(true) 252 | } 253 | 254 | // function defined for the tests 255 | func (db *DB) del() { 256 | defer db.Close() 257 | db.session.DB(db.name).DropDatabase() 258 | } 259 | -------------------------------------------------------------------------------- /database/database_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "testing" 4 | 5 | const ( 6 | test_coll = "test_trantor" 7 | test_host = "127.0.0.1" 8 | ) 9 | 10 | func TestInit(t *testing.T) { 11 | db := Init(test_host, test_coll) 12 | defer db.Close() 13 | } 14 | 15 | func TestCopy(t *testing.T) { 16 | db := Init(test_host, test_coll) 17 | defer db.del() 18 | 19 | db2 := db.Copy() 20 | 21 | if db.name != db2.name { 22 | t.Errorf("Names don't match") 23 | } 24 | names1, err := db.session.DatabaseNames() 25 | if err != nil { 26 | t.Errorf("Error on db1: ", err) 27 | } 28 | names2, err := db2.session.DatabaseNames() 29 | if err != nil { 30 | t.Errorf("Error on db1: ", err) 31 | } 32 | if len(names1) != len(names2) { 33 | t.Errorf("len(names) don't match") 34 | } 35 | for i, _ := range names1 { 36 | if names1[i] != names2[i] { 37 | t.Errorf("Names don't match") 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /database/news.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | log "github.com/cihub/seelog" 5 | 6 | "time" 7 | 8 | "gopkg.in/mgo.v2" 9 | "gopkg.in/mgo.v2/bson" 10 | ) 11 | 12 | const ( 13 | news_coll = "news" 14 | ) 15 | 16 | type News struct { 17 | Date time.Time 18 | Text string 19 | } 20 | 21 | func indexNews(coll *mgo.Collection) { 22 | idx := mgo.Index{ 23 | Key: []string{"-date"}, 24 | Background: true, 25 | } 26 | err := coll.EnsureIndex(idx) 27 | if err != nil { 28 | log.Error("Error indexing news: ", err) 29 | } 30 | } 31 | 32 | func addNews(coll *mgo.Collection, text string) error { 33 | var news News 34 | news.Text = text 35 | news.Date = time.Now() 36 | return coll.Insert(news) 37 | } 38 | 39 | func getNews(coll *mgo.Collection, num int, days int) (news []News, err error) { 40 | query := bson.M{} 41 | if days != 0 { 42 | duration := time.Duration(-24*days) * time.Hour 43 | date := time.Now().Add(duration) 44 | query = bson.M{"date": bson.M{"$gt": date}} 45 | } 46 | q := coll.Find(query).Sort("-date").Limit(num) 47 | err = q.All(&news) 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /database/news_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "testing" 4 | 5 | func TestNews(t *testing.T) { 6 | const text = "Some news text" 7 | 8 | db := Init(test_host, test_coll) 9 | defer db.del() 10 | 11 | err := db.AddNews(text) 12 | if err != nil { 13 | t.Errorf("db.News(", text, ") return an error: ", err) 14 | } 15 | 16 | news, err := db.GetNews(1, 1) 17 | if err != nil { 18 | t.Fatalf("db.GetNews() return an error: ", err) 19 | } 20 | if len(news) < 1 { 21 | t.Fatalf("No news found.") 22 | } 23 | if news[0].Text != text { 24 | t.Errorf("News text don't match : '", news[0].Text, "' <=> '", text, "'") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/stats.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | log "github.com/cihub/seelog" 5 | 6 | "time" 7 | 8 | "gopkg.in/mgo.v2" 9 | "gopkg.in/mgo.v2/bson" 10 | ) 11 | 12 | const ( 13 | stats_coll = "statistics" 14 | hourly_visits_coll = "visits.hourly" 15 | daily_visits_coll = "visits.daily" 16 | monthly_visits_coll = "visits.monthly" 17 | hourly_downloads_coll = "downloads.hourly" 18 | daily_downloads_coll = "downloads.daily" 19 | monthly_downloads_coll = "downloads.monthly" 20 | 21 | // FIXME: this should return to the config.go 22 | TAGS_DISPLAY = 50 23 | BOOKS_FRONT_PAGE = 6 24 | ) 25 | 26 | type dbUpdate struct { 27 | src *mgo.Collection 28 | dst *mgo.Collection 29 | } 30 | 31 | type VisitType int 32 | 33 | const ( 34 | Hourly_visits = iota 35 | Daily_visits 36 | Monthly_visits 37 | Hourly_downloads 38 | Daily_downloads 39 | Monthly_downloads 40 | ) 41 | 42 | type Visits struct { 43 | Date time.Time "date" 44 | Count int "count" 45 | } 46 | 47 | func indexStats(coll *mgo.Collection) { 48 | indexes := []mgo.Index{ 49 | { 50 | Key: []string{"section"}, 51 | Background: true, 52 | }, 53 | { 54 | Key: []string{"-date", "section"}, 55 | Background: true, 56 | }, 57 | } 58 | 59 | for _, idx := range indexes { 60 | err := coll.EnsureIndex(idx) 61 | if err != nil { 62 | log.Error("Error indexing stats: ", err) 63 | } 64 | } 65 | } 66 | 67 | func GetTags(tagsColl *mgo.Collection) ([]string, error) { 68 | var result []struct { 69 | Tag string "_id" 70 | } 71 | err := tagsColl.Find(nil).Sort("-count").All(&result) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | tags := make([]string, len(result)) 77 | for i, r := range result { 78 | tags[i] = r.Tag 79 | } 80 | return tags, nil 81 | } 82 | 83 | func GetBooksVisited(visitedColl *mgo.Collection) ([]bson.ObjectId, error) { 84 | var result []struct { 85 | Book bson.ObjectId "_id" 86 | } 87 | err := visitedColl.Find(nil).Sort("-count").All(&result) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | books := make([]bson.ObjectId, len(result)) 93 | for i, r := range result { 94 | books[i] = r.Book 95 | } 96 | return books, nil 97 | } 98 | 99 | func GetVisits(visitsColl *mgo.Collection) ([]Visits, error) { 100 | var result []Visits 101 | err := visitsColl.Find(nil).All(&result) 102 | return result, err 103 | } 104 | 105 | func (u *dbUpdate) UpdateTags() error { 106 | var tags []struct { 107 | Tag string "_id" 108 | Count int "count" 109 | } 110 | err := u.src.Pipe([]bson.M{ 111 | {"$project": bson.M{"subject": 1}}, 112 | {"$unwind": "$subject"}, 113 | {"$group": bson.M{"_id": "$subject", "count": bson.M{"$sum": 1}}}, 114 | {"$sort": bson.M{"count": -1}}, 115 | {"$limit": TAGS_DISPLAY}, 116 | }).All(&tags) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | u.dst.DropCollection() 122 | for _, tag := range tags { 123 | err = u.dst.Insert(tag) 124 | if err != nil { 125 | return err 126 | } 127 | } 128 | return nil 129 | } 130 | 131 | func (u *dbUpdate) UpdateMostBooks(section string) error { 132 | const numDays = 30 133 | start := time.Now().UTC().Add(-numDays * 24 * time.Hour) 134 | 135 | var books []struct { 136 | Book string "_id" 137 | Count int "count" 138 | } 139 | err := u.src.Pipe([]bson.M{ 140 | {"$match": bson.M{"date": bson.M{"$gt": start}, "section": section}}, 141 | {"$project": bson.M{"id": 1}}, 142 | {"$group": bson.M{"_id": "$id", "count": bson.M{"$sum": 1}}}, 143 | {"$sort": bson.M{"count": -1}}, 144 | {"$limit": BOOKS_FRONT_PAGE}, 145 | }).All(&books) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | u.dst.DropCollection() 151 | for _, book := range books { 152 | err = u.dst.Insert(book) 153 | if err != nil { 154 | return err 155 | } 156 | } 157 | return nil 158 | } 159 | 160 | func (u *dbUpdate) UpdateHourVisits(isDownloads bool) error { 161 | const numDays = 2 162 | spanStore := numDays * 24 * time.Hour 163 | return u.updateVisits(hourInc, spanStore, isDownloads) 164 | } 165 | 166 | func (u *dbUpdate) UpdateDayVisits(isDownloads bool) error { 167 | const numDays = 30 168 | spanStore := numDays * 24 * time.Hour 169 | return u.updateVisits(dayInc, spanStore, isDownloads) 170 | } 171 | 172 | func (u *dbUpdate) UpdateMonthVisits(isDownloads bool) error { 173 | const numDays = 365 174 | spanStore := numDays * 24 * time.Hour 175 | return u.updateVisits(monthInc, spanStore, isDownloads) 176 | } 177 | 178 | func hourInc(date time.Time) time.Time { 179 | const span = time.Hour 180 | return date.Add(span).Truncate(span) 181 | } 182 | 183 | func dayInc(date time.Time) time.Time { 184 | const span = 24 * time.Hour 185 | return date.Add(span).Truncate(span) 186 | } 187 | 188 | func monthInc(date time.Time) time.Time { 189 | const span = 24 * time.Hour 190 | return date.AddDate(0, 1, 1-date.Day()).Truncate(span) 191 | } 192 | 193 | func (u *dbUpdate) updateVisits(incTime func(time.Time) time.Time, spanStore time.Duration, isDownloads bool) error { 194 | start := u.calculateStart(spanStore) 195 | for start.Before(time.Now().UTC()) { 196 | stop := incTime(start) 197 | 198 | var count int 199 | var err error 200 | if isDownloads { 201 | count, err = u.countDownloads(start, stop) 202 | } else { 203 | count = u.countVisits(start, stop) 204 | } 205 | if err != nil { 206 | return err 207 | } 208 | 209 | err = u.dst.Insert(bson.M{"date": start, "count": count}) 210 | if err != nil { 211 | return err 212 | } 213 | 214 | start = stop 215 | } 216 | 217 | _, err := u.dst.RemoveAll(bson.M{"date": bson.M{"$lt": time.Now().UTC().Add(-spanStore)}}) 218 | return err 219 | } 220 | 221 | func (u *dbUpdate) calculateStart(spanStore time.Duration) time.Time { 222 | var date struct { 223 | Id bson.ObjectId `bson:"_id"` 224 | Date time.Time `bson:"date"` 225 | } 226 | err := u.dst.Find(bson.M{}).Sort("-date").One(&date) 227 | if err == nil { 228 | u.dst.RemoveId(date.Id) 229 | return date.Date 230 | } 231 | return time.Now().UTC().Add(-spanStore).Truncate(time.Hour) 232 | } 233 | 234 | func (u *dbUpdate) countVisits(start time.Time, stop time.Time) int { 235 | var result struct { 236 | Count int "count" 237 | } 238 | err := u.src.Pipe([]bson.M{ 239 | {"$match": bson.M{"date": bson.M{"$gte": start, "$lt": stop}}}, 240 | {"$group": bson.M{"_id": "$session"}}, 241 | {"$group": bson.M{"_id": 1, "count": bson.M{"$sum": 1}}}, 242 | }).One(&result) 243 | if err != nil { 244 | return 0 245 | } 246 | 247 | return result.Count 248 | } 249 | 250 | func (u *dbUpdate) countDownloads(start time.Time, stop time.Time) (int, error) { 251 | query := bson.M{"date": bson.M{"$gte": start, "$lt": stop}, "section": "download"} 252 | return u.src.Find(query).Count() 253 | } 254 | -------------------------------------------------------------------------------- /database/users.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | log "github.com/cihub/seelog" 5 | 6 | "bytes" 7 | "crypto/rand" 8 | "errors" 9 | 10 | "code.google.com/p/go.crypto/scrypt" 11 | "gopkg.in/mgo.v2" 12 | "gopkg.in/mgo.v2/bson" 13 | ) 14 | 15 | const ( 16 | user_coll = "users" 17 | pass_salt = "ImperialLibSalt" 18 | ) 19 | 20 | type User struct { 21 | user db_user 22 | err error 23 | coll *mgo.Collection 24 | } 25 | 26 | type db_user struct { 27 | User string 28 | Pass []byte 29 | Salt []byte 30 | Role string 31 | } 32 | 33 | func getUser(coll *mgo.Collection, name string) *User { 34 | u := new(User) 35 | if !validUserName(name) { 36 | u.err = errors.New("Invalid username") 37 | return u 38 | } 39 | 40 | u.coll = coll 41 | err := u.coll.Find(bson.M{"user": name}).One(&u.user) 42 | if err != nil { 43 | log.Warn("Error on database checking user ", name, ": ", err) 44 | u.err = errors.New("User not found") 45 | return u 46 | } 47 | return u 48 | } 49 | 50 | func addUser(coll *mgo.Collection, name string, pass string) error { 51 | if !validUserName(name) { 52 | return errors.New("Invalid user name") 53 | } 54 | num, err := coll.Find(bson.M{"user": name}).Count() 55 | if err != nil { 56 | log.Error("Error on database checking user ", name, ": ", err) 57 | return errors.New("An error happen on the database") 58 | } 59 | if num != 0 { 60 | return errors.New("User name already exist") 61 | } 62 | 63 | var user db_user 64 | user.Pass, user.Salt, err = hashPass(pass) 65 | if err != nil { 66 | log.Error("Error hashing password: ", err) 67 | return errors.New("An error happen storing the password") 68 | } 69 | user.User = name 70 | user.Role = "" 71 | return coll.Insert(user) 72 | } 73 | 74 | func validUserName(name string) bool { 75 | return name != "" 76 | } 77 | 78 | func (u User) Valid(pass string) bool { 79 | if u.err != nil { 80 | return false 81 | } 82 | return validatePass(pass, u.user) 83 | } 84 | 85 | func (u User) Role() string { 86 | return u.user.Role 87 | } 88 | 89 | func (u *User) SetPassword(pass string) error { 90 | if u.err != nil { 91 | return u.err 92 | } 93 | hash, salt, err := hashPass(pass) 94 | if err != nil { 95 | return err 96 | } 97 | return u.coll.Update(bson.M{"user": u.user.User}, bson.M{"$set": bson.M{"pass": hash, "salt": salt}}) 98 | } 99 | 100 | func hashPass(pass string) (hash []byte, salt []byte, err error) { 101 | salt, err = genSalt() 102 | if err != nil { 103 | return 104 | } 105 | hash, err = calculateHash(pass, salt) 106 | return 107 | } 108 | 109 | func genSalt() ([]byte, error) { 110 | const ( 111 | saltLen = 64 112 | ) 113 | 114 | b := make([]byte, saltLen) 115 | _, err := rand.Read(b) 116 | return b, err 117 | } 118 | 119 | func validatePass(pass string, user db_user) bool { 120 | hash, err := calculateHash(pass, user.Salt) 121 | if err != nil { 122 | return false 123 | } 124 | return bytes.Compare(user.Pass, hash) == 0 125 | } 126 | 127 | func calculateHash(pass string, salt []byte) ([]byte, error) { 128 | const ( 129 | N = 16384 130 | r = 8 131 | p = 1 132 | keyLen = 32 133 | ) 134 | 135 | bpass := []byte(pass) 136 | return scrypt.Key(bpass, salt, N, r, p, keyLen) 137 | } 138 | -------------------------------------------------------------------------------- /database/users_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "testing" 4 | 5 | const ( 6 | name, pass = "user", "mypass" 7 | ) 8 | 9 | func TestUserEmpty(t *testing.T) { 10 | db := Init(test_host, test_coll) 11 | defer db.del() 12 | 13 | if db.User("").Valid("") { 14 | t.Errorf("user.Valid() with an empty password return true") 15 | } 16 | } 17 | 18 | func TestAddUser(t *testing.T) { 19 | db := Init(test_host, test_coll) 20 | defer db.del() 21 | 22 | tAddUser(t, db) 23 | if !db.User(name).Valid(pass) { 24 | t.Errorf("user.Valid() return false for a valid user") 25 | } 26 | } 27 | 28 | func TestEmptyUsername(t *testing.T) { 29 | db := Init(test_host, test_coll) 30 | defer db.del() 31 | 32 | tAddUser(t, db) 33 | if db.User("").Valid(pass) { 34 | t.Errorf("user.Valid() return true for an invalid user") 35 | } 36 | } 37 | 38 | func tAddUser(t *testing.T, db *DB) { 39 | err := db.AddUser(name, pass) 40 | if err != nil { 41 | t.Errorf("db.Adduser(", name, ", ", pass, ") return an error: ", err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /description.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Imperial Library of Trantor", 3 | "description": "The Imperial Library of Trantor (also known as Galactic Library) is a repository of DRM-free ebooks on ePub format. A community base library with thousands of books on multiple languages (english, german, spamish, french, ...) and different topics like novels, essays, computing, ...", 4 | "relation": "http://xfmro77i3lixucja.onion/", 5 | "keywords": "epub, ebook, book, download, novel, essay, computing, trantor, library, imperial library, drm-free, ereader, kindle", 6 | "type": "ebook library", 7 | "language": "en,de,fr,es,it,gr,zh", 8 | "contactInformation": "zenow@riseup.net" 9 | } 10 | -------------------------------------------------------------------------------- /img/bright_squares.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trantor-library/trantor/efcc9cdf8ef7fae24ca6384429d76884e57e78d6/img/bright_squares.png -------------------------------------------------------------------------------- /img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trantor-library/trantor/efcc9cdf8ef7fae24ca6384429d76884e57e78d6/img/favicon.ico -------------------------------------------------------------------------------- /img/feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trantor-library/trantor/efcc9cdf8ef7fae24ca6384429d76884e57e78d6/img/feed.png -------------------------------------------------------------------------------- /img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trantor-library/trantor/efcc9cdf8ef7fae24ca6384429d76884e57e78d6/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trantor-library/trantor/efcc9cdf8ef7fae24ca6384429d76884e57e78d6/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /js/Chart.min.js: -------------------------------------------------------------------------------- 1 | var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= 2 | Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);dc;)a=dc?c:!isNaN(parseFloat(b))&& 3 | isFinite(b)&&a)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? 4 | b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? 5 | 0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== 6 | a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);ea?-0.5*e*Math.pow(2,10* 8 | (a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* 9 | a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, 10 | scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", 11 | animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", 12 | scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, 13 | c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, 14 | onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, 15 | pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", 16 | scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); 17 | d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;fe&&(e=a[f].value),a[f].valuel&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; 21 | h=Number.MAX_VALUE;for(f=0;fe&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;gt?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0t?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< 35 | h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;ed?h:d;d+=10}r=q-d-t;m= 36 | Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0children.length-1||0>pos))return this.sliding?this.$element.one("slid",function(){that.to(pos)}):activePos==pos?this.pause().cycle():this.slide(pos>activePos?"next":"prev",$(children[pos]))},pause:function(e){return e||(this.paused=!0),this.$element.find(".next, .prev").length&&$.support.transition.end&&(this.$element.trigger($.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){return this.sliding?void 0:this.slide("next")},prev:function(){return this.sliding?void 0:this.slide("prev")},slide:function(type,next){var e,$active=this.$element.find(".item.active"),$next=next||$active[type](),isCycling=this.interval,direction="next"==type?"left":"right",fallback="next"==type?"first":"last",that=this;if(this.sliding=!0,isCycling&&this.pause(),$next=$next.length?$next:this.$element.find(".item")[fallback](),e=$.Event("slide",{relatedTarget:$next[0]}),!$next.hasClass("active")){if($.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(e),e.isDefaultPrevented())return;$next.addClass(type),$next[0].offsetWidth,$active.addClass(direction),$next.addClass(direction),this.$element.one($.support.transition.end,function(){$next.removeClass([type,direction].join(" ")).addClass("active"),$active.removeClass(["active",direction].join(" ")),that.sliding=!1,setTimeout(function(){that.$element.trigger("slid")},0)})}else{if(this.$element.trigger(e),e.isDefaultPrevented())return;$active.removeClass("active"),$next.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return isCycling&&this.cycle(),this}}};var old=$.fn.carousel;$.fn.carousel=function(option){return this.each(function(){var $this=$(this),data=$this.data("carousel"),options=$.extend({},$.fn.carousel.defaults,"object"==typeof option&&option),action="string"==typeof option?option:options.slide;data||$this.data("carousel",data=new Carousel(this,options)),"number"==typeof option?data.to(option):action?data[action]():options.interval&&data.cycle()})},$.fn.carousel.defaults={interval:5e3,pause:"hover"},$.fn.carousel.Constructor=Carousel,$.fn.carousel.noConflict=function(){return $.fn.carousel=old,this},$(document).on("click.carousel.data-api","[data-slide]",function(e){var href,$this=$(this),$target=$($this.attr("data-target")||(href=$this.attr("href"))&&href.replace(/.*(?=#[^\s]+$)/,"")),options=$.extend({},$target.data(),$this.data());$target.carousel(options),e.preventDefault()})}(window.jQuery),!function($){"use strict";var Collapse=function(element,options){this.$element=$(element),this.options=$.extend({},$.fn.collapse.defaults,options),this.options.parent&&(this.$parent=$(this.options.parent)),this.options.toggle&&this.toggle()};Collapse.prototype={constructor:Collapse,dimension:function(){var hasWidth=this.$element.hasClass("width");return hasWidth?"width":"height"},show:function(){var dimension,scroll,actives,hasData;if(!this.transitioning){if(dimension=this.dimension(),scroll=$.camelCase(["scroll",dimension].join("-")),actives=this.$parent&&this.$parent.find("> .accordion-group > .in"),actives&&actives.length){if(hasData=actives.data("collapse"),hasData&&hasData.transitioning)return;actives.collapse("hide"),hasData||actives.data("collapse",null)}this.$element[dimension](0),this.transition("addClass",$.Event("show"),"shown"),$.support.transition&&this.$element[dimension](this.$element[0][scroll])}},hide:function(){var dimension;this.transitioning||(dimension=this.dimension(),this.reset(this.$element[dimension]()),this.transition("removeClass",$.Event("hide"),"hidden"),this.$element[dimension](0))},reset:function(size){var dimension=this.dimension();return this.$element.removeClass("collapse")[dimension](size||"auto")[0].offsetWidth,this.$element[null!==size?"addClass":"removeClass"]("collapse"),this},transition:function(method,startEvent,completeEvent){var that=this,complete=function(){"show"==startEvent.type&&that.reset(),that.transitioning=0,that.$element.trigger(completeEvent)};this.$element.trigger(startEvent),startEvent.isDefaultPrevented()||(this.transitioning=1,this.$element[method]("in"),$.support.transition&&this.$element.hasClass("collapse")?this.$element.one($.support.transition.end,complete):complete())},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var old=$.fn.collapse;$.fn.collapse=function(option){return this.each(function(){var $this=$(this),data=$this.data("collapse"),options="object"==typeof option&&option;data||$this.data("collapse",data=new Collapse(this,options)),"string"==typeof option&&data[option]()})},$.fn.collapse.defaults={toggle:!0},$.fn.collapse.Constructor=Collapse,$.fn.collapse.noConflict=function(){return $.fn.collapse=old,this},$(document).on("click.collapse.data-api","[data-toggle=collapse]",function(e){var href,$this=$(this),target=$this.attr("data-target")||e.preventDefault()||(href=$this.attr("href"))&&href.replace(/.*(?=#[^\s]+$)/,""),option=$(target).data("collapse")?"toggle":$this.data();$this[$(target).hasClass("in")?"addClass":"removeClass"]("collapsed"),$(target).collapse(option)})}(window.jQuery),!function($){"use strict";function clearMenus(){$(toggle).each(function(){getParent($(this)).removeClass("open")})}function getParent($this){var $parent,selector=$this.attr("data-target");return selector||(selector=$this.attr("href"),selector=selector&&/#/.test(selector)&&selector.replace(/.*(?=#[^\s]*$)/,"")),$parent=$(selector),$parent.length||($parent=$this.parent()),$parent}var toggle="[data-toggle=dropdown]",Dropdown=function(element){var $el=$(element).on("click.dropdown.data-api",this.toggle);$("html").on("click.dropdown.data-api",function(){$el.parent().removeClass("open")})};Dropdown.prototype={constructor:Dropdown,toggle:function(){var $parent,isActive,$this=$(this);if(!$this.is(".disabled, :disabled"))return $parent=getParent($this),isActive=$parent.hasClass("open"),clearMenus(),isActive||$parent.toggleClass("open"),$this.focus(),!1},keydown:function(e){var $this,$items,$parent,isActive,index;if(/(38|40|27)/.test(e.keyCode)&&($this=$(this),e.preventDefault(),e.stopPropagation(),!$this.is(".disabled, :disabled"))){if($parent=getParent($this),isActive=$parent.hasClass("open"),!isActive||isActive&&27==e.keyCode)return $this.click();$items=$("[role=menu] li:not(.divider):visible a",$parent),$items.length&&(index=$items.index($items.filter(":focus")),38==e.keyCode&&index>0&&index--,40==e.keyCode&&$items.length-1>index&&index++,~index||(index=0),$items.eq(index).focus())}}};var old=$.fn.dropdown;$.fn.dropdown=function(option){return this.each(function(){var $this=$(this),data=$this.data("dropdown");data||$this.data("dropdown",data=new Dropdown(this)),"string"==typeof option&&data[option].call($this)})},$.fn.dropdown.Constructor=Dropdown,$.fn.dropdown.noConflict=function(){return $.fn.dropdown=old,this},$(document).on("click.dropdown.data-api touchstart.dropdown.data-api",clearMenus).on("click.dropdown touchstart.dropdown.data-api",".dropdown form",function(e){e.stopPropagation()}).on("touchstart.dropdown.data-api",".dropdown-menu",function(e){e.stopPropagation()}).on("click.dropdown.data-api touchstart.dropdown.data-api",toggle,Dropdown.prototype.toggle).on("keydown.dropdown.data-api touchstart.dropdown.data-api",toggle+", [role=menu]",Dropdown.prototype.keydown)}(window.jQuery),!function($){"use strict";var Modal=function(element,options){this.options=options,this.$element=$(element).delegate('[data-dismiss="modal"]',"click.dismiss.modal",$.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};Modal.prototype={constructor:Modal,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var that=this,e=$.Event("show");this.$element.trigger(e),this.isShown||e.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.backdrop(function(){var transition=$.support.transition&&that.$element.hasClass("fade");that.$element.parent().length||that.$element.appendTo(document.body),that.$element.show(),transition&&that.$element[0].offsetWidth,that.$element.addClass("in").attr("aria-hidden",!1),that.enforceFocus(),transition?that.$element.one($.support.transition.end,function(){that.$element.focus().trigger("shown")}):that.$element.focus().trigger("shown")}))},hide:function(e){e&&e.preventDefault(),e=$.Event("hide"),this.$element.trigger(e),this.isShown&&!e.isDefaultPrevented()&&(this.isShown=!1,this.escape(),$(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),$.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal())},enforceFocus:function(){var that=this;$(document).on("focusin.modal",function(e){that.$element[0]===e.target||that.$element.has(e.target).length||that.$element.focus()})},escape:function(){var that=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(e){27==e.which&&that.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var that=this,timeout=setTimeout(function(){that.$element.off($.support.transition.end),that.hideModal()},500);this.$element.one($.support.transition.end,function(){clearTimeout(timeout),that.hideModal()})},hideModal:function(){this.$element.hide().trigger("hidden"),this.backdrop()},removeBackdrop:function(){this.$backdrop.remove(),this.$backdrop=null},backdrop:function(callback){var animate=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var doAnimate=$.support.transition&&animate;this.$backdrop=$(' 2 |
3 |
4 |

Chief Librarian: Las Zenow <zenow@riseup.net>
5 | Fork the source code from gitorious
.

6 |
7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Imperial Library of Trantor 11 | 12 | 13 | 14 | 15 | 48 | 49 | 95 |
96 | {{range .Notif}} 97 |
98 | 99 | {{.Title}} {{.Msg}} 100 |
101 | {{end}} 102 | -------------------------------------------------------------------------------- /templates/help.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .S}} 2 | 3 |

Advanced Search

4 |

It is possible to search books in an specific language adding lang:code to the search string. See for example some searches: 5 |

10 |

11 | 12 |

There is other topics than lang that can be used: 13 |

17 |

18 | 19 |

Other ways to access the library

20 | 21 |
OPDS
22 |

The Imperial Library of Trantor has support for OPDS, witch allows you to browse and download books from any ebook reading software that supports OPDS. To use it just add the following url as your catalog address in your device: 23 |

{{.S.BaseURL}}/?fmt=opds
24 |

25 | 26 |

In android devices works fine with fbreader configuring orbot to transparently proxy it's internet connection over tor.

27 | 28 |
RSS
29 |

With RSS you can use your feed reader to see the lattest updates on a certain search or the news of the library. To get the RSS url of some search just click on the .

30 | 31 |
hummin
32 |

For the command line geeks there is a cli client: hummin

33 | 34 |

To install it in a debian based system (like ubuntu) just do: 35 |

36 | $ sudo apt-get install golang
37 | $ sudo go get github.com/trantor-library/hummin
38 | $ hummin
39 | 
40 |

41 | 42 |
JSON
43 |

There is a JSON API for the library, with it you can write bots and other cool things like hummin.

44 | 45 |

You can add ?fmt=json to almost any page on trantor and you'll get a json of the contents of the page. I'll love to hear if you do something cool with it, please tell me by email.

46 | 47 | {{template "footer.html"}} 48 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .S}} 2 | 3 | {{range .News}} 4 |
5 |
6 | News! {{.Text}} 7 |
8 |
9 | {{end}} 10 | 11 |
12 |

There are {{.Count}} books on the library.

13 |
14 |
15 |

Last books added: 16 | 17 | (more) 18 | 19 | 20 |

21 |
22 | 23 | 37 | 38 |
39 |

Most visited books:

40 |
41 | 55 | 56 |
57 |

Most downloaded books:

58 |
59 | 73 | 74 |
75 |

{{range .Tags}}{{.}} {{end}}

76 |
77 | 78 | {{template "footer.html"}} 79 | -------------------------------------------------------------------------------- /templates/index.opds: -------------------------------------------------------------------------------- 1 | 2 | 11 | {{.S.BaseURL}} 12 | 15 | 18 | 21 | 22 | The Imperial Libary of Trantor 23 | 24 | The Imperial Library of Trantor 25 | {{.S.BaseURL}} 26 | zenow@riseup.net 27 | 28 | {{.S.Updated}} 29 | {{.S.BaseURL}}/img/favicon.ico 30 | 31 | 32 | Last books added 33 | 36 | {{.S.Updated}} 37 | {{.S.BaseURL}}/search/ 38 | 39 | {{$updated := .S.Updated}} 40 | {{$baseurl := .S.BaseURL}} 41 | {{range .Tags}} 42 | 43 | {{html .}} 44 | 48 | {{$updated}} 49 | {{$baseurl}}/search/?subject:{{urlquery .}} 50 | 51 | {{end}} 52 | 53 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .S}} 2 | 3 | 23 | 24 |
25 |

Log In

26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 | 39 | 67 | 68 | {{template "footer.html"}} 69 | -------------------------------------------------------------------------------- /templates/new.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .S}} 2 | 3 | {{if .Books}} 4 | 8 | {{end}} 9 |

Found {{.Found}} books.

10 |
    11 | {{if .Prev}} 12 | 15 | {{end}} 16 | {{if .Next}} 17 | 20 | {{end}} 21 |
22 | 23 | {{range .Books}} 24 | {{$titleFound := .TitleFound}} 25 | {{$authorFound := .AuthorFound}} 26 | {{with .B}} 27 |
28 |
29 |

{{if .Cover}}{{.Title}}{{end}}

30 |
31 |
32 |

{{.Title}} ({{$titleFound}})
33 | {{if .Author}}Author: {{range .Author}}{{.}}, {{end}} ({{$authorFound}})
{{end}} 34 | {{if .Publisher}}Publisher: {{.Publisher}}
{{end}} 35 | {{if .Subject}}Tags: {{range .Subject}}{{.}}, {{end}}
{{end}} 36 | {{if .Isbn}}ISBN: {{.Isbn}}
{{end}} 37 | {{if .Date}}Date: {{.Date}}
{{end}} 38 | {{if .Lang}}Lang: {{range .Lang}}{{.}} {{end}}
{{end}} 39 | {{.Description}} 40 |

41 |
42 |
43 |
44 | Save 45 | Edit 46 | Delete 47 |
48 |

49 |
50 | download 51 | read it! 52 |
53 |
54 |
55 | {{end}} 56 | {{end}} 57 |
    58 | {{if .Prev}} 59 | 62 | {{end}} 63 | {{if .Next}} 64 | 67 | {{end}} 68 |
69 | {{if .Books}} 70 |
71 | Save All 72 | Delete All 73 |
74 | {{end}} 75 | 76 | {{template "footer.html"}} 77 | -------------------------------------------------------------------------------- /templates/news.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .S}} 2 | 3 |

News: 4 | 5 |

6 | 7 |
8 | {{range .News}} 9 |
10 |
{{.Date}}
11 |
{{.Text}}
12 |
13 | {{end}} 14 |
15 | 16 | {{template "footer.html"}} 17 | -------------------------------------------------------------------------------- /templates/news.rss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{with .S}} 5 | Imperial Library of Trantor - News 6 | News of the library 7 | {{.BaseURL}}/news/ 8 | {{end}} 9 | 10 | {{$baseURL := .S.BaseURL}} 11 | {{range .News}} 12 | 13 | {{.Date}} 14 | {{.Text}} 15 | {{$baseURL}}/news/ 16 | 17 | {{end}} 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/read.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .S}} 2 | 3 |
    4 | {{if .Prev}} 5 | 8 | {{end}} 9 |
  • 10 | Back 11 |
  • 12 | {{if .Next}} 13 | 16 | {{end}} 17 |
18 | 19 |
20 |
21 | {{range .Chapters}} 22 | {{range .In}} 23 | 28 | {{end}} 29 | {{end}} 30 | {{if .Chapters}} 31 | 32 | {{end}} 33 |
34 | 35 |
36 | 47 | 48 |
49 |
50 | 51 |
    52 | {{if .Prev}} 53 | 56 | {{end}} 57 |
  • 58 | Back 59 |
  • 60 | {{if .Next}} 61 | 64 | {{end}} 65 |
66 | 67 | {{template "footer.html"}} 68 | -------------------------------------------------------------------------------- /templates/search.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .S}} 2 | 3 | 4 |
    5 | {{if .Prev}} 6 | 9 | {{else}} 10 |
    11 | {{end}} 12 |

    Found {{.Found}} books. Page {{.Page}}
    13 | {{if .Next}} 14 |

    17 | {{end}} 18 |
19 | 20 | {{with .Books}} 21 | {{range .}} 22 |
23 | 26 |
27 |
28 |
29 |

30 | {{if .Lang}}{{.Lang}}{{end}} 31 | {{.Title}} 32 | {{if .Publisher}}{{.Publisher}}{{end}}
33 | {{range .Author}}{{.}}, {{end}} 34 |

35 |
36 |
37 |
38 | download 39 | read it! 40 |
41 |
42 |
43 |
44 |
45 | {{end}} 46 | {{end}} 47 | 48 |
    49 | {{if .Prev}} 50 | 53 | {{else}} 54 |
    55 | {{end}} 56 |
    57 | 58 |
    59 | 60 | 61 |
    62 | 63 |
    64 | {{if .Next}} 65 | 68 | {{end}} 69 |
70 | 71 | {{template "footer.html"}} 72 | -------------------------------------------------------------------------------- /templates/search.opds: -------------------------------------------------------------------------------- 1 | 2 | 11 | {{.S.BaseURL}}/search/?q={{.S.Search}} 12 | {{.S.BaseURL}}/img/favicon.ico 13 | 14 | 17 | 20 | 23 | {{if .Prev}} 24 | 27 | 30 | {{end}} 31 | {{if .Next}} 32 | 35 | {{end}} 36 | 40 | 43 | 44 | {{.Found}} 45 | {{.ItemsPage}} 46 | 47 | search {{.S.Search}} 48 | 49 | The Imperial Library of Trantor 50 | {{.S.BaseURL}} 51 | zenow@riseup.net 52 | 53 | {{.S.Updated}} 54 | 55 | 56 | {{$updated := .S.Updated}} 57 | {{$baseurl := .S.BaseURL}} 58 | {{range .Books}} 59 | 60 | {{html .Title}} 61 | {{$baseurl}}/book/{{.Id}} 62 | {{$updated}} 63 | 64 | {{range .Author}} 65 | 66 | {{html .}} 67 | 68 | {{end}} 69 | {{if .Contributor}} 70 | 71 | {{html .Contributor}} 72 | 73 | {{end}} 74 | 75 | {{if .Isbn}} 76 | urn:isbn:{{.Isbn}} 77 | {{end}} 78 | {{html .Publisher}} 79 | {{if .Date}} 80 | {{.Date}} 81 | {{end}} 82 | 83 | {{range .Lang}} 84 | {{.}} 85 | {{end}} 86 | {{range .Subject}} 87 | 89 | {{end}} 90 | {{html .Description}} 91 | 92 | 93 | 94 | 95 | 98 | 99 | {{end}} 100 | 101 | -------------------------------------------------------------------------------- /templates/search.rss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{with .S}} 5 | Imperial Library of Trantor 6 | {{if .Search}} 7 | Book search: {{.Search}} 8 | {{else}} 9 | Last books added 10 | {{end}} 11 | {{.BaseURL}} 12 | {{end}} 13 | 14 | {{$baseURL := .S.BaseURL}} 15 | {{range .Books}} 16 | 17 | {{.Title}} - {{index .Author 0}} 18 | {{.Description}} 19 | {{$baseURL}}/book/{{.Id}} 20 | {{if .Isbn}} 21 | ISBN: {{.Isbn}} 22 | {{end}} 23 | 24 | {{range .Subject}} 25 | {{if .}} 26 | {{.}} 27 | {{end}} 28 | {{end}} 29 | 30 | {{end}} 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /templates/settings.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .S}} 2 | 3 | 23 | 24 |

Settings

25 | 26 |
27 | Change your pasword 28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 | 36 |
37 | 38 |
39 | 40 |
41 | 42 |
43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 | 52 | {{template "footer.html"}} 53 | -------------------------------------------------------------------------------- /templates/stats.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .S}} 2 | 3 | 4 | 5 |
6 |
7 |

Visits

8 |

Hourly:

9 | 10 |

Daily:

11 | 12 |

Monthly:

13 | 14 |
15 |
16 |

Downloads

17 |

Hourly:

18 | 19 |

Daily:

20 | 21 |

Monthly:

22 | 23 |
24 |
25 | 26 | 61 | 62 | {{template "footer.html"}} 63 | -------------------------------------------------------------------------------- /templates/upload.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .S}} 2 | 3 |

Upload your epubs to help the library.

4 | 5 |
6 | 7 | 8 |
9 | 10 |

The uploaded books will be reviewed by the librarians and included if they comply our quality standards.

11 | 12 | {{template "footer.html"}} 13 | -------------------------------------------------------------------------------- /trantor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/cihub/seelog" 5 | 6 | "io" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | "git.gitorious.org/trantor/trantor.git/database" 12 | "git.gitorious.org/trantor/trantor.git/storage" 13 | "github.com/gorilla/mux" 14 | ) 15 | 16 | type statusData struct { 17 | S Status 18 | } 19 | 20 | func aboutHandler(h handler) { 21 | var data statusData 22 | data.S = GetStatus(h) 23 | data.S.About = true 24 | loadTemplate(h, "about", data) 25 | } 26 | 27 | func helpHandler(h handler) { 28 | var data statusData 29 | data.S = GetStatus(h) 30 | data.S.Help = true 31 | loadTemplate(h, "help", data) 32 | } 33 | 34 | func logoutHandler(h handler) { 35 | h.sess.LogOut() 36 | h.sess.Notify("Log out!", "Bye bye "+h.sess.User, "success") 37 | h.sess.Save(h.w, h.r) 38 | log.Info("User ", h.sess.User, " log out") 39 | http.Redirect(h.w, h.r, "/", http.StatusFound) 40 | } 41 | 42 | type bookData struct { 43 | S Status 44 | Book database.Book 45 | Description []string 46 | FlaggedBadQuality bool 47 | } 48 | 49 | func bookHandler(h handler) { 50 | id := mux.Vars(h.r)["id"] 51 | var data bookData 52 | data.S = GetStatus(h) 53 | book, err := h.db.GetBookId(id) 54 | if err != nil { 55 | notFound(h) 56 | return 57 | } 58 | data.Book = book 59 | data.Description = strings.Split(data.Book.Description, "\n") 60 | data.FlaggedBadQuality = false 61 | for _, reporter := range book.BadQualityReporters { 62 | if reporter == h.sess.User || reporter == h.sess.Id() { 63 | data.FlaggedBadQuality = true 64 | break 65 | } 66 | } 67 | loadTemplate(h, "book", data) 68 | } 69 | 70 | func downloadHandler(h handler) { 71 | id := mux.Vars(h.r)["id"] 72 | book, err := h.db.GetBookId(id) 73 | if err != nil { 74 | notFound(h) 75 | return 76 | } 77 | 78 | if !book.Active { 79 | if !h.sess.IsAdmin() { 80 | notFound(h) 81 | return 82 | } 83 | } 84 | 85 | f, err := h.store.Get(book.Id, EPUB_FILE) 86 | if err != nil { 87 | notFound(h) 88 | return 89 | } 90 | defer f.Close() 91 | 92 | headers := h.w.Header() 93 | headers["Content-Type"] = []string{"application/epub+zip"} 94 | headers["Content-Disposition"] = []string{"attachment; filename=\"" + book.Title + ".epub\""} 95 | 96 | io.Copy(h.w, f) 97 | } 98 | 99 | func flagHandler(h handler) { 100 | id := mux.Vars(h.r)["id"] 101 | user := h.sess.Id() 102 | if h.sess.User != "" { 103 | user = h.sess.User 104 | } 105 | err := h.db.FlagBadQuality(id, user) 106 | if err != nil { 107 | log.Warn("An error ocurred while flaging ", id, ": ", err) 108 | } 109 | h.sess.Notify("Flagged!", "Book marked as bad quality, thank you", "success") 110 | h.sess.Save(h.w, h.r) 111 | http.Redirect(h.w, h.r, h.r.Referer(), http.StatusFound) 112 | } 113 | 114 | type indexData struct { 115 | S Status 116 | Books []database.Book 117 | VisitedBooks []database.Book 118 | DownloadedBooks []database.Book 119 | Count int 120 | Tags []string 121 | News []newsEntry 122 | } 123 | 124 | func indexHandler(h handler) { 125 | var data indexData 126 | 127 | data.Tags, _ = h.db.GetTags() 128 | data.S = GetStatus(h) 129 | data.S.Home = true 130 | data.Books, data.Count, _ = h.db.GetBooks("", BOOKS_FRONT_PAGE, 0) 131 | data.VisitedBooks, _ = h.db.GetVisitedBooks() 132 | data.DownloadedBooks, _ = h.db.GetDownloadedBooks() 133 | data.News = getNews(1, DAYS_NEWS_INDEXPAGE, h.db) 134 | loadTemplate(h, "index", data) 135 | } 136 | 137 | func notFound(h handler) { 138 | var data statusData 139 | 140 | data.S = GetStatus(h) 141 | h.w.WriteHeader(http.StatusNotFound) 142 | loadTemplate(h, "404", data) 143 | } 144 | 145 | func updateLogger() error { 146 | logger, err := log.LoggerFromConfigAsFile(LOGGER_CONFIG) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | return log.ReplaceLogger(logger) 152 | } 153 | 154 | func main() { 155 | defer log.Flush() 156 | err := updateLogger() 157 | if err != nil { 158 | log.Error("Error loading the logger xml: ", err) 159 | } 160 | log.Info("Start the imperial library of trantor") 161 | 162 | db := database.Init(DB_IP, DB_NAME) 163 | defer db.Close() 164 | 165 | store, err := storage.Init(STORE_PATH) 166 | if err != nil { 167 | log.Critical("Problem initializing store: ", err) 168 | os.Exit(1) 169 | } 170 | 171 | InitTasks(db) 172 | sg := InitStats(db, store) 173 | InitUpload(db, store) 174 | 175 | initRouter(db, sg) 176 | log.Error(http.ListenAndServe(":"+PORT, nil)) 177 | } 178 | 179 | func initRouter(db *database.DB, sg *StatsGatherer) { 180 | const id_pattern = "[0-9a-zA-Z\\-\\_]{16}" 181 | 182 | r := mux.NewRouter() 183 | var notFoundHandler http.HandlerFunc 184 | notFoundHandler = sg.Gather(notFound) 185 | r.NotFoundHandler = notFoundHandler 186 | 187 | r.HandleFunc("/", sg.Gather(indexHandler)) 188 | r.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, ROBOTS_PATH) }) 189 | r.HandleFunc("/description.json", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, DESCRIPTION_PATH) }) 190 | r.HandleFunc("/opensearch.xml", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, OPENSEARCH_PATH) }) 191 | 192 | r.HandleFunc("/book/{id:"+id_pattern+"}", sg.Gather(bookHandler)) 193 | r.HandleFunc("/search/", sg.Gather(searchHandler)) 194 | r.HandleFunc("/upload/", sg.Gather(uploadHandler)).Methods("GET") 195 | r.HandleFunc("/upload/", sg.Gather(uploadPostHandler)).Methods("POST") 196 | r.HandleFunc("/read/{id:"+id_pattern+"}", sg.Gather(readStartHandler)) 197 | r.HandleFunc("/read/{id:"+id_pattern+"}/{file:.*}", sg.Gather(readHandler)) 198 | r.HandleFunc("/content/{id:"+id_pattern+"}/{file:.*}", sg.Gather(contentHandler)) 199 | r.HandleFunc("/about/", sg.Gather(aboutHandler)) 200 | r.HandleFunc("/help/", sg.Gather(helpHandler)) 201 | r.HandleFunc("/download/{id:"+id_pattern+"}/{epub:.*}", sg.Gather(downloadHandler)) 202 | r.HandleFunc("/cover/{id:"+id_pattern+"}/{size}/{img:.*}", sg.Gather(coverHandler)) 203 | r.HandleFunc("/stats/", sg.Gather(statsHandler)) 204 | r.HandleFunc("/flag/bad_quality/{id:"+id_pattern+"}", sg.Gather(flagHandler)) 205 | 206 | r.HandleFunc("/login/", sg.Gather(loginHandler)).Methods("GET") 207 | r.HandleFunc("/login/", sg.Gather(loginPostHandler)).Methods("POST") 208 | r.HandleFunc("/create_user/", sg.Gather(createUserHandler)).Methods("POST") 209 | r.HandleFunc("/logout/", sg.Gather(logoutHandler)) 210 | r.HandleFunc("/dashboard/", sg.Gather(dashboardHandler)) 211 | r.HandleFunc("/settings/", sg.Gather(settingsHandler)) 212 | 213 | r.HandleFunc("/new/", sg.Gather(newHandler)) 214 | r.HandleFunc("/save/{id:"+id_pattern+"}", sg.Gather(saveHandler)).Methods("POST") 215 | r.HandleFunc("/edit/{id:"+id_pattern+"}", sg.Gather(editHandler)) 216 | r.HandleFunc("/store/{ids:("+id_pattern+"/)+}", sg.Gather(storeHandler)) 217 | r.HandleFunc("/delete/{ids:("+id_pattern+"/)+}", sg.Gather(deleteHandler)) 218 | 219 | r.HandleFunc("/news/", sg.Gather(newsHandler)) 220 | r.HandleFunc("/news/edit", sg.Gather(editNewsHandler)).Methods("GET") 221 | r.HandleFunc("/news/edit", sg.Gather(postNewsHandler)).Methods("POST") 222 | 223 | h := http.FileServer(http.Dir(IMG_PATH)) 224 | r.Handle("/img/{img}", http.StripPrefix("/img/", h)) 225 | h = http.FileServer(http.Dir(CSS_PATH)) 226 | r.Handle("/css/{css}", http.StripPrefix("/css/", h)) 227 | h = http.FileServer(http.Dir(JS_PATH)) 228 | r.Handle("/js/{js}", http.StripPrefix("/js/", h)) 229 | http.Handle("/", r) 230 | } 231 | -------------------------------------------------------------------------------- /upload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/cihub/seelog" 5 | 6 | "bytes" 7 | "crypto/rand" 8 | "encoding/base64" 9 | "io/ioutil" 10 | "mime/multipart" 11 | "regexp" 12 | "strings" 13 | 14 | "git.gitorious.org/go-pkg/epubgo.git" 15 | "git.gitorious.org/trantor/trantor.git/database" 16 | "git.gitorious.org/trantor/trantor.git/storage" 17 | ) 18 | 19 | func InitUpload(database *database.DB, store *storage.Store) { 20 | uploadChannel = make(chan uploadRequest, CHAN_SIZE) 21 | go uploadWorker(database, store) 22 | } 23 | 24 | var uploadChannel chan uploadRequest 25 | 26 | type uploadRequest struct { 27 | file multipart.File 28 | filename string 29 | } 30 | 31 | func uploadWorker(database *database.DB, store *storage.Store) { 32 | db := database.Copy() 33 | defer db.Close() 34 | 35 | for req := range uploadChannel { 36 | processFile(req, db, store) 37 | } 38 | } 39 | 40 | func processFile(req uploadRequest, db *database.DB, store *storage.Store) { 41 | defer req.file.Close() 42 | 43 | epub, err := openMultipartEpub(req.file) 44 | if err != nil { 45 | log.Warn("Not valid epub uploaded file ", req.filename, ": ", err) 46 | return 47 | } 48 | defer epub.Close() 49 | 50 | book, id := parseFile(epub, store) 51 | req.file.Seek(0, 0) 52 | size, err := store.Store(id, req.file, EPUB_FILE) 53 | if err != nil { 54 | log.Error("Error storing book (", id, "): ", err) 55 | return 56 | } 57 | 58 | book["filesize"] = size 59 | err = db.AddBook(book) 60 | if err != nil { 61 | log.Error("Error storing metadata (", id, "): ", err) 62 | return 63 | } 64 | log.Info("File uploaded: ", req.filename) 65 | } 66 | 67 | func uploadPostHandler(h handler) { 68 | problem := false 69 | 70 | h.r.ParseMultipartForm(20000000) 71 | filesForm := h.r.MultipartForm.File["epub"] 72 | for _, f := range filesForm { 73 | file, err := f.Open() 74 | if err != nil { 75 | log.Error("Can not open uploaded file ", f.Filename, ": ", err) 76 | h.sess.Notify("Upload problem!", "There was a problem with book "+f.Filename, "error") 77 | problem = true 78 | continue 79 | } 80 | uploadChannel <- uploadRequest{file, f.Filename} 81 | } 82 | 83 | if !problem { 84 | if len(filesForm) > 0 { 85 | h.sess.Notify("Upload successful!", "Thank you for your contribution", "success") 86 | } else { 87 | h.sess.Notify("Upload problem!", "No books where uploaded.", "error") 88 | } 89 | } 90 | uploadHandler(h) 91 | } 92 | 93 | func uploadHandler(h handler) { 94 | var data uploadData 95 | data.S = GetStatus(h) 96 | data.S.Upload = true 97 | loadTemplate(h, "upload", data) 98 | } 99 | 100 | type uploadData struct { 101 | S Status 102 | } 103 | 104 | func openMultipartEpub(file multipart.File) (*epubgo.Epub, error) { 105 | buff, _ := ioutil.ReadAll(file) 106 | reader := bytes.NewReader(buff) 107 | return epubgo.Load(reader, int64(len(buff))) 108 | } 109 | 110 | func parseFile(epub *epubgo.Epub, store *storage.Store) (metadata map[string]interface{}, id string) { 111 | book := map[string]interface{}{} 112 | for _, m := range epub.MetadataFields() { 113 | data, err := epub.Metadata(m) 114 | if err != nil { 115 | continue 116 | } 117 | switch m { 118 | case "creator": 119 | book["author"] = parseAuthr(data) 120 | case "description": 121 | book[m] = parseDescription(data) 122 | case "subject": 123 | book[m] = parseSubject(data) 124 | case "date": 125 | book[m] = parseDate(data) 126 | case "language": 127 | book["lang"] = GuessLang(epub, data) 128 | case "title", "contributor", "publisher": 129 | book[m] = cleanStr(strings.Join(data, ", ")) 130 | case "identifier": 131 | attr, _ := epub.MetadataAttr(m) 132 | for i, d := range data { 133 | if attr[i]["scheme"] == "ISBN" { 134 | book["isbn"] = d 135 | } 136 | } 137 | default: 138 | book[m] = strings.Join(data, ", ") 139 | } 140 | } 141 | 142 | id = genId() 143 | book["id"] = id 144 | book["cover"] = GetCover(epub, id, store) 145 | return book, id 146 | } 147 | 148 | func genId() string { 149 | b := make([]byte, 12) 150 | rand.Read(b) 151 | return base64.URLEncoding.EncodeToString(b) 152 | } 153 | 154 | func cleanStr(str string) string { 155 | str = strings.Replace(str, "'", "'", -1) 156 | exp, _ := regexp.Compile("&[^;]*;") 157 | str = exp.ReplaceAllString(str, "") 158 | exp, _ = regexp.Compile("[ ,]*$") 159 | str = exp.ReplaceAllString(str, "") 160 | return str 161 | } 162 | 163 | func parseAuthr(creator []string) []string { 164 | exp1, _ := regexp.Compile("^(.*\\( *([^\\)]*) *\\))*$") 165 | exp2, _ := regexp.Compile("^[^:]*: *(.*)$") 166 | res := make([]string, len(creator)) 167 | for i, s := range creator { 168 | auth := exp1.FindStringSubmatch(s) 169 | if auth != nil { 170 | res[i] = cleanStr(strings.Join(auth[2:], ", ")) 171 | } else { 172 | auth := exp2.FindStringSubmatch(s) 173 | if auth != nil { 174 | res[i] = cleanStr(auth[1]) 175 | } else { 176 | res[i] = cleanStr(s) 177 | } 178 | } 179 | } 180 | return res 181 | } 182 | 183 | func parseDescription(description []string) string { 184 | str := cleanStr(strings.Join(description, "\n")) 185 | str = strings.Replace(str, "

", "\n", -1) 186 | exp, _ := regexp.Compile("<[^>]*>") 187 | str = exp.ReplaceAllString(str, "") 188 | str = strings.Replace(str, "&", "&", -1) 189 | str = strings.Replace(str, "<", "<", -1) 190 | str = strings.Replace(str, ">", ">", -1) 191 | str = strings.Replace(str, "\\n", "\n", -1) 192 | return str 193 | } 194 | 195 | func parseSubject(subject []string) []string { 196 | var res []string 197 | for _, s := range subject { 198 | res = append(res, strings.Split(s, " / ")...) 199 | } 200 | return res 201 | } 202 | 203 | func parseDate(date []string) string { 204 | if len(date) == 0 { 205 | return "" 206 | } 207 | return strings.Replace(date[0], "Unspecified: ", "", -1) 208 | } 209 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/cihub/seelog" 5 | 6 | "net/http" 7 | ) 8 | 9 | func loginHandler(h handler) { 10 | if h.sess.User != "" { 11 | http.Redirect(h.w, h.r, "/dashboard/", http.StatusFound) 12 | return 13 | } 14 | 15 | var data statusData 16 | data.S = GetStatus(h) 17 | loadTemplate(h, "login", data) 18 | } 19 | 20 | func loginPostHandler(h handler) { 21 | user := h.r.FormValue("user") 22 | pass := h.r.FormValue("pass") 23 | if h.db.User(user).Valid(pass) { 24 | log.Info("User ", user, " log in") 25 | h.sess.LogIn(user) 26 | h.sess.Notify("Successful login!", "Welcome "+user, "success") 27 | } else { 28 | log.Warn("User ", user, " bad user or password") 29 | h.sess.Notify("Invalid login!", "user or password invalid", "error") 30 | } 31 | h.sess.Save(h.w, h.r) 32 | http.Redirect(h.w, h.r, h.r.Referer(), http.StatusFound) 33 | } 34 | 35 | func createUserHandler(h handler) { 36 | pass := h.r.FormValue("pass") 37 | confirmPass := h.r.FormValue("confirmPass") 38 | if pass != confirmPass { 39 | h.sess.Notify("Registration error!", "Passwords don't match", "error") 40 | } else { 41 | user := h.r.FormValue("user") 42 | err := h.db.AddUser(user, pass) 43 | if err == nil { 44 | h.sess.Notify("Account created!", "Welcome "+user+". Now you can login", "success") 45 | } else { 46 | h.sess.Notify("Registration error!", "There was some database problem, if it keeps happening please inform me", "error") 47 | } 48 | } 49 | h.sess.Save(h.w, h.r) 50 | http.Redirect(h.w, h.r, h.r.Referer(), http.StatusFound) 51 | } 52 | 53 | func dashboardHandler(h handler) { 54 | if h.sess.User == "" { 55 | notFound(h) 56 | return 57 | } 58 | 59 | var data statusData 60 | data.S = GetStatus(h) 61 | data.S.Dasboard = true 62 | loadTemplate(h, "dashboard", data) 63 | } 64 | 65 | func settingsHandler(h handler) { 66 | if h.sess.User == "" { 67 | notFound(h) 68 | return 69 | } 70 | if h.r.Method == "POST" { 71 | current_pass := h.r.FormValue("currpass") 72 | pass1 := h.r.FormValue("password1") 73 | pass2 := h.r.FormValue("password2") 74 | switch { 75 | case !h.db.User(h.sess.User).Valid(current_pass): 76 | h.sess.Notify("Password error!", "The current password given don't match with the user password. Try again", "error") 77 | case pass1 != pass2: 78 | h.sess.Notify("Passwords don't match!", "The new password and the confirmation password don't match. Try again", "error") 79 | default: 80 | h.db.User(h.sess.User).SetPassword(pass1) 81 | h.sess.Notify("Password updated!", "Your new password is correctly set.", "success") 82 | } 83 | h.sess.Save(h.w, h.r) 84 | } 85 | 86 | var data statusData 87 | data.S = GetStatus(h) 88 | loadTemplate(h, "settings", data) 89 | } 90 | --------------------------------------------------------------------------------