首次提交:初始化项目代码

This commit is contained in:
sunct
2026-07-31 15:24:14 +08:00
commit 8d3c97bd01
73 changed files with 11720 additions and 0 deletions
+95
View File
@@ -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"`
}
+159
View File
@@ -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))
}
+40
View File
@@ -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)
}
+144
View File
@@ -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()
}
+152
View File
@@ -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"`
}
+627
View File
@@ -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
}