diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index 46db8fc3c1..d9b31431ac 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -45,6 +45,7 @@ type Spaces struct { ExtendedSpacePropertiesCacheTTL int `yaml:"extended_space_properties_cache_ttl" env:"GRAPH_SPACES_EXTENDED_SPACE_PROPERTIES_CACHE_TTL" desc:"Max TTL in seconds for the spaces property cache." introductionVersion:"pre5.0"` UsersCacheTTL int `yaml:"users_cache_ttl" env:"GRAPH_SPACES_USERS_CACHE_TTL" desc:"Max TTL in seconds for the spaces users cache." introductionVersion:"pre5.0"` GroupsCacheTTL int `yaml:"groups_cache_ttl" env:"GRAPH_SPACES_GROUPS_CACHE_TTL" desc:"Max TTL in seconds for the spaces groups cache." introductionVersion:"pre5.0"` + TemplatePath string `yaml:"template_path" env:"GRAPH_SPACES_TEMPLATE_PATH" desc:"The path to a folder containg files and folder that should be created on any space." introductionVersion:"5.0"` } type LDAP struct { diff --git a/services/graph/pkg/service/v0/drives.go b/services/graph/pkg/service/v0/drives.go index cad1abb6a9..645e99417b 100644 --- a/services/graph/pkg/service/v0/drives.go +++ b/services/graph/pkg/service/v0/drives.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io/fs" "math" "net/http" "net/url" @@ -488,6 +489,26 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) { errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } + + ctx, cs3, err := g.getCS3Client(gatewayClient, resp.GetStorageSpace().GetRoot()) + if err != nil { + logger.Error().Err(err).Msg("could not get cs3 client") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "could not get cs3 client, aborting") + return + } + + fsys, err := fs.Sub(_spaceTemplateFS, "spacetemplate") + if err != nil { + logger.Error().Err(err).Msg("could not create drive: error parsing fs") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + if err := applyTemplate(ctx, cs3, gatewayClient, resp.GetStorageSpace().GetRoot(), fsys.(fs.ReadDirFS)); err != nil { + logger.Error().Err(err).Msg("could not apply template to space") + return + } + render.Status(r, http.StatusCreated) render.JSON(w, r, newDrive) } diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 25c19b2dce..ff1a28ea37 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -3,6 +3,7 @@ package svc import ( "crypto/tls" "crypto/x509" + "embed" "errors" "fmt" "net/http" @@ -35,6 +36,11 @@ const ( displayNameAttr = "displayName" ) +var ( + //go:embed spacetemplate/* + _spaceTemplateFS embed.FS +) + // Service defines the service handlers. type Service interface { ServeHTTP(http.ResponseWriter, *http.Request) diff --git a/services/graph/pkg/service/v0/spacetemplate/.space/image.jpg b/services/graph/pkg/service/v0/spacetemplate/.space/image.jpg new file mode 100644 index 0000000000..11e7f78b4a Binary files /dev/null and b/services/graph/pkg/service/v0/spacetemplate/.space/image.jpg differ diff --git a/services/graph/pkg/service/v0/spacetemplate/.space/readme.md b/services/graph/pkg/service/v0/spacetemplate/.space/readme.md new file mode 100644 index 0000000000..bcd32e9de0 --- /dev/null +++ b/services/graph/pkg/service/v0/spacetemplate/.space/readme.md @@ -0,0 +1 @@ +Space description goes here diff --git a/services/graph/pkg/service/v0/utils.go b/services/graph/pkg/service/v0/utils.go index ded1ca58c7..09e695652c 100644 --- a/services/graph/pkg/service/v0/utils.go +++ b/services/graph/pkg/service/v0/utils.go @@ -3,14 +3,22 @@ package svc import ( "context" "encoding/json" + "errors" "io" + "io/fs" "net/http" + "os" + "path/filepath" "reflect" + "strings" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" cs3User "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + v1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/v2/pkg/storage/utils/metadata" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" "golang.org/x/sync/errgroup" @@ -425,3 +433,92 @@ func cs3ReceivedShareToLibreGraphPermissions(ctx context.Context, logger *log.Lo return permission, nil } + +func (g Graph) getCS3Client(gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId) (context.Context, *metadata.CS3, error) { + mdc := metadata.NewCS3(g.config.Reva.Address, "com.owncloud.api.storage-users") + mdc.SpaceRoot = root + ctx, err := utils.GetServiceUserContext(g.config.ServiceAccount.ServiceAccountID, gwc, g.config.ServiceAccount.ServiceAccountSecret) + return ctx, mdc, err +} + +func applyTemplate(ctx context.Context, mdc *metadata.CS3, gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, fsys fs.ReadDirFS) error { + entries, err := fsys.ReadDir(".") + if err != nil { + return err + } + + updateSpaceRequest := &storageprovider.UpdateStorageSpaceRequest{ + // Prepare the object to apply the diff from. The properties on StorageSpace will overwrite + // the original storage space. + StorageSpace: &storageprovider.StorageSpace{ + Id: &storageprovider.StorageSpaceId{ + OpaqueId: storagespace.FormatResourceID(*root), + }, + Root: root, + }, + } + + updateSpaceRequest.StorageSpace.Opaque, err = uploadFolder(ctx, mdc, "", "", updateSpaceRequest.StorageSpace.Opaque, fsys, entries) + if err != nil { + return err + } + + if len(updateSpaceRequest.StorageSpace.Opaque.Map) == 0 { + return nil + } + + resp, err := gwc.UpdateStorageSpace(ctx, updateSpaceRequest) + switch { + case err != nil: + return err + case resp.Status.Code == rpc.Code_CODE_OK: + return nil + default: + return errors.New(resp.Status.Message) + } +} + +func uploadFolder(ctx context.Context, mdc *metadata.CS3, pathOnDisc, pathOnSpace string, opaque *v1beta1.Opaque, fsys fs.ReadDirFS, entries []os.DirEntry) (*v1beta1.Opaque, error) { + for _, entry := range entries { + spacePath := filepath.Join(pathOnSpace, entry.Name()) + discPath := filepath.Join(pathOnDisc, entry.Name()) + + if entry.IsDir() { + err := mdc.MakeDirIfNotExist(ctx, spacePath) + if err != nil { + return opaque, err + } + + entries, err := fsys.ReadDir(discPath) + if err != nil { + return opaque, err + } + + opaque, err = uploadFolder(ctx, mdc, discPath, spacePath, opaque, fsys, entries) + if err != nil { + return opaque, err + } + continue + } + + b, err := fs.ReadFile(fsys, discPath) + if err != nil { + return opaque, err + } + + if err := mdc.SimpleUpload(ctx, spacePath, b); err != nil { + return opaque, err + } + + // TODO: use upload to avoid second stat + i, err := mdc.Stat(ctx, spacePath) + if err != nil { + return opaque, err + } + + identifier := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())) + opaque = utils.AppendPlainToOpaque(opaque, identifier, storagespace.FormatResourceID(*i.Id)) + } + + return opaque, nil +}