任务一完成
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mojocn/base64Captcha"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Config 验证码配置
|
||||
type Config struct {
|
||||
KeyLong int
|
||||
ImgWidth int
|
||||
ImgHeight int
|
||||
OpenCaptcha int // 0-关闭 1-开启
|
||||
OpenCaptchaTimeout int // 验证码超时时间(秒)
|
||||
}
|
||||
|
||||
// Captcha 验证码
|
||||
type Captcha struct {
|
||||
config Config
|
||||
store base64Captcha.Store
|
||||
}
|
||||
|
||||
// RedisStore Redis存储
|
||||
type RedisStore struct {
|
||||
client redis.UniversalClient
|
||||
expiration time.Duration
|
||||
prefix string
|
||||
}
|
||||
|
||||
// NewRedisStore 创建Redis存储
|
||||
func NewRedisStore(client redis.UniversalClient, expiration time.Duration) *RedisStore {
|
||||
return &RedisStore{
|
||||
client: client,
|
||||
expiration: expiration,
|
||||
prefix: "captcha:",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RedisStore) Set(id string, value string) error {
|
||||
return s.client.Set(context.Background(), s.prefix+id, value, s.expiration).Err()
|
||||
}
|
||||
|
||||
func (s *RedisStore) Get(id string, clear bool) string {
|
||||
ctx := context.Background()
|
||||
key := s.prefix + id
|
||||
val, err := s.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if clear {
|
||||
s.client.Del(ctx, key)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (s *RedisStore) Verify(id, answer string, clear bool) bool {
|
||||
v := s.Get(id, clear)
|
||||
return v == answer
|
||||
}
|
||||
|
||||
// NewCaptcha 创建验证码实例
|
||||
func NewCaptcha(config Config, store base64Captcha.Store) *Captcha {
|
||||
if store == nil {
|
||||
store = base64Captcha.DefaultMemStore
|
||||
}
|
||||
return &Captcha{
|
||||
config: config,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成验证码
|
||||
func (c *Captcha) Generate() (id, b64s, answer string, err error) {
|
||||
driver := base64Captcha.NewDriverDigit(
|
||||
c.config.ImgHeight,
|
||||
c.config.ImgWidth,
|
||||
c.config.KeyLong,
|
||||
0.7,
|
||||
80,
|
||||
)
|
||||
cp := base64Captcha.NewCaptcha(driver, c.store)
|
||||
return cp.Generate()
|
||||
}
|
||||
|
||||
// Verify 验证验证码
|
||||
func (c *Captcha) Verify(id, answer string, clear bool) bool {
|
||||
return c.store.Verify(id, answer, clear)
|
||||
}
|
||||
|
||||
// IsOpen 是否开启验证码
|
||||
func (c *Captcha) IsOpen() bool {
|
||||
return c.config.OpenCaptcha > 0
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package casbin
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/casbin/casbin/v2"
|
||||
"github.com/casbin/casbin/v2/model"
|
||||
gormadapter "github.com/casbin/gorm-adapter/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
syncedCachedEnforcer *casbin.SyncedCachedEnforcer
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// InitCasbin 初始化Casbin
|
||||
func InitCasbin(db *gorm.DB, modelPath string) (*casbin.SyncedCachedEnforcer, error) {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
adapter, adapterErr := gormadapter.NewAdapterByDB(db)
|
||||
if adapterErr != nil {
|
||||
err = adapterErr
|
||||
return
|
||||
}
|
||||
|
||||
var m model.Model
|
||||
if modelPath != "" {
|
||||
m, err = model.NewModelFromFile(modelPath)
|
||||
} else {
|
||||
// 默认RBAC模型
|
||||
m, err = model.NewModelFromString(`
|
||||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
|
||||
[role_definition]
|
||||
g = _, _
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act
|
||||
`)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
syncedCachedEnforcer, err = casbin.NewSyncedCachedEnforcer(m, adapter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
syncedCachedEnforcer.SetExpireTime(60 * 60)
|
||||
_ = syncedCachedEnforcer.LoadPolicy()
|
||||
})
|
||||
return syncedCachedEnforcer, err
|
||||
}
|
||||
|
||||
// GetEnforcer 获取Enforcer实例
|
||||
func GetEnforcer() *casbin.SyncedCachedEnforcer {
|
||||
return syncedCachedEnforcer
|
||||
}
|
||||
|
||||
// CheckPermission 检查权限
|
||||
func CheckPermission(sub, obj, act string) (bool, error) {
|
||||
if syncedCachedEnforcer == nil {
|
||||
return false, nil
|
||||
}
|
||||
return syncedCachedEnforcer.Enforce(sub, obj, act)
|
||||
}
|
||||
|
||||
// AddPolicy 添加策略
|
||||
func AddPolicy(sub, obj, act string) (bool, error) {
|
||||
return syncedCachedEnforcer.AddPolicy(sub, obj, act)
|
||||
}
|
||||
|
||||
// RemovePolicy 删除策略
|
||||
func RemovePolicy(sub, obj, act string) (bool, error) {
|
||||
return syncedCachedEnforcer.RemovePolicy(sub, obj, act)
|
||||
}
|
||||
|
||||
// GetPoliciesForUser 获取用户的所有策略
|
||||
func GetPoliciesForUser(sub string) [][]string {
|
||||
policies, _ := syncedCachedEnforcer.GetFilteredPolicy(0, sub)
|
||||
return policies
|
||||
}
|
||||
|
||||
// UpdateCasbinApi 更新API权限
|
||||
func UpdateCasbinApi(oldPath, newPath, oldMethod, newMethod string) error {
|
||||
_, err := syncedCachedEnforcer.UpdatePolicy(
|
||||
[]string{"", oldPath, oldMethod},
|
||||
[]string{"", newPath, newMethod},
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTokenExpired = errors.New("token已过期")
|
||||
ErrTokenNotValidYet = errors.New("token尚未激活")
|
||||
ErrTokenMalformed = errors.New("token格式错误")
|
||||
ErrTokenInvalid = errors.New("无效的token")
|
||||
)
|
||||
|
||||
// CustomClaims 自定义Claims
|
||||
type CustomClaims struct {
|
||||
UserID uint `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
BufferTime int64 `json:"bufferTime"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// JWT JWT工具
|
||||
type JWT struct {
|
||||
SigningKey []byte
|
||||
Issuer string
|
||||
ExpiresAt time.Duration
|
||||
BufferTime time.Duration
|
||||
}
|
||||
|
||||
// NewJWT 创建JWT实例
|
||||
func NewJWT(signingKey, issuer string, expiresAt, bufferTime time.Duration) *JWT {
|
||||
return &JWT{
|
||||
SigningKey: []byte(signingKey),
|
||||
Issuer: issuer,
|
||||
ExpiresAt: expiresAt,
|
||||
BufferTime: bufferTime,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateToken 创建token
|
||||
func (j *JWT) CreateToken(userID uint, username string, authorityID uint) (string, error) {
|
||||
claims := CustomClaims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
AuthorityID: authorityID,
|
||||
BufferTime: int64(j.BufferTime / time.Second),
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: j.Issuer,
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(j.ExpiresAt)),
|
||||
NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(j.SigningKey)
|
||||
}
|
||||
|
||||
// ParseToken 解析token
|
||||
func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return j.SigningKey, nil
|
||||
})
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, jwt.ErrTokenExpired):
|
||||
return nil, ErrTokenExpired
|
||||
case errors.Is(err, jwt.ErrTokenMalformed):
|
||||
return nil, ErrTokenMalformed
|
||||
case errors.Is(err, jwt.ErrTokenNotValidYet):
|
||||
return nil, ErrTokenNotValidYet
|
||||
default:
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
}
|
||||
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
|
||||
// NeedRefresh 判断是否需要刷新token
|
||||
func (j *JWT) NeedRefresh(claims *CustomClaims) bool {
|
||||
return claims.ExpiresAt.Time.Sub(time.Now()) < j.BufferTime
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package timer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// Timer 定时任务管理器
|
||||
type Timer struct {
|
||||
cron *cron.Cron
|
||||
tasks map[string]cron.EntryID
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewTimer 创建定时任务管理器
|
||||
func NewTimer() *Timer {
|
||||
return &Timer{
|
||||
cron: cron.New(cron.WithSeconds()),
|
||||
tasks: make(map[string]cron.EntryID),
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动定时任务
|
||||
func (t *Timer) Start() {
|
||||
t.cron.Start()
|
||||
}
|
||||
|
||||
// Stop 停止定时任务
|
||||
func (t *Timer) Stop() {
|
||||
t.cron.Stop()
|
||||
}
|
||||
|
||||
// AddTask 添加定时任务
|
||||
func (t *Timer) AddTask(name, spec string, cmd func()) error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
// 如果任务已存在,先删除
|
||||
if id, ok := t.tasks[name]; ok {
|
||||
t.cron.Remove(id)
|
||||
}
|
||||
|
||||
id, err := t.cron.AddFunc(spec, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.tasks[name] = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTask 删除定时任务
|
||||
func (t *Timer) RemoveTask(name string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if id, ok := t.tasks[name]; ok {
|
||||
t.cron.Remove(id)
|
||||
delete(t.tasks, name)
|
||||
}
|
||||
}
|
||||
|
||||
// HasTask 检查任务是否存在
|
||||
func (t *Timer) HasTask(name string) bool {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
_, ok := t.tasks[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ListTasks 列出所有任务
|
||||
func (t *Timer) ListTasks() []string {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
names := make([]string, 0, len(t.tasks))
|
||||
for name := range t.tasks {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"path"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
)
|
||||
|
||||
// Aliyun 阿里云OSS
|
||||
type Aliyun struct {
|
||||
Endpoint string
|
||||
AccessKeyID string
|
||||
AccessKeySecret string
|
||||
BucketName string
|
||||
BucketURL string
|
||||
BasePath string
|
||||
}
|
||||
|
||||
// NewAliyun 创建阿里云OSS
|
||||
func NewAliyun(endpoint, accessKeyID, accessKeySecret, bucketName, bucketURL, basePath string) *Aliyun {
|
||||
return &Aliyun{
|
||||
Endpoint: endpoint,
|
||||
AccessKeyID: accessKeyID,
|
||||
AccessKeySecret: accessKeySecret,
|
||||
BucketName: bucketName,
|
||||
BucketURL: bucketURL,
|
||||
BasePath: basePath,
|
||||
}
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
func (a *Aliyun) Upload(file *multipart.FileHeader) (string, string, error) {
|
||||
client, err := oss.New(a.Endpoint, a.AccessKeyID, a.AccessKeySecret)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
bucket, err := client.Bucket(a.BucketName)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 生成文件名和路径
|
||||
fileName := GenerateFileName(file.Filename)
|
||||
filePath := GeneratePath(a.BasePath)
|
||||
key := path.Join(filePath, fileName)
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 上传
|
||||
if err = bucket.PutObject(key, src); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 返回访问URL和key
|
||||
accessURL := a.BucketURL + "/" + key
|
||||
return accessURL, key, nil
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
func (a *Aliyun) Delete(key string) error {
|
||||
client, err := oss.New(a.Endpoint, a.AccessKeyID, a.AccessKeySecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bucket, err := client.Bucket(a.BucketName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.DeleteObject(key)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mime/multipart"
|
||||
"path"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
)
|
||||
|
||||
// AwsS3 AWS S3存储
|
||||
type AwsS3 struct {
|
||||
Bucket string
|
||||
Region string
|
||||
Endpoint string
|
||||
SecretID string
|
||||
SecretKey string
|
||||
BaseURL string
|
||||
PathPrefix string
|
||||
S3ForcePathStyle bool
|
||||
DisableSSL bool
|
||||
}
|
||||
|
||||
// NewAwsS3 创建AWS S3
|
||||
func NewAwsS3(bucket, region, endpoint, secretID, secretKey, baseURL, pathPrefix string, s3ForcePathStyle, disableSSL bool) *AwsS3 {
|
||||
return &AwsS3{
|
||||
Bucket: bucket,
|
||||
Region: region,
|
||||
Endpoint: endpoint,
|
||||
SecretID: secretID,
|
||||
SecretKey: secretKey,
|
||||
BaseURL: baseURL,
|
||||
PathPrefix: pathPrefix,
|
||||
S3ForcePathStyle: s3ForcePathStyle,
|
||||
DisableSSL: disableSSL,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AwsS3) getClient(ctx context.Context) (*s3.Client, error) {
|
||||
cfg, err := config.LoadDefaultConfig(ctx,
|
||||
config.WithRegion(a.Region),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(a.SecretID, a.SecretKey, "")),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
||||
if a.Endpoint != "" {
|
||||
o.BaseEndpoint = aws.String(a.Endpoint)
|
||||
}
|
||||
o.UsePathStyle = a.S3ForcePathStyle
|
||||
})
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
func (a *AwsS3) Upload(file *multipart.FileHeader) (string, string, error) {
|
||||
ctx := context.Background()
|
||||
client, err := a.getClient(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 生成文件名和路径
|
||||
fileName := GenerateFileName(file.Filename)
|
||||
filePath := GeneratePath(a.PathPrefix)
|
||||
key := path.Join(filePath, fileName)
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 上传
|
||||
contentType := file.Header.Get("Content-Type")
|
||||
_, err = client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(a.Bucket),
|
||||
Key: aws.String(key),
|
||||
Body: src,
|
||||
ContentType: aws.String(contentType),
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 返回访问URL和key
|
||||
accessURL := a.BaseURL + "/" + key
|
||||
return accessURL, key, nil
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
func (a *AwsS3) Delete(key string) error {
|
||||
ctx := context.Background()
|
||||
client, err := a.getClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(a.Bucket),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"path"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
)
|
||||
|
||||
// CloudflareR2 Cloudflare R2存储
|
||||
type CloudflareR2 struct {
|
||||
Bucket string
|
||||
BaseURL string
|
||||
Path string
|
||||
AccountID string
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
}
|
||||
|
||||
// NewCloudflareR2 创建Cloudflare R2
|
||||
func NewCloudflareR2(bucket, baseURL, r2Path, accountID, accessKeyID, secretAccessKey string) *CloudflareR2 {
|
||||
return &CloudflareR2{
|
||||
Bucket: bucket,
|
||||
BaseURL: baseURL,
|
||||
Path: r2Path,
|
||||
AccountID: accountID,
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CloudflareR2) getClient(ctx context.Context) (*s3.Client, error) {
|
||||
endpoint := fmt.Sprintf("https://%s.r2.cloudflarestorage.com", c.AccountID)
|
||||
|
||||
cfg, err := config.LoadDefaultConfig(ctx,
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(c.AccessKeyID, c.SecretAccessKey, "")),
|
||||
config.WithRegion("auto"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
||||
o.BaseEndpoint = aws.String(endpoint)
|
||||
})
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
func (c *CloudflareR2) Upload(file *multipart.FileHeader) (string, string, error) {
|
||||
ctx := context.Background()
|
||||
client, err := c.getClient(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 生成文件名和路径
|
||||
fileName := GenerateFileName(file.Filename)
|
||||
filePath := GeneratePath(c.Path)
|
||||
key := path.Join(filePath, fileName)
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 上传
|
||||
contentType := file.Header.Get("Content-Type")
|
||||
_, err = client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(c.Bucket),
|
||||
Key: aws.String(key),
|
||||
Body: src,
|
||||
ContentType: aws.String(contentType),
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 返回访问URL和key
|
||||
accessURL := c.BaseURL + "/" + key
|
||||
return accessURL, key, nil
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
func (c *CloudflareR2) Delete(key string) error {
|
||||
ctx := context.Background()
|
||||
client, err := c.getClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(c.Bucket),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"path"
|
||||
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-obs/obs"
|
||||
)
|
||||
|
||||
// HuaweiObs 华为云OBS
|
||||
type HuaweiObs struct {
|
||||
Path string
|
||||
Bucket string
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
// NewHuaweiObs 创建华为云OBS
|
||||
func NewHuaweiObs(obsPath, bucket, endpoint, accessKey, secretKey string) *HuaweiObs {
|
||||
return &HuaweiObs{
|
||||
Path: obsPath,
|
||||
Bucket: bucket,
|
||||
Endpoint: endpoint,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HuaweiObs) getClient() (*obs.ObsClient, error) {
|
||||
return obs.New(h.AccessKey, h.SecretKey, h.Endpoint)
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
func (h *HuaweiObs) Upload(file *multipart.FileHeader) (string, string, error) {
|
||||
client, err := h.getClient()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// 生成文件名和路径
|
||||
fileName := GenerateFileName(file.Filename)
|
||||
filePath := GeneratePath(h.Path)
|
||||
key := path.Join(filePath, fileName)
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 上传
|
||||
input := &obs.PutObjectInput{}
|
||||
input.Bucket = h.Bucket
|
||||
input.Key = key
|
||||
input.Body = src
|
||||
_, err = client.PutObject(input)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 返回访问URL和key
|
||||
accessURL := "https://" + h.Bucket + "." + h.Endpoint + "/" + key
|
||||
return accessURL, key, nil
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
func (h *HuaweiObs) Delete(key string) error {
|
||||
client, err := h.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
input := &obs.DeleteObjectInput{}
|
||||
input.Bucket = h.Bucket
|
||||
input.Key = key
|
||||
_, err = client.DeleteObject(input)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
// Local 本地存储
|
||||
type Local struct {
|
||||
StorePath string
|
||||
BasePath string
|
||||
}
|
||||
|
||||
// NewLocal 创建本地存储
|
||||
func NewLocal(storePath, basePath string) *Local {
|
||||
return &Local{
|
||||
StorePath: storePath,
|
||||
BasePath: basePath,
|
||||
}
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
func (l *Local) Upload(file *multipart.FileHeader) (string, string, error) {
|
||||
// 生成文件名和路径
|
||||
fileName := GenerateFileName(file.Filename)
|
||||
filePath := GeneratePath(l.BasePath)
|
||||
fullPath := path.Join(l.StorePath, filePath)
|
||||
|
||||
// 创建目录
|
||||
if err := os.MkdirAll(fullPath, os.ModePerm); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 打开源文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 创建目标文件
|
||||
dstPath := path.Join(fullPath, fileName)
|
||||
dst, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// 复制文件
|
||||
if _, err = io.Copy(dst, src); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 返回访问路径和存储key
|
||||
accessPath := path.Join("/", filePath, fileName)
|
||||
key := path.Join(filePath, fileName)
|
||||
return accessPath, key, nil
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
func (l *Local) Delete(key string) error {
|
||||
fullPath := path.Join(l.StorePath, key)
|
||||
return os.Remove(fullPath)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mime/multipart"
|
||||
"path"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
// Minio MinIO存储
|
||||
type Minio struct {
|
||||
ID string
|
||||
Secret string
|
||||
Bucket string
|
||||
Endpoint string
|
||||
BasePath string
|
||||
UseSSL bool
|
||||
}
|
||||
|
||||
// NewMinio 创建MinIO
|
||||
func NewMinio(id, secret, bucket, endpoint, basePath string, useSSL bool) *Minio {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
func (m *Minio) Upload(file *multipart.FileHeader) (string, string, error) {
|
||||
client, err := m.getClient()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 生成文件名和路径
|
||||
fileName := GenerateFileName(file.Filename)
|
||||
filePath := GeneratePath(m.BasePath)
|
||||
key := path.Join(filePath, fileName)
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
func (m *Minio) Delete(key string) error {
|
||||
client, err := m.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.RemoveObject(context.Background(), m.Bucket, key, minio.RemoveObjectOptions{})
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mime/multipart"
|
||||
"path"
|
||||
|
||||
"github.com/qiniu/go-sdk/v7/auth/qbox"
|
||||
"github.com/qiniu/go-sdk/v7/storage"
|
||||
)
|
||||
|
||||
// Qiniu 七牛云
|
||||
type Qiniu struct {
|
||||
Zone string
|
||||
Bucket string
|
||||
ImgPath string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
UseHTTPS bool
|
||||
UseCdnDomains bool
|
||||
}
|
||||
|
||||
// NewQiniu 创建七牛云
|
||||
func NewQiniu(zone, bucket, imgPath, accessKey, secretKey string, useHTTPS, useCdnDomains bool) *Qiniu {
|
||||
return &Qiniu{
|
||||
Zone: zone,
|
||||
Bucket: bucket,
|
||||
ImgPath: imgPath,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
UseHTTPS: useHTTPS,
|
||||
UseCdnDomains: useCdnDomains,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Qiniu) getZone() *storage.Region {
|
||||
switch q.Zone {
|
||||
case "ZoneHuadong":
|
||||
return &storage.ZoneHuadong
|
||||
case "ZoneHuabei":
|
||||
return &storage.ZoneHuabei
|
||||
case "ZoneHuanan":
|
||||
return &storage.ZoneHuanan
|
||||
case "ZoneBeimei":
|
||||
return &storage.ZoneBeimei
|
||||
case "ZoneXinjiapo":
|
||||
return &storage.ZoneXinjiapo
|
||||
default:
|
||||
return &storage.ZoneHuadong
|
||||
}
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
func (q *Qiniu) Upload(file *multipart.FileHeader) (string, string, error) {
|
||||
mac := qbox.NewMac(q.AccessKey, q.SecretKey)
|
||||
putPolicy := storage.PutPolicy{Scope: q.Bucket}
|
||||
upToken := putPolicy.UploadToken(mac)
|
||||
|
||||
cfg := storage.Config{
|
||||
Zone: q.getZone(),
|
||||
UseHTTPS: q.UseHTTPS,
|
||||
UseCdnDomains: q.UseCdnDomains,
|
||||
}
|
||||
formUploader := storage.NewFormUploader(&cfg)
|
||||
|
||||
// 生成文件名和路径
|
||||
fileName := GenerateFileName(file.Filename)
|
||||
filePath := GeneratePath("")
|
||||
key := path.Join(filePath, fileName)
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 上传
|
||||
ret := storage.PutRet{}
|
||||
err = formUploader.Put(context.Background(), &ret, upToken, key, src, file.Size, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 返回访问URL和key
|
||||
accessURL := q.ImgPath + "/" + key
|
||||
return accessURL, key, nil
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
func (q *Qiniu) Delete(key string) error {
|
||||
mac := qbox.NewMac(q.AccessKey, q.SecretKey)
|
||||
cfg := storage.Config{Zone: q.getZone()}
|
||||
bucketManager := storage.NewBucketManager(mac, &cfg)
|
||||
return bucketManager.Delete(q.Bucket, key)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/tencentyun/cos-go-sdk-v5"
|
||||
)
|
||||
|
||||
// Tencent 腾讯云COS
|
||||
type Tencent struct {
|
||||
Bucket string
|
||||
Region string
|
||||
SecretID string
|
||||
SecretKey string
|
||||
BaseURL string
|
||||
PathPrefix string
|
||||
}
|
||||
|
||||
// NewTencent 创建腾讯云COS
|
||||
func NewTencent(bucket, region, secretID, secretKey, baseURL, pathPrefix string) *Tencent {
|
||||
return &Tencent{
|
||||
Bucket: bucket,
|
||||
Region: region,
|
||||
SecretID: secretID,
|
||||
SecretKey: secretKey,
|
||||
BaseURL: baseURL,
|
||||
PathPrefix: pathPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tencent) getClient() *cos.Client {
|
||||
u, _ := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", t.Bucket, t.Region))
|
||||
b := &cos.BaseURL{BucketURL: u}
|
||||
return cos.NewClient(b, &http.Client{
|
||||
Transport: &cos.AuthorizationTransport{
|
||||
SecretID: t.SecretID,
|
||||
SecretKey: t.SecretKey,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
func (t *Tencent) Upload(file *multipart.FileHeader) (string, string, error) {
|
||||
client := t.getClient()
|
||||
|
||||
// 生成文件名和路径
|
||||
fileName := GenerateFileName(file.Filename)
|
||||
filePath := GeneratePath(t.PathPrefix)
|
||||
key := path.Join(filePath, fileName)
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 上传
|
||||
_, err = client.Object.Put(context.Background(), key, src, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 返回访问URL和key
|
||||
accessURL := t.BaseURL + "/" + key
|
||||
return accessURL, key, nil
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
func (t *Tencent) Delete(key string) error {
|
||||
client := t.getClient()
|
||||
_, err := client.Object.Delete(context.Background(), key)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFileEmpty = errors.New("文件为空")
|
||||
ErrFileTooLarge = errors.New("文件过大")
|
||||
ErrFileTypeError = errors.New("文件类型不允许")
|
||||
)
|
||||
|
||||
// OSS 对象存储接口
|
||||
type OSS interface {
|
||||
Upload(file *multipart.FileHeader) (string, string, error)
|
||||
Delete(key string) error
|
||||
}
|
||||
|
||||
// Config 上传配置
|
||||
type Config struct {
|
||||
Type string // local, aliyun, tencent, qiniu, minio, aws-s3, huawei-obs, cloudflare-r2
|
||||
Path string
|
||||
MaxSize int64
|
||||
}
|
||||
|
||||
// GenerateFileName 生成文件名
|
||||
func GenerateFileName(originalName string) string {
|
||||
ext := path.Ext(originalName)
|
||||
name := uuid.New().String()
|
||||
return name + ext
|
||||
}
|
||||
|
||||
// GeneratePath 生成存储路径
|
||||
func GeneratePath(basePath string) string {
|
||||
now := time.Now()
|
||||
return path.Join(basePath, now.Format("2006/01/02"))
|
||||
}
|
||||
|
||||
// CheckFileSize 检查文件大小
|
||||
func CheckFileSize(size int64, maxSize int64) error {
|
||||
if size > maxSize {
|
||||
return ErrFileTooLarge
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckFileType 检查文件类型
|
||||
func CheckFileType(filename string, allowTypes []string) error {
|
||||
ext := strings.ToLower(path.Ext(filename))
|
||||
for _, t := range allowTypes {
|
||||
if ext == t || t == "*" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrFileTypeError
|
||||
}
|
||||
|
||||
// DefaultAllowTypes 默认允许的文件类型
|
||||
var DefaultAllowTypes = []string{
|
||||
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp",
|
||||
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
||||
".txt", ".md", ".json", ".xml", ".csv",
|
||||
".zip", ".rar", ".7z", ".tar", ".gz",
|
||||
".mp3", ".mp4", ".avi", ".mov", ".wmv",
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
breakpointDir = "./breakpointDir/"
|
||||
finishDir = "./fileDir/"
|
||||
)
|
||||
|
||||
// BreakPointContinue 断点续传
|
||||
// content: 文件片段内容
|
||||
// fileName: 文件名
|
||||
// contentNumber: 当前片段序号
|
||||
// contentTotal: 总片段数
|
||||
// fileMd5: 文件MD5
|
||||
func BreakPointContinue(content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string) (string, error) {
|
||||
path := breakpointDir + fileMd5 + "/"
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
pathC, err := makeFileContent(content, fileName, path, contentNumber)
|
||||
return pathC, err
|
||||
}
|
||||
|
||||
// CheckMd5 检查Md5
|
||||
func CheckMd5(content []byte, chunkMd5 string) bool {
|
||||
fileMd5 := MD5V(content)
|
||||
return fileMd5 == chunkMd5
|
||||
}
|
||||
|
||||
// makeFileContent 创建切片内容
|
||||
func makeFileContent(content []byte, fileName string, FileDir string, contentNumber int) (string, error) {
|
||||
if strings.Contains(fileName, "..") || strings.Contains(FileDir, "..") {
|
||||
return "", errors.New("文件名或路径不合法")
|
||||
}
|
||||
path := FileDir + fileName + "_" + strconv.Itoa(contentNumber)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.Write(content)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// MakeFile 合并切片文件
|
||||
func MakeFile(fileName string, FileMd5 string) (string, error) {
|
||||
rd, err := os.ReadDir(breakpointDir + FileMd5)
|
||||
if err != nil {
|
||||
return finishDir + fileName, err
|
||||
}
|
||||
_ = os.MkdirAll(finishDir, os.ModePerm)
|
||||
fd, err := os.OpenFile(finishDir+fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return finishDir + fileName, err
|
||||
}
|
||||
defer fd.Close()
|
||||
for k := range rd {
|
||||
content, _ := os.ReadFile(breakpointDir + FileMd5 + "/" + fileName + "_" + strconv.Itoa(k))
|
||||
_, err = fd.Write(content)
|
||||
if err != nil {
|
||||
_ = os.Remove(finishDir + fileName)
|
||||
return finishDir + fileName, err
|
||||
}
|
||||
}
|
||||
return finishDir + fileName, nil
|
||||
}
|
||||
|
||||
// RemoveChunk 移除切片
|
||||
func RemoveChunk(FileMd5 string) error {
|
||||
return os.RemoveAll(breakpointDir + FileMd5)
|
||||
}
|
||||
|
||||
// GetBreakpointDir 获取断点续传目录
|
||||
func GetBreakpointDir() string {
|
||||
return breakpointDir
|
||||
}
|
||||
|
||||
// GetFinishDir 获取完成文件目录
|
||||
func GetFinishDir() string {
|
||||
return finishDir
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"strconv"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// GetColumnName 获取Excel列名 (1->A, 2->B, ..., 27->AA)
|
||||
func GetColumnName(n int) string {
|
||||
columnName := ""
|
||||
for n > 0 {
|
||||
n--
|
||||
columnName = string(rune('A'+n%26)) + columnName
|
||||
n /= 26
|
||||
}
|
||||
return columnName
|
||||
}
|
||||
|
||||
// CreateExcelFile 创建Excel文件
|
||||
func CreateExcelFile() *excelize.File {
|
||||
return excelize.NewFile()
|
||||
}
|
||||
|
||||
// OpenExcelFile 打开Excel文件
|
||||
func OpenExcelFile(file *multipart.FileHeader) (*excelize.File, error) {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer src.Close()
|
||||
return excelize.OpenReader(src)
|
||||
}
|
||||
|
||||
// WriteExcelRows 写入Excel行数据
|
||||
func WriteExcelRows(f *excelize.File, sheetName string, rows [][]interface{}) error {
|
||||
for i, row := range rows {
|
||||
for j, cell := range row {
|
||||
cellName := fmt.Sprintf("%s%d", GetColumnName(j+1), i+1)
|
||||
if err := f.SetCellValue(sheetName, cellName, cell); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteExcelRow 写入单行数据
|
||||
func WriteExcelRow(f *excelize.File, sheetName string, rowIndex int, row []interface{}) error {
|
||||
for j, cell := range row {
|
||||
cellName := fmt.Sprintf("%s%d", GetColumnName(j+1), rowIndex)
|
||||
if err := f.SetCellValue(sheetName, cellName, cell); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExcelToBuffer 将Excel文件写入Buffer
|
||||
func ExcelToBuffer(f *excelize.File) (*bytes.Buffer, error) {
|
||||
return f.WriteToBuffer()
|
||||
}
|
||||
|
||||
// ReadExcelRows 读取Excel所有行
|
||||
func ReadExcelRows(f *excelize.File, sheetName string) ([][]string, error) {
|
||||
return f.GetRows(sheetName)
|
||||
}
|
||||
|
||||
// SetCellValueAuto 自动类型设置单元格值
|
||||
func SetCellValueAuto(f *excelize.File, sheetName, cell string, value string) error {
|
||||
// 尝试解析为数字
|
||||
if v, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return f.SetCellValue(sheetName, cell, v)
|
||||
}
|
||||
if v, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||
return f.SetCellValue(sheetName, cell, v)
|
||||
}
|
||||
return f.SetCellValue(sheetName, cell, value)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// MD5V 计算字节数组的MD5值
|
||||
func MD5V(data []byte) string {
|
||||
h := md5.New()
|
||||
h.Write(data)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// MD5String 计算字符串的MD5值
|
||||
func MD5String(s string) string {
|
||||
return MD5V([]byte(s))
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ZipFiles 压缩文件
|
||||
func ZipFiles(filename string, files []string, oldForm, newForm string) error {
|
||||
newZipFile, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer newZipFile.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(newZipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, file := range files {
|
||||
if err = addFileToZip(zipWriter, file, oldForm, newForm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addFileToZip(zipWriter *zip.Writer, filename, oldForm, newForm string) error {
|
||||
fileToZip, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileToZip.Close()
|
||||
|
||||
info, err := fileToZip.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header.Name = strings.Replace(filename, oldForm, newForm, 1)
|
||||
header.Method = zip.Deflate
|
||||
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(writer, fileToZip)
|
||||
return err
|
||||
}
|
||||
|
||||
// Unzip 解压文件
|
||||
func Unzip(src, dest string) ([]string, error) {
|
||||
var filenames []string
|
||||
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
fpath := filepath.Join(dest, f.Name)
|
||||
|
||||
// 安全检查:防止路径遍历攻击
|
||||
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||||
continue
|
||||
}
|
||||
|
||||
filenames = append(filenames, fpath)
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(fpath, os.ModePerm)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
outFile.Close()
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, rc)
|
||||
outFile.Close()
|
||||
rc.Close()
|
||||
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
}
|
||||
return filenames, nil
|
||||
}
|
||||
Reference in New Issue
Block a user