Skip to content

Commit 24badc8

Browse files
authored
Clean up partially-downloaded ZIP files if aborted (#388)
* Clean up partially-downloaded ZIP files if aborted This fixes a bug where src-cli would leave partially-downloaded ZIP files behind when the user hit Ctrl-C while a download was ongoing. The `fetchRepositoryArchive` would then run into a context-cancelation error when doing the request or the `io.Copy` and not clean up the file. The code here checks whether the cause of the error is a cancelation and, if so, cleans up the zip file. * Add changelog entry * Clean up partial files on any error
1 parent 62c59a5 commit 24badc8

4 files changed

Lines changed: 209 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ All notable changes to `src-cli` are documented in this file.
1717

1818
### Fixed
1919

20+
- If `src campaign [validate|apply|preview]` was aborted while it was downloading repository archives it could leave behind partial ZIP files that would produce an error on the next run. This is now fixed by deleting partial files on abort. [#388](https://github.com/sourcegraph/src-cli/pull/388)
21+
2022
### Removed
2123

2224
## 3.22.1

internal/campaigns/archive_fetcher.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ func (wc *WorkspaceCreator) Create(ctx context.Context, repo *graphql.Repository
3434
}
3535

3636
if !exists {
37-
if err := fetchRepositoryArchive(ctx, wc.client, repo, path); err != nil {
37+
err = fetchRepositoryArchive(ctx, wc.client, repo, path)
38+
if err != nil {
39+
// If the context got cancelled, or we ran out of disk space, or
40+
// ... while we were downloading the file, we remove the partially
41+
// downloaded file.
42+
os.Remove(path)
43+
3844
return "", errors.Wrap(err, "fetching ZIP archive")
3945
}
4046
}

internal/campaigns/archive_fetcher_test.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,163 @@
11
package campaigns
22

33
import (
4+
"bytes"
5+
"context"
46
"io/ioutil"
7+
"net/http"
8+
"net/http/httptest"
59
"os"
610
"path/filepath"
711
"testing"
12+
"time"
13+
14+
"github.com/google/go-cmp/cmp"
15+
"github.com/sourcegraph/src-cli/internal/api"
16+
"github.com/sourcegraph/src-cli/internal/campaigns/graphql"
817
)
918

19+
func TestWorkspaceCreator_Create(t *testing.T) {
20+
workspaceTmpDir := func(t *testing.T) string {
21+
testTempDir, err := ioutil.TempDir("", "executor-integration-test-*")
22+
if err != nil {
23+
t.Fatal(err)
24+
}
25+
t.Cleanup(func() { os.Remove(testTempDir) })
26+
27+
return testTempDir
28+
}
29+
30+
repo := &graphql.Repository{
31+
ID: "src-cli",
32+
Name: "github.com/sourcegraph/src-cli",
33+
DefaultBranch: &graphql.Branch{Name: "main", Target: struct{ OID string }{OID: "d34db33f"}},
34+
}
35+
36+
archive := mockRepoArchive{
37+
repo: repo,
38+
files: map[string]string{
39+
"README.md": "# Welcome to the README\n",
40+
},
41+
}
42+
43+
t.Run("success", func(t *testing.T) {
44+
requestsReceived := 0
45+
callback := func(_ http.ResponseWriter, _ *http.Request) {
46+
requestsReceived += 1
47+
}
48+
49+
ts := httptest.NewServer(newZipArchivesMux(t, callback, archive))
50+
defer ts.Close()
51+
52+
var clientBuffer bytes.Buffer
53+
client := api.NewClient(api.ClientOpts{Endpoint: ts.URL, Out: &clientBuffer})
54+
55+
testTempDir := workspaceTmpDir(t)
56+
57+
creator := &WorkspaceCreator{dir: testTempDir, client: client}
58+
workspace, err := creator.Create(context.Background(), repo)
59+
if err != nil {
60+
t.Fatalf("unexpected error: %s", err)
61+
}
62+
63+
wantZipFile := "github.com-sourcegraph-src-cli-d34db33f.zip"
64+
ok, err := dirContains(creator.dir, wantZipFile)
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
if !ok {
69+
t.Fatalf("temp dir doesnt contain zip file")
70+
}
71+
72+
haveUnzippedFiles, err := readWorkspaceFiles(workspace)
73+
if err != nil {
74+
t.Fatalf("error walking workspace: %s", err)
75+
}
76+
77+
if !cmp.Equal(archive.files, haveUnzippedFiles) {
78+
t.Fatalf("wrong files in workspace:\n%s", cmp.Diff(archive.files, haveUnzippedFiles))
79+
}
80+
81+
// Create it a second time and make sure that the server wasn't called
82+
_, err = creator.Create(context.Background(), repo)
83+
if err != nil {
84+
t.Fatalf("unexpected error: %s", err)
85+
}
86+
87+
if requestsReceived != 1 {
88+
t.Fatalf("wrong number of requests received: %d", requestsReceived)
89+
}
90+
91+
// Third time, but this time with cleanup, _after_ unzipping
92+
creator.deleteZips = true
93+
_, err = creator.Create(context.Background(), repo)
94+
if err != nil {
95+
t.Fatalf("unexpected error: %s", err)
96+
}
97+
98+
if requestsReceived != 1 {
99+
t.Fatalf("wrong number of requests received: %d", requestsReceived)
100+
}
101+
102+
ok, err = dirContains(creator.dir, wantZipFile)
103+
if err != nil {
104+
t.Fatal(err)
105+
}
106+
if ok {
107+
t.Fatalf("temp dir contains zip file but should not")
108+
}
109+
})
110+
111+
t.Run("canceled", func(t *testing.T) {
112+
// We create a context that is canceled after the server sent down the
113+
// first file to simulate a slow download that's aborted by the user hitting Ctrl-C.
114+
115+
firstFileWritten := make(chan struct{})
116+
callback := func(w http.ResponseWriter, r *http.Request) {
117+
// We flush the headers and the first file
118+
w.(http.Flusher).Flush()
119+
120+
// Wait a bit for the client to start writing the file
121+
time.Sleep(50 * time.Millisecond)
122+
123+
// Cancel the context to simulate the Ctrl-C
124+
firstFileWritten <- struct{}{}
125+
126+
<-r.Context().Done()
127+
}
128+
129+
ctx, cancel := context.WithCancel(context.Background())
130+
go func() {
131+
<-firstFileWritten
132+
cancel()
133+
}()
134+
135+
ts := httptest.NewServer(newZipArchivesMux(t, callback, archive))
136+
defer ts.Close()
137+
138+
var clientBuffer bytes.Buffer
139+
client := api.NewClient(api.ClientOpts{Endpoint: ts.URL, Out: &clientBuffer})
140+
141+
testTempDir := workspaceTmpDir(t)
142+
143+
creator := &WorkspaceCreator{dir: testTempDir, client: client}
144+
145+
_, err := creator.Create(ctx, repo)
146+
if err == nil {
147+
t.Fatalf("error is nil")
148+
}
149+
150+
zipFile := "github.com-sourcegraph-src-cli-d34db33f.zip"
151+
ok, err := dirContains(creator.dir, zipFile)
152+
if err != nil {
153+
t.Fatal(err)
154+
}
155+
if ok {
156+
t.Fatalf("zip file in temp dir was not cleaned up")
157+
}
158+
})
159+
}
160+
10161
func TestMkdirAll(t *testing.T) {
11162
// TestEnsureAll does most of the heavy lifting here; we're just testing the
12163
// MkdirAll scenarios here around whether the directory exists.
@@ -139,3 +290,46 @@ func isDir(t *testing.T, path string) bool {
139290

140291
return st.IsDir()
141292
}
293+
294+
func readWorkspaceFiles(workspace string) (map[string]string, error) {
295+
files := map[string]string{}
296+
297+
err := filepath.Walk(workspace, func(path string, info os.FileInfo, err error) error {
298+
if err != nil {
299+
return err
300+
}
301+
if info.IsDir() {
302+
return nil
303+
}
304+
305+
content, err := ioutil.ReadFile(path)
306+
if err != nil {
307+
return err
308+
}
309+
310+
rel, err := filepath.Rel(workspace, path)
311+
if err != nil {
312+
return err
313+
}
314+
315+
files[rel] = string(content)
316+
return nil
317+
})
318+
319+
return files, err
320+
}
321+
322+
func dirContains(dir, filename string) (bool, error) {
323+
files, err := ioutil.ReadDir(dir)
324+
if err != nil {
325+
return false, err
326+
}
327+
328+
for _, f := range files {
329+
if f.Name() == filename {
330+
return true, nil
331+
}
332+
}
333+
334+
return false, nil
335+
}

internal/campaigns/executor_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func TestExecutor_Integration(t *testing.T) {
120120

121121
for _, tc := range tests {
122122
t.Run(tc.name, func(t *testing.T) {
123-
ts := httptest.NewServer(newZipArchivesMux(t, tc.archives))
123+
ts := httptest.NewServer(newZipArchivesMux(t, nil, tc.archives...))
124124
defer ts.Close()
125125

126126
var clientBuffer bytes.Buffer
@@ -215,7 +215,7 @@ func addToPath(t *testing.T, relPath string) {
215215
os.Setenv("PATH", fmt.Sprintf("%s%c%s", dummyDockerPath, os.PathListSeparator, os.Getenv("PATH")))
216216
}
217217

218-
func newZipArchivesMux(t *testing.T, archives []mockRepoArchive) *http.ServeMux {
218+
func newZipArchivesMux(t *testing.T, callback http.HandlerFunc, archives ...mockRepoArchive) *http.ServeMux {
219219
mux := http.NewServeMux()
220220

221221
for _, archive := range archives {
@@ -241,6 +241,10 @@ func newZipArchivesMux(t *testing.T, archives []mockRepoArchive) *http.ServeMux
241241
if _, err := f.Write([]byte(body)); err != nil {
242242
t.Errorf("failed to write body for %s to zip: %s", name, err)
243243
}
244+
245+
if callback != nil {
246+
callback(w, r)
247+
}
244248
}
245249
if err := zipWriter.Close(); err != nil {
246250
t.Fatalf("closing zipWriter failed: %s", err)

0 commit comments

Comments
 (0)