1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-09-16 12:06:23 -07:00

Use more transactions, and replace getNextFreeID with getLatestID

This commit is contained in:
Eggbertx 2022-12-24 12:25:23 -08:00
parent 34d50970db
commit e87915ecb9
11 changed files with 194 additions and 96 deletions

View file

@ -32,15 +32,27 @@ func NewIPBan(ban *IPBan) error {
if ban.ID > 0 {
return ErrBanAlreadyInserted
}
var err error
ban.ID, err = getNextFreeID("DBPREFIXip_ban")
tx, err := BeginTx()
if err != nil {
return err
}
_, err = ExecSQL(query,
defer tx.Rollback()
stmt, err := PrepareSQL(query, tx)
if err != nil {
return err
}
defer stmt.Close()
if _, err = stmt.Exec(
ban.StaffID, ban.BoardID, ban.BannedForPostID, ban.CopyPostText, ban.IsThreadBan, ban.IsActive, ban.IP,
ban.AppealAt, ban.ExpiresAt, ban.Permanent, ban.StaffNote, ban.Message, ban.CanAppeal)
return err
ban.AppealAt, ban.ExpiresAt, ban.Permanent, ban.StaffNote, ban.Message, ban.CanAppeal,
); err != nil {
return err
}
ban.ID, err = getLatestID("DBPREFIXip_ban", tx)
if err != nil {
return err
}
return tx.Commit()
}
// CheckIPBan returns the latest active IP ban for the given IP, as well as any errors. If the
@ -135,7 +147,6 @@ func (ipb *IPBan) Deactivate(staffID int) error {
if err != nil {
return err
}
defer stmt1.Close()
if _, err = stmt1.Exec(ipb.ID); err != nil {
return err
@ -145,7 +156,6 @@ func (ipb *IPBan) Deactivate(staffID int) error {
if err != nil {
return err
}
defer stmt2.Close()
if _, err = stmt2.Exec(ipb.ID); err != nil {
return err
}
@ -201,17 +211,31 @@ func CheckNameBan(name string, boardID int) (*UsernameBan, error) {
func NewNameBan(name string, isRegex bool, boardID int, staffID int, staffNote string) (*UsernameBan, error) {
const query = `INSERT INTO DBPREFIXusername_ban (board_id, staff_id, staff_note, username, is_regex) VALUES(?,?,?,?,?)`
var ban UsernameBan
var err error
if ban.ID, err = getNextFreeID("DBPREFIXusername_ban"); err != nil {
return nil, err
}
if boardID > 0 {
ban.BoardID = new(int)
*ban.BoardID = boardID
}
if _, err = ExecSQL(query, ban.BoardID, staffID, staffNote, name, isRegex); err != nil {
tx, err := BeginTx()
if err != nil {
return nil, err
}
defer tx.Rollback()
stmt, err := PrepareSQL(query, tx)
if err != nil {
return nil, err
}
if _, err = stmt.Exec(ban.BoardID, staffID, staffNote, name, isRegex); err != nil {
return nil, err
}
if ban.ID, err = getLatestID("DBPREFIXusername_ban", tx); err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
ban.StaffID = staffID
ban.StaffNote = staffNote
ban.Username = name
@ -318,17 +342,30 @@ func GetFilenameBans(boardID int, limit int) ([]FilenameBan, error) {
func NewFilenameBan(filename string, isRegex bool, boardID int, staffID int, staffNote string) (*FilenameBan, error) {
const query = `INSERT INTO DBPREFIXfilename_ban (board_id, staff_id, staff_note, filename, is_regex) VALUES(?,?,?,?,?)`
var ban FilenameBan
var err error
if ban.ID, err = getNextFreeID("DBPREFIXfilename_ban"); err != nil {
return nil, err
}
if boardID > 0 {
ban.BoardID = new(int)
*ban.BoardID = boardID
}
if _, err = ExecSQL(query, ban.BoardID, staffID, staffNote, filename, isRegex); err != nil {
tx, err := BeginTx()
if err != nil {
return nil, err
}
defer tx.Rollback()
stmt, err := PrepareSQL(query, tx)
if err != nil {
return nil, err
}
if _, err = stmt.Exec(ban.BoardID, staffID, staffNote, filename, isRegex); err != nil {
return nil, err
}
if ban.ID, err = getLatestID("DBPREFIXfilename_ban", tx); err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
ban.StaffID = staffID
ban.StaffNote = staffNote
ban.Filename = filename
@ -422,14 +459,27 @@ func NewFileChecksumBan(checksum string, boardID int, staffID int, staffNote str
const query = `INSERT INTO DBPREFIXfile_ban (board_id, staff_id, staff_note, checksum) VALUES(?,?,?,?)`
var ban FileBan
var err error
if ban.ID, err = getNextFreeID("DBPREFIXfile_ban"); err != nil {
return nil, err
}
if boardID > 0 {
ban.BoardID = new(int)
*ban.BoardID = boardID
}
if _, err = ExecSQL(query, ban.BoardID, staffID, staffNote, checksum); err != nil {
tx, err := BeginTx()
if err != nil {
return nil, err
}
defer tx.Rollback()
stmt, err := PrepareSQL(query, tx)
if err != nil {
return nil, err
}
if _, err = stmt.Exec(ban.BoardID, staffID, staffNote, checksum); err != nil {
return nil, err
}
if ban.ID, err = getLatestID("DBPREFIXfile_ban", tx); err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
ban.StaffID = staffID

View file

@ -56,11 +56,14 @@ func tmpSqlAdjust() error {
}
if numConstraints > 0 {
query = `ALTER TABLE DBPREFIXwordfilters DROP FOREIGN KEY IF EXISTS wordfilters_board_id_fk`
} else {
query = ""
}
case "postgres":
query = `ALTER TABLE DBPREFIXwordfilters DROP CONSTRAINT IF EXISTS board_id_fk`
case "sqlite3":
return nil
_, err = ExecSQL(`PRAGMA foreign_keys = ON`)
return err
}
if _, err = gcdb.ExecSQL(query); err != nil {
return err

View file

@ -328,33 +328,47 @@ func (p *Post) Insert(bumpThread bool, boardID int, locked bool, stickied bool,
message, message_raw, password)
VALUES(?,?,?,CURRENT_TIMESTAMP,?,?,?,?,?,?,?,?)`
bumpSQL := `UPDATE DBPREFIXthreads SET last_bump = CURRENT_TIMESTAMP WHERE id = ?`
var err error
tx, err := BeginTx()
if err != nil {
return err
}
defer tx.Rollback()
if p.ThreadID == 0 {
// thread doesn't exist yet, this is a new post
p.IsTopPost = true
var threadID int
threadID, err = createThread(boardID, locked, stickied, anchored, cyclical)
threadID, err = createThread(tx, boardID, locked, stickied, anchored, cyclical)
if err != nil {
return err
}
p.ThreadID = threadID
}
id, err := getNextFreeID("DBPREFIXposts")
stmt, err := PrepareSQL(insertSQL, tx)
if err != nil {
return err
}
if _, err = ExecSQL(insertSQL,
if _, err = stmt.Exec(
p.ThreadID, p.IsTopPost, p.IP, p.Name, p.Tripcode, p.IsRoleSignature, p.Email, p.Subject,
p.Message, p.MessageRaw, p.Password,
); err != nil {
return err
}
p.ID = id
if bumpThread {
_, err = ExecSQL(bumpSQL, p.ThreadID)
if p.ID, err = getLatestID("DBPREFIXposts", tx); err != nil {
return err
}
return err
if bumpThread {
stmt2, err := PrepareSQL(bumpSQL, tx)
if err != nil {
return err
}
if _, err = stmt2.Exec(p.ThreadID); err != nil {
return err
}
}
return tx.Commit()
}
func (p *Post) WebPath() string {

View file

@ -115,11 +115,11 @@ func buildNewDatabase(dbType string) error {
if err = initDB("initdb_" + dbType + ".sql"); err != nil {
return errors.New("database initialization failed: " + err.Error())
}
if err = createDefaultBoardIfNoneExist(); err != nil {
return errors.New("failed creating default board if non already exists: " + err.Error())
}
if err = createDefaultAdminIfNoStaff(); err != nil {
return errors.New("failed creating default admin account: " + err.Error())
}
if err = createDefaultBoardIfNoneExist(); err != nil {
return errors.New("failed creating default board if non already exists: " + err.Error())
}
return nil
}

View file

@ -35,13 +35,6 @@ func GetAllSections(onlyNonHidden bool) ([]Section, error) {
return sections, nil
}
func getNextSectionListOrder() (int, error) {
const query = `SELECT COALESCE(MAX(position) + 1, 0) FROM DBPREFIXsections`
var id int
err := QueryRowSQL(query, interfaceSlice(), interfaceSlice(&id))
return id, err
}
// getOrCreateDefaultSectionID creates the default section if no sections have been created yet,
// returns default section ID if it exists
func getOrCreateDefaultSectionID() (sectionID int, err error) {
@ -86,16 +79,37 @@ func DeleteSection(id int) error {
// If position < 0, it will use the ID
func NewSection(name string, abbreviation string, hidden bool, position int) (*Section, error) {
const sqlINSERT = `INSERT INTO DBPREFIXsections (name, abbreviation, hidden, position) VALUES (?,?,?,?)`
const sqlPosition = `SELECT COALESCE(MAX(position) + 1, 1) FROM DBPREFIXsections`
id, err := getNextFreeID("DBPREFIXsections")
tx, err := BeginTx()
if err != nil {
return nil, err
}
if position < 0 {
// position not specified, use the ID
position = id
defer tx.Rollback()
stmt, err := PrepareSQL(sqlINSERT, tx)
if err != nil {
return nil, err
}
if _, err = ExecSQL(sqlINSERT, name, abbreviation, hidden, position); err != nil {
if position < 0 {
// position not specified
stmt2, err := PrepareSQL(sqlPosition, tx)
if err != nil {
return nil, err
}
if err = stmt2.QueryRow().Scan(&position); err != nil {
return nil, err
}
}
if _, err = stmt.Exec(name, abbreviation, hidden, position); err != nil {
return nil, err
}
id, err := getLatestID("DBPREFIXsections", tx)
if err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
return &Section{

View file

@ -17,24 +17,28 @@ var (
ErrThreadDoesNotExist = errors.New("thread does not exist")
)
func createThread(boardID int, locked bool, stickied bool, anchored bool, cyclical bool) (threadID int, err error) {
const sql = `INSERT INTO DBPREFIXthreads (board_id, locked, stickied, anchored, cyclical) VALUES (?,?,?,?,?)`
//Retrieves next free ID, explicitly inserts it, keeps retrying until succesfull insert or until a non-pk error is encountered.
//This is done because mysql doesnt support RETURNING and both LAST_INSERT_ID() and last_row_id() are not thread-safe
isPrimaryKeyError := true
for isPrimaryKeyError {
threadID, err = getNextFreeID("DBPREFIXthreads")
if err != nil {
return 0, err
}
_, err = ExecSQL(sql, boardID, locked, stickied, anchored, cyclical)
isPrimaryKeyError, err = errFilterDuplicatePrimaryKey(err)
if err != nil {
return 0, err
}
func createThread(tx *sql.Tx, boardID int, locked bool, stickied bool, anchored bool, cyclical bool) (threadID int, err error) {
const insertQuery = `INSERT INTO DBPREFIXthreads (board_id, locked, stickied, anchored, cyclical) VALUES (?,?,?,?,?)`
stmt, err := PrepareSQL(insertQuery, tx)
if err != nil {
return 0, err
}
return threadID, nil
if tx == nil {
defer stmt.Close()
}
if _, err = stmt.Exec(boardID, locked, stickied, anchored, cyclical); err != nil {
return 0, err
}
stmt2, err := PrepareSQL(`SELECT MAX(id) FROM DBPREFIXthreads`, tx)
if err != nil {
return 0, err
}
if tx == nil {
defer stmt2.Close()
}
err = stmt2.QueryRow().Scan(&threadID)
return threadID, err
}
// GetThread returns a a thread object from the database

View file

@ -50,7 +50,7 @@ func (p *Post) nextFileOrder() (int, error) {
func (p *Post) AttachFile(upload *Upload) error {
if upload == nil {
return nil //
return nil // no upload to attach, so no error
}
const query = `INSERT INTO DBPREFIXfiles (
post_id, file_order, original_filename, filename, checksum, file_size,
@ -59,10 +59,17 @@ func (p *Post) AttachFile(upload *Upload) error {
if upload.ID > 0 {
return ErrAlreadyAttached
}
uploadID, err := getNextFreeID("DBPREFIXfiles")
tx, err := BeginTx()
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := PrepareSQL(query, tx)
if err != nil {
return err
}
if upload.FileOrder < 1 {
upload.FileOrder, err = p.nextFileOrder()
if err != nil {
@ -70,14 +77,16 @@ func (p *Post) AttachFile(upload *Upload) error {
}
}
upload.PostID = p.ID
if _, err = ExecSQL(query,
if _, err = stmt.Exec(
&upload.PostID, &upload.FileOrder, &upload.OriginalFilename, &upload.Filename, &upload.Checksum, &upload.FileSize,
&upload.IsSpoilered, &upload.ThumbnailWidth, &upload.ThumbnailHeight, &upload.Width, &upload.Height,
); err != nil {
return err
}
upload.ID = uploadID
return nil
if upload.ID, err = getLatestID("DBPREFIXfiles", tx); err != nil {
return err
}
return tx.Commit()
}
// ThumbnailPath returns the thumbnail path of the upload, given an thumbnail type ("thumbnail" or "catalog")

View file

@ -20,6 +20,18 @@ var (
ErrNotConnected = errors.New("error connecting to database")
)
// BeginTx begins a new transaction for the gochan database
func BeginTx() (*sql.Tx, error) {
if gcdb == nil {
return nil, ErrNotConnected
}
ctx := context.Background()
return gcdb.BeginTx(ctx, &sql.TxOptions{
Isolation: 0,
ReadOnly: false,
})
}
// PrepareSQL is used for generating a prepared SQL statement formatted according to the configured database driver
func PrepareSQL(query string, tx *sql.Tx) (*sql.Stmt, error) {
if gcdb == nil {
@ -128,17 +140,6 @@ func QuerySQL(query string, a ...interface{}) (*sql.Rows, error) {
return gcdb.QuerySQL(query, a...)
}
func BeginTx() (*sql.Tx, error) {
if gcdb == nil {
return nil, ErrNotConnected
}
ctx := context.Background()
return gcdb.BeginTx(ctx, &sql.TxOptions{
Isolation: 0,
ReadOnly: false,
})
}
func ParseSQLTimeString(str string) (time.Time, error) {
var t time.Time
var err error
@ -150,10 +151,20 @@ func ParseSQLTimeString(str string) (time.Time, error) {
return t, fmt.Errorf("unrecognized timestamp string format %q", str)
}
func getNextFreeID(tableName string) (ID int, err error) {
var sql = `SELECT COALESCE(MAX(id), 0) + 1 FROM ` + tableName
err = QueryRowSQL(sql, interfaceSlice(), interfaceSlice(&ID))
return ID, err
// getLatestID returns the latest inserted id column value from the given table
func getLatestID(tableName string, tx *sql.Tx) (id int, err error) {
query := `SELECT MAX(id) FROM ` + tableName
if tx != nil {
var stmt *sql.Stmt
stmt, err = PrepareSQL(query, tx)
if err != nil {
return 0, err
}
err = stmt.QueryRow().Scan(&id)
} else {
err = QueryRowSQL(query, nil, interfaceSlice(&id))
}
return
}
func doesTableExist(tableName string) (bool, error) {
@ -219,7 +230,7 @@ func interfaceSlice(args ...interface{}) []interface{} {
return args
}
func errFilterDuplicatePrimaryKey(err error) (isPKerror bool, nonPKerror error) {
/* func errFilterDuplicatePrimaryKey(err error) (isPKerror bool, nonPKerror error) {
if err == nil {
return false, nil
}
@ -235,4 +246,4 @@ func errFilterDuplicatePrimaryKey(err error) (isPKerror bool, nonPKerror error)
}
}
return true, nil
}
} */

View file

@ -238,6 +238,9 @@ var funcMap = template.FuncMap{
}
return dir
},
"boardPagePath": func(board *gcsql.Board, page int) string {
return config.WebPath(board.Dir, strconv.Itoa(page)+".html")
},
"webPath": func(part ...string) string {
return config.WebPath(part...)
},

View file

@ -180,6 +180,7 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
gcutil.LogWarning().
Str("spam", "badReferer").
Str("IP", post.IP).
Int("threadID", post.ThreadID).
Msg("Rejected post from possible spambot")
serverutil.ServeError(writer, "Your post looks like spam", wantsJSON, nil)
return

View file

@ -40,24 +40,13 @@
<a href="#">Scroll to top</a><br/>
<table id="pages">
<tr>
{{/*<td>{{if gt $.currentPage 1}}
<form method="GET" action='{{.board.PagePath "prev"}}'>
<input type="submit" value="Previous" />
</form>
{{- else}}Previous{{end}}</td> */}}
<td>{{range $_,$i := makeLoop .numPages 1 -}}
{{- if eq $.currentPage $i -}}
[<b>{{$i}}</b>]
{{- else -}}
[<a href="{{$.board.PagePath $i }}">{{$i}}</a>]
[<a href="{{boardPagePath $.board $i}}">{{$i}}</a>]
{{- end -}}
{{- end}}</td>
{{/*
<td>{{if lt $.currentPage .numPages}}
<form method="GET" action="{{.board.PagePath `next` }}">
<input type="submit" value="Next" />
</form>
{{else}}Next{{end}}</td> */}}
</tr>
</table>
<span id="boardmenu-bottom">