Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/code-storage-go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,24 @@ func main() {
}
```

### Inspect file metadata

```go
meta, err := repo.HeadFile(context.Background(), storage.HeadFileOptions{
Path: "README.md",
Ref: "main",
Headers: storage.FileRequestHeaders{
IfNoneMatch: `"b10b5ha"`,
Range: "bytes=0-1023",
},
})
if err != nil {
log.Fatal(err)
}

fmt.Println(meta.StatusCode, meta.ETag, meta.ContentRange)
```

### Download an archive

```go
Expand Down
13 changes: 13 additions & 0 deletions packages/code-storage-go/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func (f *apiFetcher) buildURL(path string, params url.Values) string {

type requestOptions struct {
allowedStatus map[int]bool
// extraHeaders merges request headers on top of SDK defaults; empty values are skipped.
extraHeaders map[string]string
}

func (f *apiFetcher) request(ctx context.Context, method string, path string, params url.Values, body interface{}, jwt string, opts *requestOptions) (*http.Response, error) {
Expand Down Expand Up @@ -63,6 +65,13 @@ func (f *apiFetcher) request(ctx context.Context, method string, path string, pa
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if opts != nil {
for k, v := range opts.extraHeaders {
if v != "" {
req.Header.Set(k, v)
}
}
}

resp, err := f.httpClient.Do(req)
if err != nil {
Expand Down Expand Up @@ -116,6 +125,10 @@ func (f *apiFetcher) get(ctx context.Context, path string, params url.Values, jw
return f.request(ctx, http.MethodGet, path, params, nil, jwt, opts)
}

func (f *apiFetcher) head(ctx context.Context, path string, params url.Values, jwt string, opts *requestOptions) (*http.Response, error) {
return f.request(ctx, http.MethodHead, path, params, nil, jwt, opts)
}

func (f *apiFetcher) post(ctx context.Context, path string, params url.Values, body interface{}, jwt string, opts *requestOptions) (*http.Response, error) {
return f.request(ctx, http.MethodPost, path, params, body, jwt, opts)
}
Expand Down
144 changes: 137 additions & 7 deletions packages/code-storage-go/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func (r *Repo) ImportRemoteURL(ctx context.Context, options RemoteURLOptions) (s
}

// FileStream returns the raw response for streaming file contents.
// 206, 304, 412 and 416 status codes pass through to the caller.
func (r *Repo) FileStream(ctx context.Context, options GetFileOptions) (*http.Response, error) {
if strings.TrimSpace(options.Path) == "" {
return nil, errors.New("getFileStream path is required")
Expand All @@ -104,6 +105,42 @@ func (r *Repo) FileStream(ctx context.Context, options GetFileOptions) (*http.Re
return nil, fmt.Errorf("archive stream generate jwt: %w", err)
}

params := buildGetFileParams(options)
reqOpts := buildFileRequestOptions(options.Headers)

resp, err := r.client.api.get(ctx, "repos/file", params, jwtToken, reqOpts)
if err != nil {
return nil, err
}

return resp, nil
}

// HeadFile issues HEAD /repos/file and returns parsed response metadata.
func (r *Repo) HeadFile(ctx context.Context, options HeadFileOptions) (FileMetadata, error) {
if strings.TrimSpace(options.Path) == "" {
return FileMetadata{}, errors.New("headFile path is required")
}

ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitRead}, TTL: ttl})
if err != nil {
return FileMetadata{}, fmt.Errorf("headFile generate jwt: %w", err)
}

params := buildGetFileParams(options)
reqOpts := buildFileRequestOptions(options.Headers)

resp, err := r.client.api.head(ctx, "repos/file", params, jwtToken, reqOpts)
if err != nil {
return FileMetadata{}, err
}
defer resp.Body.Close()

return parseFileMetadataHeaders(resp), nil
}

func buildGetFileParams(options GetFileOptions) url.Values {
params := url.Values{}
params.Set("path", options.Path)
if options.Ref != "" {
Expand All @@ -115,13 +152,60 @@ func (r *Repo) FileStream(ctx context.Context, options GetFileOptions) (*http.Re
if options.EphemeralBase != nil {
params.Set("ephemeral_base", strconv.FormatBool(*options.EphemeralBase))
}
return params
}

resp, err := r.client.api.get(ctx, "repos/file", params, jwtToken, nil)
if err != nil {
return nil, err
func buildFileRequestOptions(headers FileRequestHeaders) *requestOptions {
extra := map[string]string{}
if headers.Range != "" {
extra["Range"] = headers.Range
}
if headers.IfMatch != "" {
extra["If-Match"] = headers.IfMatch
}
if headers.IfNoneMatch != "" {
extra["If-None-Match"] = headers.IfNoneMatch
}
if headers.IfModifiedSince != "" {
extra["If-Modified-Since"] = headers.IfModifiedSince
}
if headers.IfUnmodifiedSince != "" {
extra["If-Unmodified-Since"] = headers.IfUnmodifiedSince
}
if headers.IfRange != "" {
extra["If-Range"] = headers.IfRange
}

return resp, nil
allowed := map[int]bool{304: true, 412: true, 416: true}
opts := &requestOptions{allowedStatus: allowed}
if len(extra) > 0 {
opts.extraHeaders = extra
}
return opts
}

func parseFileMetadataHeaders(resp *http.Response) FileMetadata {
meta := FileMetadata{
StatusCode: resp.StatusCode,
BlobSHA: resp.Header.Get("X-Blob-Sha"),
LastCommitSHA: resp.Header.Get("X-Last-Commit-Sha"),
ETag: resp.Header.Get("ETag"),
AcceptRanges: resp.Header.Get("Accept-Ranges"),
ContentRange: resp.Header.Get("Content-Range"),
ContentType: resp.Header.Get("Content-Type"),
RawLastModified: resp.Header.Get("Last-Modified"),
}
if v := resp.Header.Get("Content-Length"); v != "" {
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
meta.Size = n
}
}
if meta.RawLastModified != "" {
if t, err := http.ParseTime(meta.RawLastModified); err == nil {
meta.LastModified = t
}
}
return meta
}

// ArchiveStream returns the raw response for streaming repository archives.
Expand Down Expand Up @@ -177,6 +261,18 @@ func (r *Repo) ListFiles(ctx context.Context, options ListFilesOptions) (ListFil
if options.Ephemeral != nil {
params.Set("ephemeral", strconv.FormatBool(*options.Ephemeral))
}
if options.Path != "" {
params.Set("path", options.Path)
}
if options.Recursive != nil {
params.Set("recursive", strconv.FormatBool(*options.Recursive))
}
if options.Cursor != "" {
params.Set("cursor", options.Cursor)
}
if options.Limit > 0 {
params.Set("limit", itoa(options.Limit))
}
if len(params) == 0 {
params = nil
}
Expand All @@ -192,7 +288,23 @@ func (r *Repo) ListFiles(ctx context.Context, options ListFilesOptions) (ListFil
return ListFilesResult{}, err
}

return ListFilesResult{Paths: payload.Paths, Ref: payload.Ref}, nil
result := ListFilesResult{
Paths: payload.Paths,
Ref: payload.Ref,
NextCursor: payload.NextCursor,
HasMore: payload.HasMore,
}
if len(payload.Entries) > 0 {
result.Entries = make([]TreeEntry, 0, len(payload.Entries))
for _, entry := range payload.Entries {
result.Entries = append(result.Entries, TreeEntry{
Path: entry.Path,
Type: TreeEntryType(entry.Type),
Mode: entry.Mode,
})
}
}
return result, nil
}

// ListFilesWithMetadata lists files with mode/size and last commit metadata.
Expand All @@ -210,6 +322,18 @@ func (r *Repo) ListFilesWithMetadata(ctx context.Context, options ListFilesWithM
if options.Ephemeral != nil {
params.Set("ephemeral", strconv.FormatBool(*options.Ephemeral))
}
if options.Path != "" {
params.Set("path", options.Path)
}
if options.Recursive != nil {
params.Set("recursive", strconv.FormatBool(*options.Recursive))
}
if options.Cursor != "" {
params.Set("cursor", options.Cursor)
}
if options.Limit > 0 {
params.Set("limit", itoa(options.Limit))
}
if len(params) == 0 {
params = nil
}
Expand All @@ -226,15 +350,18 @@ func (r *Repo) ListFilesWithMetadata(ctx context.Context, options ListFilesWithM
}

result := ListFilesWithMetadataResult{
Ref: payload.Ref,
Commits: make(map[string]CommitMetadata, len(payload.Commits)),
Ref: payload.Ref,
Commits: make(map[string]CommitMetadata, len(payload.Commits)),
NextCursor: payload.NextCursor,
HasMore: payload.HasMore,
}
for _, file := range payload.Files {
result.Files = append(result.Files, FileWithMetadata{
Path: file.Path,
Mode: file.Mode,
Size: file.Size,
LastCommitSHA: file.LastCommitSHA,
Type: TreeEntryType(file.Type),
})
}
for sha, commit := range payload.Commits {
Expand Down Expand Up @@ -362,6 +489,9 @@ func (r *Repo) ListCommits(ctx context.Context, options ListCommitsOptions) (Lis
if options.Ephemeral != nil {
params.Set("ephemeral", strconv.FormatBool(*options.Ephemeral))
}
if options.Path != "" {
params.Set("path", options.Path)
}
if len(params) == 0 {
params = nil
}
Expand Down
Loading
Loading