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 }