-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprocessor.go
More file actions
161 lines (139 loc) · 4.11 KB
/
Copy pathprocessor.go
File metadata and controls
161 lines (139 loc) · 4.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package aperiodic
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
type DownloadedFile struct {
Year int
Month int
Day *int
Filename string
}
// parquetFilename is the on-disk name for one downloaded object.
//
// Both parts are zero-padded so a directory listing sorts chronologically. That
// is a choice about local filenames, not a mirror of the R2 key, which writes
// the month unpadded — do not "align" the two.
func parquetFilename(f FileInfo) string {
if f.Day == nil {
return fmt.Sprintf("%d-%02d.parquet", f.Year, f.Month)
}
return fmt.Sprintf("%d-%02d-%02d.parquet", f.Year, f.Month, *f.Day)
}
// resolveFilenames names every file and rejects the set if two share a name.
//
// Load-bearing, not defensive tidiness: before FileInfo carried a day, every
// daily file in a month resolved to the same name, so a month of downloads
// raced to truncate and rewrite a single path — and the CLI still exited 0
// reporting one success per file. Colliding names must fail the run rather than
// let whichever goroutine finishes last decide the contents.
func resolveFilenames(files []FileInfo) ([]string, error) {
filenames := make([]string, len(files))
seen := make(map[string]int, len(files))
for i, f := range files {
name := parquetFilename(f)
if first, duplicate := seen[name]; duplicate {
return nil, fmt.Errorf(
"refusing to download: files %d and %d both resolve to %s",
first, i, name,
)
}
seen[name] = i
filenames[i] = name
}
return filenames, nil
}
func (c *AperiodicClient) DownloadFilesConcurrently(files []FileInfo, maxConcurrent int, outputDir string) ([]DownloadedFile, error) {
if len(files) == 0 {
return nil, nil
}
filenames, err := resolveFilenames(files)
if err != nil {
return nil, err
}
if err := os.MkdirAll(outputDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create output directory: %w", err)
}
results := make([]DownloadedFile, len(files))
errs := make([]error, len(files))
var wg sync.WaitGroup
semaphore := make(chan struct{}, maxConcurrent)
for i, file := range files {
wg.Add(1)
go func(i int, f FileInfo) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
filename := filenames[i]
destPath := filepath.Join(outputDir, filename)
if err := c.downloadToFile(f.URL, destPath, 3); err != nil {
errs[i] = fmt.Errorf("failed to download %s: %w", filename, err)
return
}
results[i] = DownloadedFile{
Year: f.Year,
Month: f.Month,
Day: f.Day,
Filename: filename,
}
}(i, file)
}
wg.Wait()
for _, err := range errs {
if err != nil {
return nil, err
}
}
return results, nil
}
func (c *AperiodicClient) downloadToFile(url, destPath string, maxRetries int) error {
var lastErr error
for i := 0; i <= maxRetries; i++ {
if i > 0 {
time.Sleep(time.Duration(i) * time.Second) // Simple backoff
}
retryable, err := c.downloadOnce(url, destPath)
if err == nil {
return nil
}
if !retryable {
return err
}
lastErr = err
}
return lastErr
}
// downloadOnce makes a single attempt, closing the response body and the
// destination file before it returns. It reports whether the failure is worth
// another attempt: transport and HTTP errors are, a local filesystem error is
// not.
//
// Living outside downloadToFile's loop is the point. A `defer` in a loop body
// runs only when the whole function returns, so every attempt's handles stayed
// open for the length of the retry sequence — leaving a failed attempt free to
// flush into a path a later attempt had already truncated.
func (c *AperiodicClient) downloadOnce(url, destPath string) (retryable bool, err error) {
resp, err := c.HTTPClient.Get(url)
if err != nil {
return true, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return true, fmt.Errorf("bad status: %s", resp.Status)
}
out, err := os.Create(destPath)
if err != nil {
return false, fmt.Errorf("failed to create file: %w", err)
}
_, copyErr := io.Copy(out, resp.Body)
closeErr := out.Close()
if copyErr != nil {
return true, copyErr
}
return true, closeErr
}