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
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:
1 parent
b39e4dc28f
commit
5e2d1e902b
10 files changed
+205
-18
No files matched your search
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user