mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
fix: resolve 6 medium issues from full go-review
- MED-1: Document soundboard channelID=0 server-wide permission intent - MED-2: Document os.Exit(0) in update handler skipping deferred cleanup - MED-3: Replace os.ReadFile/WriteFile with streaming io.Copy in backup restore to avoid loading entire DB into memory - MED-4: Add GetAllVoiceStates bulk query, eliminating N+1 per-channel queries in collectAllVoiceStates - MED-5: Wrap handlePatchSettings updates in a transaction for atomicity - MED-6: Add rows.Err() check after scan loop in getReactionsBatch - MED-9: Replace manual port-stripping in serverHost with net.SplitHostPort for correct IPv6 handling
This commit is contained in:
@@ -2,6 +2,7 @@ package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -141,14 +142,10 @@ func handleRestoreBackup(database *db.DB) http.Handler {
|
||||
slog.Warn("pre-restore backup failed", "err", err)
|
||||
}
|
||||
|
||||
// Copy the backup file over the live database.
|
||||
src, err := os.ReadFile(backupPath)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to read backup file")
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(dbPath, src, 0o644); err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to write database file")
|
||||
// Stream the backup file over the live database to avoid loading
|
||||
// the entire DB into memory (could be hundreds of MiB).
|
||||
if err := copyFile(backupPath, dbPath); err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to restore database file")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -162,3 +159,23 @@ func handleRestoreBackup(database *db.DB) http.Handler {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// copyFile streams src to dst without loading the entire file into memory.
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open source: %w", err)
|
||||
}
|
||||
defer in.Close() //nolint:errcheck
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create destination: %w", err)
|
||||
}
|
||||
defer out.Close() //nolint:errcheck
|
||||
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return fmt.Errorf("copy: %w", err)
|
||||
}
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
@@ -41,11 +41,30 @@ func handlePatchSettings(database *db.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
actor := actorFromContext(r)
|
||||
|
||||
// Apply all settings atomically so a mid-loop failure doesn't leave
|
||||
// partial updates.
|
||||
tx, err := database.Begin()
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to start transaction")
|
||||
return
|
||||
}
|
||||
for key, value := range updates {
|
||||
if err := database.SetSetting(key, value); err != nil {
|
||||
if _, txErr := tx.Exec(
|
||||
`INSERT INTO settings (key, value) VALUES (?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
||||
key, value,
|
||||
); txErr != nil {
|
||||
_ = tx.Rollback()
|
||||
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to update setting: "+key)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to commit settings")
|
||||
return
|
||||
}
|
||||
for key := range updates {
|
||||
slog.Info("setting changed", "actor_id", actor, "key", key)
|
||||
_ = database.LogAudit(actor, "setting_change", "setting", 0,
|
||||
fmt.Sprintf("%s updated", key))
|
||||
|
||||
@@ -109,7 +109,10 @@ func handleApplyUpdate(u *updater.Updater, hub HubBroadcaster, _ string) http.Ha
|
||||
return
|
||||
}
|
||||
|
||||
// Exit current process.
|
||||
// Exit current process. os.Exit skips deferred cleanup intentionally —
|
||||
// the process must die to release the file lock on its own binary
|
||||
// before the new process can replace it on Windows. SQLite WAL mode
|
||||
// protects DB integrity on unclean shutdown.
|
||||
slog.Info("update: new process spawned, exiting current process")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
+12
-14
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -118,21 +119,18 @@ func generateTURNCredentials(userID int64, secret string) turnCredentials {
|
||||
}
|
||||
}
|
||||
|
||||
// serverHost extracts the host for ICE server URLs from the request or falls
|
||||
// back to "localhost".
|
||||
// serverHost extracts the host (without port) for ICE server URLs from the
|
||||
// request, or falls back to "localhost". Uses net.SplitHostPort for correct
|
||||
// handling of IPv6 addresses with ports (e.g. "[::1]:8443").
|
||||
func serverHost(r *http.Request) string {
|
||||
if host := r.Host; host != "" {
|
||||
// Strip port if present.
|
||||
for i := len(host) - 1; i >= 0; i-- {
|
||||
if host[i] == ':' {
|
||||
return host[:i]
|
||||
}
|
||||
if host[i] == ']' {
|
||||
// IPv6 with no port.
|
||||
return host
|
||||
}
|
||||
}
|
||||
host := r.Host
|
||||
if host == "" {
|
||||
return "localhost"
|
||||
}
|
||||
h, _, err := net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
// No port present — return as-is.
|
||||
return host
|
||||
}
|
||||
return "localhost"
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -373,6 +373,9 @@ func (d *DB) getReactionsBatch(msgIDs []int64, requestingUserID int64) (map[int6
|
||||
ri.Me = me != 0
|
||||
result[msgID] = append(result[msgID], ri)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("getReactionsBatch rows: %w", rows.Err())
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,39 @@ func (d *DB) GetChannelVoiceStates(channelID int64) ([]VoiceState, error) {
|
||||
return states, nil
|
||||
}
|
||||
|
||||
// GetAllVoiceStates returns voice states across all voice channels in a single
|
||||
// query. Used at startup to build the ready payload without N+1 per-channel queries.
|
||||
func (d *DB) GetAllVoiceStates() ([]VoiceState, error) {
|
||||
rows, err := d.sqlDB.Query(
|
||||
`SELECT vs.user_id, vs.channel_id, u.username,
|
||||
vs.muted, vs.deafened, vs.speaking,
|
||||
vs.camera, vs.screenshare
|
||||
FROM voice_states vs
|
||||
JOIN users u ON u.id = vs.user_id
|
||||
ORDER BY vs.channel_id, vs.joined_at ASC`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAllVoiceStates: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var states []VoiceState
|
||||
for rows.Next() {
|
||||
vs, scanErr := scanVoiceStateRow(rows)
|
||||
if scanErr != nil {
|
||||
return nil, fmt.Errorf("GetAllVoiceStates scan: %w", scanErr)
|
||||
}
|
||||
states = append(states, vs)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("GetAllVoiceStates rows: %w", rows.Err())
|
||||
}
|
||||
if states == nil {
|
||||
states = []VoiceState{}
|
||||
}
|
||||
return states, nil
|
||||
}
|
||||
|
||||
// UpdateVoiceMute sets the muted field for the given user's voice state.
|
||||
// It is safe to call when the user is not in any channel (no-op).
|
||||
func (d *DB) UpdateVoiceMute(userID int64, muted bool) error {
|
||||
|
||||
+4
-17
@@ -290,21 +290,8 @@ func (h *Hub) buildReady(database *db.DB, userID int64) ([]byte, error) {
|
||||
}), nil
|
||||
}
|
||||
|
||||
// collectAllVoiceStates gathers voice states for all voice-type channels.
|
||||
func collectAllVoiceStates(database *db.DB, channels []db.Channel) ([]db.VoiceState, error) {
|
||||
var all []db.VoiceState
|
||||
for _, ch := range channels {
|
||||
if ch.Type != "voice" {
|
||||
continue
|
||||
}
|
||||
states, err := database.GetChannelVoiceStates(ch.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all = append(all, states...)
|
||||
}
|
||||
if all == nil {
|
||||
all = []db.VoiceState{}
|
||||
}
|
||||
return all, nil
|
||||
// collectAllVoiceStates gathers voice states across all channels in a single
|
||||
// query, replacing the previous N+1 per-channel pattern.
|
||||
func collectAllVoiceStates(database *db.DB, _ []db.Channel) ([]db.VoiceState, error) {
|
||||
return database.GetAllVoiceStates()
|
||||
}
|
||||
|
||||
@@ -645,6 +645,8 @@ func (h *Hub) handleSoundboard(c *Client, payload json.RawMessage) {
|
||||
return
|
||||
}
|
||||
|
||||
// channelID=0: soundboard is a server-wide permission with no per-channel
|
||||
// override. The client does not send a channel_id in the payload.
|
||||
if !h.requireChannelPerm(c, 0, permissions.UseSoundboard, "USE_SOUNDBOARD") {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user