-
Notifications
You must be signed in to change notification settings - Fork 1
Pr/inline artifacts #1862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pr/inline artifacts #1862
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| package artifact | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "crypto/sha256" | ||
| "encoding/hex" | ||
| "fmt" | ||
| "io" | ||
|
|
||
| "github.com/flanksource/duty/models" | ||
| "github.com/google/uuid" | ||
| "gorm.io/gorm" | ||
| ) | ||
|
|
||
| type BlobStore interface { | ||
| Write(data Data, artifact *models.Artifact) (*models.Artifact, error) | ||
| Read(artifactID uuid.UUID) (*Data, error) | ||
| io.Closer | ||
| } | ||
|
|
||
| type blobStore struct { | ||
| fs FilesystemRW | ||
| db *gorm.DB | ||
| backend string | ||
| } | ||
|
|
||
| func NewBlobStore(fs FilesystemRW, db *gorm.DB, backend string) BlobStore { | ||
| return &blobStore{fs: fs, db: db, backend: backend} | ||
| } | ||
|
|
||
| func (s *blobStore) Write(data Data, a *models.Artifact) (*models.Artifact, error) { | ||
| if a == nil { | ||
| a = &models.Artifact{} | ||
| } | ||
| if data.Content == nil { | ||
| return nil, fmt.Errorf("artifact data content is nil") | ||
| } | ||
| defer func() { _ = data.Content.Close() }() | ||
|
|
||
| checksum := sha256.New() | ||
| mimeReader := io.TeeReader(data.Content, checksum) | ||
|
|
||
| mw := &mimeWriter{Max: maxBytesForMimeDetection} | ||
| fileReader := io.TeeReader(mimeReader, mw) | ||
|
|
||
| info, err := s.fs.Write(s.db.Statement.Context, data.Filename, fileReader) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check how blobStore is instantiated and whether db already has a statement context set
rg -n -A5 "NewBlobStore\(" --type=goRepository: flanksource/duty Length of output: 2010 🏁 Script executed: cat -n artifact/blob_store.goRepository: flanksource/duty Length of output: 3442 🏁 Script executed: fd -t f "\.go$" | xargs rg -l "gorm.Open|\.DB\(\)" | head -5Repository: flanksource/duty Length of output: 143 🏁 Script executed: rg -n "func.*\(\).*\*gorm\.DB" --type=go | head -20Repository: flanksource/duty Length of output: 235 🏁 Script executed: sed -n '400,430p' context/context.goRepository: flanksource/duty Length of output: 615 🏁 Script executed: rg "db\.Statement\.Context" --type=go -C2Repository: flanksource/duty Length of output: 661 🏁 Script executed: rg "Statement\." --type=go | head -20Repository: flanksource/duty Length of output: 1155 🏁 Script executed: rg "type FilesystemRW" -A10 --type=goRepository: flanksource/duty Length of output: 434 🏁 Script executed: web_search
How to get context from gorm.DB instance without executing queryRepository: flanksource/duty Length of output: 152 🌐 Web query:
💡 Result: Yes, when you create a fresh *gorm.DB instance using gorm.Open, the db.Statement.Context field is nil by default. This is the expected behavior for a newly initialized DB connection, as no context has been explicitly set yet. To use a context with database operations, you must explicitly set it using db.WithContext(ctx) or db.Session(&gorm.Session{Context: ctx}). This creates a new DB session with the context propagated to its Statement. In hooks/callbacks (BeforeCreate, etc.), you can access the context via tx.Statement.Context, but only after a context has been set on the session. This design supports goroutine safety and method chaining - fresh DB instances are reusable until a chain/finisher method initializes the Statement, at which point you need Session/WithContext to create safe new instances. Citations:
🏁 Script executed: rg "\.WithContext\(" --type=go -B2 -A2 | head -40Repository: flanksource/duty Length of output: 1758 🏁 Script executed: rg "db\.(WithContext|Statement)" --type=go -B3 -A3 | grep -A3 -B3 "func"Repository: flanksource/duty Length of output: 42 🏁 Script executed: rg "func.*\(s \*\w+Store\)" --type=go -A15 | grep -E "(func|\.db\.|\.DB)" | head -30Repository: flanksource/duty Length of output: 1990 🏁 Script executed: rg "Statement\.Context" --type=go -B5 | grep -E "(Before|After|callback)" -A3 -B3 | head -20Repository: flanksource/duty Length of output: 42 🏁 Script executed: rg "v\.WithContext" --type=go -B5 -A10Repository: flanksource/duty Length of output: 679 🏁 Script executed: # Check if there's any test coverage that shows the actual behavior
rg "blobStore\)" --type=go -A10 | grep -E "(Write|Read)" -B5 -A5 | head -50Repository: flanksource/duty Length of output: 1077 🏁 Script executed: # Check the test files to see if blobStore tests show the actual runtime behavior
cat tests/artifacts_test.go | grep -A30 "NewBlobStore"Repository: flanksource/duty Length of output: 2023 🏁 Script executed: # Check if there are any recent issues or PRs related to this
git log --oneline --all artifact/blob_store.go | head -10Repository: flanksource/duty Length of output: 150 Unsafe access to
Accept Also applies to: 91-91 🤖 Prompt for AI Agents |
||
| if err != nil { | ||
| return nil, fmt.Errorf("writing artifact %s: %w", data.Filename, err) | ||
| } | ||
|
|
||
| if data.ContentType == "" { | ||
| data.ContentType = mw.Detect().String() | ||
| } | ||
|
|
||
| // For inline store, the artifact already has content set | ||
| if inlineArt := InlineArtifact(info); inlineArt != nil { | ||
| a.Content = inlineArt.Content | ||
| a.CompressionType = inlineArt.CompressionType | ||
| } | ||
|
|
||
| a.Path = data.Filename | ||
| a.Filename = info.Name() | ||
| a.Size = info.Size() | ||
| a.ContentType = data.ContentType | ||
| a.Checksum = hex.EncodeToString(checksum.Sum(nil)) | ||
|
|
||
| if err := s.db.Create(a).Error; err != nil { | ||
| return nil, fmt.Errorf("saving artifact to db: %w", err) | ||
| } | ||
|
|
||
| return a, nil | ||
| } | ||
|
|
||
| func (s *blobStore) Read(artifactID uuid.UUID) (*Data, error) { | ||
| var a models.Artifact | ||
| if err := s.db.Where("id = ?", artifactID).First(&a).Error; err != nil { | ||
| return nil, fmt.Errorf("finding artifact %s: %w", artifactID, err) | ||
| } | ||
|
|
||
| if a.IsInline() { | ||
| content, err := a.GetContent() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("decompressing inline artifact %s: %w", artifactID, err) | ||
| } | ||
| return &Data{ | ||
| Content: io.NopCloser(bytes.NewReader(content)), | ||
| ContentLength: a.Size, | ||
| Checksum: a.Checksum, | ||
| ContentType: a.ContentType, | ||
| Filename: a.Filename, | ||
| }, nil | ||
| } | ||
|
|
||
| r, err := s.fs.Read(s.db.Statement.Context, a.Path) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("reading artifact %s from %s: %w", artifactID, a.Path, err) | ||
| } | ||
|
|
||
| return &Data{ | ||
| Content: r, | ||
| ContentLength: a.Size, | ||
| Checksum: a.Checksum, | ||
| ContentType: a.ContentType, | ||
| Filename: a.Filename, | ||
| }, nil | ||
| } | ||
|
|
||
| func (s *blobStore) Close() error { | ||
| return s.fs.Close() | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| package aws |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,48 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| //go:build !fast | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package aws | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "io/fs" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/aws/aws-sdk-go-v2/service/s3/types" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/flanksource/commons/utils" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/samber/lo" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type S3FileInfo struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object types.Object | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (obj S3FileInfo) Name() string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if obj.Object.Key == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return *obj.Object.Key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (obj S3FileInfo) Size() int64 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return utils.Deref(obj.Object.Size) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (obj S3FileInfo) Mode() fs.FileMode { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fs.FileMode(0644) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (obj S3FileInfo) ModTime() time.Time { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return lo.FromPtr(obj.Object.LastModified) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (obj S3FileInfo) FullPath() string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return *obj.Object.Key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+38
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing nil check in
Suggested fix func (obj S3FileInfo) FullPath() string {
+ if obj.Object.Key == nil {
+ return ""
+ }
return *obj.Object.Key
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (obj S3FileInfo) IsDir() bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return strings.HasSuffix(obj.Name(), "/") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: wc -l artifact/clients/aws/fileinfo.goRepository: flanksource/duty Length of output: 95 🏁 Script executed: cat -n artifact/clients/aws/fileinfo.goRepository: flanksource/duty Length of output: 1128 🏁 Script executed: # Search for any imports and interface implementations in the file
head -30 artifact/clients/aws/fileinfo.goRepository: flanksource/duty Length of output: 507 🏁 Script executed: # Check if S3FileInfo implements fs.FileInfo interface
rg -A 2 "type S3FileInfo" artifact/clients/aws/fileinfo.goRepository: flanksource/duty Length of output: 107 🏁 Script executed: # Check Name() method implementation
rg -A 3 "func \(obj S3FileInfo\) Name\(\)" artifact/clients/aws/fileinfo.goRepository: flanksource/duty Length of output: 124 🏁 Script executed: # Search for any usage of S3FileInfo Mode() or IsDir()
rg "Mode\(\)|IsDir\(\)" artifact/clients/aws/ -B 2 -A 2Repository: flanksource/duty Length of output: 551 🏁 Script executed: # Search for usage of S3FileInfo in the codebase
rg "S3FileInfo" --type go -B 2 -A 2Repository: flanksource/duty Length of output: 2109 🏁 Script executed: # Search for patterns checking Mode().IsDir() in the codebase
rg "Mode\(\)\.IsDir|\.IsDir\(\)" --type go -B 2 -A 2 | head -50Repository: flanksource/duty Length of output: 625 🏁 Script executed: # Check if there are any tests for S3FileInfo
fd -e test.go artifact/clients/awsRepository: flanksource/duty Length of output: 445 🏁 Script executed: # Search for tests in artifact/clients/aws directory
fd . artifact/clients/aws --type fRepository: flanksource/duty Length of output: 120 🏁 Script executed: # Check artifact/fs/s3.go to see how S3FileInfo is used
sed -n '1,50p' artifact/fs/s3.goRepository: flanksource/duty Length of output: 1049 🏁 Script executed: # Look for more context on how S3FileInfo is used
rg -B 5 -A 10 "S3FileInfo{" artifact/fs/s3.goRepository: flanksource/duty Length of output: 145 🏁 Script executed: # Check if Mode() is used directly with S3FileInfo anywhere
rg "fileInfo.*Mode|\.Mode\(\)" artifact/ --type go -B 2 -A 2Repository: flanksource/duty Length of output: 42 🏁 Script executed: # Search for S3FileInfo usage with proper escaping
rg "S3FileInfo\{" artifact/ --type go -B 5 -A 5Repository: flanksource/duty Length of output: 915 🏁 Script executed: # Look at the full s3.go to understand usage context
wc -l artifact/fs/s3.goRepository: flanksource/duty Length of output: 81 🏁 Script executed: # Get more of s3.go to see how S3FileInfo is used
sed -n '40,120p' artifact/fs/s3.goRepository: flanksource/duty Length of output: 1848 🏁 Script executed: # Check if there's any code that relies on Mode().IsDir() matching IsDir()
rg "Mode\(\).*IsDir|IsDir.*Mode\(\)" --type goRepository: flanksource/duty Length of output: 42 🏁 Script executed: # Check the artifact.FileInfo type to confirm it extends fs.FileInfo
rg "interface.*FileInfo|type.*FileInfo" artifact/clients/aws/ artifact/fs/ --type go -B 2 -A 5Repository: flanksource/duty Length of output: 1229 🏁 Script executed: # Look at artifact package to understand FileInfo interface
fd . artifact/ -name "*.go" -type f | head -20Repository: flanksource/duty Length of output: 290 🏁 Script executed: # Search for artifact.FileInfo definition
rg "type.*FileInfo|interface.*FileInfo" artifact/ --type go -B 2 -A 5 | head -40Repository: flanksource/duty Length of output: 1223 🏁 Script executed: # Check if IsDir() or Mode() are actually used on S3FileInfo in the codebase
rg "fileInfo.*IsDir|fileInfo.*Mode" artifact/ --type go -B 3 -A 3Repository: flanksource/duty Length of output: 42 🏁 Script executed: # Look at how ReadDir output is used
rg "ReadDir" artifact/ --type go -B 2 -A 5 | head -60Repository: flanksource/duty Length of output: 2556
For keys ending in Suggested fix func (obj S3FileInfo) Mode() fs.FileMode {
- return fs.FileMode(0644)
+ if obj.IsDir() {
+ return fs.ModeDir | 0o755
+ }
+ return 0o644
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (obj S3FileInfo) Sys() interface{} { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return obj.Object | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package azure | ||
|
|
||
| import ( | ||
| "io/fs" | ||
| "time" | ||
| ) | ||
|
|
||
| type BlobFileInfo struct { | ||
| BlobName string | ||
| BlobSize int64 | ||
| LastMod time.Time | ||
| ContentType string | ||
| } | ||
|
|
||
| func (f BlobFileInfo) Name() string { return f.BlobName } | ||
| func (f BlobFileInfo) Size() int64 { return f.BlobSize } | ||
| func (f BlobFileInfo) Mode() fs.FileMode { return 0644 } | ||
| func (f BlobFileInfo) ModTime() time.Time { return f.LastMod } | ||
| func (f BlobFileInfo) IsDir() bool { return false } | ||
| func (f BlobFileInfo) Sys() any { return nil } | ||
| func (f BlobFileInfo) FullPath() string { return f.BlobName } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package gcp | ||
|
|
||
| import ( | ||
| "io/fs" | ||
| "time" | ||
|
|
||
| gcs "cloud.google.com/go/storage" | ||
| ) | ||
|
|
||
| type GCSFileInfo struct { | ||
| Object *gcs.ObjectAttrs | ||
| } | ||
|
|
||
| func (GCSFileInfo) IsDir() bool { | ||
| return false | ||
| } | ||
|
|
||
| func (obj GCSFileInfo) ModTime() time.Time { | ||
| return obj.Object.Updated | ||
| } | ||
|
|
||
| func (obj GCSFileInfo) Mode() fs.FileMode { | ||
| return fs.FileMode(0644) | ||
| } | ||
|
|
||
| func (obj GCSFileInfo) Name() string { | ||
| return obj.Object.Name | ||
| } | ||
|
|
||
| func (obj GCSFileInfo) Size() int64 { | ||
| return obj.Object.Size | ||
| } | ||
|
|
||
| func (obj GCSFileInfo) Sys() interface{} { | ||
| return obj.Object | ||
| } | ||
|
|
||
| func (obj GCSFileInfo) FullPath() string { | ||
| return obj.Object.Name | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package sftp | ||
|
|
||
| import ( | ||
| "time" | ||
|
|
||
| "github.com/pkg/sftp" | ||
| "golang.org/x/crypto/ssh" | ||
| ) | ||
|
|
||
| // SSHConnect creates an SFTP client connection. | ||
| // NOTE: Uses InsecureIgnoreHostKey because artifact storage targets are | ||
| // configured by admins via trusted connection objects, not user input. | ||
| func SSHConnect(host, user, password string) (*sftp.Client, error) { | ||
| config := &ssh.ClientConfig{ | ||
| User: user, | ||
| Auth: []ssh.AuthMethod{ | ||
| ssh.Password(password), | ||
| }, | ||
| HostKeyCallback: ssh.InsecureIgnoreHostKey(), //nolint:gosec | ||
Check failureCode scanning / CodeQL Use of insecure HostKeyCallback implementation High
Configuring SSH ClientConfig with insecure HostKeyCallback implementation from
this source Error loading related location Loading |
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
moshloop marked this conversation as resolved.
Dismissed
|
||
| Timeout: 30 * time.Second, | ||
| } | ||
|
Comment on lines
+14
to
+21
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check if the file exists and read the relevant lines
if [ -f "artifact/clients/sftp/sftp.go" ]; then
echo "=== Lines 1-30 of artifact/clients/sftp/sftp.go ==="
head -30 artifact/clients/sftp/sftp.go | cat -n
else
echo "File not found, searching for sftp.go files..."
find . -name "sftp.go" -type f
fiRepository: flanksource/duty Length of output: 787 🏁 Script executed: # Also check total lines in the file to understand context
wc -l artifact/clients/sftp/sftp.go 2>/dev/null || echo "File not accessible"Repository: flanksource/duty Length of output: 92 🏁 Script executed: # Search for any host key validation or certificate pinning elsewhere in the codebase
rg "HostKeyCallback|known_hosts|InsecureIgnoreHostKey" --type go -B 2 -A 2Repository: flanksource/duty Length of output: 547 Do not disable SSH host key verification.
🧰 Tools🪛 GitHub Check: CodeQL[failure] 14-14: Use of insecure HostKeyCallback implementation 🤖 Prompt for AI Agents |
||
|
|
||
| conn, err := ssh.Dial("tcp", host, config) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| client, err := sftp.NewClient(conn) | ||
| if err != nil { | ||
| conn.Close() | ||
| return nil, err | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| return client, nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.