mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
619 lines
18 KiB
Go
619 lines
18 KiB
Go
package store
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/minio/minio-go/v7"
|
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
objectStoreConfigKey = "config/config.yaml"
|
|
objectStoreAuthPrefix = "auths"
|
|
)
|
|
|
|
// ObjectStoreConfig captures configuration for the object storage-backed token store.
|
|
type ObjectStoreConfig struct {
|
|
Endpoint string
|
|
Bucket string
|
|
AccessKey string
|
|
SecretKey string
|
|
Region string
|
|
Prefix string
|
|
LocalRoot string
|
|
UseSSL bool
|
|
PathStyle bool
|
|
}
|
|
|
|
// ObjectTokenStore persists configuration and authentication metadata using an S3-compatible object storage backend.
|
|
// Files are mirrored to a local workspace so existing file-based flows continue to operate.
|
|
type ObjectTokenStore struct {
|
|
client *minio.Client
|
|
cfg ObjectStoreConfig
|
|
spoolRoot string
|
|
configPath string
|
|
authDir string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewObjectTokenStore initializes an object storage backed token store.
|
|
func NewObjectTokenStore(cfg ObjectStoreConfig) (*ObjectTokenStore, error) {
|
|
cfg.Endpoint = strings.TrimSpace(cfg.Endpoint)
|
|
cfg.Bucket = strings.TrimSpace(cfg.Bucket)
|
|
cfg.AccessKey = strings.TrimSpace(cfg.AccessKey)
|
|
cfg.SecretKey = strings.TrimSpace(cfg.SecretKey)
|
|
cfg.Prefix = strings.Trim(cfg.Prefix, "/")
|
|
|
|
if cfg.Endpoint == "" {
|
|
return nil, fmt.Errorf("object store: endpoint is required")
|
|
}
|
|
if cfg.Bucket == "" {
|
|
return nil, fmt.Errorf("object store: bucket is required")
|
|
}
|
|
if cfg.AccessKey == "" {
|
|
return nil, fmt.Errorf("object store: access key is required")
|
|
}
|
|
if cfg.SecretKey == "" {
|
|
return nil, fmt.Errorf("object store: secret key is required")
|
|
}
|
|
|
|
root := strings.TrimSpace(cfg.LocalRoot)
|
|
if root == "" {
|
|
if cwd, err := os.Getwd(); err == nil {
|
|
root = filepath.Join(cwd, "objectstore")
|
|
} else {
|
|
root = filepath.Join(os.TempDir(), "objectstore")
|
|
}
|
|
}
|
|
absRoot, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("object store: resolve spool directory: %w", err)
|
|
}
|
|
|
|
configDir := filepath.Join(absRoot, "config")
|
|
authDir := filepath.Join(absRoot, "auths")
|
|
|
|
if err = os.MkdirAll(configDir, 0o700); err != nil {
|
|
return nil, fmt.Errorf("object store: create config directory: %w", err)
|
|
}
|
|
if err = os.MkdirAll(authDir, 0o700); err != nil {
|
|
return nil, fmt.Errorf("object store: create auth directory: %w", err)
|
|
}
|
|
|
|
options := &minio.Options{
|
|
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
|
|
Secure: cfg.UseSSL,
|
|
Region: cfg.Region,
|
|
}
|
|
if cfg.PathStyle {
|
|
options.BucketLookup = minio.BucketLookupPath
|
|
}
|
|
|
|
client, err := minio.New(cfg.Endpoint, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("object store: create client: %w", err)
|
|
}
|
|
|
|
return &ObjectTokenStore{
|
|
client: client,
|
|
cfg: cfg,
|
|
spoolRoot: absRoot,
|
|
configPath: filepath.Join(configDir, "config.yaml"),
|
|
authDir: authDir,
|
|
}, nil
|
|
}
|
|
|
|
// SetBaseDir implements the optional interface used by authenticators; it is a no-op because
|
|
// the object store controls its own workspace.
|
|
func (s *ObjectTokenStore) SetBaseDir(string) {}
|
|
|
|
// ConfigPath returns the managed configuration file path inside the spool directory.
|
|
func (s *ObjectTokenStore) ConfigPath() string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return s.configPath
|
|
}
|
|
|
|
// AuthDir returns the local directory containing mirrored auth files.
|
|
func (s *ObjectTokenStore) AuthDir() string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return s.authDir
|
|
}
|
|
|
|
// Bootstrap ensures the target bucket exists and synchronizes data from the object storage backend.
|
|
func (s *ObjectTokenStore) Bootstrap(ctx context.Context, exampleConfigPath string) error {
|
|
if s == nil {
|
|
return fmt.Errorf("object store: not initialized")
|
|
}
|
|
if err := s.ensureBucket(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := s.syncConfigFromBucket(ctx, exampleConfigPath); err != nil {
|
|
return err
|
|
}
|
|
if err := s.syncAuthFromBucket(ctx); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Save persists authentication metadata to disk and uploads it to the object storage backend.
|
|
func (s *ObjectTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (string, error) {
|
|
if auth == nil {
|
|
return "", fmt.Errorf("object store: auth is nil")
|
|
}
|
|
|
|
path, err := s.resolveAuthPath(auth)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if path == "" {
|
|
return "", fmt.Errorf("object store: missing file path attribute for %s", auth.ID)
|
|
}
|
|
|
|
if auth.Disabled {
|
|
if _, statErr := os.Stat(path); errors.Is(statErr, fs.ErrNotExist) {
|
|
return "", nil
|
|
}
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if err = os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
return "", fmt.Errorf("object store: create auth directory: %w", err)
|
|
}
|
|
|
|
switch {
|
|
case auth.Storage != nil:
|
|
if err = auth.Storage.SaveTokenToFile(path); err != nil {
|
|
return "", err
|
|
}
|
|
case auth.Metadata != nil:
|
|
raw, errMarshal := json.Marshal(auth.Metadata)
|
|
if errMarshal != nil {
|
|
return "", fmt.Errorf("object store: marshal metadata: %w", errMarshal)
|
|
}
|
|
if existing, errRead := os.ReadFile(path); errRead == nil {
|
|
if jsonEqual(existing, raw) {
|
|
return path, nil
|
|
}
|
|
} else if errRead != nil && !errors.Is(errRead, fs.ErrNotExist) {
|
|
return "", fmt.Errorf("object store: read existing metadata: %w", errRead)
|
|
}
|
|
tmp := path + ".tmp"
|
|
if errWrite := os.WriteFile(tmp, raw, 0o600); errWrite != nil {
|
|
return "", fmt.Errorf("object store: write temp auth file: %w", errWrite)
|
|
}
|
|
if errRename := os.Rename(tmp, path); errRename != nil {
|
|
return "", fmt.Errorf("object store: rename auth file: %w", errRename)
|
|
}
|
|
default:
|
|
return "", fmt.Errorf("object store: nothing to persist for %s", auth.ID)
|
|
}
|
|
|
|
if auth.Attributes == nil {
|
|
auth.Attributes = make(map[string]string)
|
|
}
|
|
auth.Attributes["path"] = path
|
|
|
|
if strings.TrimSpace(auth.FileName) == "" {
|
|
auth.FileName = auth.ID
|
|
}
|
|
|
|
if err = s.uploadAuth(ctx, path); err != nil {
|
|
return "", err
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
// List enumerates auth JSON files from the mirrored workspace.
|
|
func (s *ObjectTokenStore) List(_ context.Context) ([]*cliproxyauth.Auth, error) {
|
|
dir := strings.TrimSpace(s.AuthDir())
|
|
if dir == "" {
|
|
return nil, fmt.Errorf("object store: auth directory not configured")
|
|
}
|
|
entries := make([]*cliproxyauth.Auth, 0, 32)
|
|
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
if !strings.HasSuffix(strings.ToLower(d.Name()), ".json") {
|
|
return nil
|
|
}
|
|
auth, err := s.readAuthFile(path, dir)
|
|
if err != nil {
|
|
log.WithError(err).Warnf("object store: skip auth %s", path)
|
|
return nil
|
|
}
|
|
if auth != nil {
|
|
entries = append(entries, auth)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("object store: walk auth directory: %w", err)
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
// Delete removes an auth file locally and remotely.
|
|
func (s *ObjectTokenStore) Delete(ctx context.Context, id string) error {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return fmt.Errorf("object store: id is empty")
|
|
}
|
|
path, err := s.resolveDeletePath(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if err = os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("object store: delete auth file: %w", err)
|
|
}
|
|
if err = s.deleteAuthObject(ctx, path); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PersistAuthFiles uploads the provided auth files to the object storage backend.
|
|
func (s *ObjectTokenStore) PersistAuthFiles(ctx context.Context, _ string, paths ...string) error {
|
|
if len(paths) == 0 {
|
|
return nil
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
for _, p := range paths {
|
|
trimmed := strings.TrimSpace(p)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
abs := trimmed
|
|
if !filepath.IsAbs(abs) {
|
|
abs = filepath.Join(s.authDir, trimmed)
|
|
}
|
|
if err := s.uploadAuth(ctx, abs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PersistConfig uploads the local configuration file to the object storage backend.
|
|
func (s *ObjectTokenStore) PersistConfig(ctx context.Context) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
data, err := os.ReadFile(s.configPath)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return s.deleteObject(ctx, objectStoreConfigKey)
|
|
}
|
|
return fmt.Errorf("object store: read config file: %w", err)
|
|
}
|
|
if len(data) == 0 {
|
|
return s.deleteObject(ctx, objectStoreConfigKey)
|
|
}
|
|
return s.putObject(ctx, objectStoreConfigKey, data, "application/x-yaml")
|
|
}
|
|
|
|
func (s *ObjectTokenStore) ensureBucket(ctx context.Context) error {
|
|
exists, err := s.client.BucketExists(ctx, s.cfg.Bucket)
|
|
if err != nil {
|
|
return fmt.Errorf("object store: check bucket: %w", err)
|
|
}
|
|
if exists {
|
|
return nil
|
|
}
|
|
if err = s.client.MakeBucket(ctx, s.cfg.Bucket, minio.MakeBucketOptions{Region: s.cfg.Region}); err != nil {
|
|
return fmt.Errorf("object store: create bucket: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ObjectTokenStore) syncConfigFromBucket(ctx context.Context, example string) error {
|
|
key := s.prefixedKey(objectStoreConfigKey)
|
|
_, err := s.client.StatObject(ctx, s.cfg.Bucket, key, minio.StatObjectOptions{})
|
|
switch {
|
|
case err == nil:
|
|
object, errGet := s.client.GetObject(ctx, s.cfg.Bucket, key, minio.GetObjectOptions{})
|
|
if errGet != nil {
|
|
return fmt.Errorf("object store: fetch config: %w", errGet)
|
|
}
|
|
defer object.Close()
|
|
data, errRead := io.ReadAll(object)
|
|
if errRead != nil {
|
|
return fmt.Errorf("object store: read config: %w", errRead)
|
|
}
|
|
if errWrite := os.WriteFile(s.configPath, normalizeLineEndingsBytes(data), 0o600); errWrite != nil {
|
|
return fmt.Errorf("object store: write config: %w", errWrite)
|
|
}
|
|
case isObjectNotFound(err):
|
|
if _, statErr := os.Stat(s.configPath); errors.Is(statErr, fs.ErrNotExist) {
|
|
if example != "" {
|
|
if errCopy := misc.CopyConfigTemplate(example, s.configPath); errCopy != nil {
|
|
return fmt.Errorf("object store: copy example config: %w", errCopy)
|
|
}
|
|
} else {
|
|
if errCreate := os.MkdirAll(filepath.Dir(s.configPath), 0o700); errCreate != nil {
|
|
return fmt.Errorf("object store: prepare config directory: %w", errCreate)
|
|
}
|
|
if errWrite := os.WriteFile(s.configPath, []byte{}, 0o600); errWrite != nil {
|
|
return fmt.Errorf("object store: create empty config: %w", errWrite)
|
|
}
|
|
}
|
|
}
|
|
data, errRead := os.ReadFile(s.configPath)
|
|
if errRead != nil {
|
|
return fmt.Errorf("object store: read local config: %w", errRead)
|
|
}
|
|
if len(data) > 0 {
|
|
if errPut := s.putObject(ctx, objectStoreConfigKey, data, "application/x-yaml"); errPut != nil {
|
|
return errPut
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("object store: stat config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ObjectTokenStore) syncAuthFromBucket(ctx context.Context) error {
|
|
if err := os.RemoveAll(s.authDir); err != nil {
|
|
return fmt.Errorf("object store: reset auth directory: %w", err)
|
|
}
|
|
if err := os.MkdirAll(s.authDir, 0o700); err != nil {
|
|
return fmt.Errorf("object store: recreate auth directory: %w", err)
|
|
}
|
|
|
|
prefix := s.prefixedKey(objectStoreAuthPrefix + "/")
|
|
objectCh := s.client.ListObjects(ctx, s.cfg.Bucket, minio.ListObjectsOptions{
|
|
Prefix: prefix,
|
|
Recursive: true,
|
|
})
|
|
for object := range objectCh {
|
|
if object.Err != nil {
|
|
return fmt.Errorf("object store: list auth objects: %w", object.Err)
|
|
}
|
|
rel := strings.TrimPrefix(object.Key, prefix)
|
|
if rel == "" || strings.HasSuffix(rel, "/") {
|
|
continue
|
|
}
|
|
relPath := filepath.FromSlash(rel)
|
|
if filepath.IsAbs(relPath) {
|
|
log.WithField("key", object.Key).Warn("object store: skip auth outside mirror")
|
|
continue
|
|
}
|
|
cleanRel := filepath.Clean(relPath)
|
|
if cleanRel == "." || cleanRel == ".." || strings.HasPrefix(cleanRel, ".."+string(os.PathSeparator)) {
|
|
log.WithField("key", object.Key).Warn("object store: skip auth outside mirror")
|
|
continue
|
|
}
|
|
local := filepath.Join(s.authDir, cleanRel)
|
|
if err := os.MkdirAll(filepath.Dir(local), 0o700); err != nil {
|
|
return fmt.Errorf("object store: prepare auth subdir: %w", err)
|
|
}
|
|
reader, errGet := s.client.GetObject(ctx, s.cfg.Bucket, object.Key, minio.GetObjectOptions{})
|
|
if errGet != nil {
|
|
return fmt.Errorf("object store: download auth %s: %w", object.Key, errGet)
|
|
}
|
|
data, errRead := io.ReadAll(reader)
|
|
_ = reader.Close()
|
|
if errRead != nil {
|
|
return fmt.Errorf("object store: read auth %s: %w", object.Key, errRead)
|
|
}
|
|
if errWrite := os.WriteFile(local, data, 0o600); errWrite != nil {
|
|
return fmt.Errorf("object store: write auth %s: %w", local, errWrite)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ObjectTokenStore) uploadAuth(ctx context.Context, path string) error {
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(s.authDir, path)
|
|
if err != nil {
|
|
return fmt.Errorf("object store: resolve auth relative path: %w", err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return s.deleteAuthObject(ctx, path)
|
|
}
|
|
return fmt.Errorf("object store: read auth file: %w", err)
|
|
}
|
|
if len(data) == 0 {
|
|
return s.deleteAuthObject(ctx, path)
|
|
}
|
|
key := objectStoreAuthPrefix + "/" + filepath.ToSlash(rel)
|
|
return s.putObject(ctx, key, data, "application/json")
|
|
}
|
|
|
|
func (s *ObjectTokenStore) deleteAuthObject(ctx context.Context, path string) error {
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(s.authDir, path)
|
|
if err != nil {
|
|
return fmt.Errorf("object store: resolve auth relative path: %w", err)
|
|
}
|
|
key := objectStoreAuthPrefix + "/" + filepath.ToSlash(rel)
|
|
return s.deleteObject(ctx, key)
|
|
}
|
|
|
|
func (s *ObjectTokenStore) putObject(ctx context.Context, key string, data []byte, contentType string) error {
|
|
if len(data) == 0 {
|
|
return s.deleteObject(ctx, key)
|
|
}
|
|
fullKey := s.prefixedKey(key)
|
|
reader := bytes.NewReader(data)
|
|
_, err := s.client.PutObject(ctx, s.cfg.Bucket, fullKey, reader, int64(len(data)), minio.PutObjectOptions{
|
|
ContentType: contentType,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("object store: put object %s: %w", fullKey, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ObjectTokenStore) deleteObject(ctx context.Context, key string) error {
|
|
fullKey := s.prefixedKey(key)
|
|
err := s.client.RemoveObject(ctx, s.cfg.Bucket, fullKey, minio.RemoveObjectOptions{})
|
|
if err != nil {
|
|
if isObjectNotFound(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("object store: delete object %s: %w", fullKey, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ObjectTokenStore) prefixedKey(key string) string {
|
|
key = strings.TrimLeft(key, "/")
|
|
if s.cfg.Prefix == "" {
|
|
return key
|
|
}
|
|
return strings.TrimLeft(s.cfg.Prefix+"/"+key, "/")
|
|
}
|
|
|
|
func (s *ObjectTokenStore) resolveAuthPath(auth *cliproxyauth.Auth) (string, error) {
|
|
if auth == nil {
|
|
return "", fmt.Errorf("object store: auth is nil")
|
|
}
|
|
if auth.Attributes != nil {
|
|
if path := strings.TrimSpace(auth.Attributes["path"]); path != "" {
|
|
if filepath.IsAbs(path) {
|
|
return path, nil
|
|
}
|
|
return filepath.Join(s.authDir, path), nil
|
|
}
|
|
}
|
|
fileName := strings.TrimSpace(auth.FileName)
|
|
if fileName == "" {
|
|
fileName = strings.TrimSpace(auth.ID)
|
|
}
|
|
if fileName == "" {
|
|
return "", fmt.Errorf("object store: auth %s missing filename", auth.ID)
|
|
}
|
|
if !strings.HasSuffix(strings.ToLower(fileName), ".json") {
|
|
fileName += ".json"
|
|
}
|
|
return filepath.Join(s.authDir, fileName), nil
|
|
}
|
|
|
|
func (s *ObjectTokenStore) resolveDeletePath(id string) (string, error) {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return "", fmt.Errorf("object store: id is empty")
|
|
}
|
|
// Absolute paths are honored as-is; callers must ensure they point inside the mirror.
|
|
if filepath.IsAbs(id) {
|
|
return id, nil
|
|
}
|
|
// Treat any non-absolute id (including nested like "team/foo") as relative to the mirror authDir.
|
|
// Normalize separators and guard against path traversal.
|
|
clean := filepath.Clean(filepath.FromSlash(id))
|
|
if clean == "." || clean == ".." || strings.HasPrefix(clean, ".."+string(os.PathSeparator)) {
|
|
return "", fmt.Errorf("object store: invalid auth identifier %s", id)
|
|
}
|
|
// Ensure .json suffix.
|
|
if !strings.HasSuffix(strings.ToLower(clean), ".json") {
|
|
clean += ".json"
|
|
}
|
|
return filepath.Join(s.authDir, clean), nil
|
|
}
|
|
|
|
func (s *ObjectTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read file: %w", err)
|
|
}
|
|
if len(data) == 0 {
|
|
return nil, nil
|
|
}
|
|
metadata := make(map[string]any)
|
|
if err = json.Unmarshal(data, &metadata); err != nil {
|
|
return nil, fmt.Errorf("unmarshal auth json: %w", err)
|
|
}
|
|
provider := strings.TrimSpace(valueAsString(metadata["type"]))
|
|
if provider == "" {
|
|
provider = "unknown"
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat auth file: %w", err)
|
|
}
|
|
rel, errRel := filepath.Rel(baseDir, path)
|
|
if errRel != nil {
|
|
rel = filepath.Base(path)
|
|
}
|
|
rel = normalizeAuthID(rel)
|
|
attr := map[string]string{"path": path}
|
|
if email := strings.TrimSpace(valueAsString(metadata["email"])); email != "" {
|
|
attr["email"] = email
|
|
}
|
|
auth := &cliproxyauth.Auth{
|
|
ID: rel,
|
|
Provider: provider,
|
|
FileName: rel,
|
|
Label: labelFor(metadata),
|
|
Status: cliproxyauth.StatusActive,
|
|
Attributes: attr,
|
|
Metadata: metadata,
|
|
CreatedAt: info.ModTime(),
|
|
UpdatedAt: info.ModTime(),
|
|
LastRefreshedAt: time.Time{},
|
|
NextRefreshAfter: time.Time{},
|
|
}
|
|
return auth, nil
|
|
}
|
|
|
|
func normalizeLineEndingsBytes(data []byte) []byte {
|
|
replaced := bytes.ReplaceAll(data, []byte{'\r', '\n'}, []byte{'\n'})
|
|
return bytes.ReplaceAll(replaced, []byte{'\r'}, []byte{'\n'})
|
|
}
|
|
|
|
func isObjectNotFound(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
resp := minio.ToErrorResponse(err)
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return true
|
|
}
|
|
switch resp.Code {
|
|
case "NoSuchKey", "NotFound", "NoSuchBucket":
|
|
return true
|
|
}
|
|
return false
|
|
}
|