后端重构

This commit is contained in:
Yvan
2026-01-14 14:34:49 +08:00
parent f5f77b32fe
commit 30842b42e5
274 changed files with 39754 additions and 1005 deletions
+63 -29
View File
@@ -1,12 +1,20 @@
package upload
import (
"errors"
"io"
"mime/multipart"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"kra/pkg/utils"
)
var mu sync.Mutex
// Local 本地存储
type Local struct {
StorePath string
@@ -23,44 +31,70 @@ func NewLocal(storePath, basePath string) *Local {
// UploadFile 上传文件
func (l *Local) UploadFile(file *multipart.FileHeader) (string, string, error) {
// 生成文件名和路径
fileName := GenerateFileName(file.Filename)
filePath := GeneratePath(l.BasePath)
fullPath := path.Join(l.StorePath, filePath)
// 读取文件后缀
ext := filepath.Ext(file.Filename)
// 读取文件名并加密
name := strings.TrimSuffix(file.Filename, ext)
name = utils.MD5V([]byte(name))
// 拼接新文件名
filename := name + "_" + time.Now().Format("20060102150405") + ext
// 创建目录
if err := os.MkdirAll(fullPath, os.ModePerm); err != nil {
return "", "", err
// 尝试创建此路径
mkdirErr := os.MkdirAll(l.StorePath, os.ModePerm)
if mkdirErr != nil {
return "", "", errors.New("function os.MkdirAll() failed, err:" + mkdirErr.Error())
}
// 打开源文件
src, err := file.Open()
if err != nil {
return "", "", err
}
defer src.Close()
// 拼接路径和文件名
p := l.StorePath + "/" + filename
filePath := l.BasePath + "/" + filename
// 创建目标文件
dstPath := path.Join(fullPath, fileName)
dst, err := os.Create(dstPath)
if err != nil {
return "", "", err
f, openError := file.Open() // 读取文件
if openError != nil {
return "", "", errors.New("function file.Open() failed, err:" + openError.Error())
}
defer dst.Close()
defer f.Close()
// 复制文件
if _, err = io.Copy(dst, src); err != nil {
return "", "", err
out, createErr := os.Create(p)
if createErr != nil {
return "", "", errors.New("function os.Create() failed, err:" + createErr.Error())
}
defer out.Close()
// 返回访问路径和存储key
accessPath := path.Join("/", filePath, fileName)
key := path.Join(filePath, fileName)
return accessPath, key, nil
_, copyErr := io.Copy(out, f) // 传输(拷贝)文件
if copyErr != nil {
return "", "", errors.New("function io.Copy() failed, err:" + copyErr.Error())
}
return filePath, filename, nil
}
// DeleteFile 删除文件
func (l *Local) DeleteFile(key string) error {
fullPath := path.Join(l.StorePath, key)
return os.Remove(fullPath)
// 检查 key 是否为空
if key == "" {
return errors.New("key不能为空")
}
// 验证 key 是否包含非法字符或尝试访问存储路径之外的文件
if strings.Contains(key, "..") || strings.ContainsAny(key, `\/:*?"<>|`) {
return errors.New("非法的key")
}
p := filepath.Join(l.StorePath, key)
// 检查文件是否存在
if _, err := os.Stat(p); os.IsNotExist(err) {
return errors.New("文件不存在")
}
// 使用文件锁防止并发删除
mu.Lock()
defer mu.Unlock()
err := os.Remove(p)
if err != nil {
return errors.New("文件删除失败: " + err.Error())
}
return nil
}
+82 -47
View File
@@ -1,84 +1,119 @@
package upload
import (
"bytes"
"context"
"errors"
"io"
"mime"
"mime/multipart"
"path"
"path/filepath"
"strings"
"time"
"kra/pkg/utils"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var MinioClient *Minio // 优化性能,但是不支持动态配置
// Minio MinIO存储
type Minio struct {
ID string
Secret string
Bucket string
Endpoint string
BasePath string
UseSSL bool
Client *minio.Client
Bucket string
BasePath string
BucketURL string
}
// NewMinio 创建MinIO
func NewMinio(id, secret, bucket, endpoint, basePath string, useSSL bool) *Minio {
func NewMinio(accessKeyID, accessKeySecret, bucket, endpoint, basePath string, useSSL bool) *Minio {
if MinioClient != nil {
return MinioClient
}
return &Minio{
ID: id,
Secret: secret,
Bucket: bucket,
Endpoint: endpoint,
BasePath: basePath,
UseSSL: useSSL,
}
}
func (m *Minio) getClient() (*minio.Client, error) {
return minio.New(m.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(m.ID, m.Secret, ""),
Secure: m.UseSSL,
// GetMinio 获取MinIO客户端实例
func GetMinio(endpoint, accessKeyID, accessKeySecret, bucketName string, useSSL bool) (*Minio, error) {
if MinioClient != nil {
return MinioClient, nil
}
// Initialize minio client object.
minioClient, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, accessKeySecret, ""),
Secure: useSSL,
})
if err != nil {
return nil, err
}
// 尝试创建bucket
err = minioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{})
if err != nil {
// Check to see if we already own this bucket (which happens if you run this twice)
exists, errBucketExists := minioClient.BucketExists(context.Background(), bucketName)
if errBucketExists == nil && exists {
// log.Printf("We already own %s\n", bucketName)
} else {
return nil, err
}
}
MinioClient = &Minio{Client: minioClient, Bucket: bucketName}
return MinioClient, nil
}
// UploadFile 上传文件
func (m *Minio) UploadFile(file *multipart.FileHeader) (string, string, error) {
client, err := m.getClient()
f, openError := file.Open()
if openError != nil {
return "", "", errors.New("function file.Open() Failed, err:" + openError.Error())
}
filecontent := bytes.Buffer{}
_, err := io.Copy(&filecontent, f)
if err != nil {
return "", "", err
return "", "", errors.New("读取文件失败, err:" + err.Error())
}
f.Close()
// 对文件名进行加密存储
ext := filepath.Ext(file.Filename)
filename := utils.MD5V([]byte(strings.TrimSuffix(file.Filename, ext))) + ext
var filePathRes string
if m.BasePath == "" {
filePathRes = "uploads" + "/" + time.Now().Format("2006-01-02") + "/" + filename
} else {
filePathRes = m.BasePath + "/" + time.Now().Format("2006-01-02") + "/" + filename
}
// 生成文件名和路径
fileName := GenerateFileName(file.Filename)
filePath := GeneratePath(m.BasePath)
key := path.Join(filePath, fileName)
// 根据文件扩展名检测 MIME 类型
contentType := mime.TypeByExtension(ext)
if contentType == "" {
contentType = "application/octet-stream"
}
// 打开文件
src, err := file.Open()
// 设置超时10分钟
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10)
defer cancel()
// Upload the file with PutObject 大文件自动切换为分片上传
info, err := m.Client.PutObject(ctx, m.Bucket, filePathRes, &filecontent, file.Size, minio.PutObjectOptions{ContentType: contentType})
if err != nil {
return "", "", err
return "", "", errors.New("上传文件到minio失败, err:" + err.Error())
}
defer src.Close()
// 上传
_, err = client.PutObject(context.Background(), m.Bucket, key, src, file.Size, minio.PutObjectOptions{
ContentType: file.Header.Get("Content-Type"),
})
if err != nil {
return "", "", err
}
// 返回访问URL和key
protocol := "http"
if m.UseSSL {
protocol = "https"
}
accessURL := protocol + "://" + m.Endpoint + "/" + m.Bucket + "/" + key
return accessURL, key, nil
return m.BucketURL + "/" + info.Key, filePathRes, nil
}
// DeleteFile 删除文件
func (m *Minio) DeleteFile(key string) error {
client, err := m.getClient()
if err != nil {
return err
}
return client.RemoveObject(context.Background(), m.Bucket, key, minio.RemoveObjectOptions{})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// Delete the object from MinIO
err := m.Client.RemoveObject(ctx, m.Bucket, key, minio.RemoveObjectOptions{})
return err
}
+145
View File
@@ -29,6 +29,151 @@ type Config struct {
MaxSize int64
}
// OSSConfig 完整的OSS配置
type OSSConfig struct {
OssType string
// Local配置
LocalPath string
LocalStorePath string
// 七牛云配置
QiniuZone string
QiniuBucket string
QiniuImgPath string
QiniuAccessKey string
QiniuSecretKey string
QiniuUseHTTPS bool
QiniuUseCdnDomains bool
// 腾讯云COS配置
TencentBucket string
TencentRegion string
TencentSecretID string
TencentSecretKey string
TencentBaseURL string
TencentPathPrefix string
// 阿里云OSS配置
AliyunEndpoint string
AliyunAccessKeyID string
AliyunAccessKeySecret string
AliyunBucketName string
AliyunBucketURL string
AliyunBasePath string
// 华为云OBS配置
HuaweiPath string
HuaweiBucket string
HuaweiEndpoint string
HuaweiAccessKey string
HuaweiSecretKey string
// AWS S3配置
AwsBucket string
AwsRegion string
AwsEndpoint string
AwsSecretID string
AwsSecretKey string
AwsBaseURL string
AwsPathPrefix string
AwsS3ForcePathStyle bool
AwsDisableSSL bool
// Cloudflare R2配置
CloudflareBucket string
CloudflareBaseURL string
CloudflarePath string
CloudflareAccountID string
CloudflareAccessKeyID string
CloudflareSecretAccessKey string
// MinIO配置
MinioEndpoint string
MinioAccessKeyID string
MinioAccessKeySecret string
MinioBucketName string
MinioUseSSL bool
MinioBasePath string
MinioBucketURL string
}
// NewOSS 根据配置创建OSS实例
func NewOSS(cfg *OSSConfig) (OSS, error) {
switch cfg.OssType {
case "local":
return NewLocal(cfg.LocalStorePath, cfg.LocalPath), nil
case "qiniu":
return NewQiniu(
cfg.QiniuZone,
cfg.QiniuBucket,
cfg.QiniuImgPath,
cfg.QiniuAccessKey,
cfg.QiniuSecretKey,
cfg.QiniuUseHTTPS,
cfg.QiniuUseCdnDomains,
), nil
case "tencent-cos":
return NewTencent(
cfg.TencentBucket,
cfg.TencentRegion,
cfg.TencentSecretID,
cfg.TencentSecretKey,
cfg.TencentBaseURL,
cfg.TencentPathPrefix,
), nil
case "aliyun-oss":
return NewAliyun(
cfg.AliyunEndpoint,
cfg.AliyunAccessKeyID,
cfg.AliyunAccessKeySecret,
cfg.AliyunBucketName,
cfg.AliyunBucketURL,
cfg.AliyunBasePath,
), nil
case "huawei-obs":
return NewHuaweiObs(
cfg.HuaweiPath,
cfg.HuaweiBucket,
cfg.HuaweiEndpoint,
cfg.HuaweiAccessKey,
cfg.HuaweiSecretKey,
), nil
case "aws-s3":
return NewAwsS3(
cfg.AwsBucket,
cfg.AwsRegion,
cfg.AwsEndpoint,
cfg.AwsSecretID,
cfg.AwsSecretKey,
cfg.AwsBaseURL,
cfg.AwsPathPrefix,
cfg.AwsS3ForcePathStyle,
cfg.AwsDisableSSL,
), nil
case "cloudflare-r2":
return NewCloudflareR2(
cfg.CloudflareBucket,
cfg.CloudflareBaseURL,
cfg.CloudflarePath,
cfg.CloudflareAccountID,
cfg.CloudflareAccessKeyID,
cfg.CloudflareSecretAccessKey,
), nil
case "minio":
return NewMinio(
cfg.MinioAccessKeyID,
cfg.MinioAccessKeySecret,
cfg.MinioBucketName,
cfg.MinioEndpoint,
cfg.MinioBasePath,
cfg.MinioUseSSL,
), nil
default:
return NewLocal(cfg.LocalStorePath, cfg.LocalPath), nil
}
}
// GenerateFileName 生成文件名
func GenerateFileName(originalName string) string {
ext := path.Ext(originalName)