31f506c50c
Moved Reload from the UserStore into the UserCache. Replaced many of the calls to Reload with CacheRemove. Simplified the error functions. Fixed a bug in the errors where they didn't have any CSS. Oops. Added CustomErrorJS(), unused for now, but it may be useful in the future, especially for plugins. Fixed many problems in the themes. Added the Stick(), Unstick(), CreateActionReply(), Lock() and Unlock() methods to *Topic. Renamed ip-search.html to ip-search-results.html Added the Activate() and initPerms methods to *User. Tempra Cursive is hiding from sight for this one commit. Added Font Awesome to /public/, it isn't used yet.
359 lines
9.2 KiB
Go
359 lines
9.2 KiB
Go
/*
|
|
*
|
|
* Gosora Topic Store
|
|
* Copyright Azareal 2017 - 2018
|
|
*
|
|
*/
|
|
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"log"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"./query_gen/lib"
|
|
)
|
|
|
|
// TODO: Add the watchdog goroutine
|
|
// TODO: Add BulkGetMap
|
|
// TODO: Add some sort of update method
|
|
// ? - Should we add stick, lock, unstick, and unlock methods? These might be better on the Topics not the TopicStore
|
|
var topics TopicStore
|
|
|
|
type TopicStore interface {
|
|
Get(id int) (*Topic, error)
|
|
BypassGet(id int) (*Topic, error)
|
|
Delete(id int) error
|
|
Exists(id int) bool
|
|
AddLastTopic(item *Topic, fid int) error // unimplemented
|
|
// TODO: Implement these two methods
|
|
//GetReplies() ([]*Reply, error)
|
|
//GetRepliesRange(lower int, higher int) ([]*Reply, error)
|
|
GetGlobalCount() int
|
|
}
|
|
|
|
type TopicCache interface {
|
|
CacheGet(id int) (*Topic, error)
|
|
CacheGetUnsafe(id int) (*Topic, error)
|
|
CacheSet(item *Topic) error
|
|
CacheAdd(item *Topic) error
|
|
CacheAddUnsafe(item *Topic) error
|
|
CacheRemove(id int) error
|
|
CacheRemoveUnsafe(id int) error
|
|
Flush()
|
|
Reload(id int) error
|
|
GetLength() int
|
|
SetCapacity(capacity int)
|
|
GetCapacity() int
|
|
}
|
|
|
|
type MemoryTopicStore struct {
|
|
items map[int]*Topic
|
|
length int64 // sync/atomic only lets us operate on int32s and int64s
|
|
capacity int
|
|
get *sql.Stmt
|
|
exists *sql.Stmt
|
|
topicCount *sql.Stmt
|
|
delete *sql.Stmt
|
|
sync.RWMutex
|
|
}
|
|
|
|
// NewMemoryTopicStore gives you a new instance of MemoryTopicStore
|
|
func NewMemoryTopicStore(capacity int) *MemoryTopicStore {
|
|
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
existsStmt, err := qgen.Builder.SimpleSelect("topics", "tid", "tid = ?", "", "")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
topicCountStmt, err := qgen.Builder.SimpleCount("topics", "", "")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
deleteStmt, err := qgen.Builder.SimpleDelete("topics", "tid = ?")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return &MemoryTopicStore{
|
|
items: make(map[int]*Topic),
|
|
capacity: capacity,
|
|
get: getStmt,
|
|
exists: existsStmt,
|
|
topicCount: topicCountStmt,
|
|
delete: deleteStmt,
|
|
}
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) CacheGet(id int) (*Topic, error) {
|
|
mts.RLock()
|
|
item, ok := mts.items[id]
|
|
mts.RUnlock()
|
|
if ok {
|
|
return item, nil
|
|
}
|
|
return item, ErrNoRows
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) CacheGetUnsafe(id int) (*Topic, error) {
|
|
item, ok := mts.items[id]
|
|
if ok {
|
|
return item, nil
|
|
}
|
|
return item, ErrNoRows
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) Get(id int) (*Topic, error) {
|
|
mts.RLock()
|
|
topic, ok := mts.items[id]
|
|
mts.RUnlock()
|
|
if ok {
|
|
return topic, nil
|
|
}
|
|
|
|
topic = &Topic{ID: id}
|
|
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
|
if err == nil {
|
|
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
|
|
_ = mts.CacheAdd(topic)
|
|
}
|
|
return topic, err
|
|
}
|
|
|
|
// BypassGet will always bypass the cache and pull the topic directly from the database
|
|
func (mts *MemoryTopicStore) BypassGet(id int) (*Topic, error) {
|
|
topic := &Topic{ID: id}
|
|
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
|
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
|
|
return topic, err
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) Reload(id int) error {
|
|
topic := &Topic{ID: id}
|
|
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
|
if err == nil {
|
|
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
|
|
_ = mts.CacheSet(topic)
|
|
} else {
|
|
_ = mts.CacheRemove(id)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// TODO: Use a transaction here
|
|
func (mts *MemoryTopicStore) Delete(id int) error {
|
|
topic, err := mts.Get(id)
|
|
if err != nil {
|
|
return nil // Already gone, maybe we should check for other errors here
|
|
}
|
|
|
|
topicCreator, err := users.Get(topic.CreatedBy)
|
|
if err == nil {
|
|
wcount := wordCount(topic.Content)
|
|
err = topicCreator.decreasePostStats(wcount, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if err != ErrNoRows {
|
|
return err
|
|
}
|
|
|
|
err = fstore.DecrementTopicCount(topic.ParentID)
|
|
if err != nil && err != ErrNoRows {
|
|
return err
|
|
}
|
|
|
|
mts.Lock()
|
|
mts.CacheRemoveUnsafe(id)
|
|
_, err = mts.delete.Exec(id)
|
|
mts.Unlock()
|
|
return err
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) Exists(id int) bool {
|
|
return mts.exists.QueryRow(id).Scan(&id) == nil
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) CacheSet(item *Topic) error {
|
|
mts.Lock()
|
|
_, ok := mts.items[item.ID]
|
|
if ok {
|
|
mts.items[item.ID] = item
|
|
} else if int(mts.length) >= mts.capacity {
|
|
mts.Unlock()
|
|
return ErrStoreCapacityOverflow
|
|
} else {
|
|
mts.items[item.ID] = item
|
|
atomic.AddInt64(&mts.length, 1)
|
|
}
|
|
mts.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) CacheAdd(item *Topic) error {
|
|
if int(mts.length) >= mts.capacity {
|
|
return ErrStoreCapacityOverflow
|
|
}
|
|
mts.Lock()
|
|
mts.items[item.ID] = item
|
|
mts.Unlock()
|
|
atomic.AddInt64(&mts.length, 1)
|
|
return nil
|
|
}
|
|
|
|
// TODO: Make these length increments thread-safe. Ditto for the other DataStores
|
|
func (mts *MemoryTopicStore) CacheAddUnsafe(item *Topic) error {
|
|
if int(mts.length) >= mts.capacity {
|
|
return ErrStoreCapacityOverflow
|
|
}
|
|
mts.items[item.ID] = item
|
|
atomic.AddInt64(&mts.length, 1)
|
|
return nil
|
|
}
|
|
|
|
// TODO: Make these length decrements thread-safe. Ditto for the other DataStores
|
|
func (mts *MemoryTopicStore) CacheRemove(id int) error {
|
|
mts.Lock()
|
|
delete(mts.items, id)
|
|
mts.Unlock()
|
|
atomic.AddInt64(&mts.length, -1)
|
|
return nil
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) CacheRemoveUnsafe(id int) error {
|
|
delete(mts.items, id)
|
|
atomic.AddInt64(&mts.length, -1)
|
|
return nil
|
|
}
|
|
|
|
// ? - What is this? Do we need it? Should it be in the main store interface?
|
|
func (mts *MemoryTopicStore) AddLastTopic(item *Topic, fid int) error {
|
|
// Coming Soon...
|
|
return nil
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) Flush() {
|
|
mts.Lock()
|
|
mts.items = make(map[int]*Topic)
|
|
mts.length = 0
|
|
mts.Unlock()
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) GetLength() int {
|
|
return int(mts.length)
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) SetCapacity(capacity int) {
|
|
mts.capacity = capacity
|
|
}
|
|
|
|
func (mts *MemoryTopicStore) GetCapacity() int {
|
|
return mts.capacity
|
|
}
|
|
|
|
// Return the total number of topics on these forums
|
|
func (mts *MemoryTopicStore) GetGlobalCount() int {
|
|
var tcount int
|
|
err := mts.topicCount.QueryRow().Scan(&tcount)
|
|
if err != nil {
|
|
LogError(err)
|
|
}
|
|
return tcount
|
|
}
|
|
|
|
type SQLTopicStore struct {
|
|
get *sql.Stmt
|
|
exists *sql.Stmt
|
|
topicCount *sql.Stmt
|
|
delete *sql.Stmt
|
|
}
|
|
|
|
func NewSQLTopicStore() *SQLTopicStore {
|
|
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
existsStmt, err := qgen.Builder.SimpleSelect("topics", "tid", "tid = ?", "", "")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
topicCountStmt, err := qgen.Builder.SimpleCount("topics", "", "")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
deleteStmt, err := qgen.Builder.SimpleDelete("topics", "tid = ?")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return &SQLTopicStore{
|
|
get: getStmt,
|
|
exists: existsStmt,
|
|
topicCount: topicCountStmt,
|
|
delete: deleteStmt,
|
|
}
|
|
}
|
|
|
|
func (sts *SQLTopicStore) Get(id int) (*Topic, error) {
|
|
topic := Topic{ID: id}
|
|
err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
|
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
|
|
return &topic, err
|
|
}
|
|
|
|
// BypassGet is an alias of Get(), as we don't have a cache for SQLTopicStore
|
|
func (sts *SQLTopicStore) BypassGet(id int) (*Topic, error) {
|
|
topic := &Topic{ID: id}
|
|
err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
|
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
|
|
return topic, err
|
|
}
|
|
|
|
func (sts *SQLTopicStore) Exists(id int) bool {
|
|
return sts.exists.QueryRow(id).Scan(&id) == nil
|
|
}
|
|
|
|
// TODO: Use a transaction here
|
|
func (sts *SQLTopicStore) Delete(id int) error {
|
|
topic, err := sts.Get(id)
|
|
if err != nil {
|
|
return nil // Already gone, maybe we should check for other errors here
|
|
}
|
|
|
|
topicCreator, err := users.Get(topic.CreatedBy)
|
|
if err == nil {
|
|
wcount := wordCount(topic.Content)
|
|
err = topicCreator.decreasePostStats(wcount, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if err != ErrNoRows {
|
|
return err
|
|
}
|
|
|
|
err = fstore.DecrementTopicCount(topic.ParentID)
|
|
if err != nil && err != ErrNoRows {
|
|
return err
|
|
}
|
|
|
|
_, err = sts.delete.Exec(id)
|
|
return err
|
|
}
|
|
|
|
// ? - What're we going to do about this?
|
|
func (sts *SQLTopicStore) AddLastTopic(item *Topic, fid int) error {
|
|
// Coming Soon...
|
|
return nil
|
|
}
|
|
|
|
// Return the total number of topics on these forums
|
|
func (sts *SQLTopicStore) GetGlobalCount() int {
|
|
var tcount int
|
|
err := sts.topicCount.QueryRow().Scan(&tcount)
|
|
if err != nil {
|
|
LogError(err)
|
|
}
|
|
return tcount
|
|
}
|