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

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
+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
}