feat: add service layer implementing proto FileService interface

Implements all 23 FileServiceServer methods: file upload/download/list/
preview/content/delete, multipart upload lifecycle, bucket CRUD, folder
CRUD with tree support, file-to-folder upload, file move, and share link
create/delete/info/download. Includes PO-to-proto conversion helpers.
This commit is contained in:
向宁 2026-05-25 13:04:01 +08:00
parent 4927de90cc
commit 2647314fe7
2 changed files with 445 additions and 0 deletions

439
internal/service/file.go Normal file
View File

@ -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)
}

View File

@ -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)