| @@ -46,6 +46,7 @@ func migrate(configPath string) { | |||
| migrateOne(db, clitypes.Package{}) | |||
| migrateOne(db, clitypes.PinnedObject{}) | |||
| migrateOne(db, clitypes.UserSpace{}) | |||
| migrateOne(db, clitypes.SpaceSyncTask{}) | |||
| fmt.Println("migrate success") | |||
| } | |||
| @@ -18,6 +18,7 @@ import ( | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/mount" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/repl" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/services" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/spacesyncer" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/ticktock" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/uploader" | |||
| stgglb "gitlink.org.cn/cloudream/jcs-pub/common/globals" | |||
| @@ -140,7 +141,7 @@ func serveHTTP(configPath string, opts serveHTTPOptions) { | |||
| // 元数据缓存 | |||
| metaCacheHost := metacache.NewHost(db) | |||
| go metaCacheHost.Serve() | |||
| stgMeta := metaCacheHost.AddStorageMeta() | |||
| spaceMeta := metaCacheHost.AddStorageMeta() | |||
| hubMeta := metaCacheHost.AddHubMeta() | |||
| conMeta := metaCacheHost.AddConnectivity() | |||
| @@ -159,19 +160,24 @@ func serveHTTP(configPath string, opts serveHTTPOptions) { | |||
| stgPool := pool.NewPool() | |||
| // 下载策略 | |||
| strgSel := strategy.NewSelector(config.Cfg().DownloadStrategy, stgMeta, hubMeta, conMeta) | |||
| strgSel := strategy.NewSelector(config.Cfg().DownloadStrategy, spaceMeta, hubMeta, conMeta) | |||
| // 下载器 | |||
| dlder := downloader.NewDownloader(config.Cfg().Downloader, conCol, stgPool, strgSel, db) | |||
| // 上传器 | |||
| uploader := uploader.NewUploader(publock, conCol, stgPool, stgMeta, db) | |||
| uploader := uploader.NewUploader(publock, conCol, stgPool, spaceMeta, db) | |||
| // 定时任务 | |||
| tktk := ticktock.New(config.Cfg().TickTock, db, stgMeta, stgPool, evtPub, publock) | |||
| tktk := ticktock.New(config.Cfg().TickTock, db, spaceMeta, stgPool, evtPub, publock) | |||
| tktk.Start() | |||
| defer tktk.Stop() | |||
| // 用户空间同步功能 | |||
| spaceSync := spacesyncer.New(db, stgPool, spaceMeta) | |||
| spaceSyncChan := spaceSync.Start() | |||
| defer spaceSync.Stop() | |||
| // 交互式命令行 | |||
| rep := repl.New(db, tktk) | |||
| replCh := rep.Start() | |||
| @@ -189,7 +195,7 @@ func serveHTTP(configPath string, opts serveHTTPOptions) { | |||
| mntChan := mnt.Start() | |||
| defer mnt.Stop() | |||
| svc := services.NewService(publock, dlder, acStat, uploader, strgSel, stgMeta, db, evtPub, mnt, stgPool) | |||
| svc := services.NewService(publock, dlder, acStat, uploader, strgSel, spaceMeta, db, evtPub, mnt, stgPool, spaceSync) | |||
| // HTTP接口 | |||
| httpCfgJSON := config.Cfg().HTTP | |||
| @@ -217,6 +223,7 @@ func serveHTTP(configPath string, opts serveHTTPOptions) { | |||
| evtPubEvt := evtPubChan.Receive() | |||
| conColEvt := conColChan.Receive() | |||
| acStatEvt := acStatChan.Receive() | |||
| spaceSyncEvt := spaceSyncChan.Receive() | |||
| replEvt := replCh.Receive() | |||
| httpEvt := httpChan.Receive() | |||
| mntEvt := mntChan.Receive() | |||
| @@ -295,6 +302,13 @@ loop: | |||
| } | |||
| acStatEvt = acStatChan.Receive() | |||
| case e := <-spaceSyncEvt.Chan(): | |||
| if e.Err != nil { | |||
| logger.Errorf("receive space sync event: %v", err) | |||
| break loop | |||
| } | |||
| spaceSyncEvt = spaceSyncChan.Receive() | |||
| case e := <-replEvt.Chan(): | |||
| if e.Err != nil { | |||
| logger.Errorf("receive repl event: %v", err) | |||
| @@ -17,7 +17,9 @@ import ( | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/downloader/strategy" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/metacache" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/services" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/spacesyncer" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/uploader" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| stgglb "gitlink.org.cn/cloudream/jcs-pub/common/globals" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/models/datamap" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/connectivity" | |||
| @@ -58,7 +60,7 @@ func doTest(svc *services.Service) { | |||
| ft = ioswitch2.NewFromTo() | |||
| ft.AddFrom(ioswitch2.NewFromShardstore("Full1AE5436AF72D8EF93923486E0E167315CEF0C91898064DADFAC22216FFBC5E3D", *space1, ioswitch2.RawStream())) | |||
| ft.AddTo(ioswitch2.NewToBaseStore(*space2, "test3.txt")) | |||
| ft.AddTo(ioswitch2.NewToBaseStore(*space2, clitypes.PathFromComps("test3.txt"))) | |||
| plans := exec.NewPlanBuilder() | |||
| parser.Parse(ft, plans) | |||
| fmt.Println(plans) | |||
| @@ -178,7 +180,12 @@ func test(configPath string) { | |||
| // 上传器 | |||
| uploader := uploader.NewUploader(publock, conCol, stgPool, stgMeta, db) | |||
| svc := services.NewService(publock, dlder, acStat, uploader, strgSel, stgMeta, db, evtPub, nil, stgPool) | |||
| // 用户空间同步功能 | |||
| spaceSync := spacesyncer.New(db, stgPool, stgMeta) | |||
| spaceSyncChan := spaceSync.Start() | |||
| defer spaceSync.Stop() | |||
| svc := services.NewService(publock, dlder, acStat, uploader, strgSel, stgMeta, db, evtPub, nil, stgPool, spaceSync) | |||
| go func() { | |||
| doTest(svc) | |||
| @@ -189,6 +196,7 @@ func test(configPath string) { | |||
| evtPubEvt := evtPubChan.Receive() | |||
| conColEvt := conColChan.Receive() | |||
| acStatEvt := acStatChan.Receive() | |||
| spaceSyncEvt := spaceSyncChan.Receive() | |||
| loop: | |||
| for { | |||
| @@ -262,6 +270,13 @@ loop: | |||
| break loop | |||
| } | |||
| acStatEvt = acStatChan.Receive() | |||
| case e := <-spaceSyncEvt.Chan(): | |||
| if e.Err != nil { | |||
| logger.Errorf("receive space sync event: %v", err) | |||
| break loop | |||
| } | |||
| spaceSyncEvt = spaceSyncChan.Receive() | |||
| } | |||
| } | |||
| } | |||
| @@ -19,6 +19,7 @@ import ( | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/mount" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/mount/vfstest" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/services" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/spacesyncer" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/uploader" | |||
| stgglb "gitlink.org.cn/cloudream/jcs-pub/common/globals" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/models/datamap" | |||
| @@ -158,6 +159,11 @@ func vfsTest(configPath string, opts serveHTTPOptions) { | |||
| // 上传器 | |||
| uploader := uploader.NewUploader(publock, conCol, stgPool, stgMeta, db) | |||
| // 用户空间同步功能 | |||
| spaceSync := spacesyncer.New(db, stgPool, stgMeta) | |||
| spaceSyncChan := spaceSync.Start() | |||
| defer spaceSync.Stop() | |||
| // 挂载 | |||
| mntCfg := config.Cfg().Mount | |||
| if !opts.DisableMount && mntCfg != nil && mntCfg.Enabled { | |||
| @@ -171,7 +177,7 @@ func vfsTest(configPath string, opts serveHTTPOptions) { | |||
| mntChan := mnt.Start() | |||
| defer mnt.Stop() | |||
| svc := services.NewService(publock, dlder, acStat, uploader, strgSel, stgMeta, db, evtPub, mnt, stgPool) | |||
| svc := services.NewService(publock, dlder, acStat, uploader, strgSel, stgMeta, db, evtPub, mnt, stgPool, spaceSync) | |||
| // HTTP接口 | |||
| httpCfgJSON := config.Cfg().HTTP | |||
| @@ -208,6 +214,7 @@ func vfsTest(configPath string, opts serveHTTPOptions) { | |||
| evtPubEvt := evtPubChan.Receive() | |||
| conColEvt := conColChan.Receive() | |||
| acStatEvt := acStatChan.Receive() | |||
| spaceSyncEvt := spaceSyncChan.Receive() | |||
| httpEvt := httpChan.Receive() | |||
| mntEvt := mntChan.Receive() | |||
| @@ -284,6 +291,12 @@ loop: | |||
| } | |||
| acStatEvt = acStatChan.Receive() | |||
| case e := <-spaceSyncEvt.Chan(): | |||
| if e.Err != nil { | |||
| logger.Errorf("receive space sync event: %v", err) | |||
| } | |||
| spaceSyncEvt = spaceSyncChan.Receive() | |||
| case e := <-httpEvt.Chan(): | |||
| if e.Err != nil { | |||
| logger.Errorf("receive http event: %v", err) | |||
| @@ -39,7 +39,7 @@ var cfg Config | |||
| // TODO 这里的modeulName参数弄成可配置的更好 | |||
| func Init(configPath string) error { | |||
| if configPath == "" { | |||
| return config.DefaultLoad("client", &cfg) | |||
| return config.Load("config.json", &cfg) | |||
| } | |||
| return config.Load(configPath, &cfg) | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| package db | |||
| import "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| type SpaceSyncTaskDB struct { | |||
| *DB | |||
| } | |||
| func (db *DB) SpaceSyncTask() *SpaceSyncTaskDB { | |||
| return &SpaceSyncTaskDB{db} | |||
| } | |||
| func (db *SpaceSyncTaskDB) Create(ctx SQLContext, task *types.SpaceSyncTask) error { | |||
| return ctx.Create(task).Error | |||
| } | |||
| func (db *SpaceSyncTaskDB) GetAll(ctx SQLContext) ([]types.SpaceSyncTask, error) { | |||
| var tasks []types.SpaceSyncTask | |||
| err := ctx.Find(&tasks).Order("TaskID ASC").Error | |||
| return tasks, err | |||
| } | |||
| func (*SpaceSyncTaskDB) Delete(ctx SQLContext, taskID types.SpaceSyncTaskID) error { | |||
| return ctx.Delete(&types.SpaceSyncTask{}, taskID).Error | |||
| } | |||
| func (*SpaceSyncTaskDB) BatchDelete(ctx SQLContext, taskIDs []types.SpaceSyncTaskID) error { | |||
| if len(taskIDs) == 0 { | |||
| return nil | |||
| } | |||
| return ctx.Where("TaskID IN (?)", taskIDs).Delete(&types.SpaceSyncTask{}).Error | |||
| } | |||
| @@ -0,0 +1,99 @@ | |||
| package db | |||
| import ( | |||
| "context" | |||
| "fmt" | |||
| "reflect" | |||
| "gorm.io/gorm/schema" | |||
| ) | |||
| // 必须给结构体(而不是指针)实现此接口。FromString实现为静态方法 | |||
| type StringDBValuer interface { | |||
| ToString() (string, error) | |||
| FromString(str string) (any, error) | |||
| } | |||
| type StringSerializer struct { | |||
| } | |||
| func (StringSerializer) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue interface{}) error { | |||
| if dbValue == nil { | |||
| fieldValue := reflect.New(field.FieldType) | |||
| field.ReflectValueOf(ctx, dst).Set(fieldValue.Elem()) | |||
| return nil | |||
| } | |||
| str := "" | |||
| switch v := dbValue.(type) { | |||
| case []byte: | |||
| str = string(v) | |||
| case string: | |||
| str = v | |||
| default: | |||
| return fmt.Errorf("expected []byte or string, got: %T", dbValue) | |||
| } | |||
| if field.FieldType.Kind() == reflect.Struct { | |||
| val := reflect.Zero(field.FieldType) | |||
| sv, ok := val.Interface().(StringDBValuer) | |||
| if !ok { | |||
| return fmt.Errorf("ref of field type %v is not StringDBValuer", field.FieldType) | |||
| } | |||
| v2, err := sv.FromString(str) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| field.ReflectValueOf(ctx, dst).Set(reflect.ValueOf(v2)) | |||
| return nil | |||
| } | |||
| if field.FieldType.Kind() == reflect.Ptr { | |||
| val := reflect.Zero(field.FieldType.Elem()) | |||
| sv, ok := val.Interface().(StringDBValuer) | |||
| if !ok { | |||
| return fmt.Errorf("field type %v is not StringDBValuer", field.FieldType) | |||
| } | |||
| v2, err := sv.FromString(str) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| field.ReflectValueOf(ctx, dst).Set(reflect.ValueOf(v2)) | |||
| return nil | |||
| } | |||
| return fmt.Errorf("unsupported field type: %v", field.FieldType) | |||
| } | |||
| func (StringSerializer) Value(ctx context.Context, field *schema.Field, dst reflect.Value, fieldValue interface{}) (interface{}, error) { | |||
| val := reflect.ValueOf(fieldValue) | |||
| if val.Kind() == reflect.Struct { | |||
| sv, ok := val.Interface().(StringDBValuer) | |||
| if !ok { | |||
| return nil, fmt.Errorf("ref of field type %v is not StringDBValuer", field.FieldType) | |||
| } | |||
| return sv.ToString() | |||
| } | |||
| if val.Kind() == reflect.Ptr { | |||
| sv, ok := val.Elem().Interface().(StringDBValuer) | |||
| if !ok { | |||
| return nil, fmt.Errorf("field type %v is not StringDBValuer", field.FieldType) | |||
| } | |||
| return sv.ToString() | |||
| } | |||
| return nil, fmt.Errorf("unsupported field type: %v", field.FieldType) | |||
| } | |||
| func init() { | |||
| schema.RegisterSerializer("string", StringSerializer{}) | |||
| } | |||
| @@ -84,7 +84,12 @@ func (s *ObjectService) Upload(ctx *gin.Context) { | |||
| return | |||
| } | |||
| up, err := s.svc.Uploader.BeginUpdate(req.Info.PackageID, req.Info.Affinity, req.Info.CopyTo, req.Info.CopyToPath) | |||
| copyToPath := make([]clitypes.JPath, 0, len(req.Info.CopyToPath)) | |||
| for _, p := range req.Info.CopyToPath { | |||
| copyToPath = append(copyToPath, clitypes.PathFromJcsPathString(p)) | |||
| } | |||
| up, err := s.svc.Uploader.BeginUpdate(req.Info.PackageID, req.Info.Affinity, req.Info.CopyTo, copyToPath) | |||
| if err != nil { | |||
| log.Warnf("begin update: %s", err.Error()) | |||
| ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, fmt.Sprintf("begin update: %v", err))) | |||
| @@ -109,7 +114,7 @@ func (s *ObjectService) Upload(ctx *gin.Context) { | |||
| } | |||
| path = filepath.ToSlash(path) | |||
| err = up.Upload(path, f) | |||
| err = up.Upload(clitypes.PathFromJcsPathString(path), f) | |||
| if err != nil { | |||
| log.Warnf("uploading file: %s", err.Error()) | |||
| ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, fmt.Sprintf("uploading file %v: %v", file.Filename, err))) | |||
| @@ -110,7 +110,12 @@ func (s *PackageService) CreateLoad(ctx *gin.Context) { | |||
| return | |||
| } | |||
| up, err := s.svc.Uploader.BeginCreateUpload(req.Info.BucketID, req.Info.Name, req.Info.CopyTo, req.Info.CopyToPath) | |||
| copyToPath := make([]clitypes.JPath, 0, len(req.Info.CopyToPath)) | |||
| for _, p := range req.Info.CopyToPath { | |||
| copyToPath = append(copyToPath, clitypes.PathFromJcsPathString(p)) | |||
| } | |||
| up, err := s.svc.Uploader.BeginCreateUpload(req.Info.BucketID, req.Info.Name, req.Info.CopyTo, copyToPath) | |||
| if err != nil { | |||
| log.Warnf("begin package create upload: %s", err.Error()) | |||
| ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "%v", err)) | |||
| @@ -135,7 +140,7 @@ func (s *PackageService) CreateLoad(ctx *gin.Context) { | |||
| } | |||
| path = filepath.ToSlash(path) | |||
| err = up.Upload(path, f) | |||
| err = up.Upload(clitypes.PathFromJcsPathString(path), f) | |||
| if err != nil { | |||
| log.Warnf("uploading file: %s", err.Error()) | |||
| ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, fmt.Sprintf("uploading file %v: %v", file.Filename, err))) | |||
| @@ -14,6 +14,7 @@ import ( | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/downloader" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/http/types" | |||
| cliapi "gitlink.org.cn/cloudream/jcs-pub/client/sdk/api/v1" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/ecode" | |||
| ) | |||
| @@ -155,7 +156,12 @@ func (s *PresignedService) ObjectUpload(ctx *gin.Context) { | |||
| return | |||
| } | |||
| up, err := s.svc.Uploader.BeginUpdate(req.PackageID, req.Affinity, req.CopyTo, req.CopyToPath) | |||
| copyToPath := make([]clitypes.JPath, 0, len(req.CopyToPath)) | |||
| for _, p := range req.CopyToPath { | |||
| copyToPath = append(copyToPath, clitypes.PathFromJcsPathString(p)) | |||
| } | |||
| up, err := s.svc.Uploader.BeginUpdate(req.PackageID, req.Affinity, req.CopyTo, copyToPath) | |||
| if err != nil { | |||
| log.Warnf("begin update: %s", err.Error()) | |||
| ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, fmt.Sprintf("begin update: %v", err))) | |||
| @@ -165,7 +171,7 @@ func (s *PresignedService) ObjectUpload(ctx *gin.Context) { | |||
| path := filepath.ToSlash(req.Path) | |||
| err = up.Upload(path, ctx.Request.Body) | |||
| err = up.Upload(clitypes.PathFromJcsPathString(path), ctx.Request.Body) | |||
| if err != nil { | |||
| log.Warnf("uploading file: %s", err.Error()) | |||
| ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, fmt.Sprintf("uploading file %v: %v", req.Path, err))) | |||
| @@ -52,7 +52,6 @@ func (s *Server) InitRouters(rt gin.IRoutes, ah *auth.Auth) { | |||
| rt.POST(cliapi.UserSpaceUpdatePath, certAuth, s.UserSpace().Update) | |||
| rt.POST(cliapi.UserSpaceDeletePath, certAuth, s.UserSpace().Delete) | |||
| rt.POST(cliapi.UserSpaceTestPath, certAuth, s.UserSpace().Test) | |||
| rt.POST(cliapi.UserSpaceSpaceToSpacePath, certAuth, s.UserSpace().SpaceToSpace) | |||
| rt.GET(cliapi.BucketGetByNamePath, certAuth, s.Bucket().GetByName) | |||
| rt.POST(cliapi.BucketCreatePath, certAuth, s.Bucket().Create) | |||
| @@ -63,6 +62,10 @@ func (s *Server) InitRouters(rt gin.IRoutes, ah *auth.Auth) { | |||
| rt.POST(cliapi.ObjectUploadPartPath, certAuth, s.Object().UploadPart) | |||
| rt.POST(cliapi.ObjectCompleteMultipartUploadPath, certAuth, s.Object().CompleteMultipartUpload) | |||
| rt.POST(cliapi.SpaceSyncerCreateTaskPath, certAuth, s.SpaceSyncer().CreateTask) | |||
| rt.GET(cliapi.SpaceSyncerGetTaskPath, certAuth, s.SpaceSyncer().GetTask) | |||
| rt.POST(cliapi.SpaceSyncerCancelTaskPath, certAuth, s.SpaceSyncer().CancelTask) | |||
| rt.GET(cliapi.PresignedObjectListByPathPath, signAuth, s.Presigned().ObjectListByPath) | |||
| rt.GET(cliapi.PresignedObjectDownloadByPathPath, signAuth, s.Presigned().ObjectDownloadByPath) | |||
| rt.GET(cliapi.PresignedObjectDownloadPath, signAuth, s.Presigned().ObjectDownload) | |||
| @@ -0,0 +1,103 @@ | |||
| package http | |||
| import ( | |||
| "net/http" | |||
| "github.com/gin-gonic/gin" | |||
| "gitlink.org.cn/cloudream/common/pkgs/logger" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/http/types" | |||
| cliapi "gitlink.org.cn/cloudream/jcs-pub/client/sdk/api/v1" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/ecode" | |||
| ) | |||
| type SpaceSyncerService struct { | |||
| *Server | |||
| } | |||
| func (s *Server) SpaceSyncer() *SpaceSyncerService { | |||
| return &SpaceSyncerService{s} | |||
| } | |||
| func (s *SpaceSyncerService) CreateTask(ctx *gin.Context) { | |||
| log := logger.WithField("HTTP", "SpaceSyncer.CreateTask") | |||
| req, err := types.ShouldBindJSONEx[cliapi.SpaceSyncerCreateTask](ctx) | |||
| if err != nil { | |||
| log.Warnf("binding body: %s", err.Error()) | |||
| ctx.JSON(http.StatusBadRequest, types.Failed(ecode.BadArgument, "missing argument or invalid argument")) | |||
| return | |||
| } | |||
| if len(req.DestPathes) != len(req.DestUserSpaceIDs) { | |||
| log.Warnf("destPathes and destUserSpaceIDs should have the same length") | |||
| ctx.JSON(http.StatusBadRequest, types.Failed(ecode.BadArgument, "destPathes and destUserSpaceIDs should have the same length")) | |||
| return | |||
| } | |||
| if len(req.DestPathes) == 0 { | |||
| log.Warnf("must have at least one dest") | |||
| ctx.JSON(http.StatusBadRequest, types.Failed(ecode.BadArgument, "must have at least one dest")) | |||
| return | |||
| } | |||
| dests := make([]clitypes.SpaceSyncDest, 0, len(req.DestUserSpaceIDs)) | |||
| for _, id := range req.DestUserSpaceIDs { | |||
| dests = append(dests, clitypes.SpaceSyncDest{ | |||
| DestUserSpaceID: clitypes.UserSpaceID(id), | |||
| DestPath: clitypes.PathFromJcsPathString(req.DestPathes[0]), | |||
| }) | |||
| } | |||
| info, err := s.svc.SpaceSyncer.CreateTask(clitypes.SpaceSyncTask{ | |||
| Trigger: req.Trigger, | |||
| Mode: req.Mode, | |||
| Filters: req.Filters, | |||
| Options: req.Options, | |||
| SrcUserSpaceID: req.SrcUserSpaceID, | |||
| SrcPath: clitypes.PathFromJcsPathString(req.SrcPath), | |||
| Dests: dests, | |||
| }) | |||
| if err != nil { | |||
| log.Warnf("start task: %s", err.Error()) | |||
| ctx.JSON(http.StatusInternalServerError, types.Failed(ecode.OperationFailed, "start task: %v", err)) | |||
| return | |||
| } | |||
| ctx.JSON(http.StatusOK, types.OK(cliapi.SpaceSyncerCreateTaskResp{ | |||
| Task: info.Task, | |||
| })) | |||
| } | |||
| func (s *SpaceSyncerService) CancelTask(ctx *gin.Context) { | |||
| log := logger.WithField("HTTP", "SpaceSyncer.CancelTask") | |||
| var req cliapi.SpaceSyncerCancelTask | |||
| if err := ctx.ShouldBindJSON(&req); err != nil { | |||
| log.Warnf("binding body: %s", err.Error()) | |||
| ctx.JSON(http.StatusBadRequest, types.Failed(ecode.BadArgument, "missing argument or invalid argument")) | |||
| return | |||
| } | |||
| s.svc.SpaceSyncer.CancelTask(req.TaskID) | |||
| ctx.JSON(http.StatusOK, types.OK(cliapi.SpaceSyncerCancelTaskResp{})) | |||
| } | |||
| func (s *SpaceSyncerService) GetTask(ctx *gin.Context) { | |||
| log := logger.WithField("HTTP", "SpaceSyncer.GetTask") | |||
| var req cliapi.SpaceSyncerGetTask | |||
| if err := ctx.ShouldBindQuery(&req); err != nil { | |||
| log.Warnf("binding query: %s", err.Error()) | |||
| ctx.JSON(http.StatusBadRequest, types.Failed(ecode.BadArgument, "missing argument or invalid argument")) | |||
| return | |||
| } | |||
| task := s.svc.SpaceSyncer.GetTask(req.TaskID) | |||
| if task == nil { | |||
| ctx.JSON(http.StatusOK, types.Failed(ecode.DataNotFound, "task not found")) | |||
| return | |||
| } | |||
| ctx.JSON(http.StatusOK, types.OK(cliapi.SpaceSyncerGetTaskResp{Task: *task})) | |||
| } | |||
| @@ -8,6 +8,7 @@ import ( | |||
| "gitlink.org.cn/cloudream/common/pkgs/logger" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/http/types" | |||
| cliapi "gitlink.org.cn/cloudream/jcs-pub/client/sdk/api/v1" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/ecode" | |||
| ) | |||
| @@ -51,7 +52,7 @@ func (s *UserSpaceService) CreatePackage(ctx *gin.Context) { | |||
| return | |||
| } | |||
| pkg, err := s.svc.Uploader.UserSpaceUpload(req.UserSpaceID, req.Path, req.BucketID, req.Name, req.SpaceAffinity) | |||
| pkg, err := s.svc.Uploader.UserSpaceUpload(req.UserSpaceID, clitypes.PathFromJcsPathString(req.Path), req.BucketID, req.Name, req.SpaceAffinity) | |||
| if err != nil { | |||
| log.Warnf("userspace create package: %s", err.Error()) | |||
| ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, fmt.Sprintf("userspace create package: %v", err))) | |||
| @@ -166,25 +167,3 @@ func (s *UserSpaceService) Test(ctx *gin.Context) { | |||
| ctx.JSON(http.StatusOK, types.OK(resp)) | |||
| } | |||
| func (s *UserSpaceService) SpaceToSpace(ctx *gin.Context) { | |||
| log := logger.WithField("HTTP", "UserSpace.SpaceToSpace") | |||
| var req cliapi.UserSpaceSpaceToSpace | |||
| if err := ctx.ShouldBindJSON(&req); err != nil { | |||
| log.Warnf("binding body: %s", err.Error()) | |||
| ctx.JSON(http.StatusBadRequest, types.Failed(ecode.BadArgument, "missing argument or invalid argument")) | |||
| return | |||
| } | |||
| ret, err := s.svc.UserSpaceSvc().SpaceToSpace(req.SrcUserSpaceID, req.SrcPath, req.DstUserSpaceID, req.DstPath) | |||
| if err != nil { | |||
| log.Warnf("space2space: %s", err.Error()) | |||
| ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "space2space failed")) | |||
| return | |||
| } | |||
| ctx.JSON(http.StatusOK, types.OK(cliapi.UserSpaceSpaceToSpaceResp{ | |||
| SpaceToSpaceResult: ret, | |||
| })) | |||
| } | |||
| @@ -1003,7 +1003,7 @@ func (c *Cache) doUploading(pkgs []*syncPackage) { | |||
| counter := io2.Counter(&rd) | |||
| err = upder.Upload(clitypes.JoinObjectPath(o.pathComps[2:]...), counter, uploader.UploadOption{ | |||
| err = upder.Upload(clitypes.PathFromComps(o.pathComps[2:]...), counter, uploader.UploadOption{ | |||
| CreateTime: o.modTime, | |||
| }) | |||
| if err != nil { | |||
| @@ -7,6 +7,7 @@ import ( | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/downloader/strategy" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/metacache" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/mount" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/spacesyncer" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/uploader" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/publock" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/pool" | |||
| @@ -25,6 +26,7 @@ type Service struct { | |||
| EvtPub *sysevent.Publisher | |||
| Mount *mount.Mount | |||
| StgPool *pool.Pool | |||
| SpaceSyncer *spacesyncer.SpaceSyncer | |||
| } | |||
| func NewService( | |||
| @@ -38,6 +40,7 @@ func NewService( | |||
| evtPub *sysevent.Publisher, | |||
| mount *mount.Mount, | |||
| stgPool *pool.Pool, | |||
| spaceSyncer *spacesyncer.SpaceSyncer, | |||
| ) *Service { | |||
| return &Service{ | |||
| PubLock: publock, | |||
| @@ -50,5 +53,6 @@ func NewService( | |||
| EvtPub: evtPub, | |||
| Mount: mount, | |||
| StgPool: stgPool, | |||
| SpaceSyncer: spaceSyncer, | |||
| } | |||
| } | |||
| @@ -3,13 +3,8 @@ package services | |||
| import ( | |||
| "context" | |||
| "fmt" | |||
| "path" | |||
| "strings" | |||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | |||
| "gitlink.org.cn/cloudream/common/pkgs/logger" | |||
| "gitlink.org.cn/cloudream/common/pkgs/trie" | |||
| cdssdk "gitlink.org.cn/cloudream/common/sdks/storage" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| "gorm.io/gorm" | |||
| @@ -22,7 +17,6 @@ import ( | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/ioswitch2/parser" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/publock/reqbuilder" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/factory" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| ) | |||
| type UserSpaceService struct { | |||
| @@ -58,7 +52,7 @@ func (svc *UserSpaceService) Create(req cliapi.UserSpaceCreate) (*cliapi.UserSpa | |||
| Credential: req.Credential, | |||
| ShardStore: req.ShardStore, | |||
| Features: req.Features, | |||
| WorkingDir: req.WorkingDir, | |||
| WorkingDir: clitypes.PathFromJcsPathString(req.WorkingDir), | |||
| Revision: 0, | |||
| } | |||
| err = db2.UserSpace().Create(tx, &space) | |||
| @@ -170,7 +164,7 @@ func (svc *UserSpaceService) Test(req cliapi.UserSpaceTest) (*cliapi.UserSpaceTe | |||
| Name: "test", | |||
| Storage: req.Storage, | |||
| Credential: req.Credential, | |||
| WorkingDir: req.WorikingDir, | |||
| WorkingDir: clitypes.PathFromJcsPathString(req.WorikingDir), | |||
| }, | |||
| } | |||
| blder := factory.GetBuilder(&detail) | |||
| @@ -179,8 +173,7 @@ func (svc *UserSpaceService) Test(req cliapi.UserSpaceTest) (*cliapi.UserSpaceTe | |||
| return nil, ecode.Newf(ecode.OperationFailed, "%v", err) | |||
| } | |||
| // TODO 可以考虑增加一个专门用于检查配置的接口F | |||
| _, err = baseStore.ListAll("") | |||
| err = baseStore.Test() | |||
| if err != nil { | |||
| return nil, ecode.Newf(ecode.OperationFailed, "%v", err) | |||
| } | |||
| @@ -202,6 +195,8 @@ func (svc *UserSpaceService) DownloadPackage(packageID clitypes.PackageID, users | |||
| return err | |||
| } | |||
| rootJPath := clitypes.PathFromJcsPathString(rootPath) | |||
| var pinned []clitypes.ObjectID | |||
| plans := exec.NewPlanBuilder() | |||
| for _, obj := range details { | |||
| @@ -227,7 +222,9 @@ func (svc *UserSpaceService) DownloadPackage(packageID clitypes.PackageID, users | |||
| return fmt.Errorf("unsupported download strategy: %T", strg) | |||
| } | |||
| ft.AddTo(ioswitch2.NewToBaseStore(*destStg, path.Join(rootPath, obj.Object.Path))) | |||
| objPath := clitypes.PathFromJcsPathString(obj.Object.Path) | |||
| dstPath := rootJPath.ConcatNew(objPath) | |||
| ft.AddTo(ioswitch2.NewToBaseStore(*destStg, dstPath)) | |||
| // 顺便保存到同存储服务的分片存储中 | |||
| if destStg.UserSpace.ShardStore != nil { | |||
| ft.AddTo(ioswitch2.NewToShardStore(*destStg, ioswitch2.RawStream(), "")) | |||
| @@ -262,142 +259,3 @@ func (svc *UserSpaceService) DownloadPackage(packageID clitypes.PackageID, users | |||
| return nil | |||
| } | |||
| func (svc *UserSpaceService) SpaceToSpace(srcSpaceID clitypes.UserSpaceID, srcPath string, dstSpaceID clitypes.UserSpaceID, dstPath string) (clitypes.SpaceToSpaceResult, error) { | |||
| srcSpace := svc.UserSpaceMeta.Get(srcSpaceID) | |||
| if srcSpace == nil { | |||
| return clitypes.SpaceToSpaceResult{}, fmt.Errorf("source userspace not found: %d", srcSpaceID) | |||
| } | |||
| srcStore, err := svc.StgPool.GetBaseStore(srcSpace) | |||
| if err != nil { | |||
| return clitypes.SpaceToSpaceResult{}, fmt.Errorf("get source userspace store: %w", err) | |||
| } | |||
| dstSpace := svc.UserSpaceMeta.Get(dstSpaceID) | |||
| if dstSpace == nil { | |||
| return clitypes.SpaceToSpaceResult{}, fmt.Errorf("destination userspace not found: %d", dstSpaceID) | |||
| } | |||
| dstStore, err := svc.StgPool.GetBaseStore(dstSpace) | |||
| if err != nil { | |||
| return clitypes.SpaceToSpaceResult{}, fmt.Errorf("get destination userspace store: %w", err) | |||
| } | |||
| srcPath = strings.Trim(srcPath, cdssdk.ObjectPathSeparator) | |||
| dstPath = strings.Trim(dstPath, cdssdk.ObjectPathSeparator) | |||
| if srcPath == "" { | |||
| return clitypes.SpaceToSpaceResult{}, fmt.Errorf("source path is empty") | |||
| } | |||
| if dstPath == "" { | |||
| return clitypes.SpaceToSpaceResult{}, fmt.Errorf("destination path is empty") | |||
| } | |||
| entries, cerr := srcStore.ListAll(srcPath) | |||
| if cerr != nil { | |||
| return clitypes.SpaceToSpaceResult{}, fmt.Errorf("list all from source userspace: %w", cerr) | |||
| } | |||
| srcPathComps := clitypes.SplitObjectPath(srcPath) | |||
| srcDirCompLen := len(srcPathComps) - 1 | |||
| entryTree := trie.NewTrie[*types.ListEntry]() | |||
| for _, e := range entries { | |||
| pa, ok := strings.CutSuffix(e.Path, clitypes.ObjectPathSeparator) | |||
| comps := clitypes.SplitObjectPath(pa) | |||
| e.Path = pa | |||
| e2 := e | |||
| entryTree.CreateWords(comps[srcDirCompLen:]).Value = &e2 | |||
| e2.IsDir = e2.IsDir || ok | |||
| } | |||
| entryTree.Iterate(func(path []string, node *trie.Node[*types.ListEntry], isWordNode bool) trie.VisitCtrl { | |||
| if node.Value == nil { | |||
| return trie.VisitContinue | |||
| } | |||
| if node.Value.IsDir && len(node.WordNexts) > 0 { | |||
| node.Value = nil | |||
| return trie.VisitContinue | |||
| } | |||
| if !node.Value.IsDir && len(node.WordNexts) == 0 { | |||
| node.WordNexts = nil | |||
| } | |||
| return trie.VisitContinue | |||
| }) | |||
| var filePathes []string | |||
| var dirPathes []string | |||
| entryTree.Iterate(func(path []string, node *trie.Node[*types.ListEntry], isWordNode bool) trie.VisitCtrl { | |||
| if node.Value == nil { | |||
| return trie.VisitContinue | |||
| } | |||
| if node.Value.IsDir { | |||
| dirPathes = append(dirPathes, node.Value.Path) | |||
| } else { | |||
| filePathes = append(filePathes, node.Value.Path) | |||
| } | |||
| return trie.VisitContinue | |||
| }) | |||
| mutex, err := reqbuilder.NewBuilder().UserSpace().Buzy(srcSpaceID).Buzy(dstSpaceID).MutexLock(svc.PubLock) | |||
| if err != nil { | |||
| return clitypes.SpaceToSpaceResult{}, fmt.Errorf("acquire lock: %w", err) | |||
| } | |||
| defer mutex.Unlock() | |||
| var success []string | |||
| var failed []string | |||
| for _, f := range filePathes { | |||
| newPath := strings.Replace(f, srcPath, dstPath, 1) | |||
| ft := ioswitch2.NewFromTo() | |||
| ft.AddFrom(ioswitch2.NewFromBaseStore(*srcSpace, f)) | |||
| ft.AddTo(ioswitch2.NewToBaseStore(*dstSpace, newPath)) | |||
| plans := exec.NewPlanBuilder() | |||
| err := parser.Parse(ft, plans) | |||
| if err != nil { | |||
| failed = append(failed, f) | |||
| logger.Warnf("s2s: parse plan of file %v: %v", f, err) | |||
| continue | |||
| } | |||
| exeCtx := exec.NewExecContext() | |||
| exec.SetValueByType(exeCtx, svc.StgPool) | |||
| _, cerr := plans.Execute(exeCtx).Wait(context.Background()) | |||
| if cerr != nil { | |||
| failed = append(failed, f) | |||
| logger.Warnf("s2s: execute plan of file %v: %v", f, cerr) | |||
| continue | |||
| } | |||
| success = append(success, f) | |||
| } | |||
| newDirPathes := make([]string, 0, len(dirPathes)) | |||
| for i := range dirPathes { | |||
| newDirPathes = append(newDirPathes, strings.Replace(dirPathes[i], srcPath, dstPath, 1)) | |||
| } | |||
| for _, d := range newDirPathes { | |||
| err := dstStore.Mkdir(d) | |||
| if err != nil { | |||
| failed = append(failed, d) | |||
| } else { | |||
| success = append(success, d) | |||
| } | |||
| } | |||
| return clitypes.SpaceToSpaceResult{ | |||
| Success: success, | |||
| Failed: failed, | |||
| }, nil | |||
| } | |||
| @@ -0,0 +1,38 @@ | |||
| package spacesyncer | |||
| import ( | |||
| "gitlink.org.cn/cloudream/common/pkgs/trie" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| stgtypes "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| ) | |||
| func execute(syncer *SpaceSyncer, task *task) { | |||
| switch mode := task.Task.Mode.(type) { | |||
| case *types.SpaceSyncModeFull: | |||
| executeFull(syncer, task) | |||
| case *types.SpaceSyncModeDiff: | |||
| executeDiff(syncer, task, mode) | |||
| } | |||
| } | |||
| func createDirNode(tree *trie.Trie[*stgtypes.DirEntry], pathComps []string, e *stgtypes.DirEntry) { | |||
| var ptr = &tree.Root | |||
| for _, c := range pathComps { | |||
| ptr.Value = nil | |||
| ptr = ptr.Create(c) | |||
| } | |||
| ptr.Value = e | |||
| } | |||
| func removeDirNode(tree *trie.Trie[*stgtypes.DirEntry], pathComps []string) { | |||
| var ptr = &tree.Root | |||
| for _, c := range pathComps { | |||
| ptr.Value = nil | |||
| next := ptr.WalkNext(c) | |||
| if next == nil { | |||
| break | |||
| } | |||
| } | |||
| ptr.Value = nil | |||
| ptr.RemoveSelf(true) | |||
| } | |||
| @@ -0,0 +1,240 @@ | |||
| package spacesyncer | |||
| import ( | |||
| "context" | |||
| "io" | |||
| "time" | |||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | |||
| "gitlink.org.cn/cloudream/common/pkgs/logger" | |||
| "gitlink.org.cn/cloudream/common/pkgs/trie" | |||
| "gitlink.org.cn/cloudream/common/utils/math2" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/ioswitch2" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/ioswitch2/parser" | |||
| stgtypes "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| ) | |||
| func executeDiff(syncer *SpaceSyncer, task *task, mode *clitypes.SpaceSyncModeDiff) { | |||
| log := logger.WithField("Mod", logMod).WithField("TaskID", task.Task.TaskID) | |||
| startTime := time.Now() | |||
| log.Infof("begin full sync task") | |||
| defer func() { | |||
| log.Infof("full sync task finished, time: %v", time.Since(startTime)) | |||
| }() | |||
| srcSpace := syncer.spaceMeta.Get(task.Task.SrcUserSpaceID) | |||
| if srcSpace == nil { | |||
| log.Warnf("src space %v not found", task.Task.SrcUserSpaceID) | |||
| return | |||
| } | |||
| if len(task.Task.Dests) > 1 { | |||
| log.Warnf("diff mode only support one dest now") | |||
| } | |||
| dstSpace := syncer.spaceMeta.Get(task.Task.Dests[0].DestUserSpaceID) | |||
| if dstSpace == nil { | |||
| log.Warnf("dest space %v not found", task.Task.Dests[0].DestUserSpaceID) | |||
| return | |||
| } | |||
| srcBase, err := syncer.stgPool.GetBaseStore(srcSpace) | |||
| if err != nil { | |||
| log.Warnf("get src base store error: %v", err) | |||
| return | |||
| } | |||
| dstBase, err := syncer.stgPool.GetBaseStore(dstSpace) | |||
| if err != nil { | |||
| log.Warnf("get dst base store error: %v", err) | |||
| return | |||
| } | |||
| filter := buildFilter(task) | |||
| srcReader := srcBase.ReadDir(task.Task.SrcPath) | |||
| dstReader := dstBase.ReadDir(task.Task.Dests[0].DestPath) | |||
| dirTree := trie.NewTrie[srcDstDirEntry]() | |||
| for { | |||
| e, err := srcReader.Next() | |||
| if err == io.EOF { | |||
| break | |||
| } | |||
| if err != nil { | |||
| log.Warnf("read src dir: %v", err) | |||
| return | |||
| } | |||
| if !filter(e) { | |||
| continue | |||
| } | |||
| rela := e.Path.Clone() | |||
| rela.DropFrontN(task.Task.SrcPath.Len()) | |||
| ne := e | |||
| ne.Path = rela.Clone() | |||
| if !filter(ne) { | |||
| continue | |||
| } | |||
| diffCreateSrcNode(dirTree, rela, &e) | |||
| } | |||
| for { | |||
| e, err := dstReader.Next() | |||
| if err == io.EOF { | |||
| break | |||
| } | |||
| if err != nil { | |||
| log.Warnf("read dst dir: %v", err) | |||
| return | |||
| } | |||
| if !filter(e) { | |||
| continue | |||
| } | |||
| rela := e.Path.Clone() | |||
| rela.DropFrontN(task.Task.Dests[0].DestPath.Len()) | |||
| ne := e | |||
| ne.Path = rela.Clone() | |||
| if !filter(ne) { | |||
| continue | |||
| } | |||
| diffCreateDstNode(dirTree, rela, &e) | |||
| } | |||
| var willSync []stgtypes.DirEntry | |||
| var willMkdirs []clitypes.JPath | |||
| dirTree.Iterate(func(path []string, node *trie.Node[srcDstDirEntry], isWordNode bool) trie.VisitCtrl { | |||
| if node.Value.src == nil { | |||
| // 目前不支持删除多余文件 | |||
| return trie.VisitContinue | |||
| } | |||
| if node.Value.src.IsDir { | |||
| if node.Value.dst == nil { | |||
| if node.IsEmpty() { | |||
| willMkdirs = append(willMkdirs, clitypes.PathFromComps(path...)) | |||
| } | |||
| } | |||
| } else { | |||
| if node.Value.dst == nil { | |||
| // 目标路径不存在(不是文件也不是目录),需要同步 | |||
| if node.IsEmpty() { | |||
| willSync = append(willSync, *node.Value.src) | |||
| } | |||
| } else if !node.Value.dst.IsDir { | |||
| // 目标路径是个文件,但文件指纹不同,需要同步 | |||
| if !cmpFile(mode, node.Value.src, node.Value.dst) { | |||
| willSync = append(willSync, *node.Value.src) | |||
| } | |||
| } | |||
| // 目标路径是个目录,则不进行同步 | |||
| } | |||
| return trie.VisitContinue | |||
| }) | |||
| willSyncCnt := len(willSync) | |||
| for len(willSync) > 0 { | |||
| syncs := willSync[:math2.Min(len(willSync), 50)] | |||
| willSync = willSync[len(syncs):] | |||
| ft := ioswitch2.NewFromTo() | |||
| for _, s := range syncs { | |||
| ft.AddFrom(ioswitch2.NewFromBaseStore(*srcSpace, s.Path)) | |||
| rela := s.Path.Clone() | |||
| rela.DropFrontN(task.Task.SrcPath.Len()) | |||
| dstPath := task.Task.Dests[0].DestPath.ConcatNew(rela) | |||
| to := ioswitch2.NewToBaseStore(*dstSpace, dstPath) | |||
| to.Option.ModTime = s.ModTime | |||
| ft.AddTo(to) | |||
| } | |||
| planBld := exec.NewPlanBuilder() | |||
| err := parser.Parse(ft, planBld) | |||
| if err != nil { | |||
| log.Warnf("parse fromto: %v", err) | |||
| return | |||
| } | |||
| execCtx := exec.NewWithContext(task.Context) | |||
| exec.SetValueByType(execCtx, syncer.stgPool) | |||
| _, err = planBld.Execute(execCtx).Wait(context.Background()) | |||
| if err != nil { | |||
| log.Warnf("execute plan: %v", err) | |||
| return | |||
| } | |||
| } | |||
| log.Infof("%v files synced", willSyncCnt) | |||
| if !task.Task.Options.NoEmptyDirectories && len(willMkdirs) > 0 { | |||
| for _, p := range willMkdirs { | |||
| rela := p.Clone() | |||
| rela.DropFrontN(task.Task.SrcPath.Len()) | |||
| dstPath := task.Task.Dests[0].DestPath.ConcatNew(rela) | |||
| err := dstBase.Mkdir(dstPath) | |||
| if err != nil { | |||
| log.Warnf("mkdir: %v", err) | |||
| continue | |||
| } | |||
| } | |||
| } | |||
| } | |||
| func diffCreateSrcNode(tree *trie.Trie[srcDstDirEntry], path clitypes.JPath, e *stgtypes.DirEntry) { | |||
| var ptr = &tree.Root | |||
| for _, c := range path.Comps() { | |||
| if ptr.Value.src != nil && ptr.Value.src.IsDir { | |||
| ptr.Value.src = nil | |||
| } | |||
| ptr = ptr.Create(c) | |||
| } | |||
| ptr.Value.src = e | |||
| } | |||
| func diffCreateDstNode(tree *trie.Trie[srcDstDirEntry], path clitypes.JPath, e *stgtypes.DirEntry) { | |||
| var ptr = &tree.Root | |||
| for _, c := range path.Comps() { | |||
| if ptr.Value.src != nil && ptr.Value.src.IsDir { | |||
| ptr.Value.src = nil | |||
| } | |||
| if ptr.Value.dst != nil && ptr.Value.dst.IsDir { | |||
| ptr.Value.dst = nil | |||
| } | |||
| ptr = ptr.Create(c) | |||
| } | |||
| ptr.Value.dst = e | |||
| } | |||
| type srcDstDirEntry struct { | |||
| src *stgtypes.DirEntry | |||
| dst *stgtypes.DirEntry | |||
| } | |||
| func cmpFile(diff *clitypes.SpaceSyncModeDiff, src, dst *stgtypes.DirEntry) bool { | |||
| if diff.IncludeSize && src.Size != dst.Size { | |||
| return false | |||
| } | |||
| if diff.IncludeModTime && src.ModTime != dst.ModTime { | |||
| return false | |||
| } | |||
| return true | |||
| } | |||
| @@ -0,0 +1,159 @@ | |||
| package spacesyncer | |||
| import ( | |||
| "context" | |||
| "fmt" | |||
| "io" | |||
| "time" | |||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | |||
| "gitlink.org.cn/cloudream/common/pkgs/logger" | |||
| "gitlink.org.cn/cloudream/common/pkgs/trie" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/ioswitch2" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/ioswitch2/parser" | |||
| stgtypes "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| ) | |||
| func executeFull(syncer *SpaceSyncer, task *task) { | |||
| log := logger.WithField("Mod", logMod).WithField("TaskID", task.Task.TaskID) | |||
| startTime := time.Now() | |||
| log.Infof("begin full sync task") | |||
| defer func() { | |||
| log.Infof("full sync task finished, time: %v", time.Since(startTime)) | |||
| }() | |||
| srcSpace := syncer.spaceMeta.Get(task.Task.SrcUserSpaceID) | |||
| if srcSpace == nil { | |||
| log.Warnf("src space %v not found", task.Task.SrcUserSpaceID) | |||
| return | |||
| } | |||
| dstSpaceIDs := make([]types.UserSpaceID, len(task.Task.Dests)) | |||
| for i := range task.Task.Dests { | |||
| dstSpaceIDs[i] = task.Task.Dests[i].DestUserSpaceID | |||
| } | |||
| dstSpaces := syncer.spaceMeta.GetMany(dstSpaceIDs) | |||
| for i := range dstSpaces { | |||
| if dstSpaces[i] == nil { | |||
| log.Warnf("dst space %v not found", dstSpaceIDs[i]) | |||
| return | |||
| } | |||
| } | |||
| srcBase, err := syncer.stgPool.GetBaseStore(srcSpace) | |||
| if err != nil { | |||
| log.Warnf("get src base store: %v", err) | |||
| return | |||
| } | |||
| filter := buildFilter(task) | |||
| srcDirReader := srcBase.ReadDir(task.Task.SrcPath) | |||
| defer srcDirReader.Close() | |||
| srcDirTree := trie.NewTrie[*stgtypes.DirEntry]() | |||
| fileCnt := 0 | |||
| for { | |||
| isEOF := false | |||
| ft := ioswitch2.NewFromTo() | |||
| cnt := 0 | |||
| for { | |||
| e, err := srcDirReader.Next() | |||
| if err == io.EOF { | |||
| isEOF = true | |||
| break | |||
| } | |||
| if err != nil { | |||
| log.Warnf("read src dir: %v", err) | |||
| return | |||
| } | |||
| rela := e.Path.Clone() | |||
| rela.DropFrontN(task.Task.SrcPath.Len()) | |||
| ne := e | |||
| ne.Path = rela.Clone() | |||
| if !filter(ne) { | |||
| continue | |||
| } | |||
| if e.IsDir { | |||
| // 如果是一个目录,则创建对应的Dir节点,且在创建过程中清除掉路径上的Dir信息(仅保留最后一个Dir节点) | |||
| createDirNode(srcDirTree, rela.Comps(), &e) | |||
| continue | |||
| } | |||
| fmt.Printf("rela: %v\n", rela) | |||
| // 如果是一个文件,那么它路径上的目录都可以在写入时一并创建,所以可以清理掉路径上的Dir节点 | |||
| removeDirNode(srcDirTree, rela.Comps()) | |||
| ft.AddFrom(ioswitch2.NewFromBaseStore(*srcSpace, e.Path)) | |||
| for i, dst := range dstSpaces { | |||
| dstPath := task.Task.Dests[i].DestPath.Clone() | |||
| dstPath.Concat(rela) | |||
| ft.AddTo(ioswitch2.NewToBaseStore(*dst, dstPath)) | |||
| } | |||
| cnt++ | |||
| fileCnt++ | |||
| // 每一批转发50个文件 | |||
| if cnt > 50 { | |||
| break | |||
| } | |||
| } | |||
| if len(ft.Froms) > 0 { | |||
| planBld := exec.NewPlanBuilder() | |||
| err := parser.Parse(ft, planBld) | |||
| if err != nil { | |||
| log.Warnf("parse fromto to plan: %v", err) | |||
| return | |||
| } | |||
| execCtx := exec.NewWithContext(task.Context) | |||
| exec.SetValueByType(execCtx, syncer.stgPool) | |||
| _, err = planBld.Execute(execCtx).Wait(context.Background()) | |||
| if err != nil { | |||
| log.Warnf("execute plan: %v", err) | |||
| return | |||
| } | |||
| } | |||
| if isEOF { | |||
| break | |||
| } | |||
| } | |||
| log.Infof("%v files synced", fileCnt) | |||
| if !task.Task.Options.NoEmptyDirectories { | |||
| dstBases := make([]stgtypes.BaseStore, len(dstSpaces)) | |||
| for i := range dstSpaces { | |||
| dstBases[i], err = syncer.stgPool.GetBaseStore(dstSpaces[i]) | |||
| if err != nil { | |||
| log.Warnf("get dst base store: %v", err) | |||
| continue | |||
| } | |||
| } | |||
| srcDirTree.Iterate(func(path []string, node *trie.Node[*stgtypes.DirEntry], isWordNode bool) trie.VisitCtrl { | |||
| if node.Value == nil { | |||
| return trie.VisitContinue | |||
| } | |||
| for i, base := range dstBases { | |||
| if base != nil { | |||
| dirPath := task.Task.Dests[i].DestPath.Clone() | |||
| dirPath.ConcatComps(path) | |||
| err := base.Mkdir(dirPath) | |||
| if err != nil { | |||
| log.Warnf("mkdir %v at user space %v: %v", dirPath, dstSpaces[i].String(), err) | |||
| } | |||
| } | |||
| } | |||
| return trie.VisitContinue | |||
| }) | |||
| } | |||
| } | |||
| @@ -0,0 +1,39 @@ | |||
| package spacesyncer | |||
| import ( | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| stgtypes "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| ) | |||
| type FilterFn func(info stgtypes.DirEntry) bool | |||
| func buildFilter(task *task) FilterFn { | |||
| var fns []FilterFn | |||
| for _, f := range task.Task.Filters { | |||
| switch f := f.(type) { | |||
| case *clitypes.SpaceSyncFilterSize: | |||
| fns = append(fns, filterSize(f)) | |||
| } | |||
| } | |||
| return func(info stgtypes.DirEntry) bool { | |||
| for _, fn := range fns { | |||
| if !fn(info) { | |||
| return false | |||
| } | |||
| } | |||
| return true | |||
| } | |||
| } | |||
| func filterSize(filter *clitypes.SpaceSyncFilterSize) FilterFn { | |||
| return func(info stgtypes.DirEntry) bool { | |||
| if filter.MinSize > 0 && info.Size < filter.MinSize { | |||
| return false | |||
| } | |||
| if filter.MaxSize > 0 && info.Size > filter.MaxSize { | |||
| return false | |||
| } | |||
| return true | |||
| } | |||
| } | |||
| @@ -0,0 +1,191 @@ | |||
| package spacesyncer | |||
| import ( | |||
| "context" | |||
| "fmt" | |||
| "sync" | |||
| "gitlink.org.cn/cloudream/common/pkgs/async" | |||
| "gitlink.org.cn/cloudream/common/pkgs/logger" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/db" | |||
| "gitlink.org.cn/cloudream/jcs-pub/client/internal/metacache" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| stgpool "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/pool" | |||
| ) | |||
| const ( | |||
| logMod = "SpaceSyncer" | |||
| ) | |||
| type SpaceSyncerEvent interface { | |||
| IsSpaceSyncerEvent() bool | |||
| } | |||
| type SpaceSyncer struct { | |||
| db *db.DB | |||
| stgPool *stgpool.Pool | |||
| spaceMeta *metacache.UserSpaceMeta | |||
| lock sync.Mutex | |||
| tasks map[clitypes.SpaceSyncTaskID]*task | |||
| } | |||
| func New(db *db.DB, stgPool *stgpool.Pool, spaceMeta *metacache.UserSpaceMeta) *SpaceSyncer { | |||
| return &SpaceSyncer{ | |||
| db: db, | |||
| stgPool: stgPool, | |||
| spaceMeta: spaceMeta, | |||
| tasks: make(map[clitypes.SpaceSyncTaskID]*task), | |||
| } | |||
| } | |||
| func (s *SpaceSyncer) Start() *async.UnboundChannel[SpaceSyncerEvent] { | |||
| s.lock.Lock() | |||
| defer s.lock.Unlock() | |||
| log := logger.WithField("Mod", logMod) | |||
| ch := async.NewUnboundChannel[SpaceSyncerEvent]() | |||
| allTask, err := db.DoTx01(s.db, s.db.SpaceSyncTask().GetAll) | |||
| if err != nil { | |||
| log.Warnf("load task from db: %v", err) | |||
| } else { | |||
| var rms []clitypes.SpaceSyncTaskID | |||
| for _, t := range allTask { | |||
| ctx, cancel := context.WithCancel(context.Background()) | |||
| tsk := task{ | |||
| Task: t, | |||
| Context: ctx, | |||
| CancelFn: cancel, | |||
| } | |||
| switch tr := t.Trigger.(type) { | |||
| case *clitypes.SpaceSyncTriggerOnce: | |||
| // Once类型的任务没有执行完也不执行了 | |||
| rms = append(rms, t.TaskID) | |||
| case *clitypes.SpaceSyncTriggerInterval: | |||
| triggerInterval(s, &tsk, tr) | |||
| case *clitypes.SpaceSyncTriggerAt: | |||
| triggerAt(s, &tsk, tr) | |||
| } | |||
| log.Infof("load task %v from db", t.TaskID) | |||
| } | |||
| if len(rms) > 0 { | |||
| err := s.db.SpaceSyncTask().BatchDelete(s.db.DefCtx(), rms) | |||
| if err != nil { | |||
| log.Warnf("batch delete task: %v", err) | |||
| } else { | |||
| log.Infof("%v once task deleted", len(rms)) | |||
| } | |||
| } | |||
| } | |||
| return ch | |||
| } | |||
| func (s *SpaceSyncer) Stop() { | |||
| s.lock.Lock() | |||
| defer s.lock.Unlock() | |||
| for _, t := range s.tasks { | |||
| t.CancelFn() | |||
| } | |||
| s.tasks = make(map[clitypes.SpaceSyncTaskID]*task) | |||
| } | |||
| func (s *SpaceSyncer) CreateTask(t clitypes.SpaceSyncTask) (*TaskInfo, error) { | |||
| log := logger.WithField("Mod", logMod) | |||
| d := s.db | |||
| err := d.DoTx(func(tx db.SQLContext) error { | |||
| err := d.SpaceSyncTask().Create(tx, &t) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| return nil | |||
| }) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("creating space sync task: %w", err) | |||
| } | |||
| ctx, cancel := context.WithCancel(context.Background()) | |||
| tsk := task{ | |||
| Task: t, | |||
| Context: ctx, | |||
| CancelFn: cancel, | |||
| } | |||
| s.lock.Lock() | |||
| s.tasks[t.TaskID] = &tsk | |||
| s.lock.Unlock() | |||
| switch tr := t.Trigger.(type) { | |||
| case *clitypes.SpaceSyncTriggerOnce: | |||
| triggerOnce(s, &tsk) | |||
| case *clitypes.SpaceSyncTriggerInterval: | |||
| triggerInterval(s, &tsk, tr) | |||
| case *clitypes.SpaceSyncTriggerAt: | |||
| triggerAt(s, &tsk, tr) | |||
| } | |||
| log.Infof("task %v created", t.TaskID) | |||
| return &TaskInfo{ | |||
| Task: t, | |||
| }, nil | |||
| } | |||
| func (s *SpaceSyncer) CancelTask(taskID clitypes.SpaceSyncTaskID) { | |||
| log := logger.WithField("Mod", logMod) | |||
| s.lock.Lock() | |||
| defer s.lock.Unlock() | |||
| t := s.tasks[taskID] | |||
| if t == nil { | |||
| log.Infof("task %v not found, cancel aborted", taskID) | |||
| return | |||
| } | |||
| t.CancelFn() | |||
| delete(s.tasks, taskID) | |||
| err := s.db.SpaceSyncTask().Delete(s.db.DefCtx(), taskID) | |||
| if err != nil { | |||
| log.Warnf("delete task %v from db: %v", taskID, err) | |||
| } | |||
| log.Infof("task %v canceled", taskID) | |||
| } | |||
| func (s *SpaceSyncer) GetTask(taskID clitypes.SpaceSyncTaskID) *clitypes.SpaceSyncTask { | |||
| s.lock.Lock() | |||
| defer s.lock.Unlock() | |||
| tsk := s.tasks[taskID] | |||
| if tsk == nil { | |||
| return nil | |||
| } | |||
| // TODO 考虑复制一份状态,防止修改 | |||
| t := tsk.Task | |||
| return &t | |||
| } | |||
| type TaskInfo struct { | |||
| Task clitypes.SpaceSyncTask | |||
| } | |||
| type task struct { | |||
| Task clitypes.SpaceSyncTask | |||
| Context context.Context | |||
| CancelFn func() | |||
| } | |||
| @@ -0,0 +1,109 @@ | |||
| package spacesyncer | |||
| import ( | |||
| "time" | |||
| "gitlink.org.cn/cloudream/common/pkgs/logger" | |||
| "gitlink.org.cn/cloudream/common/utils/sort2" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| ) | |||
| func triggerOnce(syncer *SpaceSyncer, task *task) { | |||
| go func() { | |||
| log := logger.WithField("Mod", logMod) | |||
| execute(syncer, task) | |||
| syncer.lock.Lock() | |||
| defer syncer.lock.Unlock() | |||
| tsk := syncer.tasks[task.Task.TaskID] | |||
| if tsk == nil { | |||
| return | |||
| } | |||
| tsk.CancelFn() | |||
| delete(syncer.tasks, task.Task.TaskID) | |||
| err := syncer.db.SpaceSyncTask().Delete(syncer.db.DefCtx(), task.Task.TaskID) | |||
| if err != nil { | |||
| log.Warnf("delete task %v from db: %v", task.Task.TaskID, err) | |||
| } | |||
| }() | |||
| } | |||
| func triggerInterval(syncer *SpaceSyncer, task *task, trigger *clitypes.SpaceSyncTriggerInterval) { | |||
| go func() { | |||
| log := logger.WithField("Mod", logMod) | |||
| ticker := time.NewTicker(time.Duration(trigger.Interval) * time.Second) | |||
| defer ticker.Stop() | |||
| loop: | |||
| for { | |||
| select { | |||
| case <-ticker.C: | |||
| execute(syncer, task) | |||
| case <-task.Context.Done(): | |||
| break loop | |||
| } | |||
| } | |||
| syncer.lock.Lock() | |||
| defer syncer.lock.Unlock() | |||
| tsk := syncer.tasks[task.Task.TaskID] | |||
| if tsk == nil { | |||
| return | |||
| } | |||
| tsk.CancelFn() | |||
| delete(syncer.tasks, task.Task.TaskID) | |||
| err := syncer.db.SpaceSyncTask().Delete(syncer.db.DefCtx(), task.Task.TaskID) | |||
| if err != nil { | |||
| log.Warnf("delete task %v from db: %v", task.Task.TaskID, err) | |||
| } | |||
| }() | |||
| } | |||
| func triggerAt(syncer *SpaceSyncer, task *task, trigger *clitypes.SpaceSyncTriggerAt) { | |||
| go func() { | |||
| log := logger.WithField("Mod", logMod) | |||
| atTimes := sort2.Sort(trigger.At, func(l, r time.Time) int { | |||
| return l.Compare(r) | |||
| }) | |||
| loop: | |||
| for _, at := range atTimes { | |||
| nowTime := time.Now() | |||
| if nowTime.After(at) { | |||
| continue | |||
| } | |||
| select { | |||
| case <-time.After(at.Sub(nowTime)): | |||
| execute(syncer, task) | |||
| case <-task.Context.Done(): | |||
| break loop | |||
| } | |||
| } | |||
| syncer.lock.Lock() | |||
| defer syncer.lock.Unlock() | |||
| tsk := syncer.tasks[task.Task.TaskID] | |||
| if tsk == nil { | |||
| return | |||
| } | |||
| tsk.CancelFn() | |||
| delete(syncer.tasks, task.Task.TaskID) | |||
| err := syncer.db.SpaceSyncTask().Delete(syncer.db.DefCtx(), task.Task.TaskID) | |||
| if err != nil { | |||
| log.Warnf("delete task %v from db: %v", task.Task.TaskID, err) | |||
| } | |||
| }() | |||
| } | |||
| @@ -4,7 +4,6 @@ import ( | |||
| "context" | |||
| "fmt" | |||
| "io" | |||
| "path" | |||
| "sync" | |||
| "time" | |||
| @@ -20,7 +19,7 @@ import ( | |||
| type CreateUploader struct { | |||
| pkg types.Package | |||
| targetSpaces []types.UserSpaceDetail | |||
| copyRoots []string | |||
| copyRoots []types.JPath | |||
| uploader *Uploader | |||
| pubLock *publock.Mutex | |||
| successes []db.AddObjectEntry | |||
| @@ -33,7 +32,7 @@ type CreateUploadResult struct { | |||
| Objects map[string]types.Object | |||
| } | |||
| func (u *CreateUploader) Upload(pa string, stream io.Reader, opts ...UploadOption) error { | |||
| func (u *CreateUploader) Upload(pa types.JPath, stream io.Reader, opts ...UploadOption) error { | |||
| opt := UploadOption{} | |||
| if len(opts) > 0 { | |||
| opt = opts[0] | |||
| @@ -50,7 +49,7 @@ func (u *CreateUploader) Upload(pa string, stream io.Reader, opts ...UploadOptio | |||
| ft.AddFrom(fromExec) | |||
| for i, space := range u.targetSpaces { | |||
| ft.AddTo(ioswitch2.NewToShardStore(space, ioswitch2.RawStream(), "shardInfo")) | |||
| ft.AddTo(ioswitch2.NewToBaseStore(space, path.Join(u.copyRoots[i], pa))) | |||
| ft.AddTo(ioswitch2.NewToBaseStore(space, u.copyRoots[i].ConcatNew(pa))) | |||
| spaceIDs = append(spaceIDs, space.UserSpace.UserSpaceID) | |||
| } | |||
| @@ -75,7 +74,7 @@ func (u *CreateUploader) Upload(pa string, stream io.Reader, opts ...UploadOptio | |||
| // 记录上传结果 | |||
| shardInfo := ret["fileHash"].(*ops2.FileInfoValue) | |||
| u.successes = append(u.successes, db.AddObjectEntry{ | |||
| Path: pa, | |||
| Path: pa.String(), | |||
| Size: shardInfo.Size, | |||
| FileHash: shardInfo.Hash, | |||
| CreateTime: opt.CreateTime, | |||
| @@ -4,7 +4,6 @@ import ( | |||
| "context" | |||
| "fmt" | |||
| "io" | |||
| "path" | |||
| "sync" | |||
| "time" | |||
| @@ -24,7 +23,7 @@ type UpdateUploader struct { | |||
| targetSpace types.UserSpaceDetail | |||
| pubLock *publock.Mutex | |||
| copyToSpaces []types.UserSpaceDetail | |||
| copyToPath []string | |||
| copyToPath []types.JPath | |||
| successes []db.AddObjectEntry | |||
| lock sync.Mutex | |||
| commited bool | |||
| @@ -45,7 +44,7 @@ type UploadOption struct { | |||
| CreateTime time.Time // 设置文件的上传时间,如果为0值,则使用开始上传时的时间。 | |||
| } | |||
| func (w *UpdateUploader) Upload(pat string, stream io.Reader, opts ...UploadOption) error { | |||
| func (w *UpdateUploader) Upload(pat types.JPath, stream io.Reader, opts ...UploadOption) error { | |||
| opt := UploadOption{} | |||
| if len(opts) > 0 { | |||
| opt = opts[0] | |||
| @@ -61,7 +60,7 @@ func (w *UpdateUploader) Upload(pat string, stream io.Reader, opts ...UploadOpti | |||
| AddTo(ioswitch2.NewToShardStore(w.targetSpace, ioswitch2.RawStream(), "shardInfo")) | |||
| for i, space := range w.copyToSpaces { | |||
| ft.AddTo(ioswitch2.NewToBaseStore(space, path.Join(w.copyToPath[i], pat))) | |||
| ft.AddTo(ioswitch2.NewToBaseStore(space, w.copyToPath[i].ConcatNew(pat))) | |||
| } | |||
| plans := exec.NewPlanBuilder() | |||
| @@ -85,7 +84,7 @@ func (w *UpdateUploader) Upload(pat string, stream io.Reader, opts ...UploadOpti | |||
| // 记录上传结果 | |||
| shardInfo := ret["shardInfo"].(*ops2.FileInfoValue) | |||
| w.successes = append(w.successes, db.AddObjectEntry{ | |||
| Path: pat, | |||
| Path: pat.String(), | |||
| Size: shardInfo.Size, | |||
| FileHash: shardInfo.Hash, | |||
| CreateTime: opt.CreateTime, | |||
| @@ -43,7 +43,7 @@ func NewUploader(pubLock *publock.Service, connectivity *connectivity.Collector, | |||
| } | |||
| } | |||
| func (u *Uploader) BeginUpdate(pkgID clitypes.PackageID, affinity clitypes.UserSpaceID, copyTo []clitypes.UserSpaceID, copyToPath []string) (*UpdateUploader, error) { | |||
| func (u *Uploader) BeginUpdate(pkgID clitypes.PackageID, affinity clitypes.UserSpaceID, copyTo []clitypes.UserSpaceID, copyToPath []clitypes.JPath) (*UpdateUploader, error) { | |||
| spaceIDs, err := u.db.UserSpace().GetAllIDs(u.db.DefCtx()) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("getting user space ids: %w", err) | |||
| @@ -137,7 +137,7 @@ func (w *Uploader) chooseUploadStorage(spaces []UploadSpaceInfo, spaceAffinity c | |||
| return spaces[0] | |||
| } | |||
| func (u *Uploader) BeginCreateUpload(bktID clitypes.BucketID, pkgName string, copyTo []clitypes.UserSpaceID, copyToPath []string) (*CreateUploader, error) { | |||
| func (u *Uploader) BeginCreateUpload(bktID clitypes.BucketID, pkgName string, copyTo []clitypes.UserSpaceID, copyToPath []clitypes.JPath) (*CreateUploader, error) { | |||
| getSpaces := u.spaceMeta.GetMany(copyTo) | |||
| spacesStgs := make([]clitypes.UserSpaceDetail, len(copyTo)) | |||
| @@ -3,8 +3,8 @@ package uploader | |||
| import ( | |||
| "context" | |||
| "fmt" | |||
| "io" | |||
| "math" | |||
| "strings" | |||
| "time" | |||
| "github.com/samber/lo" | |||
| @@ -21,7 +21,7 @@ import ( | |||
| cortypes "gitlink.org.cn/cloudream/jcs-pub/coordinator/types" | |||
| ) | |||
| func (u *Uploader) UserSpaceUpload(userSpaceID clitypes.UserSpaceID, rootPath string, targetBktID clitypes.BucketID, newPkgName string, uploadAffinity clitypes.UserSpaceID) (*clitypes.Package, error) { | |||
| func (u *Uploader) UserSpaceUpload(userSpaceID clitypes.UserSpaceID, rootPath clitypes.JPath, targetBktID clitypes.BucketID, newPkgName string, uploadAffinity clitypes.UserSpaceID) (*clitypes.Package, error) { | |||
| srcSpace := u.spaceMeta.Get(userSpaceID) | |||
| if srcSpace == nil { | |||
| return nil, fmt.Errorf("user space %d not found", userSpaceID) | |||
| @@ -105,11 +105,6 @@ func (u *Uploader) UserSpaceUpload(userSpaceID clitypes.UserSpaceID, rootPath st | |||
| delPkg() | |||
| return nil, fmt.Errorf("getting base store: %w", err) | |||
| } | |||
| entries, err := store.ListAll(rootPath) | |||
| if err != nil { | |||
| delPkg() | |||
| return nil, fmt.Errorf("listing base store: %w", err) | |||
| } | |||
| mutex, err := reqbuilder.NewBuilder().UserSpace().Buzy(srcSpace.UserSpace.UserSpaceID).Buzy(targetSapce.Space.UserSpace.UserSpaceID).MutexLock(u.pubLock) | |||
| if err != nil { | |||
| @@ -118,10 +113,35 @@ func (u *Uploader) UserSpaceUpload(userSpaceID clitypes.UserSpaceID, rootPath st | |||
| } | |||
| defer mutex.Unlock() | |||
| adds, err := u.uploadFromBaseStore(srcSpace, &targetSapce.Space, entries, rootPath) | |||
| if err != nil { | |||
| delPkg() | |||
| return nil, fmt.Errorf("uploading from base store: %w", err) | |||
| dirReader := store.ReadDir(rootPath) | |||
| var adds []db.AddObjectEntry | |||
| entries := make([]types.DirEntry, 0, 50) | |||
| for { | |||
| eof := false | |||
| for len(entries) < 50 { | |||
| entry, err := dirReader.Next() | |||
| if err == io.EOF { | |||
| eof = true | |||
| break | |||
| } | |||
| if err != nil { | |||
| delPkg() | |||
| return nil, fmt.Errorf("reading dir: %w", err) | |||
| } | |||
| entries = append(entries, entry) | |||
| } | |||
| as, err := u.uploadFromBaseStore(srcSpace, &targetSapce.Space, entries, rootPath) | |||
| if err != nil { | |||
| delPkg() | |||
| return nil, fmt.Errorf("uploading from base store: %w", err) | |||
| } | |||
| adds = append(adds, as...) | |||
| entries = entries[:0] | |||
| if eof { | |||
| break | |||
| } | |||
| } | |||
| _, err = db.DoTx21(u.db, u.db.Object().BatchAdd, pkg.PackageID, adds) | |||
| @@ -133,7 +153,7 @@ func (u *Uploader) UserSpaceUpload(userSpaceID clitypes.UserSpaceID, rootPath st | |||
| return &pkg, nil | |||
| } | |||
| func (u *Uploader) uploadFromBaseStore(srcSpace *clitypes.UserSpaceDetail, targetSpace *clitypes.UserSpaceDetail, entries []types.ListEntry, rootPath string) ([]db.AddObjectEntry, error) { | |||
| func (u *Uploader) uploadFromBaseStore(srcSpace *clitypes.UserSpaceDetail, targetSpace *clitypes.UserSpaceDetail, entries []types.DirEntry, rootPath clitypes.JPath) ([]db.AddObjectEntry, error) { | |||
| ft := ioswitch2.FromTo{} | |||
| for _, e := range entries { | |||
| @@ -143,7 +163,7 @@ func (u *Uploader) uploadFromBaseStore(srcSpace *clitypes.UserSpaceDetail, targe | |||
| } | |||
| ft.AddFrom(ioswitch2.NewFromBaseStore(*srcSpace, e.Path)) | |||
| ft.AddTo(ioswitch2.NewToShardStore(*targetSpace, ioswitch2.RawStream(), e.Path)) | |||
| ft.AddTo(ioswitch2.NewToShardStore(*targetSpace, ioswitch2.RawStream(), e.Path.String())) | |||
| } | |||
| plans := exec.NewPlanBuilder() | |||
| @@ -159,21 +179,22 @@ func (u *Uploader) uploadFromBaseStore(srcSpace *clitypes.UserSpaceDetail, targe | |||
| return nil, fmt.Errorf("executing plan: %w", err) | |||
| } | |||
| cleanRoot := strings.TrimSuffix(rootPath, clitypes.ObjectPathSeparator) | |||
| adds := make([]db.AddObjectEntry, 0, len(ret)) | |||
| for _, e := range entries { | |||
| if e.IsDir { | |||
| continue | |||
| } | |||
| pat := strings.TrimPrefix(e.Path, cleanRoot+clitypes.ObjectPathSeparator) | |||
| if pat == cleanRoot { | |||
| pat = clitypes.BaseName(e.Path) | |||
| pat := e.Path.Clone() | |||
| pat.DropFrontN(rootPath.Len() - 1) | |||
| // 如果对象路径和RootPath相同(即RootPath是一个文件),则用文件名作为对象Path | |||
| if pat.Len() > 1 { | |||
| pat.DropFrontN(1) | |||
| } | |||
| info := ret[e.Path].(*ops2.FileInfoValue) | |||
| info := ret[e.Path.String()].(*ops2.FileInfoValue) | |||
| adds = append(adds, db.AddObjectEntry{ | |||
| Path: pat, | |||
| Path: pat.String(), | |||
| Size: info.Size, | |||
| FileHash: info.Hash, | |||
| CreateTime: time.Now(), | |||
| @@ -0,0 +1,89 @@ | |||
| package api | |||
| import ( | |||
| "net/http" | |||
| "gitlink.org.cn/cloudream/common/sdks" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| ) | |||
| type SpaceSyncerService struct { | |||
| *Client | |||
| } | |||
| func (c *Client) SpaceSyncer() *SpaceSyncerService { | |||
| return &SpaceSyncerService{ | |||
| Client: c, | |||
| } | |||
| } | |||
| const SpaceSyncerCreateTaskPath = "/spaceSyncer/createTask" | |||
| type SpaceSyncerCreateTask struct { | |||
| Trigger clitypes.SpaceSyncTrigger `json:"trigger" binding:"required"` | |||
| Mode clitypes.SpaceSyncMode `json:"mode" binding:"required"` | |||
| Filters []clitypes.SpaceSyncFilter `json:"filters"` | |||
| Options clitypes.SpaceSyncOptions `json:"options" binding:"required"` | |||
| SrcUserSpaceID clitypes.UserSpaceID `json:"srcUserSpaceID" binding:"required"` | |||
| SrcPath string `json:"srcPath"` | |||
| DestUserSpaceIDs []clitypes.UserSpaceID `json:"destUserSpaceIDs" binding:"required"` | |||
| DestPathes []string `json:"destPathes" binding:"required"` | |||
| } | |||
| func (r *SpaceSyncerCreateTask) MakeParam() *sdks.RequestParam { | |||
| return sdks.MakeJSONParam(http.MethodPost, SpaceSyncerCreateTaskPath, r) | |||
| } | |||
| type SpaceSyncerCreateTaskResp struct { | |||
| Task clitypes.SpaceSyncTask `json:"task"` | |||
| } | |||
| func (r *SpaceSyncerCreateTaskResp) ParseResponse(resp *http.Response) error { | |||
| return sdks.ParseCodeDataJSONResponse(resp, r) | |||
| } | |||
| func (c *SpaceSyncerService) CreateTask(req SpaceSyncerCreateTask) (*SpaceSyncerCreateTaskResp, error) { | |||
| return JSONAPI(&c.cfg, c.httpCli, &req, &SpaceSyncerCreateTaskResp{}) | |||
| } | |||
| const SpaceSyncerGetTaskPath = "/spaceSyncer/getTask" | |||
| type SpaceSyncerGetTask struct { | |||
| TaskID clitypes.SpaceSyncTaskID `url:"taskID" binding:"required"` | |||
| } | |||
| func (r *SpaceSyncerGetTask) MakeParam() *sdks.RequestParam { | |||
| return sdks.MakeQueryParam(http.MethodGet, SpaceSyncerGetTaskPath, r) | |||
| } | |||
| type SpaceSyncerGetTaskResp struct { | |||
| Task clitypes.SpaceSyncTask `json:"task"` | |||
| } | |||
| func (r *SpaceSyncerGetTaskResp) ParseResponse(resp *http.Response) error { | |||
| return sdks.ParseCodeDataJSONResponse(resp, r) | |||
| } | |||
| func (c *SpaceSyncerService) GetTask(req SpaceSyncerGetTask) (*SpaceSyncerGetTaskResp, error) { | |||
| return JSONAPI(&c.cfg, c.httpCli, &req, &SpaceSyncerGetTaskResp{}) | |||
| } | |||
| const SpaceSyncerCancelTaskPath = "/spaceSyncer/cancelTask" | |||
| type SpaceSyncerCancelTask struct { | |||
| TaskID clitypes.SpaceSyncTaskID `json:"taskID" binding:"required"` | |||
| } | |||
| func (r *SpaceSyncerCancelTask) MakeParam() *sdks.RequestParam { | |||
| return sdks.MakeJSONParam(http.MethodPost, SpaceSyncerCancelTaskPath, r) | |||
| } | |||
| type SpaceSyncerCancelTaskResp struct{} | |||
| func (r *SpaceSyncerCancelTaskResp) ParseResponse(resp *http.Response) error { | |||
| return sdks.ParseCodeDataJSONResponse(resp, r) | |||
| } | |||
| func (c *SpaceSyncerService) CancelTask(req SpaceSyncerCancelTask) (*SpaceSyncerCancelTaskResp, error) { | |||
| return JSONAPI(&c.cfg, c.httpCli, &req, &SpaceSyncerCancelTaskResp{}) | |||
| } | |||
| @@ -0,0 +1,157 @@ | |||
| package types | |||
| import ( | |||
| "path/filepath" | |||
| "strings" | |||
| "github.com/samber/lo" | |||
| ) | |||
| type JPath struct { | |||
| comps []string | |||
| } | |||
| func (p *JPath) Len() int { | |||
| return len(p.comps) | |||
| } | |||
| func (p *JPath) Comp(idx int) string { | |||
| return p.comps[idx] | |||
| } | |||
| func (p *JPath) Comps() []string { | |||
| return p.comps | |||
| } | |||
| func (p *JPath) LastComp() string { | |||
| if len(p.comps) == 0 { | |||
| return "" | |||
| } | |||
| return p.comps[len(p.comps)-1] | |||
| } | |||
| func (p *JPath) Push(comp string) { | |||
| p.comps = append(p.comps, comp) | |||
| } | |||
| func (p *JPath) Pop() string { | |||
| if len(p.comps) == 0 { | |||
| return "" | |||
| } | |||
| comp := p.comps[len(p.comps)-1] | |||
| p.comps = p.comps[:len(p.comps)-1] | |||
| return comp | |||
| } | |||
| func (p *JPath) SplitParent() JPath { | |||
| if len(p.comps) <= 1 { | |||
| return JPath{} | |||
| } | |||
| parent := JPath{ | |||
| comps: make([]string, len(p.comps)-1), | |||
| } | |||
| copy(parent.comps, p.comps[:len(p.comps)-1]) | |||
| p.comps = p.comps[len(p.comps)-1:] | |||
| return parent | |||
| } | |||
| func (p *JPath) DropFrontN(cnt int) { | |||
| if cnt >= len(p.comps) { | |||
| p.comps = nil | |||
| return | |||
| } | |||
| if cnt <= 0 { | |||
| return | |||
| } | |||
| p.comps = p.comps[cnt:] | |||
| } | |||
| func (p *JPath) Concat(other JPath) { | |||
| p.comps = append(p.comps, other.comps...) | |||
| } | |||
| func (p *JPath) ConcatNew(other JPath) JPath { | |||
| clone := p.Clone() | |||
| clone.Concat(other) | |||
| return clone | |||
| } | |||
| func (p *JPath) ConcatComps(comps []string) { | |||
| p.comps = append(p.comps, comps...) | |||
| } | |||
| func (p *JPath) ConcatCompsNew(comps ...string) JPath { | |||
| clone := p.Clone() | |||
| clone.ConcatComps(comps) | |||
| return clone | |||
| } | |||
| func (p *JPath) Clone() JPath { | |||
| clone := JPath{ | |||
| comps: make([]string, len(p.comps)), | |||
| } | |||
| copy(clone.comps, p.comps) | |||
| return clone | |||
| } | |||
| func (p *JPath) JoinOSPath() string { | |||
| return filepath.Join(p.comps...) | |||
| } | |||
| func (p JPath) String() string { | |||
| return strings.Join(p.comps, ObjectPathSeparator) | |||
| } | |||
| func (p JPath) ToString() (string, error) { | |||
| return p.String(), nil | |||
| } | |||
| func (p JPath) FromString(s string) (any, error) { | |||
| p2 := PathFromJcsPathString(s) | |||
| return p2, nil | |||
| } | |||
| func (p JPath) MarshalJSON() ([]byte, error) { | |||
| return []byte(`"` + p.String() + `"`), nil | |||
| } | |||
| func (p *JPath) UnmarshalJSON(data []byte) error { | |||
| s := string(data) | |||
| s = strings.Trim(s, `"`) | |||
| p2 := PathFromJcsPathString(s) | |||
| *p = p2 | |||
| return nil | |||
| } | |||
| func PathFromComps(comps ...string) JPath { | |||
| c2 := make([]string, len(comps)) | |||
| copy(c2, comps) | |||
| return JPath{ | |||
| comps: c2, | |||
| } | |||
| } | |||
| func PathFromOSPathString(s string) JPath { | |||
| cleaned := filepath.Clean(s) | |||
| comps := strings.Split(cleaned, string(filepath.Separator)) | |||
| return JPath{ | |||
| comps: lo.Reject(comps, func(s string, idx int) bool { return s == "" }), | |||
| } | |||
| } | |||
| func PathFromJcsPathString(s string) JPath { | |||
| comps := strings.Split(s, ObjectPathSeparator) | |||
| i := 0 | |||
| for ; i < len(comps) && len(comps[i]) == 0; i++ { | |||
| } | |||
| comps = comps[i:] | |||
| return JPath{ | |||
| comps: comps, | |||
| } | |||
| } | |||
| @@ -0,0 +1,151 @@ | |||
| package types | |||
| import ( | |||
| "time" | |||
| "gitlink.org.cn/cloudream/common/pkgs/types" | |||
| "gitlink.org.cn/cloudream/common/utils/serder" | |||
| ) | |||
| type SpaceSyncTaskID int64 | |||
| type SpaceSyncTrigger interface { | |||
| IsSpaceSyncTrigger() bool | |||
| } | |||
| var _ = serder.UseTypeUnionInternallyTagged(types.Ref(types.NewTypeUnion[SpaceSyncTrigger]( | |||
| (*SpaceSyncTriggerOnce)(nil), | |||
| (*SpaceSyncTriggerInterval)(nil), | |||
| (*SpaceSyncTriggerAt)(nil), | |||
| )), "type") | |||
| // 仅同步一次 | |||
| type SpaceSyncTriggerOnce struct { | |||
| serder.Metadata `union:"Once"` | |||
| Type string `json:"type"` | |||
| } | |||
| func (*SpaceSyncTriggerOnce) IsSpaceSyncTrigger() bool { | |||
| return true | |||
| } | |||
| func (m *SpaceSyncTriggerOnce) OnUnionSerializing() { | |||
| m.Type = "Once" | |||
| } | |||
| // 隔一段时间同步一次 | |||
| type SpaceSyncTriggerInterval struct { | |||
| serder.Metadata `union:"Interval"` | |||
| Type string `json:"type"` | |||
| Interval int64 `json:"interval"` // 单位秒 | |||
| } | |||
| func (*SpaceSyncTriggerInterval) IsSpaceSyncTrigger() bool { | |||
| return true | |||
| } | |||
| func (m *SpaceSyncTriggerInterval) OnUnionSerializing() { | |||
| m.Type = "Interval" | |||
| } | |||
| // 在固定时间点同步 | |||
| type SpaceSyncTriggerAt struct { | |||
| serder.Metadata `union:"At"` | |||
| Type string `json:"type"` | |||
| At []time.Time `json:"at"` | |||
| } | |||
| func (*SpaceSyncTriggerAt) IsSpaceSyncTrigger() bool { | |||
| return true | |||
| } | |||
| func (m *SpaceSyncTriggerAt) OnUnionSerializing() { | |||
| m.Type = "At" | |||
| } | |||
| type SpaceSyncMode interface { | |||
| IsSpaceSyncMode() bool | |||
| } | |||
| var _ = serder.UseTypeUnionInternallyTagged(types.Ref(types.NewTypeUnion[SpaceSyncMode]( | |||
| (*SpaceSyncModeFull)(nil), | |||
| (*SpaceSyncModeDiff)(nil), | |||
| )), "type") | |||
| type SpaceSyncModeFull struct { | |||
| serder.Metadata `union:"Full"` | |||
| Type string `json:"type"` | |||
| } | |||
| func (*SpaceSyncModeFull) IsSpaceSyncMode() bool { | |||
| return true | |||
| } | |||
| func (m *SpaceSyncModeFull) OnUnionSerializing() { | |||
| m.Type = "Full" | |||
| } | |||
| type SpaceSyncModeDiff struct { | |||
| serder.Metadata `union:"Diff"` | |||
| Type string `json:"type"` | |||
| // 将文件的大小作为文件指纹的一部分 | |||
| IncludeSize bool `json:"includeSize"` | |||
| // 将文件的修改时间作为文件指纹的一部分 | |||
| IncludeModTime bool `json:"includeModTime"` | |||
| // TODO 删除目录路径多余的文件 | |||
| // DeleteExtras bool `json:"deleteExtras"` | |||
| } | |||
| func (*SpaceSyncModeDiff) IsSpaceSyncMode() bool { | |||
| return true | |||
| } | |||
| func (m *SpaceSyncModeDiff) OnUnionSerializing() { | |||
| m.Type = "Diff" | |||
| } | |||
| type SpaceSyncFilter interface { | |||
| IsSpaceSyncfilter() bool | |||
| } | |||
| var _ = serder.UseTypeUnionInternallyTagged(types.Ref(types.NewTypeUnion[SpaceSyncFilter]( | |||
| (*SpaceSyncFilterSize)(nil), | |||
| )), "type") | |||
| type SpaceSyncFilterSize struct { | |||
| serder.Metadata `union:"Size"` | |||
| Type string `json:"type"` | |||
| // 最小文件大小。为0则不限制最小文件大小 | |||
| MinSize int64 `json:"minSize"` | |||
| // 最大文件大小,为0则不限制最大文件大小 | |||
| MaxSize int64 `json:"maxSize"` | |||
| } | |||
| func (f *SpaceSyncFilterSize) IsSpaceSyncfilter() bool { | |||
| return true | |||
| } | |||
| type SpaceSyncOptions struct { | |||
| // 不保留空文件夹 | |||
| NoEmptyDirectories bool `json:"noEmptyDirectories"` | |||
| } | |||
| type SpaceSyncDest struct { | |||
| DestUserSpaceID UserSpaceID `json:"destUserSpaceID"` | |||
| DestPath JPath `json:"destPath"` | |||
| } | |||
| type SpaceSyncTask struct { | |||
| TaskID SpaceSyncTaskID `gorm:"column:TaskID; primaryKey; type:bigint; autoIncrement" json:"taskID"` | |||
| Trigger SpaceSyncTrigger `gorm:"column:Trigger; type:json; not null; serializer:union" json:"trigger"` | |||
| Mode SpaceSyncMode `gorm:"column:Mode; type:json; not null; serializer:union" json:"mode"` | |||
| Filters []SpaceSyncFilter `gorm:"column:Filters; type:json; not null; serializer:union" json:"filters"` | |||
| Options SpaceSyncOptions `gorm:"column:Options; type:json; not null; serializer:union" json:"options"` | |||
| SrcUserSpaceID UserSpaceID `gorm:"column:SrcUserSpaceID; type:bigint; not null" json:"srcUserSpaceID"` | |||
| SrcPath JPath `gorm:"column:SrcPath; type:varchar(255); not null; serializer:string" json:"srcPath"` | |||
| Dests []SpaceSyncDest `gorm:"column:Dests; type:json; not null; serializer:union" json:"dests"` | |||
| } | |||
| func (SpaceSyncTask) TableName() string { | |||
| return "SpaceSyncTask" | |||
| } | |||
| @@ -83,7 +83,7 @@ type UserSpace struct { | |||
| // 存储服务特性功能的配置 | |||
| Features []cortypes.StorageFeature `json:"features" gorm:"column:Features; type:json; serializer:union"` | |||
| // 各种组件保存数据的根目录。组件工作过程中都会以这个目录为根(除了BaseStore)。 | |||
| WorkingDir string `gorm:"column:WorkingDir; type:varchar(1024); not null" json:"workingDir"` | |||
| WorkingDir JPath `gorm:"column:WorkingDir; type:varchar(1024); not null; serializer:string" json:"workingDir"` | |||
| // 工作目录在存储系统中的真实路径。当工作路径在挂载点内时,这个字段记录的是挂载背后的真实路径。部分直接与存储系统交互的组件需要知道真实路径。 | |||
| // RealWorkingDir string `gorm:"column:RealWorkingDir; type:varchar(1024); not null" json:"realWorkingDir"` | |||
| // 用户空间信息的版本号,每一次更改都需要更新版本号 | |||
| @@ -4,6 +4,7 @@ import ( | |||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | |||
| "gitlink.org.cn/cloudream/common/utils/math2" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| ) | |||
| type From interface { | |||
| @@ -128,10 +129,10 @@ func (f *FromShardStore) GetStreamIndex() StreamIndex { | |||
| type FromBaseStore struct { | |||
| UserSpace clitypes.UserSpaceDetail | |||
| Path string | |||
| Path clitypes.JPath | |||
| } | |||
| func NewFromBaseStore(space clitypes.UserSpaceDetail, path string) *FromBaseStore { | |||
| func NewFromBaseStore(space clitypes.UserSpaceDetail, path clitypes.JPath) *FromBaseStore { | |||
| return &FromBaseStore{ | |||
| UserSpace: space, | |||
| Path: path, | |||
| @@ -209,10 +210,11 @@ func (t *ToShardStore) GetRange() math2.Range { | |||
| type ToBaseStore struct { | |||
| UserSpace clitypes.UserSpaceDetail | |||
| ObjectPath string | |||
| ObjectPath clitypes.JPath | |||
| Option types.WriteOption | |||
| } | |||
| func NewToBaseStore(space clitypes.UserSpaceDetail, objectPath string) *ToBaseStore { | |||
| func NewToBaseStore(space clitypes.UserSpaceDetail, objectPath clitypes.JPath) *ToBaseStore { | |||
| return &ToBaseStore{ | |||
| UserSpace: space, | |||
| ObjectPath: objectPath, | |||
| @@ -24,7 +24,7 @@ func init() { | |||
| type BaseRead struct { | |||
| Output exec.VarID | |||
| UserSpace clitypes.UserSpaceDetail | |||
| Path string | |||
| Path clitypes.JPath | |||
| Option types.OpenOption | |||
| } | |||
| @@ -120,8 +120,9 @@ func (o *BaseReadDyn) String() string { | |||
| type BaseWrite struct { | |||
| Input exec.VarID | |||
| UserSpace clitypes.UserSpaceDetail | |||
| Path string | |||
| Path clitypes.JPath | |||
| FileInfo exec.VarID | |||
| Option types.WriteOption | |||
| } | |||
| func (o *BaseWrite) Execute(ctx *exec.ExecContext, e *exec.Executor) error { | |||
| @@ -146,7 +147,7 @@ func (o *BaseWrite) Execute(ctx *exec.ExecContext, e *exec.Executor) error { | |||
| } | |||
| defer input.Stream.Close() | |||
| ret, err := store.Write(o.Path, input.Stream) | |||
| ret, err := store.Write(o.Path, input.Stream, o.Option) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| @@ -165,11 +166,11 @@ type BaseReadNode struct { | |||
| dag.NodeBase | |||
| From ioswitch2.From | |||
| UserSpace clitypes.UserSpaceDetail | |||
| Path string | |||
| Path clitypes.JPath | |||
| Option types.OpenOption | |||
| } | |||
| func (b *GraphNodeBuilder) NewBaseRead(from ioswitch2.From, userSpace clitypes.UserSpaceDetail, path string, opt types.OpenOption) *BaseReadNode { | |||
| func (b *GraphNodeBuilder) NewBaseRead(from ioswitch2.From, userSpace clitypes.UserSpaceDetail, path clitypes.JPath, opt types.OpenOption) *BaseReadNode { | |||
| node := &BaseReadNode{ | |||
| From: from, | |||
| UserSpace: userSpace, | |||
| @@ -253,14 +254,16 @@ type BaseWriteNode struct { | |||
| dag.NodeBase | |||
| To ioswitch2.To | |||
| UserSpace clitypes.UserSpaceDetail | |||
| Path string | |||
| Path clitypes.JPath | |||
| Option types.WriteOption | |||
| } | |||
| func (b *GraphNodeBuilder) NewBaseWrite(to ioswitch2.To, userSpace clitypes.UserSpaceDetail, path string) *BaseWriteNode { | |||
| func (b *GraphNodeBuilder) NewBaseWrite(to ioswitch2.To, userSpace clitypes.UserSpaceDetail, path clitypes.JPath, opt types.WriteOption) *BaseWriteNode { | |||
| node := &BaseWriteNode{ | |||
| To: to, | |||
| UserSpace: userSpace, | |||
| Path: path, | |||
| Option: opt, | |||
| } | |||
| b.AddNode(node) | |||
| @@ -293,5 +296,6 @@ func (t *BaseWriteNode) GenerateOp() (exec.Op, error) { | |||
| UserSpace: t.UserSpace, | |||
| Path: t.Path, | |||
| FileInfo: t.FileInfoVar().Var().VarID, | |||
| Option: t.Option, | |||
| }, nil | |||
| } | |||
| @@ -16,9 +16,9 @@ func init() { | |||
| type S2STransfer struct { | |||
| SrcSpace clitypes.UserSpaceDetail | |||
| SrcPath string | |||
| SrcPath clitypes.JPath | |||
| DstSpace clitypes.UserSpaceDetail | |||
| DstPath string | |||
| DstPath clitypes.JPath | |||
| Output exec.VarID | |||
| } | |||
| @@ -58,7 +58,7 @@ type S2STransferDyn struct { | |||
| SrcSpace clitypes.UserSpaceDetail | |||
| SrcFileInfo exec.VarID | |||
| DstSpace clitypes.UserSpaceDetail | |||
| DstPath string | |||
| DstPath clitypes.JPath | |||
| Output exec.VarID | |||
| } | |||
| @@ -102,12 +102,12 @@ func (o *S2STransferDyn) String() string { | |||
| type S2STransferNode struct { | |||
| dag.NodeBase | |||
| SrcSpace clitypes.UserSpaceDetail | |||
| SrcPath string | |||
| SrcPath clitypes.JPath | |||
| DstSpace clitypes.UserSpaceDetail | |||
| DstPath string | |||
| DstPath clitypes.JPath | |||
| } | |||
| func (b *GraphNodeBuilder) NewS2STransfer(srcSpace clitypes.UserSpaceDetail, srcPath string, dstSpace clitypes.UserSpaceDetail, dstPath string) *S2STransferNode { | |||
| func (b *GraphNodeBuilder) NewS2STransfer(srcSpace clitypes.UserSpaceDetail, srcPath clitypes.JPath, dstSpace clitypes.UserSpaceDetail, dstPath clitypes.JPath) *S2STransferNode { | |||
| n := &S2STransferNode{ | |||
| SrcSpace: srcSpace, | |||
| SrcPath: srcPath, | |||
| @@ -141,10 +141,10 @@ type S2STransferDynNode struct { | |||
| dag.NodeBase | |||
| SrcSpace clitypes.UserSpaceDetail | |||
| DstSpace clitypes.UserSpaceDetail | |||
| DstPath string | |||
| DstPath clitypes.JPath | |||
| } | |||
| func (b *GraphNodeBuilder) NewS2STransferDyn(srcSpace clitypes.UserSpaceDetail, dstSpace clitypes.UserSpaceDetail, dstPath string) *S2STransferDynNode { | |||
| func (b *GraphNodeBuilder) NewS2STransferDyn(srcSpace clitypes.UserSpaceDetail, dstSpace clitypes.UserSpaceDetail, dstPath clitypes.JPath) *S2STransferDynNode { | |||
| n := &S2STransferDynNode{ | |||
| SrcSpace: srcSpace, | |||
| DstSpace: dstSpace, | |||
| @@ -350,7 +350,7 @@ func buildToNode(ctx *state.GenerateState, t ioswitch2.To) (ops2.ToNode, error) | |||
| case *ioswitch2.ToShardStore: | |||
| tempFileName := types.MakeTempDirPath(&t.UserSpace, os2.GenerateRandomFileName(20)) | |||
| write := ctx.DAG.NewBaseWrite(t, t.UserSpace, tempFileName) | |||
| write := ctx.DAG.NewBaseWrite(t, t.UserSpace, tempFileName, types.WriteOption{}) | |||
| if err := setEnvBySpace(write, &t.UserSpace); err != nil { | |||
| return nil, fmt.Errorf("set node env by user space: %w", err) | |||
| } | |||
| @@ -370,7 +370,7 @@ func buildToNode(ctx *state.GenerateState, t ioswitch2.To) (ops2.ToNode, error) | |||
| return n, nil | |||
| case *ioswitch2.ToBaseStore: | |||
| n := ctx.DAG.NewBaseWrite(t, t.UserSpace, t.ObjectPath) | |||
| n := ctx.DAG.NewBaseWrite(t, t.UserSpace, t.ObjectPath, t.Option) | |||
| if err := setEnvBySpace(n, &t.UserSpace); err != nil { | |||
| return nil, fmt.Errorf("set node env by user space: %w", err) | |||
| @@ -38,7 +38,7 @@ func CompleteMultipart(blocks []clitypes.ObjectBlock, blockSpaces []clitypes.Use | |||
| } | |||
| // TODO 应该采取更合理的方式同时支持Parser和直接生成DAG | |||
| br := da.NewBaseWrite(nil, targetSpace, types.MakeTempDirPath(&targetSpace, os2.GenerateRandomFileName(20))) | |||
| br := da.NewBaseWrite(nil, targetSpace, types.MakeTempDirPath(&targetSpace, os2.GenerateRandomFileName(20)), types.WriteOption{}) | |||
| if err := setEnvBySpace(br, &targetSpace); err != nil { | |||
| return fmt.Errorf("set node env by user space: %w", err) | |||
| } | |||
| @@ -4,6 +4,7 @@ import ( | |||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | |||
| "gitlink.org.cn/cloudream/common/utils/math2" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| ) | |||
| type From interface { | |||
| @@ -91,6 +92,7 @@ type ToNode struct { | |||
| DataIndex int | |||
| Range math2.Range | |||
| FileHashStoreKey string | |||
| Option types.WriteOption | |||
| } | |||
| func NewToStorage(space clitypes.UserSpaceDetail, dataIndex int, fileHashStoreKey string) *ToNode { | |||
| @@ -24,7 +24,7 @@ func init() { | |||
| type BaseRead struct { | |||
| Output exec.VarID | |||
| UserSpace clitypes.UserSpaceDetail | |||
| Path string | |||
| Path clitypes.JPath | |||
| Option types.OpenOption | |||
| } | |||
| @@ -119,8 +119,9 @@ func (o *BaseReadDyn) String() string { | |||
| type BaseWrite struct { | |||
| Input exec.VarID | |||
| UserSpace clitypes.UserSpaceDetail | |||
| Path string | |||
| Path clitypes.JPath | |||
| WriteResult exec.VarID | |||
| Option types.WriteOption | |||
| } | |||
| func (o *BaseWrite) Execute(ctx *exec.ExecContext, e *exec.Executor) error { | |||
| @@ -145,7 +146,7 @@ func (o *BaseWrite) Execute(ctx *exec.ExecContext, e *exec.Executor) error { | |||
| } | |||
| defer input.Stream.Close() | |||
| ret, err := store.Write(o.Path, input.Stream) | |||
| ret, err := store.Write(o.Path, input.Stream, o.Option) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| @@ -164,11 +165,11 @@ type BaseReadNode struct { | |||
| dag.NodeBase | |||
| From ioswitchlrc.From | |||
| UserSpace clitypes.UserSpaceDetail | |||
| Path string | |||
| Path clitypes.JPath | |||
| Option types.OpenOption | |||
| } | |||
| func (b *GraphNodeBuilder) NewBaseRead(from ioswitchlrc.From, userSpace clitypes.UserSpaceDetail, path string, opt types.OpenOption) *BaseReadNode { | |||
| func (b *GraphNodeBuilder) NewBaseRead(from ioswitchlrc.From, userSpace clitypes.UserSpaceDetail, path clitypes.JPath, opt types.OpenOption) *BaseReadNode { | |||
| node := &BaseReadNode{ | |||
| From: from, | |||
| UserSpace: userSpace, | |||
| @@ -252,14 +253,16 @@ type BaseWriteNode struct { | |||
| dag.NodeBase | |||
| To ioswitchlrc.To | |||
| UserSpace clitypes.UserSpaceDetail | |||
| Path string | |||
| Path clitypes.JPath | |||
| Option types.WriteOption | |||
| } | |||
| func (b *GraphNodeBuilder) NewBaseWrite(to ioswitchlrc.To, userSpace clitypes.UserSpaceDetail, path string) *BaseWriteNode { | |||
| func (b *GraphNodeBuilder) NewBaseWrite(to ioswitchlrc.To, userSpace clitypes.UserSpaceDetail, path clitypes.JPath, opt types.WriteOption) *BaseWriteNode { | |||
| node := &BaseWriteNode{ | |||
| To: to, | |||
| UserSpace: userSpace, | |||
| Path: path, | |||
| Option: opt, | |||
| } | |||
| b.AddNode(node) | |||
| @@ -292,5 +295,6 @@ func (t *BaseWriteNode) GenerateOp() (exec.Op, error) { | |||
| UserSpace: t.UserSpace, | |||
| Path: t.Path, | |||
| WriteResult: t.FileInfoVar().Var().VarID, | |||
| Option: t.Option, | |||
| }, nil | |||
| } | |||
| @@ -103,7 +103,7 @@ func buildToNode(ctx *GenerateContext, t ioswitchlrc.To) (ops2.ToNode, error) { | |||
| switch t := t.(type) { | |||
| case *ioswitchlrc.ToNode: | |||
| tempFileName := types.MakeTempDirPath(&t.UserSpace, os2.GenerateRandomFileName(20)) | |||
| write := ctx.DAG.NewBaseWrite(t, t.UserSpace, tempFileName) | |||
| write := ctx.DAG.NewBaseWrite(t, t.UserSpace, tempFileName, t.Option) | |||
| if err := setEnvBySpace(write, &t.UserSpace); err != nil { | |||
| return nil, fmt.Errorf("set node env by user space: %w", err) | |||
| @@ -48,7 +48,7 @@ func (m *ECMultiplier) Multiply(coef [][]byte, inputs []types.HTTPRequest, chunk | |||
| } | |||
| fileName := os2.GenerateRandomFileName(10) | |||
| tempDir := path.Join(m.blder.detail.UserSpace.WorkingDir, types.TempWorkingDir) | |||
| tempDir := path.Join(m.blder.detail.UserSpace.WorkingDir.String(), types.TempWorkingDir) | |||
| m.outputs = make([]string, len(coef)) | |||
| for i := range m.outputs { | |||
| m.outputs[i] = path.Join(tempDir, fmt.Sprintf("%s_%d", fileName, i)) | |||
| @@ -97,7 +97,8 @@ func (m *ECMultiplier) Multiply(coef [][]byte, inputs []types.HTTPRequest, chunk | |||
| ret := make([]types.FileInfo, len(r.Data)) | |||
| for i, data := range r.Data { | |||
| ret[i] = types.FileInfo{ | |||
| Path: m.outputs[i], | |||
| // TODO 要确认一下output的格式 | |||
| Path: clitypes.PathFromJcsPathString(m.outputs[i]), | |||
| Size: data.Size, | |||
| Hash: clitypes.NewFullHashFromString(data.Sha256), | |||
| } | |||
| @@ -3,7 +3,6 @@ package local | |||
| import ( | |||
| "crypto/sha256" | |||
| "io" | |||
| "io/fs" | |||
| "os" | |||
| "path/filepath" | |||
| @@ -25,8 +24,10 @@ func NewBaseStore(root string, detail *clitypes.UserSpaceDetail) (*BaseStore, er | |||
| }, nil | |||
| } | |||
| func (s *BaseStore) Write(objPath string, stream io.Reader) (types.FileInfo, error) { | |||
| absObjPath := filepath.Join(s.root, objPath) | |||
| func (s *BaseStore) Write(pat clitypes.JPath, stream io.Reader, opt types.WriteOption) (types.FileInfo, error) { | |||
| log := s.getLogger() | |||
| absObjPath := filepath.Join(s.root, pat.String()) | |||
| err := os.MkdirAll(filepath.Dir(absObjPath), 0755) | |||
| if err != nil { | |||
| @@ -47,15 +48,22 @@ func (s *BaseStore) Write(objPath string, stream io.Reader) (types.FileInfo, err | |||
| return types.FileInfo{}, err | |||
| } | |||
| if !opt.ModTime.IsZero() { | |||
| err := os.Chtimes(absObjPath, opt.ModTime, opt.ModTime) | |||
| if err != nil { | |||
| log.Warnf("change file %v mod time: %v", absObjPath, err) | |||
| } | |||
| } | |||
| return types.FileInfo{ | |||
| Path: objPath, | |||
| Path: pat, | |||
| Size: counter.Count(), | |||
| Hash: clitypes.NewFullHash(hasher.Sum()), | |||
| }, nil | |||
| } | |||
| func (s *BaseStore) Read(objPath string, opt types.OpenOption) (io.ReadCloser, error) { | |||
| absObjPath := filepath.Join(s.root, objPath) | |||
| func (s *BaseStore) Read(objPath clitypes.JPath, opt types.OpenOption) (io.ReadCloser, error) { | |||
| absObjPath := filepath.Join(s.root, objPath.JoinOSPath()) | |||
| file, err := os.Open(absObjPath) | |||
| if err != nil { | |||
| return nil, err | |||
| @@ -78,8 +86,8 @@ func (s *BaseStore) Read(objPath string, opt types.OpenOption) (io.ReadCloser, e | |||
| return ret, nil | |||
| } | |||
| func (s *BaseStore) Mkdir(path string) error { | |||
| absObjPath := filepath.Join(s.root, path) | |||
| func (s *BaseStore) Mkdir(path clitypes.JPath) error { | |||
| absObjPath := filepath.Join(s.root, path.JoinOSPath()) | |||
| err := os.MkdirAll(absObjPath, 0755) | |||
| if err != nil { | |||
| return err | |||
| @@ -88,54 +96,17 @@ func (s *BaseStore) Mkdir(path string) error { | |||
| return nil | |||
| } | |||
| func (s *BaseStore) ListAll(path string) ([]types.ListEntry, error) { | |||
| absObjPath := filepath.Join(s.root, path) | |||
| var es []types.ListEntry | |||
| err := filepath.WalkDir(absObjPath, func(path string, d fs.DirEntry, err error) error { | |||
| if err != nil { | |||
| return err | |||
| } | |||
| relaPath, err := filepath.Rel(s.root, path) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if d.IsDir() { | |||
| es = append(es, types.ListEntry{ | |||
| Path: filepath.ToSlash(relaPath), | |||
| Size: 0, | |||
| IsDir: true, | |||
| }) | |||
| return nil | |||
| } | |||
| info, err := d.Info() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| es = append(es, types.ListEntry{ | |||
| Path: filepath.ToSlash(relaPath), | |||
| Size: info.Size(), | |||
| IsDir: false, | |||
| }) | |||
| return nil | |||
| }) | |||
| if os.IsNotExist(err) { | |||
| return nil, nil | |||
| func (s *BaseStore) ReadDir(pat clitypes.JPath) types.DirReader { | |||
| return &DirReader{ | |||
| absRootPath: filepath.Join(s.root, pat.JoinOSPath()), | |||
| rootJPath: pat.Clone(), | |||
| } | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return es, nil | |||
| } | |||
| func (s *BaseStore) CleanTemps() { | |||
| log := s.getLogger() | |||
| tempDir := filepath.Join(s.root, s.detail.UserSpace.WorkingDir, types.TempWorkingDir) | |||
| tempDir := filepath.Join(s.root, s.detail.UserSpace.WorkingDir.JoinOSPath(), types.TempWorkingDir) | |||
| entries, err := os.ReadDir(tempDir) | |||
| if err != nil { | |||
| log.Warnf("read temp dir: %v", err) | |||
| @@ -163,6 +134,11 @@ func (s *BaseStore) CleanTemps() { | |||
| } | |||
| } | |||
| func (s *BaseStore) Test() error { | |||
| _, err := os.Stat(s.root) | |||
| return err | |||
| } | |||
| func (s *BaseStore) getLogger() logger.Logger { | |||
| return logger.WithField("BaseStore", "Local").WithField("UserSpace", s.detail.UserSpace) | |||
| } | |||
| @@ -0,0 +1,117 @@ | |||
| package local | |||
| import ( | |||
| "io" | |||
| "os" | |||
| "path/filepath" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| ) | |||
| type DirReader struct { | |||
| // 完整的根路径(包括ReadDir的path参数),比如包括了盘符 | |||
| absRootPath string | |||
| // ReadDir函数传递进来的path参数 | |||
| rootJPath clitypes.JPath | |||
| init bool | |||
| curEntries []dirEntry | |||
| } | |||
| func (r *DirReader) Next() (types.DirEntry, error) { | |||
| if !r.init { | |||
| info, err := os.Stat(r.absRootPath) | |||
| if err != nil { | |||
| return types.DirEntry{}, err | |||
| } | |||
| if !info.IsDir() { | |||
| r.init = true | |||
| return types.DirEntry{ | |||
| Path: r.rootJPath, | |||
| Size: info.Size(), | |||
| ModTime: info.ModTime(), | |||
| IsDir: false, | |||
| }, nil | |||
| } | |||
| es, err := os.ReadDir(r.absRootPath) | |||
| if err != nil { | |||
| return types.DirEntry{}, err | |||
| } | |||
| for _, e := range es { | |||
| r.curEntries = append(r.curEntries, dirEntry{ | |||
| dir: clitypes.JPath{}, | |||
| entry: e, | |||
| }) | |||
| } | |||
| r.init = true | |||
| } | |||
| if len(r.curEntries) == 0 { | |||
| return types.DirEntry{}, io.EOF | |||
| } | |||
| entry := r.curEntries[0] | |||
| r.curEntries = r.curEntries[1:] | |||
| if entry.entry.IsDir() { | |||
| es, err := os.ReadDir(filepath.Join(r.absRootPath, entry.dir.JoinOSPath(), entry.entry.Name())) | |||
| if err != nil { | |||
| return types.DirEntry{}, nil | |||
| } | |||
| // 多个entry对象共享同一个JPath对象,但因为不会修改JPath,所以没问题 | |||
| dir := entry.dir.Clone() | |||
| dir.Push(entry.entry.Name()) | |||
| for _, e := range es { | |||
| r.curEntries = append(r.curEntries, dirEntry{ | |||
| dir: dir, | |||
| entry: e, | |||
| }) | |||
| } | |||
| } | |||
| info, err := entry.entry.Info() | |||
| if err != nil { | |||
| return types.DirEntry{}, err | |||
| } | |||
| p := r.rootJPath.ConcatNew(entry.dir) | |||
| p.Push(entry.entry.Name()) | |||
| if entry.entry.IsDir() { | |||
| return types.DirEntry{ | |||
| Path: p, | |||
| Size: 0, | |||
| ModTime: info.ModTime(), | |||
| IsDir: true, | |||
| }, nil | |||
| } | |||
| return types.DirEntry{ | |||
| Path: p, | |||
| Size: info.Size(), | |||
| ModTime: info.ModTime(), | |||
| IsDir: false, | |||
| }, nil | |||
| } | |||
| func (r *DirReader) Close() { | |||
| } | |||
| type dirEntry struct { | |||
| dir clitypes.JPath | |||
| entry os.DirEntry | |||
| } | |||
| type fileInfoDirEntry struct { | |||
| info os.FileInfo | |||
| } | |||
| func (d fileInfoDirEntry) Name() string { return d.info.Name() } | |||
| func (d fileInfoDirEntry) IsDir() bool { return d.info.IsDir() } | |||
| func (d fileInfoDirEntry) Type() os.FileMode { return d.info.Mode().Type() } | |||
| func (d fileInfoDirEntry) Info() (os.FileInfo, error) { return d.info, nil } | |||
| @@ -18,8 +18,9 @@ import ( | |||
| ) | |||
| type Multiparter struct { | |||
| detail *clitypes.UserSpaceDetail | |||
| feat *cortypes.MultipartUploadFeature | |||
| detail *clitypes.UserSpaceDetail | |||
| localStg *cortypes.LocalCred | |||
| feat *cortypes.MultipartUploadFeature | |||
| } | |||
| func (*Multiparter) MinPartSize() int64 { | |||
| @@ -31,7 +32,7 @@ func (*Multiparter) MaxPartSize() int64 { | |||
| } | |||
| func (m *Multiparter) Initiate(ctx context.Context) (types.MultipartTask, error) { | |||
| tempDir := filepath.Join(m.detail.UserSpace.WorkingDir, types.TempWorkingDir) | |||
| tempDir := filepath.Join(m.localStg.RootDir, m.detail.UserSpace.WorkingDir.JoinOSPath(), types.TempWorkingDir) | |||
| absTempDir, err := filepath.Abs(tempDir) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("get abs temp dir %v: %v", tempDir, err) | |||
| @@ -51,7 +52,7 @@ func (m *Multiparter) Initiate(ctx context.Context) (types.MultipartTask, error) | |||
| absTempDir: absTempDir, | |||
| tempFileName: tempFileName, | |||
| tempPartsDir: tempPartsDir, | |||
| joinedFilePath: types.PathJoin(m.detail.UserSpace.WorkingDir, types.TempWorkingDir, tempFileName+".joined"), | |||
| joinedFileJPath: m.detail.UserSpace.WorkingDir.ConcatCompsNew(types.TempWorkingDir, tempFileName+".joined"), | |||
| absJoinedFilePath: absJoinedFilePath, | |||
| uploadID: tempPartsDir, | |||
| }, nil | |||
| @@ -79,7 +80,7 @@ type MultipartTask struct { | |||
| absTempDir string // 应该要是绝对路径 | |||
| tempFileName string | |||
| tempPartsDir string | |||
| joinedFilePath string | |||
| joinedFileJPath clitypes.JPath | |||
| absJoinedFilePath string | |||
| uploadID string | |||
| } | |||
| @@ -115,7 +116,7 @@ func (i *MultipartTask) JoinParts(ctx context.Context, parts []types.UploadedPar | |||
| h := hasher.Sum(nil) | |||
| return types.FileInfo{ | |||
| Path: i.joinedFilePath, | |||
| Path: i.joinedFileJPath, | |||
| Size: size, | |||
| Hash: clitypes.NewFullHash(h), | |||
| }, nil | |||
| @@ -4,6 +4,7 @@ import ( | |||
| "context" | |||
| "io" | |||
| "os" | |||
| "path/filepath" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| @@ -11,9 +12,10 @@ import ( | |||
| ) | |||
| type S2STransfer struct { | |||
| feat *cortypes.S2STransferFeature | |||
| detail *clitypes.UserSpaceDetail | |||
| dstPath string | |||
| feat *cortypes.S2STransferFeature | |||
| detail *clitypes.UserSpaceDetail | |||
| localStg *cortypes.LocalCred | |||
| dstPath clitypes.JPath | |||
| } | |||
| // 只有同一个机器的存储之间才可以进行数据直传 | |||
| @@ -35,16 +37,16 @@ func (*S2STransfer) CanTransfer(src, dst *clitypes.UserSpaceDetail) bool { | |||
| } | |||
| // 执行数据直传 | |||
| func (s *S2STransfer) Transfer(ctx context.Context, src *clitypes.UserSpaceDetail, srcPath string, dstPath string) (types.FileInfo, error) { | |||
| func (s *S2STransfer) Transfer(ctx context.Context, src *clitypes.UserSpaceDetail, srcPath clitypes.JPath, dstPath clitypes.JPath) (types.FileInfo, error) { | |||
| s.dstPath = dstPath | |||
| copy, err := os.OpenFile(s.dstPath, os.O_WRONLY|os.O_CREATE, 0644) | |||
| copy, err := os.OpenFile(filepath.Join(s.localStg.RootDir, s.dstPath.JoinOSPath()), os.O_WRONLY|os.O_CREATE, 0644) | |||
| if err != nil { | |||
| return types.FileInfo{}, err | |||
| } | |||
| defer copy.Close() | |||
| srcFile, err := os.Open(srcPath) | |||
| srcFile, err := os.Open(filepath.Join(s.localStg.RootDir, srcPath.JoinOSPath())) | |||
| if err != nil { | |||
| return types.FileInfo{}, err | |||
| } | |||
| @@ -22,7 +22,7 @@ type ShardStore struct { | |||
| } | |||
| func NewShardStore(root string, detail *clitypes.UserSpaceDetail) (*ShardStore, error) { | |||
| storeAbsRoot, err := filepath.Abs(filepath.Join(root, detail.UserSpace.WorkingDir, types.ShardStoreWorkingDir)) | |||
| storeAbsRoot, err := filepath.Abs(filepath.Join(root, detail.UserSpace.WorkingDir.JoinOSPath(), types.ShardStoreWorkingDir)) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("get abs root: %w", err) | |||
| } | |||
| @@ -43,8 +43,8 @@ func (s *ShardStore) Stop() { | |||
| s.getLogger().Infof("component stop") | |||
| } | |||
| func (s *ShardStore) Store(path string, hash clitypes.FileHash, size int64) (types.FileInfo, error) { | |||
| fullTempPath := filepath.Join(s.stgRoot, path) | |||
| func (s *ShardStore) Store(path clitypes.JPath, hash clitypes.FileHash, size int64) (types.FileInfo, error) { | |||
| fullTempPath := filepath.Join(s.stgRoot, path.JoinOSPath()) | |||
| s.lock.Lock() | |||
| defer s.lock.Unlock() | |||
| @@ -77,7 +77,7 @@ func (s *ShardStore) Store(path string, hash clitypes.FileHash, size int64) (typ | |||
| return types.FileInfo{ | |||
| Hash: hash, | |||
| Size: size, | |||
| Path: s.getSlashFilePathFromHash(hash), | |||
| Path: s.getJPathFromHash(hash), | |||
| }, nil | |||
| } | |||
| @@ -94,7 +94,7 @@ func (s *ShardStore) Info(hash clitypes.FileHash) (types.FileInfo, error) { | |||
| return types.FileInfo{ | |||
| Hash: hash, | |||
| Size: info.Size(), | |||
| Path: s.getSlashFilePathFromHash(hash), | |||
| Path: s.getJPathFromHash(hash), | |||
| }, nil | |||
| } | |||
| @@ -126,7 +126,7 @@ func (s *ShardStore) ListAll() ([]types.FileInfo, error) { | |||
| infos = append(infos, types.FileInfo{ | |||
| Hash: fileHash, | |||
| Size: info.Size(), | |||
| Path: s.getSlashFilePathFromHash(fileHash), | |||
| Path: s.getJPathFromHash(fileHash), | |||
| }) | |||
| return nil | |||
| }) | |||
| @@ -207,6 +207,6 @@ func (s *ShardStore) getFilePathFromHash(hash clitypes.FileHash) string { | |||
| return filepath.Join(s.storeAbsRoot, hash.GetHashPrefix(2), string(hash)) | |||
| } | |||
| func (s *ShardStore) getSlashFilePathFromHash(hash clitypes.FileHash) string { | |||
| return types.PathJoin(s.detail.UserSpace.WorkingDir, types.ShardStoreWorkingDir, hash.GetHashPrefix(2), string(hash)) | |||
| func (s *ShardStore) getJPathFromHash(hash clitypes.FileHash) clitypes.JPath { | |||
| return s.detail.UserSpace.WorkingDir.ConcatCompsNew(types.ShardStoreWorkingDir, hash.GetHashPrefix(2), string(hash)) | |||
| } | |||
| @@ -38,7 +38,7 @@ func Test_S2S(t *testing.T) { | |||
| SK: "", | |||
| }, | |||
| }, | |||
| }, "test_data/test03.txt", "atest.txt") | |||
| }, clitypes.PathFromComps("test_data/test03.txt"), clitypes.PathFromComps("atest.txt")) | |||
| defer s2s.Close() | |||
| So(err, ShouldEqual, nil) | |||
| @@ -14,6 +14,7 @@ import ( | |||
| omsregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/oms/v2/region" | |||
| "gitlink.org.cn/cloudream/common/utils/os2" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| stgs3 "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/s3" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| cortypes "gitlink.org.cn/cloudream/jcs-pub/coordinator/types" | |||
| ) | |||
| @@ -38,12 +39,12 @@ func NewS2STransfer(detail *clitypes.UserSpaceDetail, stgType *cortypes.OBSType, | |||
| // 判断是否能从指定的源存储中直传到当前存储的目的路径 | |||
| func (*S2STransfer) CanTransfer(src, dst *clitypes.UserSpaceDetail) bool { | |||
| req := makeRequest(src, "") | |||
| req := makeRequest(src, clitypes.JPath{}) | |||
| return req != nil | |||
| } | |||
| // 执行数据直传。返回传输后的文件路径 | |||
| func (s *S2STransfer) Transfer(ctx context.Context, src *clitypes.UserSpaceDetail, srcPath string, dstPath string) (types.FileInfo, error) { | |||
| func (s *S2STransfer) Transfer(ctx context.Context, src *clitypes.UserSpaceDetail, srcPath clitypes.JPath, dstPath clitypes.JPath) (types.FileInfo, error) { | |||
| req := makeRequest(src, srcPath) | |||
| if req == nil { | |||
| return types.FileInfo{}, fmt.Errorf("unsupported source storage type: %T", src.UserSpace.Storage) | |||
| @@ -72,8 +73,8 @@ func (s *S2STransfer) Transfer(ctx context.Context, src *clitypes.UserSpaceDetai | |||
| } | |||
| // 先上传成一个临时文件 | |||
| tempDir := types.PathJoin(s.detail.UserSpace.WorkingDir, types.TempWorkingDir) | |||
| tempPrefix := types.PathJoin(tempDir, os2.GenerateRandomFileName(10)) + "/" | |||
| tempDir := stgs3.JoinKey(s.detail.UserSpace.WorkingDir.String(), types.TempWorkingDir) | |||
| tempPrefix := stgs3.JoinKey(tempDir, os2.GenerateRandomFileName(10)) + "/" | |||
| taskType := model.GetCreateTaskReqTaskTypeEnum().OBJECT | |||
| s.omsCli = oms.NewOmsClient(cli) | |||
| @@ -110,8 +111,8 @@ func (s *S2STransfer) Transfer(ctx context.Context, src *clitypes.UserSpaceDetai | |||
| _, err = obsCli.CopyObject(ctx, &awss3.CopyObjectInput{ | |||
| Bucket: aws.String(bkt), | |||
| CopySource: aws.String(types.PathJoin(bkt, tempPrefix, srcPath)), | |||
| Key: aws.String(dstPath), | |||
| CopySource: aws.String(stgs3.JoinKey(bkt, tempPrefix, srcPath.String())), | |||
| Key: aws.String(dstPath.String()), | |||
| }) | |||
| if err != nil { | |||
| return types.FileInfo{}, fmt.Errorf("copy object: %w", err) | |||
| @@ -177,7 +178,7 @@ func (s *S2STransfer) Close() { | |||
| } | |||
| } | |||
| func makeRequest(srcStg *clitypes.UserSpaceDetail, srcPath string) *model.SrcNodeReq { | |||
| func makeRequest(srcStg *clitypes.UserSpaceDetail, srcPath clitypes.JPath) *model.SrcNodeReq { | |||
| switch srcType := srcStg.UserSpace.Storage.(type) { | |||
| case *cortypes.OBSType: | |||
| cloudType := "HuaweiCloud" | |||
| @@ -193,7 +194,7 @@ func makeRequest(srcStg *clitypes.UserSpaceDetail, srcPath string) *model.SrcNod | |||
| Ak: &cred.AK, | |||
| Sk: &cred.SK, | |||
| Bucket: &srcType.Bucket, | |||
| ObjectKey: &[]string{srcPath}, | |||
| ObjectKey: &[]string{srcPath.String()}, | |||
| } | |||
| default: | |||
| @@ -38,10 +38,11 @@ func (s *ShardStore) MakeHTTPReadRequest(fileHash clitypes.FileHash) (types.HTTP | |||
| return types.HTTPRequest{}, err | |||
| } | |||
| filePath := s.GetFilePathFromHash(fileHash) | |||
| getSigned, err := cli.CreateSignedUrl(&obs.CreateSignedUrlInput{ | |||
| Method: "GET", | |||
| Bucket: s.Bucket, | |||
| Key: s.GetFilePathFromHash(fileHash), | |||
| Key: filePath.String(), | |||
| Expires: 3600, | |||
| }) | |||
| if err != nil { | |||
| @@ -7,6 +7,7 @@ import ( | |||
| "errors" | |||
| "fmt" | |||
| "io" | |||
| "time" | |||
| "github.com/aws/aws-sdk-go-v2/aws" | |||
| "github.com/aws/aws-sdk-go-v2/service/s3" | |||
| @@ -17,6 +18,10 @@ import ( | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| ) | |||
| const ( | |||
| ModTimeHeader = "X-JCS-ModTime" | |||
| ) | |||
| type BaseStore struct { | |||
| Detail *clitypes.UserSpaceDetail | |||
| Bucket string | |||
| @@ -37,17 +42,29 @@ func NewBaseStore(detail *clitypes.UserSpaceDetail, cli *s3.Client, bkt string, | |||
| }, nil | |||
| } | |||
| func (s *BaseStore) Write(objPath string, stream io.Reader) (types.FileInfo, error) { | |||
| key := objPath | |||
| func (s *BaseStore) Write(pat clitypes.JPath, stream io.Reader, opt types.WriteOption) (types.FileInfo, error) { | |||
| key := pat | |||
| meta := make(map[string]string) | |||
| if opt.ModTime.IsZero() { | |||
| mt, _ := time.Now().MarshalText() | |||
| meta[ModTimeHeader] = string(mt) | |||
| } else { | |||
| mt, err := opt.ModTime.MarshalText() | |||
| if err != nil { | |||
| return types.FileInfo{}, err | |||
| } | |||
| meta[ModTimeHeader] = string(mt) | |||
| } | |||
| counter := io2.Counter(stream) | |||
| if s.opt.UseAWSSha256 { | |||
| resp, err := s.cli.PutObject(context.TODO(), &s3.PutObjectInput{ | |||
| Bucket: aws.String(s.Bucket), | |||
| Key: aws.String(key), | |||
| Key: aws.String(key.String()), | |||
| Body: counter, | |||
| ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256, | |||
| Metadata: meta, | |||
| }) | |||
| if err != nil { | |||
| return types.FileInfo{}, err | |||
| @@ -70,9 +87,10 @@ func (s *BaseStore) Write(objPath string, stream io.Reader) (types.FileInfo, err | |||
| hashStr := io2.NewReadHasher(sha256.New(), counter) | |||
| _, err := s.cli.PutObject(context.TODO(), &s3.PutObjectInput{ | |||
| Bucket: aws.String(s.Bucket), | |||
| Key: aws.String(key), | |||
| Body: counter, | |||
| Bucket: aws.String(s.Bucket), | |||
| Key: aws.String(key.String()), | |||
| Body: counter, | |||
| Metadata: meta, | |||
| }) | |||
| if err != nil { | |||
| return types.FileInfo{}, err | |||
| @@ -85,8 +103,8 @@ func (s *BaseStore) Write(objPath string, stream io.Reader) (types.FileInfo, err | |||
| }, nil | |||
| } | |||
| func (s *BaseStore) Read(objPath string, opt types.OpenOption) (io.ReadCloser, error) { | |||
| key := objPath | |||
| func (s *BaseStore) Read(pat clitypes.JPath, opt types.OpenOption) (io.ReadCloser, error) { | |||
| key := pat | |||
| rngStr := fmt.Sprintf("bytes=%d-", opt.Offset) | |||
| if opt.Length >= 0 { | |||
| @@ -95,7 +113,7 @@ func (s *BaseStore) Read(objPath string, opt types.OpenOption) (io.ReadCloser, e | |||
| resp, err := s.cli.GetObject(context.TODO(), &s3.GetObjectInput{ | |||
| Bucket: aws.String(s.Bucket), | |||
| Key: aws.String(key), | |||
| Key: aws.String(key.String()), | |||
| Range: aws.String(rngStr), | |||
| }) | |||
| @@ -106,51 +124,21 @@ func (s *BaseStore) Read(objPath string, opt types.OpenOption) (io.ReadCloser, e | |||
| return resp.Body, nil | |||
| } | |||
| func (s *BaseStore) Mkdir(path string) error { | |||
| func (s *BaseStore) Mkdir(path clitypes.JPath) error { | |||
| _, err := s.cli.PutObject(context.TODO(), &s3.PutObjectInput{ | |||
| Bucket: aws.String(s.Bucket), | |||
| Key: aws.String(path + "/"), | |||
| Key: aws.String(path.String() + "/"), | |||
| Body: bytes.NewReader([]byte{}), | |||
| }) | |||
| return err | |||
| } | |||
| func (s *BaseStore) ListAll(path string) ([]types.ListEntry, error) { | |||
| key := path | |||
| // TODO 待测试 | |||
| input := &s3.ListObjectsInput{ | |||
| Bucket: aws.String(s.Bucket), | |||
| Prefix: aws.String(key), | |||
| Delimiter: aws.String("/"), | |||
| } | |||
| var objs []types.ListEntry | |||
| var marker *string | |||
| for { | |||
| input.Marker = marker | |||
| resp, err := s.cli.ListObjects(context.Background(), input) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| for _, obj := range resp.Contents { | |||
| objs = append(objs, types.ListEntry{ | |||
| Path: *obj.Key, | |||
| Size: *obj.Size, | |||
| IsDir: false, | |||
| }) | |||
| } | |||
| if !*resp.IsTruncated { | |||
| break | |||
| } | |||
| marker = resp.NextMarker | |||
| func (s *BaseStore) ReadDir(path clitypes.JPath) types.DirReader { | |||
| return &DirReader{ | |||
| cli: s.cli, | |||
| bucket: s.Bucket, | |||
| rootPath: path.Clone(), | |||
| } | |||
| return objs, nil | |||
| } | |||
| func (s *BaseStore) CleanTemps() { | |||
| @@ -162,7 +150,7 @@ func (s *BaseStore) CleanTemps() { | |||
| for { | |||
| resp, err := s.cli.ListObjects(context.Background(), &s3.ListObjectsInput{ | |||
| Bucket: aws.String(s.Bucket), | |||
| Prefix: aws.String(types.PathJoin(s.Detail.UserSpace.WorkingDir, types.TempWorkingDir, "/")), | |||
| Prefix: aws.String(JoinKey(s.Detail.UserSpace.WorkingDir.String(), types.TempWorkingDir, "/")), | |||
| Marker: marker, | |||
| }) | |||
| @@ -206,6 +194,15 @@ func (s *BaseStore) CleanTemps() { | |||
| } | |||
| } | |||
| func (s *BaseStore) Test() error { | |||
| _, err := s.cli.ListObjects(context.Background(), &s3.ListObjectsInput{ | |||
| Bucket: aws.String(s.Bucket), | |||
| Prefix: aws.String(""), | |||
| MaxKeys: aws.Int32(1), | |||
| }) | |||
| return err | |||
| } | |||
| func (s *BaseStore) getLogger() logger.Logger { | |||
| return logger.WithField("BaseStore", "S3").WithField("Storage", s.Detail.UserSpace.Storage.String()) | |||
| } | |||
| @@ -0,0 +1,61 @@ | |||
| package s3 | |||
| import ( | |||
| "context" | |||
| "io" | |||
| "github.com/aws/aws-sdk-go-v2/aws" | |||
| "github.com/aws/aws-sdk-go-v2/service/s3" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/storage/types" | |||
| ) | |||
| type DirReader struct { | |||
| cli *s3.Client | |||
| bucket string | |||
| rootPath clitypes.JPath | |||
| marker *string | |||
| curInfos []types.DirEntry | |||
| eof bool | |||
| } | |||
| func (r *DirReader) Next() (types.DirEntry, error) { | |||
| if len(r.curInfos) > 0 { | |||
| e := r.curInfos[0] | |||
| r.curInfos = r.curInfos[1:] | |||
| return e, nil | |||
| } | |||
| if r.eof { | |||
| return types.DirEntry{}, io.EOF | |||
| } | |||
| resp, err := r.cli.ListObjects(context.Background(), &s3.ListObjectsInput{ | |||
| Bucket: aws.String(r.bucket), | |||
| Prefix: aws.String(r.rootPath.String()), | |||
| Marker: r.marker, | |||
| }) | |||
| if err != nil { | |||
| return types.DirEntry{}, err | |||
| } | |||
| for _, obj := range resp.Contents { | |||
| key := clitypes.PathFromJcsPathString(*obj.Key) | |||
| r.curInfos = append(r.curInfos, types.DirEntry{ | |||
| Path: key, | |||
| Size: *obj.Size, | |||
| ModTime: *obj.LastModified, | |||
| IsDir: false, | |||
| }) | |||
| } | |||
| if !*resp.IsTruncated { | |||
| r.eof = true | |||
| } | |||
| r.marker = resp.NextMarker | |||
| return r.Next() | |||
| } | |||
| func (r *DirReader) Close() { | |||
| } | |||
| @@ -42,12 +42,14 @@ func (*Multiparter) MaxPartSize() int64 { | |||
| func (m *Multiparter) Initiate(ctx context.Context) (types.MultipartTask, error) { | |||
| tempFileName := os2.GenerateRandomFileName(10) | |||
| tempDir := types.PathJoin(m.detail.UserSpace.WorkingDir, types.TempWorkingDir) | |||
| tempFilePath := types.PathJoin(tempDir, tempFileName) | |||
| tempDir := m.detail.UserSpace.WorkingDir.Clone() | |||
| tempDir.Push(types.TempWorkingDir) | |||
| tempFilePath := tempDir.Clone() | |||
| tempFilePath.Push(tempFileName) | |||
| resp, err := m.cli.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ | |||
| Bucket: aws.String(m.bucket), | |||
| Key: aws.String(tempFilePath), | |||
| Key: aws.String(tempFilePath.String()), | |||
| ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256, | |||
| }) | |||
| if err != nil { | |||
| @@ -87,9 +89,9 @@ func (m *Multiparter) UploadPart(ctx context.Context, init types.MultipartInitSt | |||
| type MultipartTask struct { | |||
| cli *s3.Client | |||
| bucket string | |||
| tempDir string | |||
| tempDir clitypes.JPath | |||
| tempFileName string | |||
| tempFilePath string | |||
| tempFilePath clitypes.JPath | |||
| uploadID string | |||
| } | |||
| @@ -97,7 +99,7 @@ func (i *MultipartTask) InitState() types.MultipartInitState { | |||
| return types.MultipartInitState{ | |||
| UploadID: i.uploadID, | |||
| Bucket: i.bucket, | |||
| Key: i.tempFilePath, | |||
| Key: i.tempFilePath.String(), | |||
| } | |||
| } | |||
| @@ -120,7 +122,7 @@ func (i *MultipartTask) JoinParts(ctx context.Context, parts []types.UploadedPar | |||
| _, err := i.cli.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{ | |||
| Bucket: aws.String(i.bucket), | |||
| Key: aws.String(i.tempFilePath), | |||
| Key: aws.String(i.tempFilePath.String()), | |||
| UploadId: aws.String(i.uploadID), | |||
| MultipartUpload: &s3types.CompletedMultipartUpload{ | |||
| Parts: s3Parts, | |||
| @@ -132,7 +134,7 @@ func (i *MultipartTask) JoinParts(ctx context.Context, parts []types.UploadedPar | |||
| headResp, err := i.cli.HeadObject(ctx, &s3.HeadObjectInput{ | |||
| Bucket: aws.String(i.bucket), | |||
| Key: aws.String(i.tempFilePath), | |||
| Key: aws.String(i.tempFilePath.String()), | |||
| }) | |||
| if err != nil { | |||
| return types.FileInfo{}, err | |||
| @@ -151,7 +153,7 @@ func (i *MultipartTask) JoinParts(ctx context.Context, parts []types.UploadedPar | |||
| func (i *MultipartTask) Close() { | |||
| i.cli.AbortMultipartUpload(context.Background(), &s3.AbortMultipartUploadInput{ | |||
| Bucket: aws.String(i.bucket), | |||
| Key: aws.String(i.tempFilePath), | |||
| Key: aws.String(i.tempFilePath.String()), | |||
| UploadId: aws.String(i.uploadID), | |||
| }) | |||
| } | |||
| @@ -19,17 +19,19 @@ type ShardStoreOption struct { | |||
| type ShardStore struct { | |||
| Detail *clitypes.UserSpaceDetail | |||
| Bucket string | |||
| workingDir string | |||
| workingDir clitypes.JPath | |||
| cli *s3.Client | |||
| opt ShardStoreOption | |||
| lock sync.Mutex | |||
| } | |||
| func NewShardStore(detail *clitypes.UserSpaceDetail, cli *s3.Client, bkt string, opt ShardStoreOption) (*ShardStore, error) { | |||
| wd := detail.UserSpace.WorkingDir.Clone() | |||
| wd.Push(types.ShardStoreWorkingDir) | |||
| return &ShardStore{ | |||
| Detail: detail, | |||
| Bucket: bkt, | |||
| workingDir: types.PathJoin(detail.UserSpace.WorkingDir, types.ShardStoreWorkingDir), | |||
| workingDir: wd, | |||
| cli: cli, | |||
| opt: opt, | |||
| }, nil | |||
| @@ -43,7 +45,7 @@ func (s *ShardStore) Stop() { | |||
| s.getLogger().Infof("component stop") | |||
| } | |||
| func (s *ShardStore) Store(path string, hash clitypes.FileHash, size int64) (types.FileInfo, error) { | |||
| func (s *ShardStore) Store(path clitypes.JPath, hash clitypes.FileHash, size int64) (types.FileInfo, error) { | |||
| s.lock.Lock() | |||
| defer s.lock.Unlock() | |||
| @@ -51,13 +53,12 @@ func (s *ShardStore) Store(path string, hash clitypes.FileHash, size int64) (typ | |||
| log.Debugf("write file %v finished, size: %v, hash: %v", path, size, hash) | |||
| blockDir := s.GetFileDirFromHash(hash) | |||
| newPath := types.PathJoin(blockDir, string(hash)) | |||
| newPath := s.GetFilePathFromHash(hash) | |||
| _, err := s.cli.CopyObject(context.Background(), &s3.CopyObjectInput{ | |||
| Bucket: aws.String(s.Bucket), | |||
| CopySource: aws.String(types.PathJoin(s.Bucket, path)), | |||
| Key: aws.String(newPath), | |||
| CopySource: aws.String(JoinKey(s.Bucket, path.String())), | |||
| Key: aws.String(newPath.String()), | |||
| }) | |||
| if err != nil { | |||
| log.Warnf("copy file %v to %v: %v", path, newPath, err) | |||
| @@ -78,7 +79,7 @@ func (s *ShardStore) Info(hash clitypes.FileHash) (types.FileInfo, error) { | |||
| filePath := s.GetFilePathFromHash(hash) | |||
| info, err := s.cli.HeadObject(context.TODO(), &s3.HeadObjectInput{ | |||
| Bucket: aws.String(s.Bucket), | |||
| Key: aws.String(filePath), | |||
| Key: aws.String(filePath.String()), | |||
| }) | |||
| if err != nil { | |||
| s.getLogger().Warnf("get file %v: %v", filePath, err) | |||
| @@ -102,7 +103,7 @@ func (s *ShardStore) ListAll() ([]types.FileInfo, error) { | |||
| for { | |||
| resp, err := s.cli.ListObjects(context.Background(), &s3.ListObjectsInput{ | |||
| Bucket: aws.String(s.Bucket), | |||
| Prefix: aws.String(s.workingDir), | |||
| Prefix: aws.String(s.workingDir.String()), | |||
| Marker: marker, | |||
| }) | |||
| @@ -112,7 +113,7 @@ func (s *ShardStore) ListAll() ([]types.FileInfo, error) { | |||
| } | |||
| for _, obj := range resp.Contents { | |||
| key := types.PathBase(*obj.Key) | |||
| key := BaseKey(*obj.Key) | |||
| fileHash, err := clitypes.ParseHash(key) | |||
| if err != nil { | |||
| @@ -122,7 +123,7 @@ func (s *ShardStore) ListAll() ([]types.FileInfo, error) { | |||
| infos = append(infos, types.FileInfo{ | |||
| Hash: fileHash, | |||
| Size: *obj.Size, | |||
| Path: *obj.Key, | |||
| Path: clitypes.PathFromJcsPathString(*obj.Key), | |||
| }) | |||
| } | |||
| @@ -150,7 +151,7 @@ func (s *ShardStore) GC(avaiables []clitypes.FileHash) error { | |||
| for { | |||
| resp, err := s.cli.ListObjects(context.Background(), &s3.ListObjectsInput{ | |||
| Bucket: aws.String(s.Bucket), | |||
| Prefix: aws.String(s.workingDir), | |||
| Prefix: aws.String(s.workingDir.String()), | |||
| Marker: marker, | |||
| }) | |||
| @@ -160,7 +161,7 @@ func (s *ShardStore) GC(avaiables []clitypes.FileHash) error { | |||
| } | |||
| for _, obj := range resp.Contents { | |||
| key := types.PathBase(*obj.Key) | |||
| key := BaseKey(*obj.Key) | |||
| fileHash, err := clitypes.ParseHash(key) | |||
| if err != nil { | |||
| continue | |||
| @@ -212,10 +213,15 @@ func (s *ShardStore) getLogger() logger.Logger { | |||
| return logger.WithField("ShardStore", "S3").WithField("UserSpace", s.Detail) | |||
| } | |||
| func (s *ShardStore) GetFileDirFromHash(hash clitypes.FileHash) string { | |||
| return types.PathJoin(s.workingDir, hash.GetHashPrefix(2)) | |||
| func (s *ShardStore) GetFileDirFromHash(hash clitypes.FileHash) clitypes.JPath { | |||
| p := s.workingDir.Clone() | |||
| p.Push(hash.GetHashPrefix(2)) | |||
| return p | |||
| } | |||
| func (s *ShardStore) GetFilePathFromHash(hash clitypes.FileHash) string { | |||
| return types.PathJoin(s.workingDir, hash.GetHashPrefix(2), string(hash)) | |||
| func (s *ShardStore) GetFilePathFromHash(hash clitypes.FileHash) clitypes.JPath { | |||
| p := s.workingDir.Clone() | |||
| p.Push(hash.GetHashPrefix(2)) | |||
| p.Push(string(hash)) | |||
| return p | |||
| } | |||
| @@ -3,6 +3,7 @@ package s3 | |||
| import ( | |||
| "encoding/base64" | |||
| "fmt" | |||
| "path" | |||
| ) | |||
| func DecodeBase64Hash(hash string) ([]byte, error) { | |||
| @@ -17,3 +18,11 @@ func DecodeBase64Hash(hash string) ([]byte, error) { | |||
| return hashBytes, nil | |||
| } | |||
| func JoinKey(comps ...string) string { | |||
| return path.Join(comps...) | |||
| } | |||
| func BaseKey(key string) string { | |||
| return path.Base(key) | |||
| } | |||
| @@ -3,25 +3,36 @@ package types | |||
| import ( | |||
| "fmt" | |||
| "io" | |||
| "time" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| ) | |||
| type ListEntry struct { | |||
| Path string | |||
| Size int64 | |||
| IsDir bool | |||
| type DirEntry struct { | |||
| Path clitypes.JPath | |||
| Size int64 | |||
| ModTime time.Time | |||
| IsDir bool | |||
| } | |||
| type DirReader interface { | |||
| // 读取下一个目录条目。如果没有更多条目,那么应该返回io.EOF | |||
| Next() (DirEntry, error) | |||
| Close() | |||
| } | |||
| type BaseStore interface { | |||
| Write(path string, stream io.Reader) (FileInfo, error) | |||
| Read(path string, opt OpenOption) (io.ReadCloser, error) | |||
| Write(path clitypes.JPath, stream io.Reader, opt WriteOption) (FileInfo, error) | |||
| Read(path clitypes.JPath, opt OpenOption) (io.ReadCloser, error) | |||
| // 创建指定路径的文件夹。对于不支持空文件夹的存储系统来说,可以采用创建以/结尾的对象的方式来模拟文件夹。 | |||
| Mkdir(path string) error | |||
| // 返回指定路径下的所有文件,文件路径是包含path在内的完整路径。返回结果的第一条一定是路径本身,可能是文件,也可能是目录。 | |||
| // 如果路径不存在,那么不会返回错误,而是返回一个空列表。 | |||
| // 返回的内容严格按照存储系统的原始结果来,比如当存储系统是一个对象存储时,那么就可能不会包含目录,或者包含用于模拟的以“/”结尾的对象。 | |||
| ListAll(path string) ([]ListEntry, error) | |||
| Mkdir(path clitypes.JPath) error | |||
| // 返回指定路径下的所有文件,文件路径是包含path在内的完整路径。返回结果的第一条一定是路径本身,可能是文件,也可能是目录,路径不存在时,Next应该直接返回io.EOF。 | |||
| // Next必须按照目录的层级关系返回,但不一定要按照文件名排序。 | |||
| ReadDir(path clitypes.JPath) DirReader | |||
| // 清空临时目录。只应该在此存储服务未被使用时调用 | |||
| CleanTemps() | |||
| // 测试存储服务是否可用 | |||
| Test() error | |||
| } | |||
| type OpenOption struct { | |||
| @@ -65,3 +76,8 @@ func (o *OpenOption) String() string { | |||
| return fmt.Sprintf("%s:%s", rangeStart, rangeEnd) | |||
| } | |||
| type WriteOption struct { | |||
| // 文件修改时间,如果为0,则使用当前时间 | |||
| ModTime time.Time | |||
| } | |||
| @@ -10,6 +10,6 @@ type S2STransfer interface { | |||
| // 【静态方法】判断是否能从指定的源存储中直传到当前存储的目的路径。仅在生成计划时使用 | |||
| CanTransfer(src, dst *clitypes.UserSpaceDetail) bool | |||
| // 从远端获取文件并保存到本地路径 | |||
| Transfer(ctx context.Context, src *clitypes.UserSpaceDetail, srcPath string, dstPath string) (FileInfo, error) | |||
| Transfer(ctx context.Context, src *clitypes.UserSpaceDetail, srcPath clitypes.JPath, dstPath clitypes.JPath) (FileInfo, error) | |||
| Close() | |||
| } | |||
| @@ -10,7 +10,7 @@ type ShardStore interface { | |||
| Start(ch *StorageEventChan) | |||
| Stop() | |||
| // 将存储系统中已有的文件作为分片纳入管理范围 | |||
| Store(path string, hash clitypes.FileHash, size int64) (FileInfo, error) | |||
| Store(path clitypes.JPath, hash clitypes.FileHash, size int64) (FileInfo, error) | |||
| // 获得指定文件信息 | |||
| Info(fileHash clitypes.FileHash) (FileInfo, error) | |||
| // 获取所有文件信息,尽量保证操作是原子的 | |||
| @@ -43,7 +43,7 @@ type FeatureDesc struct{} | |||
| type FileInfo struct { | |||
| // 分片在存储系统中的路径,可以通过BaseStore读取的 | |||
| Path string | |||
| Path clitypes.JPath | |||
| // 文件大小 | |||
| Size int64 | |||
| // 分片的哈希值,不一定有值,根据来源不同,可能为空 | |||
| @@ -1,8 +1,6 @@ | |||
| package types | |||
| import ( | |||
| "path" | |||
| clitypes "gitlink.org.cn/cloudream/jcs-pub/client/types" | |||
| cortypes "gitlink.org.cn/cloudream/jcs-pub/coordinator/types" | |||
| ) | |||
| @@ -19,15 +17,9 @@ func FindFeature[T cortypes.StorageFeature](detail *clitypes.UserSpaceDetail) T | |||
| return def | |||
| } | |||
| func PathJoin(comps ...string) string { | |||
| return path.Join(comps...) | |||
| } | |||
| func PathBase(p string) string { | |||
| return path.Base(p) | |||
| } | |||
| func MakeTempDirPath(detail *clitypes.UserSpaceDetail, comps ...string) string { | |||
| cs := append([]string{detail.UserSpace.WorkingDir, TempWorkingDir}, comps...) | |||
| return PathJoin(cs...) | |||
| func MakeTempDirPath(detail *clitypes.UserSpaceDetail, comps ...string) clitypes.JPath { | |||
| p := detail.UserSpace.WorkingDir.Clone() | |||
| p.Push(TempWorkingDir) | |||
| p.ConcatComps(comps) | |||
| return p | |||
| } | |||