Cascade deletes for attachments properly when deleting a topic or deleting all of a user's posts.

Eliminate some allocs for maxAgeYear in ShowAttachment.
Split DeleteAttachment out of deleteAttachment for reuse elsewhere.
This commit is contained in:
Azareal 2020-01-23 16:17:50 +10:00
parent dc19773100
commit da26a29597
4 changed files with 120 additions and 75 deletions

View File

@ -4,8 +4,9 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"strings" "strings"
"os"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
var Attachments AttachmentStore var Attachments AttachmentStore
@ -26,9 +27,9 @@ type AttachmentStore interface {
Get(id int) (*MiniAttachment, error) Get(id int) (*MiniAttachment, error)
MiniGetList(originTable string, originID int) (alist []*MiniAttachment, err error) MiniGetList(originTable string, originID int) (alist []*MiniAttachment, err error)
BulkMiniGetList(originTable string, ids []int) (amap map[int][]*MiniAttachment, err error) BulkMiniGetList(originTable string, ids []int) (amap map[int][]*MiniAttachment, err error)
Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string, extra string) (int, error) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path, extra string) (int, error)
MoveTo(sectionID int, originID int, originTable string) error MoveTo(sectionID int, originID int, originTable string) error
MoveToByExtra(sectionID int, originTable string, extra string) error MoveToByExtra(sectionID int, originTable, extra string) error
Count() int Count() int
CountIn(originTable string, oid int) int CountIn(originTable string, oid int) int
CountInPath(path string) int CountInPath(path string) int
@ -50,7 +51,7 @@ type DefaultAttachmentStore struct {
func NewDefaultAttachmentStore(acc *qgen.Accumulator) (*DefaultAttachmentStore, error) { func NewDefaultAttachmentStore(acc *qgen.Accumulator) (*DefaultAttachmentStore, error) {
a := "attachments" a := "attachments"
return &DefaultAttachmentStore{ return &DefaultAttachmentStore{
get: acc.Select(a).Columns("originID, sectionID, uploadedBy, path, extra").Where("attachID = ?").Prepare(), get: acc.Select(a).Columns("originID, sectionID, uploadedBy, path, extra").Where("attachID=?").Prepare(),
getByObj: acc.Select(a).Columns("attachID, sectionID, uploadedBy, path, extra").Where("originTable = ? AND originID = ?").Prepare(), getByObj: acc.Select(a).Columns("attachID, sectionID, uploadedBy, path, extra").Where("originTable = ? AND originID = ?").Prepare(),
add: acc.Insert(a).Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path, extra").Fields("?,?,?,?,?,?,?").Prepare(), add: acc.Insert(a).Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path, extra").Fields("?,?,?,?,?,?,?").Prepare(),
count: acc.Count(a).Prepare(), count: acc.Count(a).Prepare(),
@ -58,7 +59,7 @@ func NewDefaultAttachmentStore(acc *qgen.Accumulator) (*DefaultAttachmentStore,
countInPath: acc.Count(a).Where("path = ?").Prepare(), countInPath: acc.Count(a).Where("path = ?").Prepare(),
move: acc.Update(a).Set("sectionID = ?").Where("originID = ? AND originTable = ?").Prepare(), move: acc.Update(a).Set("sectionID = ?").Where("originID = ? AND originTable = ?").Prepare(),
moveByExtra: acc.Update(a).Set("sectionID = ?").Where("originTable = ? AND extra = ?").Prepare(), moveByExtra: acc.Update(a).Set("sectionID = ?").Where("originTable = ? AND extra = ?").Prepare(),
delete: acc.Delete(a).Where("attachID = ?").Prepare(), delete: acc.Delete(a).Where("attachID=?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
@ -141,7 +142,7 @@ func (s *DefaultAttachmentStore) Get(id int) (*MiniAttachment, error) {
return a, nil return a, nil
} }
func (s *DefaultAttachmentStore) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string, extra string) (int, error) { func (s *DefaultAttachmentStore) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path, extra string) (int, error) {
res, err := s.add.Exec(sectionID, sectionTable, originID, originTable, uploadedBy, path, extra) res, err := s.add.Exec(sectionID, sectionTable, originID, originTable, uploadedBy, path, extra)
if err != nil { if err != nil {
return 0, err return 0, err
@ -155,7 +156,7 @@ func (s *DefaultAttachmentStore) MoveTo(sectionID int, originID int, originTable
return err return err
} }
func (s *DefaultAttachmentStore) MoveToByExtra(sectionID int, originTable string, extra string) error { func (s *DefaultAttachmentStore) MoveToByExtra(sectionID int, originTable, extra string) error {
_, err := s.moveByExtra.Exec(sectionID, originTable, extra) _, err := s.moveByExtra.Exec(sectionID, originTable, extra)
return err return err
} }
@ -188,3 +189,25 @@ func (s *DefaultAttachmentStore) Delete(aid int) error {
_, err := s.delete.Exec(aid) _, err := s.delete.Exec(aid)
return err return err
} }
// TODO: Add a table for the files and lock the file row when performing tasks related to the file
func DeleteAttachment(aid int) error {
attach, err := Attachments.Get(aid)
if err != nil {
return err
}
err = Attachments.Delete(aid)
if err != nil {
return err
}
count := Attachments.CountInPath(attach.Path)
if count == 0 {
err := os.Remove("./attachs/" + attach.Path)
if err != nil {
return err
}
}
return nil
}

View File

@ -184,25 +184,25 @@ func (t *TopicsRow) Topic() *Topic {
}*/ }*/
type TopicStmts struct { type TopicStmts struct {
getRids *sql.Stmt getRids *sql.Stmt
getReplies *sql.Stmt getReplies *sql.Stmt
addReplies *sql.Stmt addReplies *sql.Stmt
updateLastReply *sql.Stmt updateLastReply *sql.Stmt
lock *sql.Stmt lock *sql.Stmt
unlock *sql.Stmt unlock *sql.Stmt
moveTo *sql.Stmt moveTo *sql.Stmt
stick *sql.Stmt stick *sql.Stmt
unstick *sql.Stmt unstick *sql.Stmt
hasLikedTopic *sql.Stmt hasLikedTopic *sql.Stmt
createLike *sql.Stmt createLike *sql.Stmt
addLikesToTopic *sql.Stmt addLikesToTopic *sql.Stmt
delete *sql.Stmt delete *sql.Stmt
deleteLikesForTopic *sql.Stmt deleteLikesForTopic *sql.Stmt
deleteActivity *sql.Stmt deleteActivity *sql.Stmt
deleteActivitySubs *sql.Stmt deleteActivitySubs *sql.Stmt
edit *sql.Stmt edit *sql.Stmt
setPoll *sql.Stmt setPoll *sql.Stmt
createAction *sql.Stmt createAction *sql.Stmt
getTopicUser *sql.Stmt // TODO: Can we get rid of this? getTopicUser *sql.Stmt // TODO: Can we get rid of this?
getByReplyID *sql.Stmt getByReplyID *sql.Stmt
@ -214,25 +214,25 @@ func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
t := "topics" t := "topics"
topicStmts = TopicStmts{ topicStmts = TopicStmts{
getRids: acc.Select("replies").Columns("rid").Where("tid = ?").Orderby("rid ASC").Limit("?,?").Prepare(), getRids: acc.Select("replies").Columns("rid").Where("tid = ?").Orderby("rid ASC").Limit("?,?").Prepare(),
getReplies: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.ipaddress, r.likeCount, r.attachCount, r.actionType", "r.createdBy = u.uid", "r.tid = ?", "r.rid ASC", "?,?"), getReplies: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.ipaddress, r.likeCount, r.attachCount, r.actionType", "r.createdBy = u.uid", "r.tid = ?", "r.rid ASC", "?,?"),
addReplies: acc.Update(t).Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").Where("tid = ?").Prepare(), addReplies: acc.Update(t).Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").Where("tid = ?").Prepare(),
updateLastReply: acc.Update(t).Set("lastReplyID=?").Where("lastReplyID > ? AND tid = ?").Prepare(), updateLastReply: acc.Update(t).Set("lastReplyID=?").Where("lastReplyID > ? AND tid = ?").Prepare(),
lock: acc.Update(t).Set("is_closed=1").Where("tid=?").Prepare(), lock: acc.Update(t).Set("is_closed=1").Where("tid=?").Prepare(),
unlock: acc.Update(t).Set("is_closed=0").Where("tid=?").Prepare(), unlock: acc.Update(t).Set("is_closed=0").Where("tid=?").Prepare(),
moveTo: acc.Update(t).Set("parentID=?").Where("tid=?").Prepare(), moveTo: acc.Update(t).Set("parentID=?").Where("tid=?").Prepare(),
stick: acc.Update(t).Set("sticky=1").Where("tid=?").Prepare(), stick: acc.Update(t).Set("sticky=1").Where("tid=?").Prepare(),
unstick: acc.Update(t).Set("sticky=0").Where("tid=?").Prepare(), unstick: acc.Update(t).Set("sticky=0").Where("tid=?").Prepare(),
hasLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy=? and targetItem=? and targetType='topics'").Prepare(), hasLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy=? and targetItem=? and targetType='topics'").Prepare(),
createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy, createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(), createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy, createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(),
addLikesToTopic: acc.Update(t).Set("likeCount=likeCount+?").Where("tid = ?").Prepare(), addLikesToTopic: acc.Update(t).Set("likeCount=likeCount+?").Where("tid = ?").Prepare(),
delete: acc.Delete(t).Where("tid=?").Prepare(), delete: acc.Delete(t).Where("tid=?").Prepare(),
deleteLikesForTopic: acc.Delete("likes").Where("targetItem=? AND targetType='topics'").Prepare(), deleteLikesForTopic: acc.Delete("likes").Where("targetItem=? AND targetType='topics'").Prepare(),
deleteActivity: acc.Delete("activity_stream").Where("elementID=? AND elementType='topic'").Prepare(), deleteActivity: acc.Delete("activity_stream").Where("elementID=? AND elementType='topic'").Prepare(),
deleteActivitySubs: acc.Delete("activity_subscriptions").Where("targetID=? AND targetType='topic'").Prepare(), deleteActivitySubs: acc.Delete("activity_subscriptions").Where("targetID=? AND targetType='topic'").Prepare(),
edit: acc.Update(t).Set("title=?,content=?,parsed_content=?").Where("tid=?").Prepare(), // TODO: Only run the content update bits on non-polls, does this matter? edit: acc.Update(t).Set("title=?,content=?,parsed_content=?").Where("tid=?").Prepare(), // TODO: Only run the content update bits on non-polls, does this matter?
setPoll: acc.Update(t).Set("poll=?").Where("tid=? AND poll=0").Prepare(), setPoll: acc.Update(t).Set("poll=?").Where("tid=? AND poll=0").Prepare(),
createAction: acc.Insert("replies").Columns("tid, actionType, ipaddress, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(), createAction: acc.Insert("replies").Columns("tid, actionType, ipaddress, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(),
getTopicUser: acc.SimpleLeftJoin("topics AS t", "users AS u", "t.title, t.content, t.createdBy, t.createdAt, t.lastReplyAt, t.lastReplyBy, t.lastReplyID, t.is_closed, t.sticky, t.parentID, t.ipaddress, t.views, t.postCount, t.likeCount, t.attachCount,t.poll, u.name, u.avatar, u.group, u.level", "t.createdBy = u.uid", "tid = ?", "", ""), getTopicUser: acc.SimpleLeftJoin("topics AS t", "users AS u", "t.title, t.content, t.createdBy, t.createdAt, t.lastReplyAt, t.lastReplyBy, t.lastReplyID, t.is_closed, t.sticky, t.parentID, t.ipaddress, t.views, t.postCount, t.likeCount, t.attachCount,t.poll, u.name, u.avatar, u.group, u.level", "t.createdBy = u.uid", "tid = ?", "", ""),
getByReplyID: acc.SimpleLeftJoin("replies AS r", "topics AS t", "t.tid, t.title, t.content, t.createdBy, t.createdAt, t.is_closed, t.sticky, t.parentID, t.ipaddress, t.views, t.postCount, t.likeCount, t.poll, t.data", "r.tid = t.tid", "rid = ?", "", ""), getByReplyID: acc.SimpleLeftJoin("replies AS r", "topics AS t", "t.tid, t.title, t.content, t.createdBy, t.createdAt, t.is_closed, t.sticky, t.parentID, t.ipaddress, t.views, t.postCount, t.likeCount, t.poll, t.data", "r.tid = t.tid", "rid = ?", "", ""),
@ -350,6 +350,35 @@ func handleLikedTopicReplies(tid int) error {
return rows.Err() return rows.Err()
} }
func handleTopicAttachments(tid int) error {
f := func(stmt *sql.Stmt) error {
rows, err := stmt.Query(tid)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var aid int
err := rows.Scan(&aid)
if err != nil {
return err
}
err = DeleteAttachment(aid)
if err != nil && err != sql.ErrNoRows {
return err
}
}
return rows.Err()
}
err := f(userStmts.getAttachmentsOfTopic)
if err != nil {
return err
}
return f(userStmts.getAttachmentsOfTopic2)
}
// TODO: Use a transaction here // TODO: Use a transaction here
func (t *Topic) Delete() error { func (t *Topic) Delete() error {
creator, err := Users.Get(t.CreatedBy) creator, err := Users.Get(t.CreatedBy)
@ -380,6 +409,10 @@ func (t *Topic) Delete() error {
if err != nil { if err != nil {
return err return err
} }
err = handleTopicAttachments(t.ID)
if err != nil {
return err
}
_, err = topicStmts.deleteActivitySubs.Exec(t.ID) _, err = topicStmts.deleteActivitySubs.Exec(t.ID)
if err != nil { if err != nil {
return err return err
@ -389,7 +422,7 @@ func (t *Topic) Delete() error {
return err return err
} }
if t.Poll > 0 { if t.Poll > 0 {
err = (&Poll{ID:t.Poll}).Delete() err = (&Poll{ID: t.Poll}).Delete()
if err != nil { if err != nil {
return err return err
} }
@ -523,7 +556,7 @@ func (ru *ReplyUser) Init() error {
} }
// TODO: Factor TopicUser into a *Topic and *User, as this starting to become overly complicated x.x // TODO: Factor TopicUser into a *Topic and *User, as this starting to become overly complicated x.x
func (t *TopicUser) Replies(offset int, pFrag int, user *User) (rlist []*ReplyUser, ogdesc string, err error) { func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser, ogdesc string, err error) {
var likedMap map[int]int var likedMap map[int]int
if user.Liked > 0 { if user.Liked > 0 {
likedMap = make(map[int]int) likedMap = make(map[int]int)

View File

@ -150,6 +150,8 @@ type UserStmts struct {
deleteProfilePosts *sql.Stmt deleteProfilePosts *sql.Stmt
deleteReplyPosts *sql.Stmt deleteReplyPosts *sql.Stmt
getLikedRepliesOfTopic *sql.Stmt getLikedRepliesOfTopic *sql.Stmt
getAttachmentsOfTopic *sql.Stmt
getAttachmentsOfTopic2 *sql.Stmt
} }
var userStmts UserStmts var userStmts UserStmts
@ -191,6 +193,8 @@ func init() {
deleteProfilePosts: acc.Select("users_replies").Columns("rid").Where("createdBy=?").Prepare(), deleteProfilePosts: acc.Select("users_replies").Columns("rid").Where("createdBy=?").Prepare(),
deleteReplyPosts: acc.Select("replies").Columns("rid,tid").Where("createdBy=?").Prepare(), deleteReplyPosts: acc.Select("replies").Columns("rid,tid").Where("createdBy=?").Prepare(),
getLikedRepliesOfTopic: acc.Select("replies").Columns("rid").Where("tid=? AND likeCount>0").Prepare(), getLikedRepliesOfTopic: acc.Select("replies").Columns("rid").Where("tid=? AND likeCount>0").Prepare(),
getAttachmentsOfTopic: acc.Select("attachments").Columns("attachID").Where("originID=? AND originTable='topics'").Prepare(),
getAttachmentsOfTopic2: acc.Select("attachments").Columns("attachID").Where("extra=? AND originTable='replies'").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -356,6 +360,10 @@ func (u *User) DeletePosts() error {
if err != nil { if err != nil {
return err return err
} }
err = handleTopicAttachments(tid)
if err != nil {
return err
}
_, err = topicStmts.deleteActivitySubs.Exec(tid) _, err = topicStmts.deleteActivitySubs.Exec(tid)
if err != nil { if err != nil {
return err return err
@ -365,7 +373,7 @@ func (u *User) DeletePosts() error {
return err return err
} }
if poll > 0 { if poll > 0 {
err = (&Poll{ID:poll}).Delete() err = (&Poll{ID: poll}).Delete()
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,13 +3,12 @@ package routes
import ( import (
"database/sql" "database/sql"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
c "github.com/Azareal/Gosora/common" c "github.com/Azareal/Gosora/common"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
type AttachmentStmts struct { type AttachmentStmts struct {
@ -22,12 +21,14 @@ var attachmentStmts AttachmentStmts
func init() { func init() {
c.DbInits.Add(func(acc *qgen.Accumulator) error { c.DbInits.Add(func(acc *qgen.Accumulator) error {
attachmentStmts = AttachmentStmts{ attachmentStmts = AttachmentStmts{
get: acc.Select("attachments").Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path").Where("path = ? AND sectionID = ? AND sectionTable = ?").Prepare(), get: acc.Select("attachments").Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path").Where("path=? AND sectionID=? AND sectionTable=?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
} }
var maxAgeYear = "max-age=" + strconv.Itoa(int(c.Year))
func ShowAttachment(w http.ResponseWriter, r *http.Request, user c.User, filename string) c.RouteError { func ShowAttachment(w http.ResponseWriter, r *http.Request, user c.User, filename string) c.RouteError {
filename = c.Stripslashes(filename) filename = c.Stripslashes(filename)
ext := filepath.Ext("./attachs/" + filename) ext := filepath.Ext("./attachs/" + filename)
@ -61,13 +62,13 @@ func ShowAttachment(w http.ResponseWriter, r *http.Request, user c.User, filenam
} else { } else {
return c.LocalError("Unknown section", w, r, user) return c.LocalError("Unknown section", w, r, user)
} }
if originTable != "topics" && originTable != "replies" { if originTable != "topics" && originTable != "replies" {
return c.LocalError("Unknown origin", w, r, user) return c.LocalError("Unknown origin", w, r, user)
} }
if !user.Loggedin { if !user.Loggedin {
w.Header().Set("Cache-Control", "max-age="+strconv.Itoa(int(c.Year))) w.Header().Set("Cache-Control", maxAgeYear)
} else { } else {
guest := c.GuestUser guest := c.GuestUser
_, ferr := c.SimpleForumUserCheck(w, r, &guest, sid) _, ferr := c.SimpleForumUserCheck(w, r, &guest, sid)
@ -76,7 +77,7 @@ func ShowAttachment(w http.ResponseWriter, r *http.Request, user c.User, filenam
} }
h := w.Header() h := w.Header()
if guest.Perms.ViewTopic { if guest.Perms.ViewTopic {
h.Set("Cache-Control", "max-age="+strconv.Itoa(int(c.Year))) h.Set("Cache-Control", maxAgeYear)
} else { } else {
h.Set("Cache-Control", "private") h.Set("Cache-Control", "private")
} }
@ -87,32 +88,12 @@ func ShowAttachment(w http.ResponseWriter, r *http.Request, user c.User, filenam
return nil return nil
} }
// TODO: Add a table for the files and lock the file row when performing tasks related to the file
func deleteAttachment(w http.ResponseWriter, r *http.Request, user c.User, aid int, js bool) c.RouteError { func deleteAttachment(w http.ResponseWriter, r *http.Request, user c.User, aid int, js bool) c.RouteError {
attach, err := c.Attachments.Get(aid) err := c.DeleteAttachment(aid)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return c.NotFoundJSQ(w, r, nil, js) return c.NotFoundJSQ(w, r, nil, js)
} else if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
} }
return c.InternalErrorJSQ(err, w, r, js)
err = c.Attachments.Delete(aid)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
count := c.Attachments.CountInPath(attach.Path)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
if count == 0 {
err := os.Remove("./attachs/" + attach.Path)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
}
return nil
} }
// TODO: Stop duplicating this code // TODO: Stop duplicating this code