Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server-Side Space Templates #8723

Merged
merged 9 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/space-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enhancement: Make server side space templates production ready

Fixes several smaller bugs and adds some improvements to space templates, introduced with https://github.com/owncloud/ocis/pull/8558

https://github.com/owncloud/ocis/pull/8723
90 changes: 90 additions & 0 deletions ocis-pkg/l10n/l10n.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// package l10n holds translation mechanics that are used by user facing services (notifications, userlog, graph)
package l10n

import (
"context"
"errors"
"io/fs"
"os"

"github.com/leonelquinteros/gotext"
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
micrometadata "go-micro.dev/v4/metadata"
)

// Template marks a string as translatable
func Template(s string) string { return s }

// Translator is able to translate strings
type Translator struct {
fs fs.FS
defaultLocale string
domain string
}

// NewTranslator creates a Translator with library path and language code and load default domain
func NewTranslator(defaultLocale string, domain string, fsys fs.FS) Translator {
return Translator{
fs: fsys,
defaultLocale: defaultLocale,
domain: domain,
}
}

// NewTranslatorFromCommonConfig creates a new Translator from legacy config
func NewTranslatorFromCommonConfig(defaultLocale string, domain string, path string, fsys fs.FS, fsSubPath string) Translator {
var filesystem fs.FS
if path == "" {
filesystem, _ = fs.Sub(fsys, fsSubPath)
} else { // use custom path instead
filesystem = os.DirFS(path)
}
return NewTranslator(defaultLocale, domain, filesystem)
}

// Translate translates a string to the locale
func (t Translator) Translate(str, locale string) string {
return t.Locale(locale).Get(str)
}

// Locale returns the gotext.Locale, use `.Get` method to translate strings
func (t Translator) Locale(locale string) *gotext.Locale {
l := gotext.NewLocaleFS(locale, t.fs)
l.AddDomain(t.domain) // make domain configurable only if needed
if locale != "en" && len(l.GetTranslations()) == 0 {
l = gotext.NewLocaleFS(t.defaultLocale, t.fs)
l.AddDomain(t.domain) // make domain configurable only if needed
}
return l
}

// MustGetUserLocale returns the locale the user wants to use, omitting errors
func MustGetUserLocale(ctx context.Context, userID string, preferedLang string, vc settingssvc.ValueService) string {
if preferedLang != "" {
return preferedLang
}

locale, _ := GetUserLocale(ctx, userID, vc)
return locale
}

// GetUserLocale returns the locale of the user
func GetUserLocale(ctx context.Context, userID string, vc settingssvc.ValueService) (string, error) {
resp, err := vc.GetValueByUniqueIdentifiers(
micrometadata.Set(ctx, middleware.AccountID, userID),
&settingssvc.GetValueByUniqueIdentifiersRequest{
AccountUuid: userID,
SettingId: defaults.SettingUUIDProfileLanguage,
},
)
if err != nil {
return "", err
}
val := resp.GetValue().GetValue().GetListValue().GetValues()
if len(val) == 0 {
return "", errors.New("no language setting found")
}
return val[0].GetStringValue(), nil
}
25 changes: 25 additions & 0 deletions services/graph/Makefile
Original file line number Diff line number Diff line change
@@ -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 ############
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions services/graph/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
37 changes: 26 additions & 11 deletions services/graph/pkg/service/v0/drives.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -326,15 +327,18 @@ 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")
return
}

// 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.
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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).
Expand Down
6 changes: 6 additions & 0 deletions services/graph/pkg/service/v0/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 9 additions & 0 deletions services/graph/pkg/service/v0/l10n/.tx/config
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>\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 ""

6 changes: 0 additions & 6 deletions services/graph/pkg/service/v0/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package svc
import (
"crypto/tls"
"crypto/x509"
"embed"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -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)
Expand Down
Binary file not shown.

This file was deleted.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading