Skip to content

Commit 29895f0

Browse files
committed
error on out of space
1 parent ba65daf commit 29895f0

File tree

4 files changed

+434
-3
lines changed

4 files changed

+434
-3
lines changed

api/files.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,34 +114,69 @@ func apiFilesUpload(c *gin.Context) {
114114
}
115115

116116
stored := []string{}
117+
openFiles := []*os.File{}
117118

119+
// Write all files first
118120
for _, files := range c.Request.MultipartForm.File {
119121
for _, file := range files {
120122
src, err := file.Open()
121123
if err != nil {
124+
// Close any files we've opened
125+
for _, f := range openFiles {
126+
_ = f.Close()
127+
}
122128
AbortWithJSONError(c, 500, err)
123129
return
124130
}
125-
defer func() { _ = src.Close() }()
126131

127132
destPath := filepath.Join(path, filepath.Base(file.Filename))
128133
dst, err := os.Create(destPath)
129134
if err != nil {
135+
_ = src.Close()
136+
// Close any files we've opened
137+
for _, f := range openFiles {
138+
_ = f.Close()
139+
}
130140
AbortWithJSONError(c, 500, err)
131141
return
132142
}
133-
defer func() { _ = dst.Close() }()
134143

135144
_, err = io.Copy(dst, src)
145+
_ = src.Close()
136146
if err != nil {
147+
_ = dst.Close()
148+
// Close any files we've opened
149+
for _, f := range openFiles {
150+
_ = f.Close()
151+
}
137152
AbortWithJSONError(c, 500, err)
138153
return
139154
}
140155

156+
// Keep file open for batch sync
157+
openFiles = append(openFiles, dst)
141158
stored = append(stored, filepath.Join(c.Params.ByName("dir"), filepath.Base(file.Filename)))
142159
}
143160
}
144161

162+
// Sync all files at once to catch ENOSPC errors
163+
for i, dst := range openFiles {
164+
err := dst.Sync()
165+
if err != nil {
166+
// Close all files
167+
for _, f := range openFiles {
168+
_ = f.Close()
169+
}
170+
AbortWithJSONError(c, 500, fmt.Errorf("error syncing file %s: %s", stored[i], err))
171+
return
172+
}
173+
}
174+
175+
// Close all files
176+
for _, dst := range openFiles {
177+
_ = dst.Close()
178+
}
179+
145180
apiFilesUploadedCounter.WithLabelValues(c.Params.ByName("dir")).Inc()
146181
c.JSON(200, stored)
147182
}

api/files_diskfull_test.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package api
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"io"
7+
"mime/multipart"
8+
"net/http"
9+
"net/http/httptest"
10+
"os"
11+
"path/filepath"
12+
13+
"github.com/aptly-dev/aptly/aptly"
14+
ctx "github.com/aptly-dev/aptly/context"
15+
"github.com/gin-gonic/gin"
16+
"github.com/smira/flag"
17+
18+
. "gopkg.in/check.v1"
19+
)
20+
21+
type FilesUploadDiskFullSuite struct {
22+
aptlyContext *ctx.AptlyContext
23+
flags *flag.FlagSet
24+
configFile *os.File
25+
router http.Handler
26+
}
27+
28+
var _ = Suite(&FilesUploadDiskFullSuite{})
29+
30+
func (s *FilesUploadDiskFullSuite) SetUpTest(c *C) {
31+
aptly.Version = "testVersion"
32+
33+
// Create temporary config
34+
file, err := os.CreateTemp("", "aptly")
35+
c.Assert(err, IsNil)
36+
s.configFile = file
37+
38+
jsonString, err := json.Marshal(gin.H{
39+
"architectures": []string{},
40+
"rootDir": c.MkDir(),
41+
})
42+
c.Assert(err, IsNil)
43+
_, err = file.Write(jsonString)
44+
c.Assert(err, IsNil)
45+
_ = file.Close()
46+
47+
// Setup flags and context
48+
flags := flag.NewFlagSet("fakeFlags", flag.ContinueOnError)
49+
flags.Bool("no-lock", false, "dummy")
50+
flags.Int("db-open-attempts", 3, "dummy")
51+
flags.String("config", s.configFile.Name(), "dummy")
52+
flags.String("architectures", "", "dummy")
53+
s.flags = flags
54+
55+
aptlyContext, err := ctx.NewContext(s.flags)
56+
c.Assert(err, IsNil)
57+
58+
s.aptlyContext = aptlyContext
59+
s.router = Router(aptlyContext)
60+
context = aptlyContext // set global context
61+
}
62+
63+
func (s *FilesUploadDiskFullSuite) TearDownTest(c *C) {
64+
if s.configFile != nil {
65+
_ = os.Remove(s.configFile.Name())
66+
}
67+
if s.aptlyContext != nil {
68+
s.aptlyContext.Shutdown()
69+
}
70+
}
71+
72+
// TestUploadSuccessWithSync verifies that file uploads succeed when there's space
73+
// and that the Sync() call is made (by verifying the file is complete)
74+
func (s *FilesUploadDiskFullSuite) TestUploadSuccessWithSync(c *C) {
75+
// Create a test file to upload
76+
testContent := []byte("test file content for upload")
77+
78+
// Create multipart form
79+
body := &bytes.Buffer{}
80+
writer := multipart.NewWriter(body)
81+
82+
part, err := writer.CreateFormFile("file", "testfile.txt")
83+
c.Assert(err, IsNil)
84+
85+
_, err = part.Write(testContent)
86+
c.Assert(err, IsNil)
87+
88+
err = writer.Close()
89+
c.Assert(err, IsNil)
90+
91+
// Create request
92+
req, err := http.NewRequest("POST", "/api/files/testdir", body)
93+
c.Assert(err, IsNil)
94+
req.Header.Set("Content-Type", writer.FormDataContentType())
95+
96+
// Create response recorder
97+
w := httptest.NewRecorder()
98+
99+
// Call handler
100+
s.router.ServeHTTP(w, req)
101+
102+
// Check response
103+
c.Assert(w.Code, Equals, 200)
104+
105+
// Verify file was written and synced
106+
uploadedFile := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "testdir", "testfile.txt")
107+
content, err := os.ReadFile(uploadedFile)
108+
c.Assert(err, IsNil)
109+
c.Check(content, DeepEquals, testContent)
110+
}
111+
112+
// TestUploadVerifiesFileIntegrity ensures uploaded files are complete
113+
func (s *FilesUploadDiskFullSuite) TestUploadVerifiesFileIntegrity(c *C) {
114+
// Create larger test file
115+
testContent := bytes.Repeat([]byte("A"), 10000)
116+
117+
body := &bytes.Buffer{}
118+
writer := multipart.NewWriter(body)
119+
120+
part, err := writer.CreateFormFile("file", "largefile.bin")
121+
c.Assert(err, IsNil)
122+
123+
_, err = io.Copy(part, bytes.NewReader(testContent))
124+
c.Assert(err, IsNil)
125+
126+
err = writer.Close()
127+
c.Assert(err, IsNil)
128+
129+
req, err := http.NewRequest("POST", "/api/files/testdir2", body)
130+
c.Assert(err, IsNil)
131+
req.Header.Set("Content-Type", writer.FormDataContentType())
132+
133+
w := httptest.NewRecorder()
134+
s.router.ServeHTTP(w, req)
135+
136+
c.Assert(w.Code, Equals, 200)
137+
138+
// Verify complete file was written
139+
uploadedFile := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "testdir2", "largefile.bin")
140+
content, err := os.ReadFile(uploadedFile)
141+
c.Assert(err, IsNil)
142+
c.Check(len(content), Equals, len(testContent))
143+
c.Check(content, DeepEquals, testContent)
144+
}
145+
146+
// TestUploadMultipleFilesWithBatchSync tests that multiple files are synced in batch
147+
func (s *FilesUploadDiskFullSuite) TestUploadMultipleFilesWithBatchSync(c *C) {
148+
// Create multiple test files with different content
149+
testFiles := map[string][]byte{
150+
"file1.txt": []byte("content of file 1"),
151+
"file2.txt": bytes.Repeat([]byte("B"), 5000),
152+
"file3.deb": []byte("debian package content"),
153+
}
154+
155+
body := &bytes.Buffer{}
156+
writer := multipart.NewWriter(body)
157+
158+
// Add all files to multipart form
159+
for filename, content := range testFiles {
160+
part, err := writer.CreateFormFile("file", filename)
161+
c.Assert(err, IsNil)
162+
_, err = part.Write(content)
163+
c.Assert(err, IsNil)
164+
}
165+
166+
err := writer.Close()
167+
c.Assert(err, IsNil)
168+
169+
req, err := http.NewRequest("POST", "/api/files/multitest", body)
170+
c.Assert(err, IsNil)
171+
req.Header.Set("Content-Type", writer.FormDataContentType())
172+
173+
w := httptest.NewRecorder()
174+
s.router.ServeHTTP(w, req)
175+
176+
// Verify response
177+
c.Assert(w.Code, Equals, 200)
178+
179+
// Verify all files were written and synced correctly
180+
uploadDir := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "multitest")
181+
for filename, expectedContent := range testFiles {
182+
uploadedFile := filepath.Join(uploadDir, filename)
183+
content, err := os.ReadFile(uploadedFile)
184+
c.Assert(err, IsNil, Commentf("Failed to read %s", filename))
185+
c.Check(content, DeepEquals, expectedContent, Commentf("Content mismatch for %s", filename))
186+
}
187+
}

files/public.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,17 @@ func (storage *PublishedStorage) PutFile(path string, sourceFilename string) err
119119
}()
120120

121121
_, err = io.Copy(f, source)
122-
return err
122+
if err != nil {
123+
return err
124+
}
125+
126+
// Sync to ensure all data is written to disk and catch ENOSPC errors
127+
err = f.Sync()
128+
if err != nil {
129+
return fmt.Errorf("error syncing file %s: %s", path, err)
130+
}
131+
132+
return nil
123133
}
124134

125135
// Remove removes single file under public path
@@ -268,6 +278,13 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
268278
return err
269279
}
270280

281+
// Sync to ensure all data is written to disk and catch ENOSPC errors
282+
err = dst.Sync()
283+
if err != nil {
284+
_ = dst.Close()
285+
return fmt.Errorf("error syncing file %s: %s", destinationPath, err)
286+
}
287+
271288
err = dst.Close()
272289
} else if storage.linkMethod == LinkMethodSymLink {
273290
err = localSourcePool.Symlink(sourcePath, destinationPath)

0 commit comments

Comments
 (0)