mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-02 16:59:56 -05:00
Merge pull request #8723 from kobergj/SpaceTemplatesII
Server-Side Space Templates
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
SHELL := bash
|
||||
NAME := graph
|
||||
|
||||
# Where to write the files generated by this makefile.
|
||||
OUTPUT_DIR = ./pkg/service/v0/l10n
|
||||
TEMPLATE_FILE = ./pkg/service/v0/l10n/graph.pot
|
||||
|
||||
include ../../.make/recursion.mk
|
||||
|
||||
############ tooling ############
|
||||
@@ -30,6 +34,27 @@ ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this
|
||||
.PHONY: ci-node-generate
|
||||
ci-node-generate:
|
||||
|
||||
############ translations ########
|
||||
.PHONY: l10n-pull
|
||||
l10n-pull:
|
||||
cd $(OUTPUT_DIR) && tx pull --all --force --skip --minimum-perc=75
|
||||
|
||||
.PHONY: l10n-push
|
||||
l10n-push:
|
||||
cd $(OUTPUT_DIR) && tx push -s --skip
|
||||
|
||||
.PHONY: l10n-read
|
||||
l10n-read: $(GO_XGETTEXT)
|
||||
go-xgettext -o $(OUTPUT_DIR)/graph.pot --keyword=l10n.Template --add-comments -s pkg/service/v0/spacetemplates.go
|
||||
|
||||
.PHONY: l10n-write
|
||||
l10n-write:
|
||||
|
||||
.PHONY: l10n-clean
|
||||
l10n-clean:
|
||||
rm -f $(TEMPLATE_FILE);
|
||||
|
||||
|
||||
############ licenses ############
|
||||
.PHONY: ci-node-check-licenses
|
||||
ci-node-check-licenses:
|
||||
|
||||
@@ -46,6 +46,7 @@ type Spaces struct {
|
||||
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"`
|
||||
StorageUsersAddress string `yaml:"storage_users_address" env:"GRAPH_SPACES_STORAGE_USERS_ADDRESS" desc:"The address of the storage-users service." introductionVersion:"5.0"`
|
||||
DefaultLanguage string `yaml:"default_language" env:"OCIS_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details." introductionVersion:"5.0"`
|
||||
}
|
||||
|
||||
type LDAP struct {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/storagespace"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/conversions"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
|
||||
v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0"
|
||||
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
||||
@@ -326,7 +327,10 @@ func (g Graph) canCreateSpace(ctx context.Context, ownPersonalHome bool) bool {
|
||||
func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
|
||||
logger := g.logger.SubloggerWithRequestID(r.Context())
|
||||
logger.Info().Msg("calling create drive")
|
||||
us, ok := revactx.ContextGetUser(r.Context())
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
us, ok := revactx.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
logger.Debug().Msg("could not create drive: invalid user")
|
||||
errorcode.NotAllowed.Render(w, r, http.StatusUnauthorized, "invalid user")
|
||||
@@ -334,7 +338,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// TODO determine if the user tries to create his own personal space and pass that as a boolean
|
||||
canCreateSpace := g.canCreateSpace(r.Context(), false)
|
||||
canCreateSpace := g.canCreateSpace(ctx, false)
|
||||
if !canCreateSpace {
|
||||
logger.Debug().Bool("cancreatespace", canCreateSpace).Msg("could not create drive: insufficient permissions")
|
||||
// if the permission is not existing for the user in context we can assume we don't have it. Return 401.
|
||||
@@ -393,7 +397,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
|
||||
csr.Owner = us
|
||||
}
|
||||
|
||||
resp, err := gatewayClient.CreateStorageSpace(r.Context(), &csr)
|
||||
resp, err := gatewayClient.CreateStorageSpace(ctx, &csr)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not create drive: transport error")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
@@ -423,27 +427,38 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if driveType == _spaceTypeProject {
|
||||
opaque, err := g.applySpaceTemplate(gatewayClient, resp.GetStorageSpace().GetRoot(), r.URL.Query().Get("template"))
|
||||
if err != nil {
|
||||
space := resp.GetStorageSpace()
|
||||
if t := r.URL.Query().Get(TemplateParameter); t != "" && driveType == _spaceTypeProject {
|
||||
loc := l10n.MustGetUserLocale(ctx, us.GetId().GetOpaqueId(), r.Header.Get(HeaderAcceptLanguage), g.valueService)
|
||||
if err := g.applySpaceTemplate(ctx, gatewayClient, space.GetRoot(), t, loc); err != nil {
|
||||
logger.Error().Err(err).Msg("could not apply template to space")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.StorageSpace.Opaque = utils.MergeOpaques(resp.GetStorageSpace().GetOpaque(), opaque)
|
||||
// refetch the drive to get quota information - should we calculate this ourselves to avoid the extra call?
|
||||
space, err = utils.GetSpace(ctx, space.GetId().GetOpaqueId(), gatewayClient)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not refetch space")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
newDrive, err := g.cs3StorageSpaceToDrive(r.Context(), webDavBaseURL, resp.GetStorageSpace(), APIVersion_1)
|
||||
spaces, err := g.formatDrives(ctx, webDavBaseURL, []*storageprovider.StorageSpace{space}, APIVersion_1)
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Msg("could not create drive: error parsing drive")
|
||||
logger.Debug().Err(err).Msg("could not get drive: error parsing grpc response")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
newDrive.Special = g.getSpecialDriveItems(r.Context(), webDavBaseURL, resp.GetStorageSpace())
|
||||
if len(spaces) == 0 {
|
||||
logger.Error().Msg("could not convert space")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not convert space")
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusCreated)
|
||||
render.JSON(w, r, newDrive)
|
||||
render.JSON(w, r, spaces[0])
|
||||
}
|
||||
|
||||
// UpdateDrive updates the properties of a storage drive (space).
|
||||
|
||||
@@ -764,6 +764,12 @@ var _ = Describe("Graph", func() {
|
||||
Constraint: v0.Permission_CONSTRAINT_ALL,
|
||||
},
|
||||
}, nil)
|
||||
|
||||
gatewayClient.On("GetQuota", mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
TotalBytes: 500,
|
||||
}, nil)
|
||||
|
||||
jsonBody := []byte(`{"Name": "Test Space", "DriveType": "project", "Description": "This space is for testing", "DriveAlias": "project/testspace"}`)
|
||||
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/drives", bytes.NewBuffer(jsonBody)).WithContext(ctx)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:owncloud-org:p:owncloud:r:ocis-graph]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/graph.po
|
||||
minimum_perc = 75
|
||||
source_file = graph.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
@@ -0,0 +1,22 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr "Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: EMAIL\n"
|
||||
"POT-Creation-Date: 2024-03-26 15:11+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: pkg/service/v0/spacetemplates.go:29
|
||||
msgid "Here you can add a description for this Space."
|
||||
msgstr ""
|
||||
|
||||
@@ -3,7 +3,6 @@ package svc
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -36,11 +35,6 @@ const (
|
||||
displayNameAttr = "displayName"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed spacetemplate/*
|
||||
_spaceTemplateFS embed.FS
|
||||
)
|
||||
|
||||
// Service defines the service handlers.
|
||||
type Service interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1 +0,0 @@
|
||||
Here you can add a description for this Space.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,133 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/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"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed spacetemplate/*
|
||||
_spaceTemplateFS embed.FS
|
||||
|
||||
//go:embed l10n/locale
|
||||
_localeFS embed.FS
|
||||
|
||||
// subfolder where the translation files are stored
|
||||
_localeSubPath = "l10n/locale"
|
||||
|
||||
// name of the secret space folder
|
||||
_spaceFolderName = ".space"
|
||||
|
||||
// path to the image file
|
||||
_imagepath = "spacetemplate/image.png"
|
||||
|
||||
// text for the readme.md file
|
||||
_readmeText = l10n.Template("Here you can add a description for this Space.")
|
||||
|
||||
// name of the readme.md file
|
||||
_readmeName = "readme.md"
|
||||
|
||||
// domain of the graph service (transifex)
|
||||
_domain = "graph"
|
||||
|
||||
// HeaderAcceptLanguage is the header key for the accept-language header
|
||||
HeaderAcceptLanguage = "Accept-Language"
|
||||
|
||||
// TemplateParameter is the key for the template parameter in the request
|
||||
TemplateParameter = "template"
|
||||
)
|
||||
|
||||
func (g Graph) applySpaceTemplate(ctx context.Context, gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, template string, locale string) error {
|
||||
switch template {
|
||||
default:
|
||||
fallthrough
|
||||
case "none":
|
||||
return nil
|
||||
case "default":
|
||||
return g.applyDefaultTemplate(ctx, gwc, root, locale)
|
||||
}
|
||||
}
|
||||
|
||||
func (g Graph) applyDefaultTemplate(ctx context.Context, gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, locale string) error {
|
||||
mdc := metadata.NewCS3(g.config.Reva.Address, g.config.Spaces.StorageUsersAddress)
|
||||
mdc.SpaceRoot = root
|
||||
|
||||
var opaque *v1beta1.Opaque
|
||||
|
||||
// create .space folder
|
||||
if err := mdc.MakeDirIfNotExist(ctx, _spaceFolderName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// upload space image
|
||||
iid, err := imageUpload(ctx, mdc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opaque = utils.AppendPlainToOpaque(opaque, SpaceImageSpecialFolderName, iid)
|
||||
|
||||
// upload readme.md
|
||||
rid, err := readmeUpload(ctx, mdc, locale, g.config.Spaces.DefaultLanguage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opaque = utils.AppendPlainToOpaque(opaque, ReadmeSpecialFolderName, rid)
|
||||
|
||||
// update space
|
||||
resp, err := gwc.UpdateStorageSpace(ctx, &storageprovider.UpdateStorageSpaceRequest{
|
||||
StorageSpace: &storageprovider.StorageSpace{
|
||||
Id: &storageprovider.StorageSpaceId{
|
||||
OpaqueId: storagespace.FormatResourceID(*root),
|
||||
},
|
||||
Root: root,
|
||||
Opaque: opaque,
|
||||
},
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
case resp.GetStatus().GetCode() != rpc.Code_CODE_OK:
|
||||
return fmt.Errorf("could not update storage space: %s", resp.GetStatus().GetMessage())
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func imageUpload(ctx context.Context, mdc *metadata.CS3) (string, error) {
|
||||
b, err := fs.ReadFile(_spaceTemplateFS, _imagepath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := mdc.Upload(ctx, metadata.UploadRequest{
|
||||
Path: filepath.Join(_spaceFolderName, filepath.Base(_imagepath)),
|
||||
Content: b,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res.FileID, nil
|
||||
}
|
||||
|
||||
func readmeUpload(ctx context.Context, mdc *metadata.CS3, locale string, defaultLocale string) (string, error) {
|
||||
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, "", _localeFS, _localeSubPath)
|
||||
res, err := mdc.Upload(ctx, metadata.UploadRequest{
|
||||
Path: filepath.Join(_spaceFolderName, _readmeName),
|
||||
Content: []byte(t.Translate(_readmeText, locale)),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res.FileID, nil
|
||||
}
|
||||
@@ -3,22 +3,14 @@ package svc
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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"
|
||||
@@ -445,104 +437,3 @@ func cs3ReceivedShareToLibreGraphPermissions(ctx context.Context, logger *log.Lo
|
||||
|
||||
return permission, nil
|
||||
}
|
||||
|
||||
func (g Graph) applySpaceTemplate(gwc gateway.GatewayAPIClient, root *storageprovider.ResourceId, template string) (*v1beta1.Opaque, error) {
|
||||
var fsys fs.ReadDirFS
|
||||
|
||||
switch template {
|
||||
default:
|
||||
fallthrough
|
||||
case "none":
|
||||
return &v1beta1.Opaque{}, nil
|
||||
case "default":
|
||||
f, err := fs.Sub(_spaceTemplateFS, "spacetemplate")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fsys = f.(fs.ReadDirFS)
|
||||
}
|
||||
|
||||
mdc := metadata.NewCS3(g.config.Reva.Address, g.config.Spaces.StorageUsersAddress)
|
||||
mdc.SpaceRoot = root
|
||||
|
||||
ctx, err := utils.GetServiceUserContext(g.config.ServiceAccount.ServiceAccountID, gwc, g.config.ServiceAccount.ServiceAccountSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opaque, err := uploadFolder(ctx, mdc, ".", "", nil, fsys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := gwc.UpdateStorageSpace(ctx, &storageprovider.UpdateStorageSpaceRequest{
|
||||
StorageSpace: &storageprovider.StorageSpace{
|
||||
Id: &storageprovider.StorageSpaceId{
|
||||
OpaqueId: storagespace.FormatResourceID(*root),
|
||||
},
|
||||
Root: root,
|
||||
Opaque: opaque,
|
||||
},
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case resp.GetStatus().GetCode() != rpc.Code_CODE_OK:
|
||||
return nil, fmt.Errorf("could not update storage space: %s", resp.GetStatus().GetMessage())
|
||||
default:
|
||||
return opaque, nil
|
||||
}
|
||||
}
|
||||
|
||||
func uploadFolder(ctx context.Context, mdc *metadata.CS3, pathOnDisc, pathOnSpace string, opaque *v1beta1.Opaque, fsys fs.ReadDirFS) (*v1beta1.Opaque, error) {
|
||||
entries, err := fsys.ReadDir(pathOnDisc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
opaque, err = uploadEntry(ctx, mdc, pathOnDisc, pathOnSpace, opaque, fsys, entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return opaque, nil
|
||||
}
|
||||
|
||||
func uploadEntry(ctx context.Context, mdc *metadata.CS3, pathOnDisc, pathOnSpace string, opaque *v1beta1.Opaque, fsys fs.ReadDirFS, entry os.DirEntry) (*v1beta1.Opaque, error) {
|
||||
spacePath := filepath.Join(pathOnSpace, entry.Name())
|
||||
discPath := filepath.Join(pathOnDisc, entry.Name())
|
||||
|
||||
switch {
|
||||
case entry.IsDir():
|
||||
err := mdc.MakeDirIfNotExist(ctx, spacePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return uploadFolder(ctx, mdc, discPath, spacePath, opaque, fsys)
|
||||
default:
|
||||
b, err := fs.ReadFile(fsys, discPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := mdc.Upload(ctx, metadata.UploadRequest{
|
||||
Path: spacePath,
|
||||
Content: b,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identifier := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
|
||||
for _, special := range []string{ReadmeSpecialFolderName, SpaceImageSpecialFolderName} {
|
||||
if special == identifier {
|
||||
opaque = utils.AppendPlainToOpaque(opaque, identifier, res.FileID)
|
||||
break
|
||||
}
|
||||
}
|
||||
return opaque, nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user