feat(upload): extract upload subsystem + move sweep to scanner ticker (B16)
- internal/upload owns chunked-upload domain logic (fingerprint resume, part tmp+rename, assemble, session sweep) and the shared UniquePath helper used by both single-file and chunked completion paths - handlers/uploads.go is now HTTP-only: bind params, call upload.U, map sentinel errors to the unchanged status/code/message contract - B16: session sweep moved out of the UploadInit request path onto the scanner ticker via a Sweeper hook (upload.U satisfies it) - unit tests for the package without PG/Redis; contract pinned by the existing handler integration tests (all green)
This commit is contained in:
@@ -136,12 +136,12 @@ func (h *H) Upload(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
// B12: retry on O_EXCL collision — concurrent uploads with the same name
|
||||
// can both get the same candidate from uniquePath (stat-then-create race).
|
||||
// can both get the same candidate from UniquePath (stat-then-create race).
|
||||
var dst, tmp string
|
||||
var out *os.File
|
||||
for attempt := 0; attempt < 5; attempt++ {
|
||||
var e error
|
||||
dst, e = h.uniquePath(root, name)
|
||||
dst, e = h.up.UniquePath(root, name)
|
||||
if e != nil {
|
||||
err(c, http.StatusForbidden, "forbidden", e.Error())
|
||||
return
|
||||
@@ -190,28 +190,3 @@ func (h *H) Upload(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusAccepted, gin.H{"accepted": true, "path": strings.TrimPrefix(dst, root+string(os.PathSeparator))})
|
||||
}
|
||||
|
||||
// uniquePath 清洗后的 name 必须仍在 root 内;重名加 " (n)" 后缀
|
||||
func (h *H) uniquePath(root, name string) (string, error) {
|
||||
ext := filepath.Ext(name)
|
||||
base := strings.TrimSuffix(name, ext)
|
||||
for i := 0; ; i++ {
|
||||
cand := base + ext
|
||||
if i > 0 {
|
||||
cand = base + " (" + strconv.Itoa(i) + ")" + ext
|
||||
}
|
||||
p := filepath.Join(root, cand)
|
||||
if filepath.Clean(p) != filepath.Join(root, filepath.Clean(cand)) ||
|
||||
!strings.HasPrefix(filepath.Clean(p), root+string(os.PathSeparator)) {
|
||||
return "", os.ErrInvalid
|
||||
}
|
||||
if _, e := os.Stat(p); os.IsNotExist(e) {
|
||||
return p, nil
|
||||
} else if e != nil {
|
||||
return "", e
|
||||
}
|
||||
if i > 999 {
|
||||
return "", os.ErrExist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user