2014-06-29 19:41:29 -05:00
|
|
|
package database
|
|
|
|
|
|
|
|
import (
|
2017-05-21 22:48:43 +00:00
|
|
|
"time"
|
|
|
|
|
2020-11-30 19:03:31 +00:00
|
|
|
"github.com/go-pg/pg/v10"
|
|
|
|
"github.com/go-pg/pg/v10/orm"
|
2014-06-29 19:41:29 -05:00
|
|
|
)
|
|
|
|
|
2017-05-21 10:16:16 +00:00
|
|
|
type DB interface {
|
2016-07-30 07:10:33 -04:00
|
|
|
Close() error
|
|
|
|
AddBook(book Book) error
|
2017-05-21 10:16:16 +00:00
|
|
|
GetBooks(query string, length int, start int) (books []Book, num int, err error)
|
|
|
|
GetNewBooks(query string, length int, start int) (books []Book, num int, err error)
|
2016-07-30 07:59:30 -04:00
|
|
|
GetBookID(id string) (Book, error)
|
2017-05-21 10:16:16 +00:00
|
|
|
DeleteBook(id string) error
|
|
|
|
UpdateBook(id string, data map[string]interface{}) error
|
|
|
|
ActiveBook(id string) error
|
|
|
|
IsBookActive(id string) bool
|
2020-05-03 09:35:17 +00:00
|
|
|
ExistsBookHash(hash []byte) bool
|
2017-05-21 10:16:16 +00:00
|
|
|
AddUser(name string, pass string) error
|
2017-05-21 22:48:43 +00:00
|
|
|
AddRawUser(name string, hpass []byte, salt []byte, role string) error
|
2019-11-25 06:58:44 +09:30
|
|
|
SetAdminUser(name string, pass string) error
|
2016-07-30 07:10:33 -04:00
|
|
|
GetRole(name string) (string, error)
|
|
|
|
SetPassword(name string, pass string) error
|
2018-04-08 10:55:13 +00:00
|
|
|
SetRole(name, role string) error
|
2016-07-30 07:10:33 -04:00
|
|
|
ValidPassword(name string, pass string) bool
|
2018-04-08 10:55:13 +00:00
|
|
|
ListUsers() ([]User, error)
|
2017-05-21 10:16:16 +00:00
|
|
|
AddNews(text string) error
|
2017-05-21 22:48:43 +00:00
|
|
|
AddRawNews(text string, date time.Time) error
|
2016-07-30 07:10:33 -04:00
|
|
|
GetNews(num int, days int) (news []New, err error)
|
2017-05-30 22:10:21 +00:00
|
|
|
IncViews(ID string) error
|
|
|
|
IncDownloads(ID string) error
|
2018-11-19 03:14:26 +00:00
|
|
|
GetDownloadCounter(ID string) (int, error)
|
2017-06-05 23:03:24 +00:00
|
|
|
GetFrontPage() FrontPage
|
2019-03-05 19:54:21 +00:00
|
|
|
AddSubmission(submission Submission, userName string) (id int, err error)
|
2017-09-21 12:09:04 +00:00
|
|
|
UpdateSubmission(id int, status string, book *Book) error
|
|
|
|
UpdateSubmissionByBook(bookID string, status string, book *Book) error
|
2018-04-07 23:42:41 +00:00
|
|
|
UpdateSubmissionComment(submissionID, bookID, comment string) error
|
2017-09-21 12:09:04 +00:00
|
|
|
GetSubmission(submissionID string) (submission []Submission, err error)
|
2019-03-05 19:54:21 +00:00
|
|
|
GetUserSubmissions(userName string) (submission []Submission, err error)
|
2018-04-07 23:42:41 +00:00
|
|
|
GetComment(bookID string) (string, error)
|
2018-04-09 00:15:58 +00:00
|
|
|
|
|
|
|
BookLister
|
2014-06-29 19:41:29 -05:00
|
|
|
}
|
|
|
|
|
2016-09-03 15:08:10 -04:00
|
|
|
const (
|
2017-06-05 23:03:24 +00:00
|
|
|
tagsDisplay = 50
|
|
|
|
booksFrontPage = 6
|
2016-09-03 15:08:10 -04:00
|
|
|
)
|
|
|
|
|
2016-07-30 07:10:33 -04:00
|
|
|
type pgDB struct {
|
2017-05-19 00:27:14 +00:00
|
|
|
sql *pg.DB
|
2017-06-05 23:03:24 +00:00
|
|
|
frontPage FrontPage
|
|
|
|
}
|
|
|
|
|
|
|
|
type FrontPage struct {
|
|
|
|
Count int
|
|
|
|
Last []Book
|
|
|
|
Visited []Book
|
|
|
|
Download []Book
|
|
|
|
Tags []string
|
2016-07-30 07:10:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Options for the database
|
|
|
|
type Options struct {
|
|
|
|
Addr string
|
|
|
|
User string
|
|
|
|
Password string
|
|
|
|
Name string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Init the database connection
|
|
|
|
func Init(options Options) (DB, error) {
|
2018-04-08 22:11:19 +00:00
|
|
|
db, err := _init(options)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
go db.frontPageUpdater()
|
2019-03-05 20:52:07 +00:00
|
|
|
go db.submissionsCleaner()
|
2018-04-08 22:11:19 +00:00
|
|
|
|
|
|
|
return db, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func _init(options Options) (*pgDB, error) {
|
2017-05-18 22:16:16 +00:00
|
|
|
network := "tcp"
|
|
|
|
if options.Addr[0] == '/' {
|
|
|
|
network = "unix"
|
|
|
|
}
|
2016-07-30 07:10:33 -04:00
|
|
|
sql := pg.Connect(&pg.Options{
|
2017-05-18 22:16:16 +00:00
|
|
|
Network: network,
|
2016-07-30 07:10:33 -04:00
|
|
|
Addr: options.Addr,
|
|
|
|
User: options.User,
|
|
|
|
Password: options.Password,
|
|
|
|
Database: options.Name,
|
|
|
|
})
|
2016-09-03 15:08:10 -04:00
|
|
|
|
2017-05-19 00:27:14 +00:00
|
|
|
var db pgDB
|
|
|
|
db.sql = sql
|
2017-09-21 21:05:06 +00:00
|
|
|
|
|
|
|
err := db.create()
|
2018-04-08 22:11:19 +00:00
|
|
|
return &db, err
|
2015-04-21 21:52:34 -04:00
|
|
|
}
|
|
|
|
|
2016-07-30 07:10:33 -04:00
|
|
|
// Close the database connection
|
|
|
|
func (db pgDB) Close() error {
|
|
|
|
return db.sql.Close()
|
2014-06-29 19:41:29 -05:00
|
|
|
}
|
|
|
|
|
2017-09-21 21:05:06 +00:00
|
|
|
func (db pgDB) create() error {
|
2020-11-30 19:03:31 +00:00
|
|
|
orm.RegisterTable(&BookListEntry{})
|
2018-04-09 00:15:58 +00:00
|
|
|
models := []interface{}{&Book{}, &New{}, &User{}, &Visit{}, &Submission{}, &BookList{}, &BookListEntry{}}
|
2017-09-21 21:05:06 +00:00
|
|
|
for _, model := range models {
|
|
|
|
options := &orm.CreateTableOptions{
|
|
|
|
IfNotExists: true,
|
|
|
|
FKConstraints: true,
|
|
|
|
}
|
|
|
|
switch model.(type) {
|
|
|
|
case *Submission:
|
|
|
|
// XXX: ON DELETE SET NULL contraint is not supported
|
|
|
|
options.FKConstraints = false
|
|
|
|
}
|
2020-11-30 19:03:31 +00:00
|
|
|
err := db.sql.Model(model).CreateTable(options)
|
2017-09-21 21:05:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := db.sql.Exec(createSQL)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-05-21 10:16:16 +00:00
|
|
|
func RO(db DB) DB {
|
|
|
|
return &roDB{db}
|
2014-06-29 19:41:29 -05:00
|
|
|
}
|
2017-09-21 21:05:06 +00:00
|
|
|
|
|
|
|
const createSQL = `
|
2019-11-25 06:15:28 +09:30
|
|
|
|
|
|
|
-- Visits default values
|
|
|
|
ALTER TABLE visits ALTER COLUMN downloads SET DEFAULT 0 ;
|
|
|
|
ALTER TABLE visits ALTER COLUMN views SET DEFAULT 0 ;
|
|
|
|
|
|
|
|
|
|
|
|
-- Books default values
|
|
|
|
ALTER TABLE books ALTER COLUMN active SET DEFAULT false ;
|
|
|
|
|
2017-09-21 21:05:06 +00:00
|
|
|
-- Books column indexes
|
|
|
|
CREATE INDEX IF NOT EXISTS books_lang_idx ON books (lang);
|
|
|
|
CREATE INDEX IF NOT EXISTS books_isbn_idx ON books (isbn);
|
|
|
|
CREATE INDEX IF NOT EXISTS books_active_idx ON books (active);
|
2020-05-03 09:35:17 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS books_file_hash_idx ON books (file_hash);
|
2017-09-21 21:05:06 +00:00
|
|
|
|
2019-11-25 06:58:44 +09:30
|
|
|
|
|
|
|
|
2017-09-21 21:05:06 +00:00
|
|
|
-- Books trigram indexes
|
|
|
|
CREATE INDEX IF NOT EXISTS books_title_idx ON books USING GIN (title gin_trgm_ops);
|
|
|
|
CREATE INDEX IF NOT EXISTS books_contributor_idx ON books USING GIN (contributor gin_trgm_ops);
|
|
|
|
CREATE INDEX IF NOT EXISTS books_publisher_idx ON books USING GIN (publisher gin_trgm_ops);
|
2020-03-23 11:35:10 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS books_date_idx ON books USING GIN (date gin_trgm_ops);
|
2017-09-21 21:05:06 +00:00
|
|
|
CREATE OR REPLACE FUNCTION text(text[]) RETURNS text immutable AS $$
|
|
|
|
select $1::text
|
|
|
|
$$ language sql;
|
|
|
|
CREATE INDEX IF NOT EXISTS books_authors_idx ON books USING GIN (text(authors) gin_trgm_ops);
|
|
|
|
CREATE INDEX IF NOT EXISTS books_tags_idx ON books USING GIN (text(tags) gin_trgm_ops);
|
|
|
|
|
|
|
|
-- Books text search index
|
|
|
|
CREATE OR REPLACE FUNCTION books_trigger() RETURNS trigger AS $$
|
|
|
|
declare
|
|
|
|
lang_config regconfig;
|
|
|
|
begin
|
|
|
|
lang_config := 'simple';
|
|
|
|
if new.lang = 'da' then
|
|
|
|
lang_config := 'danish';
|
|
|
|
elsif new.lang = 'nl' then
|
|
|
|
lang_config := 'dutch';
|
|
|
|
elsif new.lang = 'en' then
|
|
|
|
lang_config := 'english';
|
|
|
|
elsif new.lang = 'fi' then
|
|
|
|
lang_config := 'finnish';
|
|
|
|
elsif new.lang = 'fr' then
|
|
|
|
lang_config := 'french';
|
|
|
|
elsif new.lang = 'de' then
|
|
|
|
lang_config :='german';
|
|
|
|
elsif new.lang = 'hu' then
|
|
|
|
lang_config :='hungarian';
|
|
|
|
elsif new.lang = 'it' then
|
|
|
|
lang_config :='italian';
|
|
|
|
elsif new.lang = 'no' then
|
|
|
|
lang_config :='norwegian';
|
|
|
|
elsif new.lang = 'pt' then
|
|
|
|
lang_config :='portuguese';
|
|
|
|
elsif new.lang = 'ro' then
|
|
|
|
lang_config :='romanian';
|
|
|
|
elsif new.lang = 'ru' then
|
|
|
|
lang_config :='russian';
|
|
|
|
elsif new.lang = 'es' then
|
|
|
|
lang_config :='spanish';
|
|
|
|
elsif new.lang = 'sv' then
|
|
|
|
lang_config :='swedish';
|
|
|
|
elsif new.lang = 'tr' then
|
|
|
|
lang_config :='turkish';
|
|
|
|
end if;
|
|
|
|
|
|
|
|
new.tsv :=
|
|
|
|
setweight(to_tsvector(lang_config, coalesce(new.title,'')), 'A') ||
|
|
|
|
setweight(to_tsvector('simple', coalesce(array_to_string(new.authors, ' '),'')), 'A') ||
|
|
|
|
setweight(to_tsvector('simple', coalesce(new.contributor,'')), 'B') ||
|
|
|
|
setweight(to_tsvector('simple', coalesce(new.publisher,'')), 'B') ||
|
|
|
|
setweight(to_tsvector(lang_config, coalesce(array_to_string(new.tags, ' '),'')), 'C') ||
|
|
|
|
setweight(to_tsvector(lang_config, coalesce(new.description,'')), 'D');
|
|
|
|
return new;
|
|
|
|
end
|
|
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS tsvectorupdate ON books;
|
|
|
|
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
|
|
|
|
ON books FOR EACH ROW EXECUTE PROCEDURE books_trigger();
|
|
|
|
CREATE INDEX IF NOT EXISTS books_idx ON books USING GIN (tsv);
|
|
|
|
CREATE OR REPLACE FUNCTION to_tsquery_multilingual(text) RETURNS tsquery AS $$
|
|
|
|
SELECT
|
|
|
|
to_tsquery('simple', $1) ||
|
|
|
|
to_tsquery('danish', $1) ||
|
|
|
|
to_tsquery('dutch', $1) ||
|
|
|
|
to_tsquery('english', $1) ||
|
|
|
|
to_tsquery('finnish', $1) ||
|
|
|
|
to_tsquery('french', $1) ||
|
|
|
|
to_tsquery('german', $1) ||
|
|
|
|
to_tsquery('hungarian', $1) ||
|
|
|
|
to_tsquery('italian', $1) ||
|
|
|
|
to_tsquery('norwegian', $1) ||
|
|
|
|
to_tsquery('portuguese', $1) ||
|
|
|
|
to_tsquery('romanian', $1) ||
|
|
|
|
to_tsquery('russian', $1) ||
|
|
|
|
to_tsquery('spanish', $1) ||
|
|
|
|
to_tsquery('swedish', $1) ||
|
|
|
|
to_tsquery('turkish', $1)
|
|
|
|
$$ LANGUAGE sql IMMUTABLE;
|
|
|
|
|
|
|
|
-- Visits indexes
|
|
|
|
CREATE INDEX IF NOT EXISTS visits_downloads_idx on visits(downloads DESC NULLS LAST);
|
|
|
|
CREATE INDEX IF NOT EXISTS visits_views_idx on visits(views DESC NULLS LAST);
|
2018-11-19 03:14:26 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS visits_book_id_idx on visits(book_id);
|
2017-09-21 21:05:06 +00:00
|
|
|
|
|
|
|
-- Submissions indexes
|
|
|
|
CREATE INDEX IF NOT EXISTS submissions_id_idx on submissions(submission_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS submissions_book_idx on submissions(book_id);
|
2019-03-05 19:54:21 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS submissions_user_idx on submissions(user_id);
|
2020-11-18 11:52:26 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS submissions_last_modified_idx on submissions(last_modified);
|
2017-09-21 21:05:06 +00:00
|
|
|
|
|
|
|
-- Submissions constraint
|
|
|
|
ALTER TABLE ONLY submissions
|
|
|
|
DROP CONSTRAINT IF EXISTS submissions_book_id_fkey;
|
|
|
|
ALTER TABLE ONLY submissions
|
|
|
|
ADD CONSTRAINT submissions_book_id_fkey FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE SET NULL;
|
2019-03-05 19:54:21 +00:00
|
|
|
ALTER TABLE ONLY submissions
|
|
|
|
DROP CONSTRAINT IF EXISTS submissions_user_id_fkey;
|
|
|
|
ALTER TABLE ONLY submissions
|
|
|
|
ADD CONSTRAINT submissions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
|
2018-04-09 00:15:58 +00:00
|
|
|
|
|
|
|
-- BookLists indexes
|
|
|
|
CREATE INDEX IF NOT EXISTS book_lists_list_id_idx on book_lists(list_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS book_lists_user_idx on book_lists(user_id);
|
2018-04-09 10:51:20 +00:00
|
|
|
CREATE INDEX IF NOT EXISTS book_list_entries_list_idx on book_list_entries(book_list_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS book_list_entries_book_idx on book_list_entries(book_id);
|
2017-09-21 21:05:06 +00:00
|
|
|
`
|