@@ -52,6 +52,11 @@ type GitRestoreCmd struct {
5252 Commit []string `help:"Required commit SHAs that must exist on the server, regardless of which ref points at them. May be repeated."`
5353 NoBundle bool `help:"Skip applying delta bundle."`
5454 ZstdThreads int `help:"Threads for zstd decompression (0 = all CPU cores)." default:"0"`
55+ // DownloadConcurrency > 1 fetches the snapshot with that many concurrent
56+ // range requests (requires server range support; falls back to a single
57+ // request otherwise). 1 keeps the streaming single-request download.
58+ DownloadConcurrency int `help:"Concurrent range requests for the snapshot download (1 = single streaming request)." default:"1"`
59+ DownloadChunkSizeMB int `help:"Chunk size in MiB for parallel snapshot downloads." default:"8"`
5560}
5661
5762func (c * GitRestoreCmd ) Run (ctx context.Context , api * client.Client ) error {
@@ -72,56 +77,22 @@ func (c *GitRestoreCmd) Run(ctx context.Context, api *client.Client) error {
7277
7378 fmt .Fprintf (os .Stderr , "Fetching snapshot for %s\n " , c .RepoURL ) //nolint:forbidigo
7479
75- var snap * client.GitSnapshot
76- if err := inSpan (ctx , "cachew.download_snapshot" ,
77- []attribute.KeyValue {attribute .String ("cachew.repo_url" , c .RepoURL )},
78- func (ctx context.Context ) error {
79- downloadStart := time .Now ()
80- s , err := api .OpenGitSnapshot (ctx , c .RepoURL )
81- if err != nil {
82- return err //nolint:wrapcheck // wrapped by caller
83- }
84- snap = s
85- trace .SpanFromContext (ctx ).SetAttributes (
86- attribute .String ("cachew.snapshot_commit" , s .Commit ),
87- attribute .String ("cachew.bundle_url" , s .BundleURL ),
88- attribute .Float64 ("cachew.elapsed_seconds" , time .Since (downloadStart ).Seconds ()),
89- )
90- return nil
91- }); err != nil {
80+ commit , bundleURL , err := c .fetchAndExtractSnapshot (ctx , api )
81+ if err != nil {
9282 if errors .Is (err , os .ErrNotExist ) {
9383 return errors .Errorf ("no snapshot available for %s" , c .RepoURL )
9484 }
9585 span .RecordError (err )
9686 span .SetStatus (codes .Error , err .Error ())
97- return errors .Wrap (err , "fetch snapshot" )
98- }
99- defer snap .Close ()
100- span .SetAttributes (attribute .String ("cachew.snapshot_commit" , snap .Commit ))
101-
102- fmt .Fprintf (os .Stderr , "Extracting to %s...\n " , c .Directory ) //nolint:forbidigo
103- if err := inSpan (ctx , "cachew.extract" ,
104- []attribute.KeyValue {attribute .String ("cachew.directory" , c .Directory )},
105- func (ctx context.Context ) error {
106- extractStart := time .Now ()
107- if err := snapshot .Extract (ctx , snap .Body , c .Directory , c .ZstdThreads ); err != nil {
108- return err //nolint:wrapcheck // wrapped by caller
109- }
110- elapsed := time .Since (extractStart )
111- trace .SpanFromContext (ctx ).SetAttributes (attribute .Float64 ("cachew.elapsed_seconds" , elapsed .Seconds ()))
112- fmt .Fprintf (os .Stderr , "Snapshot extracted in %s\n " , elapsed ) //nolint:forbidigo
113- return nil
114- }); err != nil {
115- span .RecordError (err )
116- span .SetStatus (codes .Error , err .Error ())
117- return errors .Wrap (err , "extract snapshot" )
87+ return errors .Wrap (err , "restore snapshot" )
11888 }
89+ span .SetAttributes (attribute .String ("cachew.snapshot_commit" , commit ))
11990 fmt .Fprintf (os .Stderr , "Snapshot restored to %s\n " , c .Directory ) //nolint:forbidigo
12091
121- if snap . BundleURL != "" && ! c .NoBundle {
92+ if bundleURL != "" && ! c .NoBundle {
12293 fmt .Fprintf (os .Stderr , "Applying delta bundle...\n " ) //nolint:forbidigo
12394 bundleStart := time .Now ()
124- if err := applyBundle (ctx , api , snap . BundleURL , c .Directory ); err != nil {
95+ if err := applyBundle (ctx , api , bundleURL , c .Directory ); err != nil {
12596 fmt .Fprintf (os .Stderr , "Warning: failed to apply delta bundle: %v\n " , err ) //nolint:forbidigo
12697 span .RecordError (err )
12798 } else {
@@ -144,6 +115,112 @@ func (c *GitRestoreCmd) Run(ctx context.Context, api *client.Client) error {
144115 return nil
145116}
146117
118+ // fetchAndExtractSnapshot downloads the snapshot and extracts it into the target
119+ // directory, returning its freshen metadata (commit and bundle URL). With a
120+ // download concurrency above 1 it downloads in parallel into a temp file, since
121+ // ParallelGet needs a WriterAt; otherwise it streams the single response
122+ // directly into extraction.
123+ func (c * GitRestoreCmd ) fetchAndExtractSnapshot (ctx context.Context , api * client.Client ) (commit , bundleURL string , err error ) {
124+ if c .DownloadConcurrency > 1 {
125+ return c .parallelFetchAndExtract (ctx , api )
126+ }
127+ return c .streamFetchAndExtract (ctx , api )
128+ }
129+
130+ // streamFetchAndExtract downloads the snapshot in a single request and pipes the
131+ // response body straight into extraction, overlapping download and extraction.
132+ func (c * GitRestoreCmd ) streamFetchAndExtract (ctx context.Context , api * client.Client ) (string , string , error ) {
133+ var snap * client.GitSnapshot
134+ if err := inSpan (ctx , "cachew.download_snapshot" ,
135+ []attribute.KeyValue {attribute .String ("cachew.repo_url" , c .RepoURL )},
136+ func (ctx context.Context ) error {
137+ downloadStart := time .Now ()
138+ s , err := api .OpenGitSnapshot (ctx , c .RepoURL )
139+ if err != nil {
140+ return err //nolint:wrapcheck // wrapped by caller
141+ }
142+ snap = s
143+ trace .SpanFromContext (ctx ).SetAttributes (
144+ attribute .String ("cachew.snapshot_commit" , s .Commit ),
145+ attribute .String ("cachew.bundle_url" , s .BundleURL ),
146+ attribute .Float64 ("cachew.elapsed_seconds" , time .Since (downloadStart ).Seconds ()),
147+ )
148+ return nil
149+ }); err != nil {
150+ return "" , "" , err
151+ }
152+ defer snap .Close ()
153+
154+ if err := c .extract (ctx , snap .Body ); err != nil {
155+ return "" , "" , err
156+ }
157+ return snap .Commit , snap .BundleURL , nil
158+ }
159+
160+ // parallelFetchAndExtract downloads the snapshot into a temp file using bounded
161+ // concurrent range requests, then extracts from the file. ParallelGet writes via
162+ // WriteAt so it cannot stream into extraction; the temp file is removed on
163+ // return.
164+ func (c * GitRestoreCmd ) parallelFetchAndExtract (ctx context.Context , api * client.Client ) (string , string , error ) {
165+ tmp , err := os .CreateTemp ("" , "cachew-snapshot-*.tar.zst" )
166+ if err != nil {
167+ return "" , "" , errors .Wrap (err , "create snapshot temp file" )
168+ }
169+ defer func () {
170+ _ = tmp .Close ()
171+ _ = os .Remove (tmp .Name ()) //nolint:gosec // name is from os.CreateTemp, not external input
172+ }()
173+
174+ var meta client.GitSnapshotMetadata
175+ if err := inSpan (ctx , "cachew.download_snapshot" ,
176+ []attribute.KeyValue {
177+ attribute .String ("cachew.repo_url" , c .RepoURL ),
178+ attribute .Int ("cachew.download_concurrency" , c .DownloadConcurrency ),
179+ attribute .Int ("cachew.download_chunk_size_mb" , c .DownloadChunkSizeMB ),
180+ },
181+ func (ctx context.Context ) error {
182+ downloadStart := time .Now ()
183+ m , err := api .DownloadGitSnapshot (ctx , c .RepoURL , tmp , int64 (c .DownloadChunkSizeMB )<< 20 , c .DownloadConcurrency )
184+ if err != nil {
185+ return err //nolint:wrapcheck // wrapped by caller
186+ }
187+ meta = m
188+ trace .SpanFromContext (ctx ).SetAttributes (
189+ attribute .String ("cachew.snapshot_commit" , m .Commit ),
190+ attribute .String ("cachew.bundle_url" , m .BundleURL ),
191+ attribute .Float64 ("cachew.elapsed_seconds" , time .Since (downloadStart ).Seconds ()),
192+ )
193+ return nil
194+ }); err != nil {
195+ return "" , "" , err
196+ }
197+
198+ if _ , err := tmp .Seek (0 , io .SeekStart ); err != nil {
199+ return "" , "" , errors .Wrap (err , "rewind snapshot temp file" )
200+ }
201+ if err := c .extract (ctx , tmp ); err != nil {
202+ return "" , "" , err
203+ }
204+ return meta .Commit , meta .BundleURL , nil
205+ }
206+
207+ // extract decompresses and unpacks the snapshot body into the target directory.
208+ func (c * GitRestoreCmd ) extract (ctx context.Context , body io.Reader ) error {
209+ fmt .Fprintf (os .Stderr , "Extracting to %s...\n " , c .Directory ) //nolint:forbidigo,gosec // c.Directory is an operator-supplied CLI path
210+ return inSpan (ctx , "cachew.extract" ,
211+ []attribute.KeyValue {attribute .String ("cachew.directory" , c .Directory )},
212+ func (ctx context.Context ) error {
213+ extractStart := time .Now ()
214+ if err := snapshot .Extract (ctx , body , c .Directory , c .ZstdThreads ); err != nil {
215+ return err //nolint:wrapcheck // wrapped by caller
216+ }
217+ elapsed := time .Since (extractStart )
218+ trace .SpanFromContext (ctx ).SetAttributes (attribute .Float64 ("cachew.elapsed_seconds" , elapsed .Seconds ()))
219+ fmt .Fprintf (os .Stderr , "Snapshot extracted in %s\n " , elapsed ) //nolint:forbidigo
220+ return nil
221+ })
222+ }
223+
147224// satisfyRefs ensures the working tree contains every requested ref and
148225// commit. It short-circuits whenever the local clone already has what was
149226// asked for, avoiding both /ensure-refs and git pull when the snapshot+bundle
0 commit comments