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:
parent
4927de90cc
commit
2647314fe7
439
internal/service/file.go
Normal file
439
internal/service/file.go
Normal 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)
|
||||
}
|
||||
6
internal/service/service.go
Normal file
6
internal/service/service.go
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user