Eric Bower
ยท
2025-04-18
minio.go
1package storage
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "log/slog"
9 "net/url"
10 "os"
11 "strconv"
12 "strings"
13 "time"
14
15 "github.com/hashicorp/golang-lru/v2/expirable"
16 "github.com/minio/madmin-go/v3"
17 "github.com/minio/minio-go/v7"
18 "github.com/minio/minio-go/v7/pkg/credentials"
19 "github.com/picosh/pico/pkg/cache"
20 "github.com/picosh/pico/pkg/send/utils"
21)
22
23type StorageMinio struct {
24 Client *minio.Client
25 Admin *madmin.AdminClient
26 BucketCache *expirable.LRU[string, CachedBucket]
27 Logger *slog.Logger
28}
29
30type CachedBucket struct {
31 Bucket
32 Error error
33}
34
35type CachedObjectInfo struct {
36 *ObjectInfo
37 Error error
38}
39
40var (
41 _ ObjectStorage = &StorageMinio{}
42 _ ObjectStorage = (*StorageMinio)(nil)
43)
44
45func NewStorageMinio(logger *slog.Logger, address, user, pass string) (*StorageMinio, error) {
46 endpoint, err := url.Parse(address)
47 if err != nil {
48 return nil, err
49 }
50 ssl := endpoint.Scheme == "https"
51
52 mClient, err := minio.New(endpoint.Host, &minio.Options{
53 Creds: credentials.NewStaticV4(user, pass, ""),
54 Secure: ssl,
55 })
56 if err != nil {
57 return nil, err
58 }
59
60 aClient, err := madmin.NewWithOptions(
61 endpoint.Host,
62 &madmin.Options{
63 Creds: credentials.NewStaticV4(user, pass, ""),
64 Secure: ssl,
65 },
66 )
67 if err != nil {
68 return nil, err
69 }
70
71 mini := &StorageMinio{
72 Client: mClient,
73 Admin: aClient,
74 BucketCache: expirable.NewLRU[string, CachedBucket](2048, nil, cache.CacheTimeout),
75 Logger: logger,
76 }
77 return mini, err
78}
79
80func (s *StorageMinio) GetBucket(name string) (Bucket, error) {
81 if cachedBucket, found := s.BucketCache.Get(name); found {
82 s.Logger.Info("bucket found in lru cache", "name", name)
83 return cachedBucket.Bucket, cachedBucket.Error
84 }
85
86 s.Logger.Info("bucket not found in lru cache", "name", name)
87
88 bucket := Bucket{
89 Name: name,
90 }
91
92 exists, err := s.Client.BucketExists(context.TODO(), bucket.Name)
93 if err != nil || !exists {
94 if err == nil {
95 err = errors.New("bucket does not exist")
96 }
97
98 s.BucketCache.Add(name, CachedBucket{bucket, err})
99 return bucket, err
100 }
101
102 s.BucketCache.Add(name, CachedBucket{bucket, nil})
103
104 return bucket, nil
105}
106
107func (s *StorageMinio) UpsertBucket(name string) (Bucket, error) {
108 bucket, err := s.GetBucket(name)
109 if err == nil {
110 return bucket, nil
111 }
112
113 err = s.Client.MakeBucket(context.TODO(), name, minio.MakeBucketOptions{})
114 if err != nil {
115 return bucket, err
116 }
117
118 s.BucketCache.Remove(name)
119
120 return bucket, nil
121}
122
123func (s *StorageMinio) GetBucketQuota(bucket Bucket) (uint64, error) {
124 info, err := s.Admin.AccountInfo(context.TODO(), madmin.AccountOpts{})
125 if err != nil {
126 return 0, nil
127 }
128 for _, b := range info.Buckets {
129 if b.Name == bucket.Name {
130 return b.Size, nil
131 }
132 }
133
134 return 0, fmt.Errorf("%s bucket not found in account info", bucket.Name)
135}
136
137func (s *StorageMinio) ListBuckets() ([]string, error) {
138 bcks := []string{}
139 buckets, err := s.Client.ListBuckets(context.Background())
140 if err != nil {
141 return bcks, err
142 }
143 for _, bucket := range buckets {
144 bcks = append(bcks, bucket.Name)
145 }
146
147 return bcks, nil
148}
149
150func (s *StorageMinio) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
151 var fileList []os.FileInfo
152
153 resolved := strings.TrimPrefix(dir, "/")
154
155 opts := minio.ListObjectsOptions{Prefix: resolved, Recursive: recursive, WithMetadata: true}
156 for obj := range s.Client.ListObjects(context.Background(), bucket.Name, opts) {
157 if obj.Err != nil {
158 return fileList, obj.Err
159 }
160
161 isDir := strings.HasSuffix(obj.Key, string(os.PathSeparator))
162
163 modTime := obj.LastModified
164
165 if mtime, ok := obj.UserMetadata["Mtime"]; ok {
166 mtimeUnix, err := strconv.Atoi(mtime)
167 if err == nil {
168 modTime = time.Unix(int64(mtimeUnix), 0)
169 }
170 }
171
172 info := &utils.VirtualFile{
173 FName: strings.TrimSuffix(strings.TrimPrefix(obj.Key, resolved), "/"),
174 FIsDir: isDir,
175 FSize: obj.Size,
176 FModTime: modTime,
177 }
178 fileList = append(fileList, info)
179 }
180
181 return fileList, nil
182}
183
184func (s *StorageMinio) DeleteBucket(bucket Bucket) error {
185 return s.Client.RemoveBucket(context.TODO(), bucket.Name)
186}
187
188func (s *StorageMinio) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
189 objInfo := &ObjectInfo{
190 Size: 0,
191 LastModified: time.Time{},
192 ETag: "",
193 }
194
195 info, err := s.Client.StatObject(context.Background(), bucket.Name, fpath, minio.StatObjectOptions{})
196 if err != nil {
197 return nil, objInfo, err
198 }
199
200 objInfo.LastModified = info.LastModified
201 objInfo.ETag = info.ETag
202 objInfo.Metadata = info.Metadata
203 objInfo.Size = info.Size
204
205 if mtime, ok := info.UserMetadata["Mtime"]; ok {
206 mtimeUnix, err := strconv.Atoi(mtime)
207 if err == nil {
208 objInfo.LastModified = time.Unix(int64(mtimeUnix), 0)
209 }
210 }
211
212 obj, err := s.Client.GetObject(context.Background(), bucket.Name, fpath, minio.GetObjectOptions{})
213 if err != nil {
214 return nil, objInfo, err
215 }
216
217 return obj, objInfo, nil
218}
219
220func (s *StorageMinio) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
221 opts := minio.PutObjectOptions{
222 UserMetadata: map[string]string{
223 "Mtime": fmt.Sprint(time.Now().Unix()),
224 },
225 }
226
227 if entry.Mtime > 0 {
228 opts.UserMetadata["Mtime"] = fmt.Sprint(entry.Mtime)
229 }
230
231 var objSize int64 = -1
232 if entry.Size > 0 {
233 objSize = entry.Size
234 }
235 info, err := s.Client.PutObject(context.TODO(), bucket.Name, fpath, contents, objSize, opts)
236
237 if err != nil {
238 return "", 0, err
239 }
240
241 return fmt.Sprintf("%s/%s", info.Bucket, info.Key), info.Size, nil
242}
243
244func (s *StorageMinio) DeleteObject(bucket Bucket, fpath string) error {
245 err := s.Client.RemoveObject(context.TODO(), bucket.Name, fpath, minio.RemoveObjectOptions{})
246 return err
247}