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.
440 lines
13 KiB
Go
440 lines
13 KiB
Go
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)
|
|
}
|