|
1 | 1 | package api |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "bytes" |
| 4 | + "crypto/sha256" |
| 5 | + "encoding/hex" |
5 | 6 | "encoding/json" |
6 | 7 | "fmt" |
7 | 8 | "io" |
@@ -148,47 +149,74 @@ type UploadItem struct { |
148 | 149 | // stored flat under destDir using its DestName (or basename). To place a file at |
149 | 150 | // an arbitrary path, set destDir to the target directory and DestName to the |
150 | 151 | // target filename. |
| 152 | +// |
| 153 | +// The multipart body is streamed through an io.Pipe so multi-GB files are never |
| 154 | +// buffered fully in memory. |
151 | 155 | func (c *Client) UploadVolumeFiles(volumeUUID, destDir string, items []UploadItem) ([]byte, error) { |
152 | | - var buf bytes.Buffer |
153 | | - writer := multipart.NewWriter(&buf) |
154 | | - |
155 | | - if err := writer.WriteField("path", destDir); err != nil { |
156 | | - return nil, err |
157 | | - } |
158 | | - |
159 | | - for _, item := range items { |
160 | | - file, err := os.Open(item.LocalPath) |
| 156 | + pr, pw := io.Pipe() |
| 157 | + writer := multipart.NewWriter(pw) |
| 158 | + |
| 159 | + go func() { |
| 160 | + // Any error here is surfaced on the read side via CloseWithError, which |
| 161 | + // makes the HTTP request fail with the same error. |
| 162 | + err := func() error { |
| 163 | + if err := writer.WriteField("path", destDir); err != nil { |
| 164 | + return err |
| 165 | + } |
| 166 | + for _, item := range items { |
| 167 | + file, err := os.Open(item.LocalPath) |
| 168 | + if err != nil { |
| 169 | + return fmt.Errorf("failed to open %s: %w", item.LocalPath, err) |
| 170 | + } |
| 171 | + name := item.DestName |
| 172 | + if name == "" { |
| 173 | + name = filepath.Base(item.LocalPath) |
| 174 | + } |
| 175 | + part, err := writer.CreateFormFile("files[]", name) |
| 176 | + if err != nil { |
| 177 | + file.Close() |
| 178 | + return err |
| 179 | + } |
| 180 | + if _, err := io.Copy(part, file); err != nil { |
| 181 | + file.Close() |
| 182 | + return err |
| 183 | + } |
| 184 | + file.Close() |
| 185 | + // Flat upload: system-server stores each by its multipart filename. |
| 186 | + if err := writer.WriteField("relative_paths[]", ""); err != nil { |
| 187 | + return err |
| 188 | + } |
| 189 | + } |
| 190 | + return writer.Close() |
| 191 | + }() |
161 | 192 | if err != nil { |
162 | | - return nil, fmt.Errorf("failed to open %s: %w", item.LocalPath, err) |
| 193 | + pw.CloseWithError(err) |
| 194 | + return |
163 | 195 | } |
| 196 | + pw.Close() |
| 197 | + }() |
164 | 198 |
|
165 | | - name := item.DestName |
166 | | - if name == "" { |
167 | | - name = filepath.Base(item.LocalPath) |
168 | | - } |
169 | | - part, err := writer.CreateFormFile("files[]", name) |
170 | | - if err != nil { |
171 | | - file.Close() |
172 | | - return nil, err |
173 | | - } |
174 | | - if _, err := io.Copy(part, file); err != nil { |
175 | | - file.Close() |
176 | | - return nil, err |
177 | | - } |
178 | | - file.Close() |
| 199 | + return c.PostStream(c.volumeFilesPath(volumeUUID, "/upload"), writer.FormDataContentType(), pr) |
| 200 | +} |
179 | 201 |
|
180 | | - // Flat upload: keep files at the destination root (system-server stores |
181 | | - // each by its multipart filename). |
182 | | - if err := writer.WriteField("relative_paths[]", ""); err != nil { |
183 | | - return nil, err |
| 202 | +// HashVolumeFile streams a file from the volume and returns its sha256 and size |
| 203 | +// without buffering it in memory. found=false (nil error) for a missing file. |
| 204 | +func (c *Client) HashVolumeFile(volumeUUID, path string) (sum string, size int64, found bool, err error) { |
| 205 | + resp, err := c.GetStream(c.volumeFilesPath(volumeUUID, "/download") + "?path=" + url.QueryEscape(path)) |
| 206 | + if err != nil { |
| 207 | + if IsNotFound(err) { |
| 208 | + return "", 0, false, nil |
184 | 209 | } |
| 210 | + return "", 0, false, err |
185 | 211 | } |
| 212 | + defer resp.Body.Close() |
186 | 213 |
|
187 | | - if err := writer.Close(); err != nil { |
188 | | - return nil, err |
| 214 | + h := sha256.New() |
| 215 | + n, err := io.Copy(h, resp.Body) |
| 216 | + if err != nil { |
| 217 | + return "", 0, false, err |
189 | 218 | } |
190 | | - |
191 | | - return c.doRaw("POST", c.volumeFilesPath(volumeUUID, "/upload"), writer.FormDataContentType(), &buf) |
| 219 | + return hex.EncodeToString(h.Sum(nil)), n, true, nil |
192 | 220 | } |
193 | 221 |
|
194 | 222 | // UploadLocalFiles is a convenience wrapper for uploading files by path using |
|
0 commit comments