Skip to content

Commit fd6ab3f

Browse files
committed
feat(artifact): add support for artifact storage with inline and external backends
Introduces a pluggable artifact storage system supporting multiple backends (S3, GCS, SMB, SFTP, local filesystem) and inline database storage. Includes compression support (gzip), filesystem abstraction interfaces, and database schema for storing artifacts either inline or in external cloud/object storage systems. External connections can be configured via connection URL properties.
1 parent 37440db commit fd6ab3f

22 files changed

Lines changed: 1479 additions & 286 deletions

File tree

artifact/clients/aws/doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package aws

artifact/clients/aws/fileinfo.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//go:build !fast
2+
3+
package aws
4+
5+
import (
6+
"io/fs"
7+
"strings"
8+
"time"
9+
10+
"github.com/aws/aws-sdk-go-v2/service/s3/types"
11+
"github.com/flanksource/commons/utils"
12+
"github.com/samber/lo"
13+
)
14+
15+
type S3FileInfo struct {
16+
Object types.Object
17+
}
18+
19+
func (obj S3FileInfo) Name() string {
20+
return *obj.Object.Key
21+
}
22+
23+
func (obj S3FileInfo) Size() int64 {
24+
return utils.Deref(obj.Object.Size)
25+
}
26+
27+
func (obj S3FileInfo) Mode() fs.FileMode {
28+
return fs.FileMode(0644)
29+
}
30+
31+
func (obj S3FileInfo) ModTime() time.Time {
32+
return lo.FromPtr(obj.Object.LastModified)
33+
}
34+
35+
func (obj S3FileInfo) FullPath() string {
36+
return *obj.Object.Key
37+
}
38+
39+
func (obj S3FileInfo) IsDir() bool {
40+
return strings.HasSuffix(obj.Name(), "/")
41+
}
42+
43+
func (obj S3FileInfo) Sys() interface{} {
44+
return obj.Object
45+
}

artifact/clients/gcp/fileinfo.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package gcp
2+
3+
import (
4+
"io/fs"
5+
"time"
6+
7+
gcs "cloud.google.com/go/storage"
8+
)
9+
10+
type GCSFileInfo struct {
11+
Object *gcs.ObjectAttrs
12+
}
13+
14+
func (GCSFileInfo) IsDir() bool {
15+
return false
16+
}
17+
18+
func (obj GCSFileInfo) ModTime() time.Time {
19+
return obj.Object.Updated
20+
}
21+
22+
func (obj GCSFileInfo) Mode() fs.FileMode {
23+
return fs.FileMode(0644)
24+
}
25+
26+
func (obj GCSFileInfo) Name() string {
27+
return obj.Object.Name
28+
}
29+
30+
func (obj GCSFileInfo) Size() int64 {
31+
return obj.Object.Size
32+
}
33+
34+
func (obj GCSFileInfo) Sys() interface{} {
35+
return obj.Object
36+
}
37+
38+
func (obj GCSFileInfo) FullPath() string {
39+
return obj.Object.Name
40+
}

artifact/clients/sftp/sftp.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package sftp
2+
3+
import (
4+
"github.com/pkg/sftp"
5+
"golang.org/x/crypto/ssh"
6+
)
7+
8+
func SSHConnect(host, user, password string) (*sftp.Client, error) {
9+
config := &ssh.ClientConfig{
10+
User: user,
11+
Auth: []ssh.AuthMethod{
12+
ssh.Password(password),
13+
},
14+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
15+
}
16+
17+
conn, err := ssh.Dial("tcp", host, config)
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
client, err := sftp.NewClient(conn)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
return client, nil
28+
}

artifact/clients/smb/smb.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package smb
2+
3+
import (
4+
"net"
5+
6+
"github.com/flanksource/duty/types"
7+
"github.com/hirochachacha/go-smb2"
8+
)
9+
10+
type SMBSession struct {
11+
net.Conn
12+
*smb2.Session
13+
*smb2.Share
14+
}
15+
16+
func SMBConnect(server string, port, share string, auth types.Authentication) (*SMBSession, error) {
17+
var err error
18+
var smb *SMBSession
19+
server = server + ":" + port
20+
conn, err := net.Dial("tcp", server)
21+
if err != nil {
22+
return nil, err
23+
}
24+
smb = &SMBSession{
25+
Conn: conn,
26+
}
27+
28+
d := &smb2.Dialer{
29+
Initiator: &smb2.NTLMInitiator{
30+
User: auth.GetUsername(),
31+
Password: auth.GetPassword(),
32+
Domain: auth.GetDomain(),
33+
},
34+
}
35+
36+
s, err := d.Dial(conn)
37+
if err != nil {
38+
return nil, err
39+
}
40+
smb.Session = s
41+
fs, err := s.Mount(share)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
smb.Share = fs
47+
48+
return smb, err
49+
}
50+
51+
func (s *SMBSession) Close() error {
52+
if s.Conn != nil {
53+
_ = s.Conn.Close()
54+
}
55+
if s.Session != nil {
56+
_ = s.Logoff()
57+
}
58+
if s.Share != nil {
59+
_ = s.Umount()
60+
}
61+
62+
return nil
63+
}

artifact/fs.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package artifact
2+
3+
import (
4+
gocontext "context"
5+
"io"
6+
"os"
7+
)
8+
9+
type FileInfo interface {
10+
os.FileInfo
11+
FullPath() string
12+
}
13+
14+
type FilesystemRW interface {
15+
io.Closer
16+
Read(ctx gocontext.Context, path string) (io.ReadCloser, error)
17+
Write(ctx gocontext.Context, path string, data io.Reader) (os.FileInfo, error)
18+
ReadDir(name string) ([]FileInfo, error)
19+
Stat(name string) (os.FileInfo, error)
20+
}

artifact/fs/gcs.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package fs
2+
3+
import (
4+
gocontext "context"
5+
"errors"
6+
"io"
7+
"os"
8+
"strings"
9+
10+
gcs "cloud.google.com/go/storage"
11+
"github.com/flanksource/duty/artifact"
12+
gcpUtil "github.com/flanksource/duty/artifact/clients/gcp"
13+
"google.golang.org/api/iterator"
14+
)
15+
16+
type gcsFS struct {
17+
*gcs.Client
18+
Bucket string
19+
}
20+
21+
func NewGCSFS(client *gcs.Client, bucket string) *gcsFS {
22+
return &gcsFS{
23+
Bucket: strings.TrimPrefix(bucket, "gcs://"),
24+
Client: client,
25+
}
26+
}
27+
28+
func (t *gcsFS) Close() error {
29+
return t.Client.Close()
30+
}
31+
32+
func (t *gcsFS) ReadDir(name string) ([]artifact.FileInfo, error) {
33+
bucket := t.Client.Bucket(t.Bucket)
34+
objs := bucket.Objects(gocontext.TODO(), &gcs.Query{Prefix: name})
35+
36+
var output []artifact.FileInfo
37+
for {
38+
obj, err := objs.Next()
39+
if err != nil {
40+
if errors.Is(err, iterator.Done) {
41+
break
42+
}
43+
return nil, err
44+
}
45+
if obj == nil {
46+
break
47+
}
48+
49+
output = append(output, gcpUtil.GCSFileInfo{Object: obj})
50+
}
51+
52+
return output, nil
53+
}
54+
55+
func (t *gcsFS) Stat(path string) (os.FileInfo, error) {
56+
obj := t.Client.Bucket(t.Bucket).Object(path)
57+
attrs, err := obj.Attrs(gocontext.TODO())
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
return &gcpUtil.GCSFileInfo{Object: attrs}, nil
63+
}
64+
65+
func (t *gcsFS) Read(ctx gocontext.Context, path string) (io.ReadCloser, error) {
66+
return t.Client.Bucket(t.Bucket).Object(path).NewReader(ctx)
67+
}
68+
69+
func (t *gcsFS) Write(ctx gocontext.Context, path string, data io.Reader) (os.FileInfo, error) {
70+
obj := t.Client.Bucket(t.Bucket).Object(path)
71+
72+
content, err := io.ReadAll(data)
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
writer := obj.NewWriter(ctx)
78+
if _, err := writer.Write(content); err != nil {
79+
return nil, err
80+
}
81+
if err := writer.Close(); err != nil {
82+
return nil, err
83+
}
84+
85+
return t.Stat(path)
86+
}

artifact/fs/local.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package fs
2+
3+
import (
4+
gocontext "context"
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/bmatcuk/doublestar/v4"
12+
"github.com/flanksource/duty/artifact"
13+
)
14+
15+
type localFS struct {
16+
base string
17+
}
18+
19+
type localFileInfo struct {
20+
os.FileInfo
21+
fullpath string
22+
}
23+
24+
func (t localFileInfo) FullPath() string {
25+
return t.fullpath
26+
}
27+
28+
func NewLocalFS(base string) *localFS {
29+
return &localFS{base: base}
30+
}
31+
32+
func (t *localFS) Close() error {
33+
return nil
34+
}
35+
36+
func (t *localFS) ReadDir(name string) ([]artifact.FileInfo, error) {
37+
if strings.Contains(name, "*") {
38+
return t.ReadDirGlob(name)
39+
}
40+
41+
path := filepath.Join(t.base, name)
42+
files, err := os.ReadDir(path)
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
output := make([]artifact.FileInfo, 0, len(files))
48+
for _, match := range files {
49+
fullPath := filepath.Join(path, match.Name())
50+
info, err := os.Stat(fullPath)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
output = append(output, localFileInfo{FileInfo: info, fullpath: fullPath})
56+
}
57+
58+
return output, nil
59+
}
60+
61+
func (t *localFS) ReadDirGlob(name string) ([]artifact.FileInfo, error) {
62+
base, pattern := doublestar.SplitPattern(filepath.Join(t.base, name))
63+
matches, err := doublestar.Glob(os.DirFS(base), pattern)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
output := make([]artifact.FileInfo, 0, len(matches))
69+
for _, match := range matches {
70+
fullPath := filepath.Join(base, match)
71+
info, err := os.Stat(fullPath)
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
output = append(output, localFileInfo{FileInfo: info, fullpath: fullPath})
77+
}
78+
79+
return output, nil
80+
}
81+
82+
func (t *localFS) Stat(name string) (os.FileInfo, error) {
83+
return os.Stat(filepath.Join(t.base, name))
84+
}
85+
86+
func (t *localFS) Read(_ gocontext.Context, path string) (io.ReadCloser, error) {
87+
return os.Open(filepath.Join(t.base, path))
88+
}
89+
90+
func (t *localFS) Write(_ gocontext.Context, path string, data io.Reader) (os.FileInfo, error) {
91+
fullpath := filepath.Join(t.base, path)
92+
93+
err := os.MkdirAll(filepath.Dir(fullpath), os.ModePerm)
94+
if err != nil {
95+
return nil, fmt.Errorf("error creating base directory: %w", err)
96+
}
97+
98+
f, err := os.Create(fullpath)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
_, err = io.Copy(f, data)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
return t.Stat(path)
109+
}

0 commit comments

Comments
 (0)