fix: upload part tmp+rename (B4), transactional last-admin DeleteUser (B5), RowsAffected order (B13)

This commit is contained in:
2026-09-14 19:42:11 +08:00
parent 8fbc58eaaa
commit b82a891c50
4 changed files with 108 additions and 27 deletions
+10 -2
View File
@@ -198,8 +198,11 @@ func (h *H) UploadPart(c *gin.Context) {
}
lo, hi := chunkRange(m, idx)
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, hi-lo)
// B4: write to .tmp then rename — prevents truncated parts from being
// reported as "received" by UploadStatus if the process crashes mid-write.
p := filepath.Join(dir, "parts", strconv.FormatInt(idx, 10))
f, e := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
tmp := p + ".tmp"
f, e := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if e != nil {
err(c, http.StatusInternalServerError, "internal", "create part")
return
@@ -207,7 +210,7 @@ func (h *H) UploadPart(c *gin.Context) {
n, e := io.Copy(f, c.Request.Body)
f.Close()
if e != nil || n != hi-lo {
os.Remove(p)
os.Remove(tmp)
var mbe *http.MaxBytesError
code, msg := "too_large", "part size mismatch"
if errors.As(e, &mbe) {
@@ -216,6 +219,11 @@ func (h *H) UploadPart(c *gin.Context) {
err(c, http.StatusRequestEntityTooLarge, code, msg)
return
}
if e := os.Rename(tmp, p); e != nil {
os.Remove(tmp)
err(c, http.StatusInternalServerError, "internal", "rename part")
return
}
c.JSON(http.StatusAccepted, gin.H{"accepted": true})
}
+6 -15
View File
@@ -8,15 +8,13 @@ import (
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"booklib/internal/auth"
"booklib/internal/store"
)
func isUnique(e error) bool {
var pgErr *pgconn.PgError
return errors.As(e, &pgErr) && pgErr.Code == "23505"
}
// isUnique is a convenience alias for store.IsUniqueViolation.
func isUnique(e error) bool { return store.IsUniqueViolation(e) }
func (h *H) ListUsers(c *gin.Context) {
users, e := h.st.ListUsers(c)
@@ -73,23 +71,16 @@ func (h *H) DeleteUser(c *gin.Context) {
err(c, http.StatusBadRequest, "bad_request", "cannot delete yourself")
return
}
target, e := h.st.GetUserByID(c, id)
if e != nil {
// B5: transactional last-admin check eliminates TOCTOU race.
if e := h.st.DeleteUser(c, id); e != nil {
if errors.Is(e, pgx.ErrNoRows) {
err(c, http.StatusNotFound, "not_found", "no such user")
return
}
dbErr(c, e)
return
}
if target.Role == "admin" {
n, _ := h.st.CountAdmins(c) // 防删光最后一个 admin
if n <= 1 {
if errors.Is(e, store.ErrLastAdmin) {
err(c, http.StatusBadRequest, "bad_request", "cannot delete the last admin")
return
}
}
if e := h.st.DeleteUser(c, id); e != nil {
dbErr(c, e)
return
}