diff --git a/internal/service/file.go b/internal/service/file.go new file mode 100644 index 0000000..826eb4b --- /dev/null +++ b/internal/service/file.go @@ -0,0 +1,439 @@ +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) +} diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 0000000..6f10596 --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,6 @@ +package service + +import "github.com/google/wire" + +// ProviderSet is the Wire provider set for the service layer. +var ProviderSet = wire.NewSet(NewFileService)