首次提交:初始化项目代码
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package auth
|
||||
|
||||
// RegisterRequest represents the request body for user registration.
|
||||
// RegisterRequest 表示用户注册的请求体。
|
||||
//
|
||||
// Fields:
|
||||
// - Username: The desired username (3-50 characters).
|
||||
// - Password: The desired password (minimum 6 characters).
|
||||
//
|
||||
// Validation:
|
||||
// - Username is required and must be 3-50 characters.
|
||||
// - Password is required and must be at least 6 characters.
|
||||
//
|
||||
// 字段:
|
||||
// - Username: 期望的用户名(3-50 个字符)。
|
||||
// - Password: 期望的密码(最少 6 个字符)。
|
||||
//
|
||||
// 验证规则:
|
||||
// - Username 是必填项,必须是 3-50 个字符。
|
||||
// - Password 是必填项,必须至少 6 个字符。
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=50"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
CaptchaID string `json:"captcha_id" binding:"required"`
|
||||
CaptchaAnswer string `json:"captcha_answer" binding:"required"`
|
||||
EmailCode string `json:"email_code"` // 邮箱注册时必填
|
||||
}
|
||||
|
||||
// LoginRequest represents the request body for user login.
|
||||
// LoginRequest 表示用户登录的请求体。
|
||||
//
|
||||
// Fields:
|
||||
// - Username: The user's username.
|
||||
// - Password: The user's password.
|
||||
//
|
||||
// Validation:
|
||||
// - Both fields are required.
|
||||
//
|
||||
// 字段:
|
||||
// - Username: 用户的用户名。
|
||||
// - Password: 用户的密码。
|
||||
//
|
||||
// 验证规则:
|
||||
// - 两个字段都是必填项。
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
CaptchaID string `json:"captcha_id" binding:"required"`
|
||||
CaptchaAnswer string `json:"captcha_answer" binding:"required"`
|
||||
}
|
||||
|
||||
// TokenResponse represents the response body for successful login.
|
||||
// TokenResponse 表示成功登录的响应体。
|
||||
//
|
||||
// Fields:
|
||||
// - Token: The JWT access token.
|
||||
//
|
||||
// 字段:
|
||||
// - Token: JWT 访问令牌。
|
||||
type TokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// UpdateProfileRequest represents the request body for updating user profile.
|
||||
// UpdateProfileRequest 表示更新用户个人信息的请求体。
|
||||
//
|
||||
// Fields:
|
||||
// - Email: The user's email address.
|
||||
// - Phone: The user's phone number.
|
||||
//
|
||||
// 字段:
|
||||
// - Email: 用户的邮箱地址。
|
||||
// - Phone: 用户的手机号码。
|
||||
type UpdateProfileRequest struct {
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest represents the request body for changing password.
|
||||
// ChangePasswordRequest 表示修改密码的请求体。
|
||||
//
|
||||
// Fields:
|
||||
// - CurrentPassword: The user's current password.
|
||||
// - NewPassword: The new password.
|
||||
//
|
||||
// 字段:
|
||||
// - CurrentPassword: 用户当前的密码。
|
||||
// - NewPassword: 新密码。
|
||||
type ChangePasswordRequest struct {
|
||||
CurrentPassword string `json:"currentPassword" binding:"required"`
|
||||
NewPassword string `json:"newPassword" binding:"required,min=6"`
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloudnest/internal/domain/auth/entity"
|
||||
"cloudnest/internal/domain/auth/repository"
|
||||
"cloudnest/internal/pkg/crypto"
|
||||
"cloudnest/internal/pkg/errors"
|
||||
"cloudnest/internal/pkg/jwt"
|
||||
)
|
||||
|
||||
type EmailVerifier interface {
|
||||
VerifyCode(email, code string) bool
|
||||
}
|
||||
|
||||
type AuthService struct {
|
||||
userRepo repository.UserRepository
|
||||
emailService EmailVerifier
|
||||
jwtSecret string
|
||||
jwtExpires int
|
||||
}
|
||||
|
||||
func NewAuthService(userRepo repository.UserRepository, emailService EmailVerifier, jwtSecret string, jwtExpires int) *AuthService {
|
||||
return &AuthService{
|
||||
userRepo: userRepo,
|
||||
emailService: emailService,
|
||||
jwtSecret: jwtSecret,
|
||||
jwtExpires: jwtExpires,
|
||||
}
|
||||
}
|
||||
|
||||
func generateUserCode() string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
b := make([]byte, 8)
|
||||
for i := range b {
|
||||
b[i] = charset[r.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (s *AuthService) Register(username, password, email, nickname, emailCode string) error {
|
||||
existing, err := s.userRepo.FindByUsername(username)
|
||||
if err == nil && existing != nil {
|
||||
return errors.Conflict("用户名已存在")
|
||||
}
|
||||
|
||||
if email != "" {
|
||||
existing, err := s.userRepo.FindByEmail(email)
|
||||
if err == nil && existing != nil {
|
||||
return errors.Conflict("邮箱已被注册")
|
||||
}
|
||||
if emailCode == "" {
|
||||
return errors.BadRequest("邮箱验证码不能为空")
|
||||
}
|
||||
if !s.emailService.VerifyCode(email, emailCode) {
|
||||
return errors.BadRequest("邮箱验证码错误")
|
||||
}
|
||||
}
|
||||
|
||||
hashed, err := crypto.HashPassword(password)
|
||||
if err != nil {
|
||||
return errors.InternalError("密码加密失败", err)
|
||||
}
|
||||
|
||||
userCode := generateUserCode()
|
||||
for {
|
||||
existing, err := s.userRepo.FindByUserCode(userCode)
|
||||
if err != nil || existing == nil {
|
||||
break
|
||||
}
|
||||
userCode = generateUserCode()
|
||||
}
|
||||
|
||||
user := entity.NewUser(username, hashed, userCode, nickname)
|
||||
if email != "" {
|
||||
user.Email = email
|
||||
}
|
||||
return s.userRepo.Create(user)
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(loginName, password string) (string, *entity.User, error) {
|
||||
var user *entity.User
|
||||
var err error
|
||||
|
||||
if strings.Contains(loginName, "@") {
|
||||
user, err = s.userRepo.FindByEmail(loginName)
|
||||
} else {
|
||||
user, err = s.userRepo.FindByUsername(loginName)
|
||||
}
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if !crypto.CheckPasswordHash(password, user.Password) {
|
||||
return "", nil, errors.Unauthorized("用户名或密码错误")
|
||||
}
|
||||
|
||||
expiresIn := time.Duration(s.jwtExpires) * time.Hour
|
||||
token, err := jwt.GenerateToken(user.ID, user.Username, user.UserCode, s.jwtSecret, expiresIn)
|
||||
return token, user, err
|
||||
}
|
||||
|
||||
func (s *AuthService) CheckStorageLimit(userCode string, fileSize int64) error {
|
||||
user, err := s.userRepo.FindByUserCode(userCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.StorageUsed+fileSize > user.StorageLimit {
|
||||
return errors.BadRequest("存储空间不足,当前已使用 " + formatSize(user.StorageUsed) + ",限制 " + formatSize(user.StorageLimit))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthService) AddStorageUsed(userCode string, fileSize int64) error {
|
||||
return s.userRepo.UpdateStorageUsed(userCode, fileSize)
|
||||
}
|
||||
|
||||
func (s *AuthService) SubtractStorageUsed(userCode string, fileSize int64) error {
|
||||
return s.userRepo.UpdateStorageUsed(userCode, -fileSize)
|
||||
}
|
||||
|
||||
func (s *AuthService) UpdateProfile(userCode, email, phone, nickname string) error {
|
||||
return s.userRepo.UpdateProfile(userCode, email, phone, nickname)
|
||||
}
|
||||
|
||||
func (s *AuthService) ChangePassword(userCode, currentPassword, newPassword string) error {
|
||||
user, err := s.userRepo.FindByUserCode(userCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !crypto.CheckPasswordHash(currentPassword, user.Password) {
|
||||
return errors.Unauthorized("当前密码不正确")
|
||||
}
|
||||
hashed, err := crypto.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return errors.InternalError("密码加密失败", err)
|
||||
}
|
||||
return s.userRepo.UpdatePassword(userCode, hashed)
|
||||
}
|
||||
|
||||
func (s *AuthService) GetUserInfo(userCode string) (*entity.User, error) {
|
||||
return s.userRepo.FindByUserCode(userCode)
|
||||
}
|
||||
|
||||
func formatSize(bytes int64) string {
|
||||
if bytes < 1024 {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
} else if bytes < 1024*1024 {
|
||||
return fmt.Sprintf("%.2f KB", float64(bytes)/1024)
|
||||
} else if bytes < 1024*1024*1024 {
|
||||
return fmt.Sprintf("%.2f MB", float64(bytes)/(1024*1024))
|
||||
}
|
||||
return fmt.Sprintf("%.2f GB", float64(bytes)/(1024*1024*1024))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"cloudnest/internal/infrastructure/database"
|
||||
"github.com/dchest/captcha"
|
||||
)
|
||||
|
||||
type CaptchaService struct {
|
||||
redis *database.RedisClient
|
||||
}
|
||||
|
||||
func NewCaptchaService(redis *database.RedisClient) *CaptchaService {
|
||||
captcha.SetCustomStore(captcha.NewMemoryStore(100, 5*60*1e9))
|
||||
return &CaptchaService{redis: redis}
|
||||
}
|
||||
|
||||
func (s *CaptchaService) Generate() (captchaID string, imageBytes []byte, err error) {
|
||||
captchaID = captcha.New()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err = captcha.WriteImage(&buf, captchaID, 240, 80); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return captchaID, buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *CaptchaService) GetImage(captchaID string) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := captcha.WriteImage(&buf, captchaID, 240, 80); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *CaptchaService) Verify(captchaID, answer string) bool {
|
||||
return captcha.VerifyString(captchaID, answer)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"time"
|
||||
|
||||
"cloudnest/internal/config"
|
||||
"cloudnest/internal/infrastructure/database"
|
||||
"cloudnest/internal/pkg/logger"
|
||||
)
|
||||
|
||||
// EmailService provides email sending and verification code management.
|
||||
// EmailService 提供邮件发送和验证码管理功能。
|
||||
type EmailService struct {
|
||||
cfg *config.EmailConfig
|
||||
redis *database.RedisClient
|
||||
}
|
||||
|
||||
// NewEmailService creates a new EmailService.
|
||||
// NewEmailService 创建一个新的 EmailService。
|
||||
func NewEmailService(cfg *config.EmailConfig, redis *database.RedisClient) *EmailService {
|
||||
return &EmailService{cfg: cfg, redis: redis}
|
||||
}
|
||||
|
||||
// SendVerifyCode generates a 6-digit verification code, sends it via email, and stores it in Redis.
|
||||
// SendVerifyCode 生成6位数字验证码,通过邮件发送,并存入 Redis。
|
||||
//
|
||||
// Returns:
|
||||
// - code: The generated verification code.
|
||||
// - err: Any error that occurred.
|
||||
func (s *EmailService) SendVerifyCode(toEmail string) (code string, err error) {
|
||||
if s.cfg.SMTPUsername == "" {
|
||||
return "", fmt.Errorf("邮件服务未配置")
|
||||
}
|
||||
|
||||
code = generateCode(6)
|
||||
subject := "CloudNest 邮箱验证码"
|
||||
body := fmt.Sprintf("您的验证码是:%s,有效期10分钟,请勿泄露给他人。", code)
|
||||
|
||||
if err = s.sendMail(toEmail, subject, body); err != nil {
|
||||
logger.Error("failed to send email", "error", err, "to", toEmail)
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
key := fmt.Sprintf("email_code:%s", toEmail)
|
||||
if err = s.redis.Client.Set(ctx, key, code, 10*time.Minute).Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// VerifyCode checks the email verification code and deletes it from Redis on success.
|
||||
// VerifyCode 检查邮箱验证码,成功时从 Redis 中删除。
|
||||
func (s *EmailService) VerifyCode(email, code string) bool {
|
||||
ctx := context.Background()
|
||||
key := fmt.Sprintf("email_code:%s", email)
|
||||
val, err := s.redis.Client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if val != code {
|
||||
return false
|
||||
}
|
||||
s.redis.Client.Del(ctx, key)
|
||||
return true
|
||||
}
|
||||
|
||||
func generateCode(length int) string {
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = byte(rand.Intn(10)) + '0'
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (s *EmailService) sendMail(to, subject, body string) error {
|
||||
from := s.cfg.SMTPUsername
|
||||
addr := fmt.Sprintf("%s:%d", s.cfg.SMTPHost, s.cfg.SMTPPort)
|
||||
|
||||
msg := []byte(fmt.Sprintf("To: %s\r\nFrom: %s <%s>\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s",
|
||||
to, s.cfg.FromName, from, subject, body))
|
||||
|
||||
if s.cfg.SMTPPort == 587 {
|
||||
return sendMailSTARTTLS(addr, s.cfg.SMTPHost, from, s.cfg.SMTPPassword, []string{to}, msg)
|
||||
}
|
||||
|
||||
auth := smtp.PlainAuth("", from, s.cfg.SMTPPassword, s.cfg.SMTPHost)
|
||||
return smtp.SendMail(addr, auth, from, []string{to}, msg)
|
||||
}
|
||||
|
||||
func sendMailSTARTTLS(addr, host, from, password string, to []string, msg []byte) error {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
tlsConfig := &tls.Config{ServerName: host}
|
||||
if err := client.StartTLS(tlsConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
auth := smtp.PlainAuth("", from, password, host)
|
||||
if err := client.Auth(auth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := client.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err := client.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.Quit()
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package file
|
||||
|
||||
// UploadResponse represents the response body for successful file upload.
|
||||
// UploadResponse 表示成功上传文件的响应体。
|
||||
//
|
||||
// Fields:
|
||||
// - Message: A success message.
|
||||
// - File: The storage path of the uploaded file.
|
||||
// - Size: The file size in bytes.
|
||||
//
|
||||
// 字段:
|
||||
// - Message: 成功消息。
|
||||
// - File: 上传文件的存储路径。
|
||||
// - Size: 文件大小(字节)。
|
||||
type UploadResponse struct {
|
||||
Message string `json:"message"`
|
||||
File string `json:"file"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// FileItem represents a single file in the listing.
|
||||
// FileItem 表示列表中的单个文件。
|
||||
//
|
||||
// Fields:
|
||||
// - Key: The storage key/path of the file.
|
||||
// - Size: The file size in bytes.
|
||||
// - LastModified: The timestamp when the file was last modified.
|
||||
//
|
||||
// 字段:
|
||||
// - Key: 文件的存储键/路径。
|
||||
// - Size: 文件大小(字节)。
|
||||
// - LastModified: 文件最后修改的时间戳。
|
||||
type FileItem struct {
|
||||
Key string `json:"key"`
|
||||
Size int64 `json:"size"`
|
||||
LastModified string `json:"last_modified"`
|
||||
}
|
||||
|
||||
// ListResponse represents the response body for file listing.
|
||||
// ListResponse 表示文件列表的响应体。
|
||||
//
|
||||
// Fields:
|
||||
// - Files: A list of file items with metadata.
|
||||
//
|
||||
// 字段:
|
||||
// - Files: 包含元数据的文件项列表。
|
||||
type ListResponse struct {
|
||||
Files []FileItem `json:"files"`
|
||||
}
|
||||
|
||||
// DeleteResponse represents the response body for successful file deletion.
|
||||
// DeleteResponse 表示成功删除文件的响应体。
|
||||
//
|
||||
// Fields:
|
||||
// - Message: A success message.
|
||||
//
|
||||
// 字段:
|
||||
// - Message: 成功消息。
|
||||
type DeleteResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// FavoriteResponse represents the response body for favorite operations.
|
||||
// FavoriteResponse 表示收藏操作的响应体。
|
||||
//
|
||||
// Fields:
|
||||
// - Message: A success message.
|
||||
// - FileKey: The file key that was favorited/unfavorited.
|
||||
//
|
||||
// 字段:
|
||||
// - Message: 成功消息。
|
||||
// - FileKey: 被收藏/取消收藏的文件键。
|
||||
type FavoriteResponse struct {
|
||||
Message string `json:"message"`
|
||||
FileKey string `json:"file_key"`
|
||||
}
|
||||
|
||||
// FavoriteListResponse represents the response body for favorite list.
|
||||
// FavoriteListResponse 表示收藏列表的响应体。
|
||||
//
|
||||
// Fields:
|
||||
// - Favorites: A list of favorite file information.
|
||||
//
|
||||
// 字段:
|
||||
// - Favorites: 收藏文件信息列表。
|
||||
type FavoriteListResponse struct {
|
||||
Favorites []FavoriteItem `json:"favorites"`
|
||||
}
|
||||
|
||||
// FavoriteItem represents a single favorite file.
|
||||
// FavoriteItem 表示单个收藏文件。
|
||||
//
|
||||
// Fields:
|
||||
// - FileKey: The storage key of the file.
|
||||
// - FileName: The original filename.
|
||||
// - CreatedAt: Timestamp when the file was favorited.
|
||||
//
|
||||
// 字段:
|
||||
// - FileKey: 文件的存储键。
|
||||
// - FileName: 原始文件名。
|
||||
// - CreatedAt: 文件被收藏的时间戳。
|
||||
type FavoriteItem struct {
|
||||
FileKey string `json:"file_key"`
|
||||
FileName string `json:"file_name"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// RecycleBinResponse represents the response body for recycle bin list.
|
||||
// RecycleBinResponse 表示回收站列表的响应体。
|
||||
//
|
||||
// Fields:
|
||||
// - Files: A list of deleted file information.
|
||||
//
|
||||
// 字段:
|
||||
// - Files: 已删除文件信息列表。
|
||||
type RecycleBinResponse struct {
|
||||
Files []DeletedFileItem `json:"files"`
|
||||
}
|
||||
|
||||
// DeletedFileItem represents a single deleted file.
|
||||
// DeletedFileItem 表示单个已删除文件。
|
||||
//
|
||||
// Fields:
|
||||
// - FileKey: The storage key of the file.
|
||||
// - FileName: The original filename.
|
||||
// - Size: The file size in bytes.
|
||||
// - DeletedAt: Timestamp when the file was deleted.
|
||||
//
|
||||
// 字段:
|
||||
// - FileKey: 文件的存储键。
|
||||
// - FileName: 原始文件名。
|
||||
// - Size: 文件大小(字节)。
|
||||
// - DeletedAt: 文件被删除的时间戳。
|
||||
type DeletedFileItem struct {
|
||||
FileKey string `json:"file_key"`
|
||||
FileName string `json:"file_name"`
|
||||
Size int64 `json:"size"`
|
||||
DeletedAt string `json:"deleted_at"`
|
||||
}
|
||||
|
||||
// PreviewResponse represents the response body for file preview info.
|
||||
// PreviewResponse 表示文件预览信息的响应体。
|
||||
//
|
||||
// Fields:
|
||||
// - FileType: The type of file (image, text, pdf, other).
|
||||
//
|
||||
// 字段:
|
||||
// - FileType: 文件类型(image, text, pdf, other)。
|
||||
type PreviewResponse struct {
|
||||
FileType string `json:"file_type"`
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"cloudnest/internal/pkg/errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"cloudnest/internal/domain/file/entity"
|
||||
"cloudnest/internal/domain/file/repository"
|
||||
infrastructureRepo "cloudnest/internal/infrastructure/repository"
|
||||
)
|
||||
|
||||
// FileService provides file management operations.
|
||||
// FileService 提供文件管理操作。
|
||||
//
|
||||
// This service implements the application use cases for file upload, download, list, and delete.
|
||||
// It organizes files by username, creating user-specific directories in the object storage.
|
||||
//
|
||||
// 此服务实现了文件上传、下载、列表和删除的应用用例。
|
||||
// 它按用户名组织文件,在对象存储中创建用户特定的目录。
|
||||
type FileService struct {
|
||||
fileRepo repository.FileRepository
|
||||
fileMetaRepo *infrastructureRepo.FileMetaRepository
|
||||
bucket string
|
||||
}
|
||||
|
||||
// NewFileService creates a new FileService instance.
|
||||
// NewFileService 创建一个新的 FileService 实例。
|
||||
//
|
||||
// Parameters:
|
||||
// - fileRepo: The file repository for storage operations.
|
||||
// - bucket: The default bucket name for file storage.
|
||||
//
|
||||
// Returns:
|
||||
// - *FileService: A pointer to the new FileService instance.
|
||||
//
|
||||
// 参数:
|
||||
// - fileRepo: 文件存储操作的仓储。
|
||||
// - bucket: 文件存储的默认桶名称。
|
||||
//
|
||||
// 返回值:
|
||||
// - *FileService: 新创建的 FileService 实例指针。
|
||||
func NewFileService(fileRepo repository.FileRepository, fileMetaRepo *infrastructureRepo.FileMetaRepository, bucket string) *FileService {
|
||||
return &FileService{
|
||||
fileRepo: fileRepo,
|
||||
fileMetaRepo: fileMetaRepo,
|
||||
bucket: bucket,
|
||||
}
|
||||
}
|
||||
|
||||
// userObject generates the storage key for a user's file.
|
||||
// userObject 为用户文件生成存储键。
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The user's username.
|
||||
// - name: The filename.
|
||||
//
|
||||
// Returns:
|
||||
// - string: The storage key in format "username/filename".
|
||||
//
|
||||
// 参数:
|
||||
// - username: 用户的用户名。
|
||||
// - name: 文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - string: 存储键,格式为 "username/filename"。
|
||||
func userObject(userCode, name string) string {
|
||||
return fmt.Sprintf("%s/%s", userCode, name)
|
||||
}
|
||||
|
||||
// Upload uploads a file to the user's directory.
|
||||
// Upload 将文件上传到用户的目录。
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The owner of the file.
|
||||
// - filename: The original filename.
|
||||
// - reader: The file content reader.
|
||||
// - size: The file size in bytes.
|
||||
// - contentType: The MIME type of the file.
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the upload fails.
|
||||
//
|
||||
// 参数:
|
||||
// - username: 文件的所有者。
|
||||
// - filename: 原始文件名。
|
||||
// - reader: 文件内容读取器。
|
||||
// - size: 文件大小(字节)。
|
||||
// - contentType: 文件的 MIME 类型。
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 如果上传失败则返回错误。
|
||||
func (s *FileService) Upload(userCode, filename string, reader io.Reader, size int64, contentType string) error {
|
||||
return s.fileRepo.Upload(s.bucket, userObject(userCode, filename), reader, size, contentType)
|
||||
}
|
||||
|
||||
// List retrieves all files for a user.
|
||||
// List 获取用户的所有文件。
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The user's username.
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A list of file keys belonging to the user.
|
||||
// - error: An error if the operation fails.
|
||||
//
|
||||
// 参数:
|
||||
// - username: 用户的用户名。
|
||||
//
|
||||
// 返回值:
|
||||
// - []string: 用户所属的文件键列表。
|
||||
// - error: 如果操作失败则返回错误。
|
||||
func (s *FileService) List(userCode string) ([]repository.FileInfo, error) {
|
||||
fileInfos, err := s.fileRepo.List(s.bucket, userCode+"/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deletedKeys, err := s.fileMetaRepo.GetDeletedFileKeys(userCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deletedSet := make(map[string]bool)
|
||||
for _, key := range deletedKeys {
|
||||
deletedSet[key] = true
|
||||
}
|
||||
|
||||
var filtered []repository.FileInfo
|
||||
for _, info := range fileInfos {
|
||||
if !deletedSet[info.Key] {
|
||||
filtered = append(filtered, info)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// PresignDownload generates a pre-signed URL for file download.
|
||||
// PresignDownload 生成用于文件下载的预签名 URL。
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The file owner's username.
|
||||
// - filename: The name of the file to download.
|
||||
//
|
||||
// Returns:
|
||||
// - string: The pre-signed download URL.
|
||||
// - error: An error if the operation fails.
|
||||
//
|
||||
// 参数:
|
||||
// - username: 文件所有者的用户名。
|
||||
// - filename: 要下载的文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - string: 预签名下载 URL。
|
||||
// - error: 如果操作失败则返回错误。
|
||||
func (s *FileService) PresignDownload(userCode, filename string) (string, error) {
|
||||
return s.fileRepo.PresignDownload(s.bucket, userObject(userCode, filename))
|
||||
}
|
||||
|
||||
// GetFileStream retrieves a file as a readable stream for proxying to the client.
|
||||
// GetFileStream 获取文件的可读流,用于代理给客户端。
|
||||
//
|
||||
// This method allows the Go backend to stream file content directly to the HTTP response,
|
||||
// avoiding CORS issues with MinIO and keeping MinIO internal to the server.
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The file owner's username.
|
||||
// - filename: The name of the file to stream.
|
||||
//
|
||||
// Returns:
|
||||
// - io.ReadCloser: A readable stream of the file content. Caller must close it.
|
||||
// - int64: The file size in bytes.
|
||||
// - string: The Content-Type of the file.
|
||||
// - error: An error if the operation fails.
|
||||
//
|
||||
// 此方法允许 Go 后端将文件内容直接流式传输到 HTTP 响应,
|
||||
// 避免 MinIO 的 CORS 问题,并保持 MinIO 对服务器内部可见。
|
||||
//
|
||||
// 参数:
|
||||
// - username: 文件所有者的用户名。
|
||||
// - filename: 要流式传输的文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - io.ReadCloser: 文件内容的可读流。调用方必须关闭它。
|
||||
// - int64: 文件大小(字节)。
|
||||
// - string: 文件的 Content-Type。
|
||||
// - error: 如果操作失败则返回错误。
|
||||
func (s *FileService) GetFileStream(userCode, filename string) (io.ReadCloser, int64, string, error) {
|
||||
return s.fileRepo.GetObject(s.bucket, userObject(userCode, filename))
|
||||
}
|
||||
|
||||
// Delete removes a file from the user's directory.
|
||||
// Delete 从用户的目录中删除文件。
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The file owner's username.
|
||||
// - filename: The name of the file to delete.
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the deletion fails.
|
||||
//
|
||||
// 参数:
|
||||
// - username: 文件所有者的用户名。
|
||||
// - filename: 要删除的文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 如果删除失败则返回错误。
|
||||
func (s *FileService) Delete(userCode, filename string) error {
|
||||
return s.fileRepo.Delete(s.bucket, userObject(userCode, filename))
|
||||
}
|
||||
|
||||
// SoftDelete moves a file to the recycle bin instead of permanent deletion.
|
||||
// SoftDelete 将文件移动到回收站而不是永久删除。
|
||||
//
|
||||
// Parameters:
|
||||
// - userID: The user's ID.
|
||||
// - username: The user's username.
|
||||
// - filename: The name of the file to delete.
|
||||
// - size: The file size in bytes.
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the operation fails.
|
||||
//
|
||||
// 参数:
|
||||
// - userID: 用户的 ID。
|
||||
// - username: 用户的用户名。
|
||||
// - filename: 要删除的文件名。
|
||||
// - size: 文件大小(字节)。
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 如果操作失败则返回错误。
|
||||
func (s *FileService) SoftDelete(userID uint, userCode, filename string, _ int64) error {
|
||||
fileKey := userObject(userCode, filename)
|
||||
|
||||
size, _, err := s.fileRepo.GetObjectInfo(s.bucket, fileKey)
|
||||
if err != nil {
|
||||
return errors.InternalError("获取文件信息失败", err)
|
||||
}
|
||||
|
||||
s.fileMetaRepo.RemoveFavorite(userCode, fileKey)
|
||||
|
||||
deletedFile := entity.NewDeletedFile(userID, userCode, fileKey, filename, size)
|
||||
return s.fileMetaRepo.AddToRecycleBin(deletedFile)
|
||||
}
|
||||
|
||||
// Favorite adds a file to user's favorites.
|
||||
// Favorite 将文件添加到用户的收藏夹。
|
||||
//
|
||||
// Parameters:
|
||||
// - userID: The user's ID.
|
||||
// - username: The user's username.
|
||||
// - filename: The name of the file to favorite.
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the operation fails.
|
||||
//
|
||||
// 参数:
|
||||
// - userID: 用户的 ID。
|
||||
// - username: 用户的用户名。
|
||||
// - filename: 要收藏的文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 如果操作失败则返回错误。
|
||||
func (s *FileService) Favorite(userID uint, userCode, filename string) error {
|
||||
fileKey := userObject(userCode, filename)
|
||||
|
||||
size, _, err := s.fileRepo.GetObjectInfo(s.bucket, fileKey)
|
||||
if err != nil {
|
||||
return errors.InternalError("获取文件信息失败", err)
|
||||
}
|
||||
|
||||
favorite := entity.NewFavorite(userID, userCode, fileKey, size)
|
||||
return s.fileMetaRepo.AddFavorite(favorite)
|
||||
}
|
||||
|
||||
// Unfavorite removes a file from user's favorites.
|
||||
// Unfavorite 从用户的收藏夹中移除文件。
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The user's username.
|
||||
// - filename: The name of the file to unfavorite.
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the operation fails.
|
||||
//
|
||||
// 参数:
|
||||
// - username: 用户的用户名。
|
||||
// - filename: 要取消收藏的文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 如果操作失败则返回错误。
|
||||
func (s *FileService) Unfavorite(userCode, filename string) error {
|
||||
fileKey := userObject(userCode, filename)
|
||||
return s.fileMetaRepo.RemoveFavorite(userCode, fileKey)
|
||||
}
|
||||
|
||||
// GetFavorites retrieves all favorites for a user.
|
||||
// GetFavorites 获取用户的所有收藏。
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The user's username.
|
||||
//
|
||||
// Returns:
|
||||
// - []entity.Favorite: A list of Favorite entities.
|
||||
// - error: An error if the operation fails.
|
||||
//
|
||||
// 参数:
|
||||
// - username: 用户的用户名。
|
||||
//
|
||||
// 返回值:
|
||||
// - []entity.Favorite: Favorite 实体列表。
|
||||
// - error: 如果操作失败则返回错误。
|
||||
func (s *FileService) GetFavorites(userCode string) ([]entity.Favorite, error) {
|
||||
favorites, err := s.fileMetaRepo.GetFavorites(userCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deletedKeys, err := s.fileMetaRepo.GetDeletedFileKeys(userCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deletedSet := make(map[string]bool)
|
||||
for _, key := range deletedKeys {
|
||||
deletedSet[key] = true
|
||||
}
|
||||
|
||||
var filtered []entity.Favorite
|
||||
for _, f := range favorites {
|
||||
if !deletedSet[f.FileKey] {
|
||||
filtered = append(filtered, f)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// GetRecycleBin retrieves all deleted files for a user.
|
||||
// GetRecycleBin 获取用户的所有已删除文件。
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The user's username.
|
||||
//
|
||||
// Returns:
|
||||
// - []entity.DeletedFile: A list of DeletedFile entities.
|
||||
// - error: An error if the operation fails.
|
||||
//
|
||||
// 参数:
|
||||
// - username: 用户的用户名。
|
||||
//
|
||||
// 返回值:
|
||||
// - []entity.DeletedFile: DeletedFile 实体列表。
|
||||
// - error: 如果操作失败则返回错误。
|
||||
func (s *FileService) GetRecycleBin(userCode string) ([]entity.DeletedFile, error) {
|
||||
deletedFiles, err := s.fileMetaRepo.GetRecycleBin(userCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range deletedFiles {
|
||||
if deletedFiles[i].Size == 0 {
|
||||
size, _, err := s.fileRepo.GetObjectInfo(s.bucket, deletedFiles[i].FileKey)
|
||||
if err == nil {
|
||||
deletedFiles[i].Size = size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deletedFiles, nil
|
||||
}
|
||||
|
||||
// RestoreFromRecycleBin restores a file from the recycle bin.
|
||||
// RestoreFromRecycleBin 从回收站恢复文件。
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The user's username.
|
||||
// - filename: The name of the file to restore.
|
||||
//
|
||||
// Returns:
|
||||
// - *entity.DeletedFile: The restored DeletedFile entity.
|
||||
// - error: An error if the operation fails.
|
||||
//
|
||||
// 参数:
|
||||
// - username: 用户的用户名。
|
||||
// - filename: 要恢复的文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - *entity.DeletedFile: 恢复的 DeletedFile 实体。
|
||||
// - error: 如果操作失败则返回错误。
|
||||
func (s *FileService) RestoreFromRecycleBin(userCode, filename string) (*entity.DeletedFile, error) {
|
||||
fileKey := userObject(userCode, filename)
|
||||
return s.fileMetaRepo.RestoreFromRecycleBin(userCode, fileKey)
|
||||
}
|
||||
|
||||
// DeleteFromRecycleBin permanently deletes a file from the recycle bin.
|
||||
// DeleteFromRecycleBin 从回收站永久删除文件。
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The user's username.
|
||||
// - filename: The name of the file to permanently delete.
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if the operation fails.
|
||||
//
|
||||
// 参数:
|
||||
// - username: 用户的用户名。
|
||||
// - filename: 要永久删除的文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 如果操作失败则返回错误。
|
||||
func (s *FileService) DeleteFromRecycleBin(userCode, filename string) error {
|
||||
fileKey := userObject(userCode, filename)
|
||||
if err := s.fileMetaRepo.DeleteFromRecycleBin(userCode, fileKey); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.fileRepo.Delete(s.bucket, fileKey)
|
||||
}
|
||||
|
||||
// PresignPreview generates a pre-signed URL for file preview.
|
||||
// PresignPreview 生成用于文件预览的预签名 URL。
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The file owner's username.
|
||||
// - filename: The name of the file to preview.
|
||||
//
|
||||
// Returns:
|
||||
// - string: The pre-signed preview URL.
|
||||
// - error: An error if the operation fails.
|
||||
//
|
||||
// 参数:
|
||||
// - username: 文件所有者的用户名。
|
||||
// - filename: 要预览的文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - string: 预签名预览 URL。
|
||||
// - error: 如果操作失败则返回错误。
|
||||
func (s *FileService) PresignPreview(userCode, filename string) (string, error) {
|
||||
return s.fileRepo.PresignDownload(s.bucket, userObject(userCode, filename))
|
||||
}
|
||||
|
||||
// GetFileExtension returns the file extension without the dot.
|
||||
// GetFileExtension 返回文件扩展名(不含点)。
|
||||
//
|
||||
// Parameters:
|
||||
// - filename: The filename.
|
||||
//
|
||||
// Returns:
|
||||
// - string: The file extension in lowercase.
|
||||
//
|
||||
// 参数:
|
||||
// - filename: 文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - string: 小写的文件扩展名。
|
||||
func (s *FileService) GetFileExtension(filename string) string {
|
||||
idx := strings.LastIndex(filename, ".")
|
||||
if idx == -1 {
|
||||
return ""
|
||||
}
|
||||
return strings.ToLower(filename[idx+1:])
|
||||
}
|
||||
|
||||
// IsImageFile checks if the file is an image based on its extension.
|
||||
// IsImageFile 根据文件扩展名检查是否为图片文件。
|
||||
//
|
||||
// Parameters:
|
||||
// - filename: The filename.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: True if the file is an image.
|
||||
//
|
||||
// 参数:
|
||||
// - filename: 文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - bool: 如果是图片文件则返回 true。
|
||||
func (s *FileService) IsImageFile(filename string) bool {
|
||||
ext := s.GetFileExtension(filename)
|
||||
imageExts := map[string]bool{
|
||||
"jpg": true,
|
||||
"jpeg": true,
|
||||
"png": true,
|
||||
"gif": true,
|
||||
"bmp": true,
|
||||
"webp": true,
|
||||
"svg": true,
|
||||
}
|
||||
return imageExts[ext]
|
||||
}
|
||||
|
||||
// IsTextFile checks if the file is a text file based on its extension.
|
||||
// IsTextFile 根据文件扩展名检查是否为文本文件。
|
||||
//
|
||||
// Parameters:
|
||||
// - filename: The filename.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: True if the file is a text file.
|
||||
//
|
||||
// 参数:
|
||||
// - filename: 文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - bool: 如果是文本文件则返回 true。
|
||||
func (s *FileService) IsTextFile(filename string) bool {
|
||||
ext := s.GetFileExtension(filename)
|
||||
textExts := map[string]bool{
|
||||
"txt": true,
|
||||
"md": true,
|
||||
"json": true,
|
||||
"xml": true,
|
||||
"html": true,
|
||||
"css": true,
|
||||
"js": true,
|
||||
"go": true,
|
||||
"py": true,
|
||||
"java": true,
|
||||
"cpp": true,
|
||||
"c": true,
|
||||
}
|
||||
return textExts[ext]
|
||||
}
|
||||
|
||||
// IsPDFFile checks if the file is a PDF file based on its extension.
|
||||
// IsPDFFile 根据文件扩展名检查是否为 PDF 文件。
|
||||
//
|
||||
// Parameters:
|
||||
// - filename: The filename.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: True if the file is a PDF.
|
||||
//
|
||||
// 参数:
|
||||
// - filename: 文件名。
|
||||
//
|
||||
// 返回值:
|
||||
// - bool: 如果是 PDF 文件则返回 true。
|
||||
func (s *FileService) IsPDFFile(filename string) bool {
|
||||
ext := s.GetFileExtension(filename)
|
||||
return ext == "pdf"
|
||||
}
|
||||
|
||||
func (s *FileService) GetChunkDir() string {
|
||||
return filepath.Join(os.TempDir(), "cloudnest_chunks")
|
||||
}
|
||||
|
||||
func (s *FileService) SaveChunk(username, hash string, chunkIndex int, data []byte) error {
|
||||
chunkDir := filepath.Join(s.GetChunkDir(), username, hash)
|
||||
if err := os.MkdirAll(chunkDir, 0755); err != nil {
|
||||
return errors.InternalError("创建分片目录失败", err)
|
||||
}
|
||||
|
||||
chunkPath := filepath.Join(chunkDir, strconv.Itoa(chunkIndex))
|
||||
if err := os.WriteFile(chunkPath, data, 0644); err != nil {
|
||||
return errors.InternalError("保存分片失败", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileService) GetUploadedChunks(username, hash string) ([]int, error) {
|
||||
chunkDir := filepath.Join(s.GetChunkDir(), username, hash)
|
||||
entries, err := os.ReadDir(chunkDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []int{}, nil
|
||||
}
|
||||
return nil, errors.InternalError("读取分片目录失败", err)
|
||||
}
|
||||
|
||||
var chunks []int
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
idx, err := strconv.Atoi(entry.Name())
|
||||
if err == nil {
|
||||
chunks = append(chunks, idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chunks, nil
|
||||
}
|
||||
|
||||
func (s *FileService) MergeChunks(username, hash, filename string, chunkCount int, fileSize int64) error {
|
||||
chunkDir := filepath.Join(s.GetChunkDir(), username, hash)
|
||||
|
||||
destPath := filepath.Join(chunkDir, "merged_"+filename)
|
||||
destFile, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return errors.InternalError("创建合并文件失败", err)
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
for i := 0; i < chunkCount; i++ {
|
||||
chunkPath := filepath.Join(chunkDir, strconv.Itoa(i))
|
||||
chunkData, err := os.ReadFile(chunkPath)
|
||||
if err != nil {
|
||||
return errors.InternalError("读取分片失败", err)
|
||||
}
|
||||
|
||||
if _, err := destFile.Write(chunkData); err != nil {
|
||||
return errors.InternalError("写入合并文件失败", err)
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Open(destPath)
|
||||
if err != nil {
|
||||
return errors.InternalError("打开合并文件失败", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
contentType := "application/octet-stream"
|
||||
|
||||
if err := s.Upload(username, filename, file, fileSize, contentType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.RemoveAll(chunkDir)
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user