repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / pkg / pobj / storage
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}