package service import ( "bytes" "context" "io" "time" pb "rag/file-system/api/file/v1" "rag/file-system/internal/biz" "rag/file-system/internal/data" "github.com/go-kratos/kratos/v2/log" "google.golang.org/protobuf/types/known/emptypb" ) // FileService implements the proto-generated FileServiceServer interface. type FileService struct { pb.UnimplementedFileServiceServer fileUC *biz.FileUsecase bucketUC *biz.BucketUsecase folderUC *biz.FolderUsecase shareUC *biz.ShareUsecase log *log.Helper } // NewFileService creates a new FileService instance. func NewFileService(fileUC *biz.FileUsecase, bucketUC *biz.BucketUsecase, folderUC *biz.FolderUsecase, shareUC *biz.ShareUsecase, logger log.Logger) *FileService { return &FileService{ fileUC: fileUC, bucketUC: bucketUC, folderUC: folderUC, shareUC: shareUC, log: log.NewHelper(logger), } } // --- File operations --- // UploadFile uploads file data to the specified bucket and key. func (s *FileService) UploadFile(ctx context.Context, req *pb.UploadFileRequest) (*pb.UploadFileResponse, error) { err := s.fileUC.UploadFile(ctx, req.BucketName, req.ObjectKey, bytes.NewReader(req.Data)) if err != nil { return nil, err } return &pb.UploadFileResponse{ Message: "uploaded", ObjectKey: req.ObjectKey, }, nil } // DownloadFile downloads file data from the specified bucket and key. func (s *FileService) DownloadFile(ctx context.Context, req *pb.DownloadFileRequest) (*pb.DownloadFileResponse, error) { body, err := s.fileUC.DownloadFile(ctx, req.BucketName, req.ObjectKey) if err != nil { return nil, err } defer body.Close() fileData, err := io.ReadAll(body) if err != nil { return nil, err } return &pb.DownloadFileResponse{ Data: fileData, ContentType: "application/octet-stream", FileName: req.ObjectKey, }, nil } // ListFiles lists objects in a bucket with pagination. func (s *FileService) ListFiles(ctx context.Context, req *pb.ListFilesRequest) (*pb.ListFilesResponse, error) { var token *string if req.ContinuationToken != "" { token = &req.ContinuationToken } result, err := s.fileUC.ListObjectsV2(ctx, req.BucketName, req.Prefix, req.MaxKeys, token) if err != nil { return nil, err } files := make([]*pb.FileInfo, 0, len(result.Files)) for _, f := range result.Files { files = append(files, &pb.FileInfo{ Key: f.Key, Size: f.Size, LastModified: f.LastModified.Format(time.RFC3339), Etag: f.ETag, }) } resp := &pb.ListFilesResponse{Files: files} if result.NextContinuationToken != nil { resp.NextContinuationToken = *result.NextContinuationToken } return resp, nil } // GetFilePreview returns a presigned URL for file preview. func (s *FileService) GetFilePreview(ctx context.Context, req *pb.GetFilePreviewRequest) (*pb.GetFilePreviewResponse, error) { url, err := s.fileUC.GetPreviewURL(ctx, req.BucketName, req.ObjectKey) if err != nil { return nil, err } return &pb.GetFilePreviewResponse{PresignedUrl: url}, nil } // GetFileContent retrieves text content of a file for display. func (s *FileService) GetFileContent(ctx context.Context, req *pb.GetFileContentRequest) (*pb.GetFileContentResponse, error) { content, err := s.fileUC.GetFileContent(ctx, req.BucketName, req.ObjectKey) if err != nil { return nil, err } return &pb.GetFileContentResponse{Content: content}, nil } // DeleteFile removes a file from the specified bucket. func (s *FileService) DeleteFile(ctx context.Context, req *pb.DeleteFileRequest) (*emptypb.Empty, error) { err := s.fileUC.DeleteFile(ctx, req.BucketName, req.ObjectKey) if err != nil { return nil, err } return &emptypb.Empty{}, nil } // --- Multipart upload --- // InitMultipartUpload starts a multipart upload session. func (s *FileService) InitMultipartUpload(ctx context.Context, req *pb.InitMultipartRequest) (*pb.InitMultipartResponse, error) { uploadID, err := s.fileUC.CreateMultipartUpload(ctx, req.BucketName, req.ObjectKey) if err != nil { return nil, err } return &pb.InitMultipartResponse{UploadId: uploadID}, nil } // UploadPart uploads a single part in a multipart upload. func (s *FileService) UploadPart(ctx context.Context, req *pb.UploadPartRequest) (*pb.UploadPartResponse, error) { etag, err := s.fileUC.UploadPart(ctx, req.BucketName, req.ObjectKey, req.UploadId, req.PartNumber, bytes.NewReader(req.Data)) if err != nil { return nil, err } return &pb.UploadPartResponse{Etag: etag}, nil } // CompleteMultipartUpload assembles all uploaded parts. func (s *FileService) CompleteMultipartUpload(ctx context.Context, req *pb.CompleteMultipartRequest) (*pb.CompleteMultipartResponse, error) { parts := make([]data.Part, 0, len(req.Parts)) for _, p := range req.Parts { parts = append(parts, data.Part{ PartNumber: p.PartNumber, ETag: p.Etag, }) } location, err := s.fileUC.CompleteMultipartUpload(ctx, req.BucketName, req.ObjectKey, req.UploadId, parts) if err != nil { return nil, err } return &pb.CompleteMultipartResponse{Location: location}, nil } // AbortMultipartUpload cancels a multipart upload session. func (s *FileService) AbortMultipartUpload(ctx context.Context, req *pb.AbortMultipartRequest) (*emptypb.Empty, error) { err := s.fileUC.AbortMultipartUpload(ctx, req.BucketName, req.ObjectKey, req.UploadId) if err != nil { return nil, err } return &emptypb.Empty{}, nil } // --- Bucket operations --- // CreateBucket creates a new S3 bucket. func (s *FileService) CreateBucket(ctx context.Context, req *pb.CreateBucketRequest) (*emptypb.Empty, error) { err := s.bucketUC.CreateBucket(ctx, req.Name) if err != nil { return nil, err } return &emptypb.Empty{}, nil } // ListBuckets returns all bucket names. func (s *FileService) ListBuckets(ctx context.Context, _ *emptypb.Empty) (*pb.ListBucketsResponse, error) { buckets, err := s.bucketUC.ListBuckets(ctx) if err != nil { return nil, err } return &pb.ListBucketsResponse{Buckets: buckets}, nil } // DeleteBucket removes an S3 bucket. func (s *FileService) DeleteBucket(ctx context.Context, req *pb.DeleteBucketRequest) (*emptypb.Empty, error) { err := s.bucketUC.DeleteBucket(ctx, req.Name) if err != nil { return nil, err } return &emptypb.Empty{}, nil } // --- Folder operations --- // CreateFolder creates a new folder under the given parent. func (s *FileService) CreateFolder(ctx context.Context, req *pb.CreateFolderRequest) (*pb.Folder, error) { var parentID *string if req.ParentId != "" { parentID = &req.ParentId } folder, err := s.folderUC.CreateFolder(ctx, parentID, req.Name, req.OwnerId) if err != nil { return nil, err } return folderPOToProto(folder), nil } // GetFolderTree returns all folders owned by a user. func (s *FileService) GetFolderTree(ctx context.Context, req *pb.GetFolderTreeRequest) (*pb.GetFolderTreeResponse, error) { folders, err := s.folderUC.GetFolderTree(ctx, req.OwnerId) if err != nil { return nil, err } pbFolders := make([]*pb.Folder, 0, len(folders)) for i := range folders { pbFolders = append(pbFolders, folderPOToProto(&folders[i])) } return &pb.GetFolderTreeResponse{Folders: pbFolders}, nil } // GetFolder returns a folder with its children (sub-folders and files). func (s *FileService) GetFolder(ctx context.Context, req *pb.GetFolderRequest) (*pb.FolderWithChildren, error) { folder, subFolders, files, err := s.folderUC.GetFolder(ctx, req.Id, req.OwnerId) if err != nil { return nil, err } if folder == nil { return nil, nil } pbSubFolders := make([]*pb.Folder, 0, len(subFolders)) for i := range subFolders { pbSubFolders = append(pbSubFolders, folderPOToProto(&subFolders[i])) } pbFiles := make([]*pb.FileMeta, 0, len(files)) for i := range files { pbFiles = append(pbFiles, fileMetaPOToProto(&files[i])) } return &pb.FolderWithChildren{ Folder: folderPOToProto(folder), SubFolders: pbSubFolders, Files: pbFiles, }, nil } // RenameFolder updates a folder's name. func (s *FileService) RenameFolder(ctx context.Context, req *pb.RenameFolderRequest) (*pb.Folder, error) { folder, err := s.folderUC.RenameFolder(ctx, req.Id, req.Name, req.OwnerId) if err != nil { return nil, err } return folderPOToProto(folder), nil } // DeleteFolder removes a folder and all its descendant files. func (s *FileService) DeleteFolder(ctx context.Context, req *pb.DeleteFolderRequest) (*emptypb.Empty, error) { err := s.folderUC.DeleteFolder(ctx, req.Id, req.OwnerId) if err != nil { return nil, err } return &emptypb.Empty{}, nil } // UploadToFolder uploads a file into a folder with metadata tracking. func (s *FileService) UploadToFolder(ctx context.Context, req *pb.UploadToFolderRequest) (*pb.FileMeta, error) { meta, err := s.folderUC.UploadToFolder(ctx, req.FolderId, req.FileName, req.Data, req.ContentType, req.OwnerId) if err != nil { return nil, err } return fileMetaPOToProto(meta), nil } // MoveFile moves a file from one folder to another. func (s *FileService) MoveFile(ctx context.Context, req *pb.MoveFileRequest) (*emptypb.Empty, error) { err := s.folderUC.MoveFile(ctx, req.Id, req.TargetFolderId, req.OwnerId) if err != nil { return nil, err } return &emptypb.Empty{}, nil } // --- Share operations --- // CreateShare creates a new share link for a resource. func (s *FileService) CreateShare(ctx context.Context, req *pb.CreateShareRequest) (*pb.ShareLink, error) { var expiresAt *time.Time if req.ExpiresAt != "" { t, err := time.Parse(time.RFC3339, req.ExpiresAt) if err != nil { return nil, err } expiresAt = &t } var maxDownloads *int if req.MaxDownloads > 0 { md := int(req.MaxDownloads) maxDownloads = &md } share, err := s.shareUC.CreateShare(ctx, req.ResourceType, req.ResourceId, req.Password, expiresAt, maxDownloads, req.CreatedBy) if err != nil { return nil, err } return shareLinkPOToProto(share), nil } // DeleteShare removes a share link. func (s *FileService) DeleteShare(ctx context.Context, req *pb.DeleteShareRequest) (*emptypb.Empty, error) { err := s.shareUC.DeleteShare(ctx, req.Id, req.CreatedBy) if err != nil { return nil, err } return &emptypb.Empty{}, nil } // GetShareInfo returns share details and associated file metadata. func (s *FileService) GetShareInfo(ctx context.Context, req *pb.GetShareInfoRequest) (*pb.ShareInfo, error) { share, fileMeta, err := s.shareUC.GetShareInfo(ctx, req.Token) if err != nil { return nil, err } if share == nil { return nil, nil } info := &pb.ShareInfo{ Token: share.Token, ResourceType: share.ResourceType, HasPassword: share.Password != nil, } if share.ExpiresAt != nil { info.ExpiresAt = share.ExpiresAt.Format(time.RFC3339) } if fileMeta != nil { info.FileName = fileMeta.Name info.FileSize = fileMeta.Size } return info, nil } // DownloadShare validates a share link and returns a presigned download URL. func (s *FileService) DownloadShare(ctx context.Context, req *pb.DownloadShareRequest) (*pb.DownloadShareResponse, error) { url, fileName, err := s.shareUC.DownloadShare(ctx, req.Token) if err != nil { return nil, err } return &pb.DownloadShareResponse{ PresignedUrl: url, FileName: fileName, }, nil } // --- Conversion helpers --- // folderPOToProto converts a data.FolderPO to a proto Folder message. func folderPOToProto(f *data.FolderPO) *pb.Folder { if f == nil { return nil } return &pb.Folder{ Id: f.ID, ParentId: derefStr(f.ParentID), Name: f.Name, OwnerId: f.OwnerID, CreatedAt: f.CreatedAt.Format(time.RFC3339), UpdatedAt: f.UpdatedAt.Format(time.RFC3339), } } // fileMetaPOToProto converts a data.FileMetaPO to a proto FileMeta message. func fileMetaPOToProto(f *data.FileMetaPO) *pb.FileMeta { if f == nil { return nil } return &pb.FileMeta{ Id: f.ID, FolderId: f.FolderID, Name: f.Name, S3Key: f.S3Key, S3Bucket: f.S3Bucket, Size: f.Size, ContentType: f.ContentType, OwnerId: f.OwnerID, CreatedAt: f.CreatedAt.Format(time.RFC3339), UpdatedAt: f.UpdatedAt.Format(time.RFC3339), } } // shareLinkPOToProto converts a data.ShareLinkPO to a proto ShareLink message. func shareLinkPOToProto(s *data.ShareLinkPO) *pb.ShareLink { if s == nil { return nil } return &pb.ShareLink{ Id: s.ID, ResourceType: s.ResourceType, ResourceId: s.ResourceID, Token: s.Token, Password: derefStr(s.Password), ExpiresAt: derefTime(s.ExpiresAt), DownloadCount: int32(s.DownloadCount), MaxDownloads: derefInt(s.MaxDownloads), CreatedBy: s.CreatedBy, CreatedAt: s.CreatedAt.Format(time.RFC3339), } } // derefStr returns the dereferenced string value or empty string if nil. func derefStr(s *string) string { if s == nil { return "" } return *s } // derefTime returns the formatted time string or empty string if nil. func derefTime(t *time.Time) string { if t == nil { return "" } return t.Format(time.RFC3339) } // derefInt returns the dereferenced int value as int32, or 0 if nil. func derefInt(i *int) int32 { if i == nil { return 0 } return int32(*i) }