fix(uploads): retry failed files and report full storage
ci / db / db (push) Successful in 1m51s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m40s
ci / module boundaries / boundaries (push) Successful in 7s
ci / api / api (push) Successful in 33s
ci / ts / ts (packages + web) (push) Failing after 5m16s

This commit is contained in:
mrleo1nid committed 2026-10-29 00:39:03 +03:00
1 parent b39e4dc28f
commit 5e2d1e902b
10 files changed
+205 -18

No files matched your search

+5
View File
@@ -10,6 +10,7 @@ import (
"path/filepath"
"strconv"
"sync"
"syscall"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
@@ -162,6 +163,10 @@ func (t *TUS) patch(w http.ResponseWriter, r *http.Request) {
}
// Обрыв посреди куска: принятое сохранено, клиент спросит HEAD и продолжит.
t.log.WarnContext(r.Context(), "tus: write interrupted", "upload_id", f.ID, "offset", now, "err", werr)
if errors.Is(werr, syscall.ENOSPC) || errors.Is(werr, syscall.EDQUOT) {
httpx.WriteProblem(w, http.StatusInsufficientStorage, "uploads.storage_full", "upload storage is full")
return
}
httpx.WriteProblem(w, http.StatusInternalServerError, "common.internal", "write interrupted")
return
}
@@ -3,12 +3,14 @@ package uploads
import (
"bytes"
"context"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"syscall"
"testing"
"github.com/go-chi/chi/v5"
@@ -19,6 +21,45 @@ import (
"pipu/api/internal/auth"
)
type failingReader struct{ err error }
func (r failingReader) Read([]byte) (int, error) { return 0, r.err }
func TestTusInterruptedWriteCanResume(t *testing.T) {
for _, tc := range []struct {
name string
err error
status int
code string
}{
{"disk full", &os.PathError{Op: "write", Path: "/uploads/file", Err: syscall.ENOSPC}, http.StatusInsufficientStorage, "uploads.storage_full"},
{"disk quota", syscall.EDQUOT, http.StatusInsufficientStorage, "uploads.storage_full"},
{"connection interrupted", io.ErrUnexpectedEOF, http.StatusInternalServerError, "common.internal"},
} {
t.Run(tc.name, func(t *testing.T) {
e := newTusEnv(t, 10)
req := httptest.NewRequest(http.MethodPatch, TusURL(e.files.f.ID), io.MultiReader(bytes.NewBufferString("hello"), failingReader{tc.err}))
req.Header.Set("Tus-Resumable", tusVersion)
req.Header.Set("Content-Type", "application/offset+octet-stream")
req.Header.Set("Upload-Offset", "0")
req = req.WithContext(auth.WithPrincipal(req.Context(), auth.Principal{UserID: e.author, Role: auth.RoleModerator}))
rec := httptest.NewRecorder()
e.h.ServeHTTP(rec, req)
require.Equal(t, tc.status, rec.Code, rec.Body.String())
assert.Contains(t, rec.Body.String(), tc.code)
assert.NotContains(t, rec.Body.String(), "/uploads/file")
assert.EqualValues(t, 5, e.files.f.Received)
assert.Equal(t, 0, e.complete)
assert.Equal(t, "5", e.do(e.author, http.MethodHead, 0, "").Header().Get("Upload-Offset"))
require.Equal(t, http.StatusNoContent, e.do(e.author, http.MethodPatch, 5, "world").Code)
assert.Equal(t, 1, e.complete)
got, err := os.ReadFile(filepath.Join(e.dir, e.files.f.StagingPath))
require.NoError(t, err)
assert.Equal(t, "helloworld", string(got))
})
}
}
type fakeFiles struct{ f File }
func (m *fakeFiles) File(_ context.Context, id uuid.UUID) (File, error) {