新增: 云OSS存储集成(七牛云+阿里云)+多桶导航+GBK编码自动转换
This commit is contained in:
652
internal/ossdrv/service.go
Normal file
652
internal/ossdrv/service.go
Normal file
@@ -0,0 +1,652 @@
|
||||
package ossdrv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"u-desk/internal/filesystem"
|
||||
"u-desk/internal/oss"
|
||||
"u-desk/internal/oss/aliyun"
|
||||
"u-desk/internal/oss/qiniu"
|
||||
)
|
||||
|
||||
// accountCredentials 账户级凭据
|
||||
type accountCredentials struct {
|
||||
Provider string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
// Manager OSS 连接管理器(两级:账户 + 桶级客户端缓存)
|
||||
type Manager struct {
|
||||
accounts sync.Map // map[string]*accountCredentials key=provider
|
||||
clients sync.Map // map[string]oss.OSSProvider key="provider:bucket"
|
||||
bucketRegions sync.Map // map[string]string key="provider:bucket" → region
|
||||
}
|
||||
|
||||
var globalManager = &Manager{}
|
||||
|
||||
func GetManager() *Manager { return globalManager }
|
||||
|
||||
// Connect 建立账户级连接(验证凭据通过 ListBuckets)
|
||||
func (m *Manager) Connect(provider, accessKey, secretKey, endpoint string) error {
|
||||
// 验证凭据
|
||||
switch provider {
|
||||
case "qiniu":
|
||||
_, err := qiniu.ListBuckets(accessKey, secretKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("七牛云连接失败: %w", err)
|
||||
}
|
||||
case "aliyun":
|
||||
_, err := aliyun.ListBuckets(accessKey, secretKey, endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("阿里云连接失败: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("不支持的 OSS 提供商: %s", provider)
|
||||
}
|
||||
|
||||
m.accounts.Store(provider, &accountCredentials{
|
||||
Provider: provider,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Endpoint: endpoint,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// getOrCreateBucketClient 懒创建桶级 OSSProvider
|
||||
func (m *Manager) getOrCreateBucketClient(provider, bucket, region string) (oss.OSSProvider, error) {
|
||||
key := provider + ":" + bucket
|
||||
if v, ok := m.clients.Load(key); ok {
|
||||
return v.(oss.OSSProvider), nil
|
||||
}
|
||||
|
||||
cred, ok := m.accounts.Load(provider)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("OSS 账户未连接: %s", provider)
|
||||
}
|
||||
c := cred.(*accountCredentials)
|
||||
|
||||
// 如果未传 region,从缓存取
|
||||
if region == "" {
|
||||
if v, ok := m.bucketRegions.Load(key); ok {
|
||||
region = v.(string)
|
||||
}
|
||||
}
|
||||
|
||||
var client oss.OSSProvider
|
||||
var err error
|
||||
|
||||
switch provider {
|
||||
case "qiniu":
|
||||
client, err = qiniu.NewClient(&qiniu.Config{
|
||||
AccessKey: c.AccessKey,
|
||||
SecretKey: c.SecretKey,
|
||||
Bucket: bucket,
|
||||
Region: region,
|
||||
UseHTTPS: true,
|
||||
})
|
||||
case "aliyun":
|
||||
client, err = aliyun.NewClient(&aliyun.Config{
|
||||
AccessKeyID: c.AccessKey,
|
||||
AccessKeySecret: c.SecretKey,
|
||||
Bucket: bucket,
|
||||
Region: region,
|
||||
Endpoint: c.Endpoint,
|
||||
UseHTTPS: true,
|
||||
})
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的提供商: %s", provider)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建桶客户端失败: %w", err)
|
||||
}
|
||||
|
||||
m.clients.Store(key, client)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// GetClient 获取已有的桶级客户端
|
||||
func (m *Manager) GetClient(provider, bucket string) oss.OSSProvider {
|
||||
if v, ok := m.clients.Load(provider + ":" + bucket); ok {
|
||||
return v.(oss.OSSProvider)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect 关闭账户及所有桶级客户端
|
||||
func (m *Manager) Disconnect(provider string) {
|
||||
m.accounts.Delete(provider)
|
||||
prefix := provider + ":"
|
||||
m.clients.Range(func(key, value any) bool {
|
||||
if strings.HasPrefix(key.(string), prefix) {
|
||||
value.(oss.OSSProvider).Close()
|
||||
m.clients.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
m.bucketRegions.Range(func(key, value any) bool {
|
||||
if strings.HasPrefix(key.(string), prefix) {
|
||||
m.bucketRegions.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Shutdown 关闭所有连接
|
||||
func (m *Manager) Shutdown() {
|
||||
m.clients.Range(func(key, value any) bool {
|
||||
value.(oss.OSSProvider).Close()
|
||||
m.clients.Delete(key)
|
||||
return true
|
||||
})
|
||||
m.accounts.Range(func(key, value any) bool {
|
||||
m.accounts.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Service OSS 文件操作服务
|
||||
type Service struct {
|
||||
manager *Manager
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{manager: GetManager()}
|
||||
}
|
||||
|
||||
func (s *Service) GetManager() *Manager { return s.manager }
|
||||
|
||||
// parseBucketPath 解析路径中的桶名和对象键
|
||||
// "/my-bucket/photos/img.jpg" → bucket="my-bucket", key="photos/img.jpg"
|
||||
func parseBucketPath(rawPath string) (bucket, key string) {
|
||||
rawPath = strings.TrimPrefix(rawPath, "/")
|
||||
if rawPath == "" {
|
||||
return "", ""
|
||||
}
|
||||
parts := strings.SplitN(rawPath, "/", 2)
|
||||
bucket = parts[0]
|
||||
if len(parts) > 1 {
|
||||
key = parts[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// listBuckets 列出所有桶
|
||||
func (s *Service) listBuckets(provider string) ([]map[string]interface{}, error) {
|
||||
cred, ok := s.manager.accounts.Load(provider)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("OSS 账户未连接: %s", provider)
|
||||
}
|
||||
c := cred.(*accountCredentials)
|
||||
|
||||
var entries []oss.BucketEntry
|
||||
var err error
|
||||
|
||||
switch provider {
|
||||
case "qiniu":
|
||||
entries, err = qiniu.ListBuckets(c.AccessKey, c.SecretKey)
|
||||
case "aliyun":
|
||||
entries, err = aliyun.ListBuckets(c.AccessKey, c.SecretKey, c.Endpoint)
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的提供商: %s", provider)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("列举存储桶失败: %w", err)
|
||||
}
|
||||
|
||||
// 缓存桶区域信息
|
||||
for _, e := range entries {
|
||||
if e.Region != "" {
|
||||
s.manager.bucketRegions.Store(provider+":"+e.Name, e.Region)
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]map[string]interface{}, len(entries))
|
||||
for i, e := range entries {
|
||||
items[i] = map[string]interface{}{
|
||||
"name": e.Name,
|
||||
"path": "/" + e.Name,
|
||||
"is_dir": true,
|
||||
"is_bucket": true,
|
||||
"size": int64(0),
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// ListDir 列出目录内容
|
||||
func (s *Service) ListDir(connID string, prefix string) ([]map[string]interface{}, error) {
|
||||
prefix = strings.TrimPrefix(prefix, "/")
|
||||
|
||||
// 根目录 → 列出所有桶
|
||||
if prefix == "" {
|
||||
return s.listBuckets(connID)
|
||||
}
|
||||
|
||||
// 解析桶名和对象前缀
|
||||
bucket, objectPrefix := parseBucketPath(prefix)
|
||||
if bucket == "" {
|
||||
return s.listBuckets(connID)
|
||||
}
|
||||
|
||||
if objectPrefix != "" && !strings.HasSuffix(objectPrefix, "/") {
|
||||
objectPrefix += "/"
|
||||
}
|
||||
|
||||
c, err := s.manager.getOrCreateBucketClient(connID, bucket, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := c.ListFiles(ctx, &oss.ListOptions{
|
||||
Prefix: objectPrefix,
|
||||
Delimiter: "/",
|
||||
MaxKeys: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("列举文件失败: %w", err)
|
||||
}
|
||||
|
||||
items := make([]map[string]interface{}, 0, len(result.Files)+len(result.Prefixes))
|
||||
bucketPrefix := "/" + bucket + "/"
|
||||
|
||||
for _, p := range result.Prefixes {
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(p, objectPrefix), "/")
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]interface{}{
|
||||
"name": name,
|
||||
"path": bucketPrefix + p,
|
||||
"is_dir": true,
|
||||
"size": int64(0),
|
||||
})
|
||||
}
|
||||
|
||||
for _, f := range result.Files {
|
||||
if strings.HasSuffix(f.Key, "/") && f.Size == 0 {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]interface{}{
|
||||
"name": path.Base(f.Key),
|
||||
"path": bucketPrefix + f.Key,
|
||||
"is_dir": false,
|
||||
"size": f.Size,
|
||||
"mod_time": f.LastModified.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// ReadFile 读取文件内容
|
||||
func (s *Service) ReadFile(connID string, rawPath string) (string, error) {
|
||||
bucket, key := parseBucketPath(rawPath)
|
||||
if bucket == "" {
|
||||
return "", fmt.Errorf("路径中缺少桶名")
|
||||
}
|
||||
|
||||
c, err := s.manager.getOrCreateBucketClient(connID, bucket, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
const maxSize int64 = 10 << 20
|
||||
ctx := context.Background()
|
||||
|
||||
info, err := c.GetFileInfo(ctx, key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取文件信息失败: %w", err)
|
||||
}
|
||||
if info.Size > maxSize {
|
||||
return "", fmt.Errorf("文件过大 (%s),超过 %d 限制", filesystem.FormatBytes(info.Size), maxSize)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := c.Download(ctx, key, &buf); err != nil {
|
||||
return "", fmt.Errorf("读取文件失败: %w", err)
|
||||
}
|
||||
return filesystem.BytesToString(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
// WriteFile 写入文件内容
|
||||
func (s *Service) WriteFile(connID string, rawPath string, content string) error {
|
||||
bucket, key := parseBucketPath(rawPath)
|
||||
if bucket == "" {
|
||||
return fmt.Errorf("路径中缺少桶名")
|
||||
}
|
||||
|
||||
c, err := s.manager.getOrCreateBucketClient(connID, bucket, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.Upload(context.Background(), key, strings.NewReader(content), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteBase64File 写入 base64 编码的二进制文件
|
||||
func (s *Service) WriteBase64File(connID string, rawPath string, base64Content string) error {
|
||||
bucket, key := parseBucketPath(rawPath)
|
||||
if bucket == "" {
|
||||
return fmt.Errorf("路径中缺少桶名")
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(base64Content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("base64 解码失败: %w", err)
|
||||
}
|
||||
|
||||
c, err := s.manager.getOrCreateBucketClient(connID, bucket, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.Upload(context.Background(), key, bytes.NewReader(data), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFileInfo 获取文件信息
|
||||
func (s *Service) GetFileInfo(connID string, rawPath string) (map[string]interface{}, error) {
|
||||
bucket, key := parseBucketPath(rawPath)
|
||||
if bucket == "" {
|
||||
return nil, fmt.Errorf("路径中缺少桶名")
|
||||
}
|
||||
|
||||
c, err := s.manager.getOrCreateBucketClient(connID, bucket, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info, err := c.GetFileInfo(context.Background(), key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取文件信息失败: %w", err)
|
||||
}
|
||||
|
||||
bucketPrefix := "/" + bucket + "/"
|
||||
return map[string]interface{}{
|
||||
"name": path.Base(info.Key),
|
||||
"path": bucketPrefix + info.Key,
|
||||
"size": info.Size,
|
||||
"size_str": filesystem.FormatBytes(info.Size),
|
||||
"is_dir": strings.HasSuffix(info.Key, "/"),
|
||||
"mod_time": info.LastModified.Format("2006-01-02 15:04:05"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateDir 创建目录
|
||||
func (s *Service) CreateDir(connID string, rawPath string) (*filesystem.FileOperationResult, error) {
|
||||
bucket, key := parseBucketPath(rawPath)
|
||||
if bucket == "" {
|
||||
return nil, fmt.Errorf("路径中缺少桶名")
|
||||
}
|
||||
|
||||
c, err := s.manager.getOrCreateBucketClient(connID, bucket, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(key, "/") {
|
||||
key += "/"
|
||||
}
|
||||
|
||||
_, err = c.Upload(context.Background(), key, strings.NewReader(""), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
|
||||
name := path.Base(strings.TrimSuffix(key, "/"))
|
||||
return &filesystem.FileOperationResult{
|
||||
Path: "/" + bucket + "/" + key,
|
||||
Name: name,
|
||||
IsDir: true,
|
||||
SizeStr: filesystem.FormatBytes(0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateFile 创建空文件
|
||||
func (s *Service) CreateFile(connID string, rawPath string) (*filesystem.FileOperationResult, error) {
|
||||
bucket, key := parseBucketPath(rawPath)
|
||||
if bucket == "" {
|
||||
return nil, fmt.Errorf("路径中缺少桶名")
|
||||
}
|
||||
|
||||
c, err := s.manager.getOrCreateBucketClient(connID, bucket, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = c.Upload(context.Background(), key, strings.NewReader(""), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建文件失败: %w", err)
|
||||
}
|
||||
|
||||
return &filesystem.FileOperationResult{
|
||||
Path: "/" + bucket + "/" + key,
|
||||
Name: path.Base(key),
|
||||
IsDir: false,
|
||||
SizeStr: filesystem.FormatBytes(0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeletePath 删除文件或目录
|
||||
func (s *Service) DeletePath(connID string, rawPath string) (*filesystem.FileOperationResult, error) {
|
||||
bucket, key := parseBucketPath(rawPath)
|
||||
if bucket == "" {
|
||||
return nil, fmt.Errorf("路径中缺少桶名")
|
||||
}
|
||||
|
||||
c, err := s.manager.getOrCreateBucketClient(connID, bucket, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
isDir := strings.HasSuffix(key, "/")
|
||||
if !isDir {
|
||||
prefix := key + "/"
|
||||
listResult, listErr := c.ListFiles(ctx, &oss.ListOptions{Prefix: prefix, MaxKeys: 1})
|
||||
if listErr == nil && len(listResult.Files) > 0 {
|
||||
isDir = true
|
||||
key = prefix
|
||||
}
|
||||
}
|
||||
|
||||
infoMap, _ := s.GetFileInfo(connID, "/"+bucket+"/"+key)
|
||||
|
||||
if isDir {
|
||||
prefix := key
|
||||
if !strings.HasSuffix(prefix, "/") {
|
||||
prefix += "/"
|
||||
}
|
||||
for {
|
||||
listResult, err := c.ListFiles(ctx, &oss.ListOptions{Prefix: prefix, MaxKeys: 1000})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("列举目录文件失败: %w", err)
|
||||
}
|
||||
if len(listResult.Files) == 0 {
|
||||
break
|
||||
}
|
||||
keys := make([]string, len(listResult.Files))
|
||||
for i, f := range listResult.Files {
|
||||
keys[i] = f.Key
|
||||
}
|
||||
if _, err := c.DeleteMultiple(ctx, keys); err != nil {
|
||||
return nil, fmt.Errorf("批量删除失败: %w", err)
|
||||
}
|
||||
if !listResult.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Delete(ctx, key) // marker 非关键,忽略错误
|
||||
} else {
|
||||
if err := c.Delete(ctx, key); err != nil {
|
||||
return nil, fmt.Errorf("删除失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
result := toOssOperationResult(infoMap, isDir)
|
||||
result.Deleted = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RenamePath 重命名(Copy + Delete)
|
||||
func (s *Service) RenamePath(connID string, oldPath string, newPath string) (*filesystem.FileOperationResult, error) {
|
||||
oldBucket, oldKey := parseBucketPath(oldPath)
|
||||
newBucket, newKey := parseBucketPath(newPath)
|
||||
if oldBucket == "" || newBucket == "" {
|
||||
return nil, fmt.Errorf("路径中缺少桶名")
|
||||
}
|
||||
|
||||
if oldBucket != newBucket {
|
||||
return nil, fmt.Errorf("不支持跨桶重命名")
|
||||
}
|
||||
|
||||
c, err := s.manager.getOrCreateBucketClient(connID, oldBucket, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
isDir := strings.HasSuffix(oldKey, "/")
|
||||
if !isDir {
|
||||
prefix := oldKey + "/"
|
||||
listResult, listErr := c.ListFiles(ctx, &oss.ListOptions{Prefix: prefix, MaxKeys: 1})
|
||||
if listErr == nil && len(listResult.Files) > 0 {
|
||||
isDir = true
|
||||
oldKey = prefix
|
||||
}
|
||||
}
|
||||
|
||||
if isDir {
|
||||
oldPrefix := oldKey
|
||||
newPrefix := newKey
|
||||
if !strings.HasSuffix(oldPrefix, "/") {
|
||||
oldPrefix += "/"
|
||||
}
|
||||
if !strings.HasSuffix(newPrefix, "/") {
|
||||
newPrefix += "/"
|
||||
}
|
||||
|
||||
for {
|
||||
listResult, err := c.ListFiles(ctx, &oss.ListOptions{Prefix: oldPrefix, MaxKeys: 1000})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("列举目录文件失败: %w", err)
|
||||
}
|
||||
if len(listResult.Files) == 0 {
|
||||
break
|
||||
}
|
||||
for _, f := range listResult.Files {
|
||||
relativeKey := strings.TrimPrefix(f.Key, oldPrefix)
|
||||
if err := c.Copy(ctx, f.Key, newPrefix+relativeKey); err != nil {
|
||||
return nil, fmt.Errorf("复制失败: %w", err)
|
||||
}
|
||||
c.Delete(ctx, f.Key)
|
||||
}
|
||||
if !listResult.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Delete(ctx, oldKey) // marker
|
||||
} else {
|
||||
if err := c.Copy(ctx, oldKey, newKey); err != nil {
|
||||
return nil, fmt.Errorf("复制失败: %w", err)
|
||||
}
|
||||
if err := c.Delete(ctx, oldKey); err != nil {
|
||||
return nil, fmt.Errorf("删除源文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
infoMap, _ := s.GetFileInfo(connID, newPath)
|
||||
result := toOssOperationResult(infoMap, isDir)
|
||||
result.OldPath = oldPath
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DownloadToTemp 下载文件到本地临时目录
|
||||
func (s *Service) DownloadToTemp(connID string, rawPath string) (string, error) {
|
||||
bucket, key := parseBucketPath(rawPath)
|
||||
if bucket == "" {
|
||||
return "", fmt.Errorf("路径中缺少桶名")
|
||||
}
|
||||
|
||||
c, err := s.manager.getOrCreateBucketClient(connID, bucket, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp("", "udesk-oss-*-"+path.Base(key))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建临时文件失败: %w", err)
|
||||
}
|
||||
localPath := f.Name()
|
||||
defer f.Close()
|
||||
|
||||
if err := c.Download(context.Background(), key, f); err != nil {
|
||||
os.Remove(localPath)
|
||||
return "", fmt.Errorf("下载文件失败: %w", err)
|
||||
}
|
||||
return localPath, nil
|
||||
}
|
||||
|
||||
// GetCommonPaths 返回常用路径
|
||||
func (s *Service) GetCommonPaths(connID string) (map[string]string, error) {
|
||||
return map[string]string{
|
||||
"root": "/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSignedURL 获取预签名 URL
|
||||
func (s *Service) GetSignedURL(connID string, rawPath string) (string, error) {
|
||||
bucket, key := parseBucketPath(rawPath)
|
||||
if bucket == "" {
|
||||
return "", fmt.Errorf("路径中缺少桶名")
|
||||
}
|
||||
|
||||
c, err := s.manager.getOrCreateBucketClient(connID, bucket, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
url, err := c.GetSignedURL(context.Background(), key, 1*time.Hour)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取签名 URL 失败: %w", err)
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func toOssOperationResult(m map[string]interface{}, isDir bool) *filesystem.FileOperationResult {
|
||||
name, _ := m["name"].(string)
|
||||
p, _ := m["path"].(string)
|
||||
size, _ := m["size"].(int64)
|
||||
modTime, _ := m["mod_time"].(string)
|
||||
|
||||
return &filesystem.FileOperationResult{
|
||||
Path: p,
|
||||
Name: name,
|
||||
Size: size,
|
||||
SizeStr: filesystem.FormatBytes(size),
|
||||
IsDir: isDir,
|
||||
ModTime: modTime,
|
||||
}
|
||||
}
|
||||
221
internal/ossdrv/service_test.go
Normal file
221
internal/ossdrv/service_test.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package ossdrv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func getEnvOrSkip(t *testing.T, key string) string {
|
||||
t.Helper()
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
t.Skipf("跳过:环境变量 %s 未设置", key)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func TestQiniuConnect(t *testing.T) {
|
||||
ak := getEnvOrSkip(t, "QINIU_AK")
|
||||
sk := getEnvOrSkip(t, "QINIU_SK")
|
||||
|
||||
m := &Manager{}
|
||||
err := m.Connect("qiniu", ak, sk, "")
|
||||
if err != nil {
|
||||
t.Fatalf("七牛云连接失败: %v", err)
|
||||
}
|
||||
|
||||
cred, ok := m.accounts.Load("qiniu")
|
||||
if !ok {
|
||||
t.Fatal("凭据未存储")
|
||||
}
|
||||
c := cred.(*accountCredentials)
|
||||
if c.AccessKey != ak {
|
||||
t.Errorf("AccessKey 不匹配: got %s", c.AccessKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQiniuListBuckets(t *testing.T) {
|
||||
ak := getEnvOrSkip(t, "QINIU_AK")
|
||||
sk := getEnvOrSkip(t, "QINIU_SK")
|
||||
|
||||
m := &Manager{}
|
||||
if err := m.Connect("qiniu", ak, sk, ""); err != nil {
|
||||
t.Skipf("跳过:连接失败: %v", err)
|
||||
}
|
||||
|
||||
svc := &Service{manager: m}
|
||||
items, err := svc.ListDir("qiniu", "/")
|
||||
if err != nil {
|
||||
t.Fatalf("列桶失败: %v", err)
|
||||
}
|
||||
if len(items) == 0 {
|
||||
t.Fatal("没有返回任何桶")
|
||||
}
|
||||
t.Logf("七牛云桶数量: %d", len(items))
|
||||
for _, item := range items {
|
||||
t.Logf(" 桶: %s (path=%s, is_dir=%v)", item["name"], item["path"], item["is_dir"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestQiniuBucketListDir(t *testing.T) {
|
||||
ak := getEnvOrSkip(t, "QINIU_AK")
|
||||
sk := getEnvOrSkip(t, "QINIU_SK")
|
||||
|
||||
m := &Manager{}
|
||||
if err := m.Connect("qiniu", ak, sk, ""); err != nil {
|
||||
t.Skipf("跳过:连接失败: %v", err)
|
||||
}
|
||||
|
||||
svc := &Service{manager: m}
|
||||
items, err := svc.ListDir("qiniu", "/")
|
||||
if err != nil || len(items) == 0 {
|
||||
t.Skipf("跳过:无法列桶")
|
||||
}
|
||||
|
||||
bucketName, _ := items[0]["name"].(string)
|
||||
t.Logf("进入桶: %s", bucketName)
|
||||
|
||||
path := "/" + bucketName + "/"
|
||||
files, err := svc.ListDir("qiniu", path)
|
||||
if err != nil {
|
||||
t.Fatalf("列桶内文件失败: %v", err)
|
||||
}
|
||||
t.Logf("桶内文件数量: %d", len(files))
|
||||
for _, f := range files {
|
||||
t.Logf(" %s (is_dir=%v, size=%v)", f["name"], f["is_dir"], f["size"])
|
||||
}
|
||||
|
||||
client := m.GetClient("qiniu", bucketName)
|
||||
if client == nil {
|
||||
t.Error("桶级客户端未缓存")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliyunConnect(t *testing.T) {
|
||||
ak := getEnvOrSkip(t, "ALIYUN_AK")
|
||||
sk := getEnvOrSkip(t, "ALIYUN_SK")
|
||||
ep := os.Getenv("ALIYUN_EP")
|
||||
if ep == "" {
|
||||
ep = "oss-cn-shenzhen.aliyuncs.com"
|
||||
}
|
||||
|
||||
m := &Manager{}
|
||||
err := m.Connect("aliyun", ak, sk, ep)
|
||||
if err != nil {
|
||||
t.Fatalf("阿里云连接失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliyunListBuckets(t *testing.T) {
|
||||
ak := getEnvOrSkip(t, "ALIYUN_AK")
|
||||
sk := getEnvOrSkip(t, "ALIYUN_SK")
|
||||
ep := os.Getenv("ALIYUN_EP")
|
||||
if ep == "" {
|
||||
ep = "oss-cn-shenzhen.aliyuncs.com"
|
||||
}
|
||||
|
||||
m := &Manager{}
|
||||
if err := m.Connect("aliyun", ak, sk, ep); err != nil {
|
||||
t.Skipf("跳过:连接失败: %v", err)
|
||||
}
|
||||
|
||||
svc := &Service{manager: m}
|
||||
items, err := svc.ListDir("aliyun", "/")
|
||||
if err != nil {
|
||||
t.Fatalf("列桶失败: %v", err)
|
||||
}
|
||||
if len(items) == 0 {
|
||||
t.Fatal("没有返回任何桶")
|
||||
}
|
||||
t.Logf("阿里云桶数量: %d", len(items))
|
||||
for _, item := range items {
|
||||
t.Logf(" 桶: %s (path=%s)", item["name"], item["path"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliyunBucketListDir(t *testing.T) {
|
||||
ak := getEnvOrSkip(t, "ALIYUN_AK")
|
||||
sk := getEnvOrSkip(t, "ALIYUN_SK")
|
||||
ep := os.Getenv("ALIYUN_EP")
|
||||
if ep == "" {
|
||||
ep = "oss-cn-shenzhen.aliyuncs.com"
|
||||
}
|
||||
|
||||
m := &Manager{}
|
||||
if err := m.Connect("aliyun", ak, sk, ep); err != nil {
|
||||
t.Skipf("跳过:连接失败: %v", err)
|
||||
}
|
||||
|
||||
svc := &Service{manager: m}
|
||||
items, err := svc.ListDir("aliyun", "/")
|
||||
if err != nil || len(items) == 0 {
|
||||
t.Skipf("跳过:无法列桶")
|
||||
}
|
||||
|
||||
var bucketName string
|
||||
for _, item := range items {
|
||||
if item["name"] == "f-kit" {
|
||||
bucketName = "f-kit"
|
||||
break
|
||||
}
|
||||
}
|
||||
if bucketName == "" {
|
||||
bucketName, _ = items[0]["name"].(string)
|
||||
}
|
||||
t.Logf("进入桶: %s", bucketName)
|
||||
|
||||
path := "/" + bucketName + "/"
|
||||
files, err := svc.ListDir("aliyun", path)
|
||||
if err != nil {
|
||||
t.Fatalf("列桶内文件失败: %v", err)
|
||||
}
|
||||
t.Logf("桶内文件数量: %d", len(files))
|
||||
for _, f := range files {
|
||||
t.Logf(" %s (is_dir=%v)", f["name"], f["is_dir"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBucketPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
wantBucket string
|
||||
wantKey string
|
||||
}{
|
||||
{"/bucket/file.txt", "bucket", "file.txt"},
|
||||
{"/bucket/dir/file.txt", "bucket", "dir/file.txt"},
|
||||
{"/bucket/", "bucket", ""},
|
||||
{"/bucket", "bucket", ""},
|
||||
{"/", "", ""},
|
||||
{"", "", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
bucket, key := parseBucketPath(tt.input)
|
||||
if bucket != tt.wantBucket || key != tt.wantKey {
|
||||
t.Errorf("parseBucketPath(%q) = (%q, %q), want (%q, %q)",
|
||||
tt.input, bucket, key, tt.wantBucket, tt.wantKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnect(t *testing.T) {
|
||||
ak := getEnvOrSkip(t, "QINIU_AK")
|
||||
sk := getEnvOrSkip(t, "QINIU_SK")
|
||||
|
||||
m := &Manager{}
|
||||
if err := m.Connect("qiniu", ak, sk, ""); err != nil {
|
||||
t.Skipf("跳过:连接失败: %v", err)
|
||||
}
|
||||
|
||||
svc := &Service{manager: m}
|
||||
items, _ := svc.ListDir("qiniu", "/")
|
||||
if len(items) > 0 {
|
||||
bucket, _ := items[0]["name"].(string)
|
||||
svc.ListDir("qiniu", "/"+bucket+"/")
|
||||
}
|
||||
|
||||
m.Disconnect("qiniu")
|
||||
|
||||
if _, ok := m.accounts.Load("qiniu"); ok {
|
||||
t.Error("账户凭据未被清除")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user