Skip to content

Commit

Permalink
Merge pull request #6189 from owncloud/cache-special-drive-items
Browse files Browse the repository at this point in the history
cache special drive items until space root changes
  • Loading branch information
micbar authored May 3, 2023
2 parents f1dbe43 + 4eaa90a commit b354e79
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 40 deletions.
2 changes: 2 additions & 0 deletions services/graph/pkg/config/defaults/defaultconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ func DefaultConfig() *config.Config {
WebDavPath: "/dav/spaces/",
DefaultQuota: "1000000000",
// 30 minutes
ExtendedSpacePropertiesCacheTTL: 1800,
// 30 minutes
GroupsCacheTTL: 1800,
// 30 minutes
UsersCacheTTL: 1800,
Expand Down
119 changes: 91 additions & 28 deletions services/graph/pkg/service/v0/driveitems.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import (
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"golang.org/x/crypto/sha3"
)

// GetRootDriveChildren implements the Service interface.
Expand Down Expand Up @@ -77,18 +79,16 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, &ListResponse{Value: files})
}

func (g Graph) getDriveItem(ctx context.Context, root storageprovider.ResourceId) (*libregraph.DriveItem, error) {
func (g Graph) getDriveItem(ctx context.Context, ref storageprovider.Reference) (*libregraph.DriveItem, error) {
client := g.GetGatewayClient()

ref := &storageprovider.Reference{
ResourceId: &root,
}
res, err := client.Stat(ctx, &storageprovider.StatRequest{Ref: ref})
res, err := client.Stat(ctx, &storageprovider.StatRequest{Ref: &ref})
if err != nil {
return nil, err
}
if res.Status.Code != cs3rpc.Code_CODE_OK {
return nil, fmt.Errorf("could not stat %s: %s", ref, res.Status.Message)
refStr, _ := storagespace.FormatReference(&ref)
return nil, fmt.Errorf("could not stat %s: %s", refStr, res.Status.Message)
}
return cs3ResourceToDriveItem(res.Info)
}
Expand Down Expand Up @@ -215,44 +215,107 @@ func (g Graph) getPathForResource(ctx context.Context, id storageprovider.Resour
return res.Path, err
}

// getExtendedSpaceProperties reads properties from the opaque and transforms them into driveItems
func (g Graph) getExtendedSpaceProperties(ctx context.Context, baseURL *url.URL, space *storageprovider.StorageSpace) []libregraph.DriveItem {
var spaceItems []libregraph.DriveItem
// getSpecialDriveItems reads properties from the opaque and transforms them into driveItems
func (g Graph) getSpecialDriveItems(ctx context.Context, baseURL *url.URL, space *storageprovider.StorageSpace) []libregraph.DriveItem {
if space.GetRoot().GetStorageId() == utils.ShareStorageProviderID {
return nil // no point in stating the ShareStorageProvider
}
if space.Opaque == nil {
return nil
}
metadata := space.Opaque.Map
names := [2]string{SpaceImageSpecialFolderName, ReadmeSpecialFolderName}

for _, itemName := range names {
if itemID, ok := metadata[itemName]; ok {
rid, _ := storagespace.ParseID(string(itemID.Value))
// add the storageID of the space, all drive items of this space belong to the same storageID
rid.StorageId = space.GetRoot().GetStorageId()
spaceItem := g.getSpecialDriveItem(ctx, rid, itemName, baseURL, space)
if spaceItem != nil {
spaceItems = append(spaceItems, *spaceItem)

imageNode := utils.ReadPlainFromOpaque(space.Opaque, SpaceImageSpecialFolderName)
readmeNode := utils.ReadPlainFromOpaque(space.Opaque, ReadmeSpecialFolderName)

cachekey := spaceRootStatKey(space.Root, imageNode, readmeNode)
// if the root is older or equal to our cache we can reuse the cached extended spaces properties
if entry := g.specialDriveItemsCache.Get(cachekey); entry != nil {
if cached, ok := entry.Value().(specialDriveItemEntry); ok {
if cached.rootMtime != nil && space.Mtime != nil {
// beware, LaterTS does not handle equalness. it returns t1 if t1 > t2, else t2, so a >= check looks like this
if utils.LaterTS(space.Mtime, cached.rootMtime) == cached.rootMtime {
return cached.specialDriveItems
}
}
}
}

var spaceItems []libregraph.DriveItem

spaceItems = g.fetchSpecialDriveItem(ctx, spaceItems, SpaceImageSpecialFolderName, imageNode, space, baseURL)
spaceItems = g.fetchSpecialDriveItem(ctx, spaceItems, ReadmeSpecialFolderName, readmeNode, space, baseURL)

// cache properties
spacePropertiesEntry := specialDriveItemEntry{
specialDriveItems: spaceItems,
rootMtime: space.Mtime,
}
g.specialDriveItemsCache.Set(cachekey, spacePropertiesEntry, time.Duration(g.config.Spaces.ExtendedSpacePropertiesCacheTTL))

return spaceItems
}

func (g Graph) getSpecialDriveItem(ctx context.Context, id storageprovider.ResourceId, itemName string, baseURL *url.URL, space *storageprovider.StorageSpace) *libregraph.DriveItem {
func (g Graph) fetchSpecialDriveItem(ctx context.Context, spaceItems []libregraph.DriveItem, itemName string, itemNode string, space *storageprovider.StorageSpace, baseURL *url.URL) []libregraph.DriveItem {
var ref storageprovider.Reference
if itemNode != "" {
rid, _ := storagespace.ParseID(itemNode)

rid.StorageId = space.GetRoot().GetStorageId()
ref = storageprovider.Reference{
ResourceId: &rid,
}
spaceItem := g.getSpecialDriveItem(ctx, ref, itemName, baseURL, space)
if spaceItem != nil {
spaceItems = append(spaceItems, *spaceItem)
}
}
return spaceItems
}

// generates a space root stat cache key used to detect changes in a space
// takes into account the special nodes because changing metadata does not affect the etag / mtime
func spaceRootStatKey(id *storageprovider.ResourceId, imagenode, readmeNode string) string {
if id == nil {
return ""
}
sha3 := sha3.NewShake256()
_, _ = sha3.Write([]byte(id.GetStorageId()))
_, _ = sha3.Write([]byte(id.GetSpaceId()))
_, _ = sha3.Write([]byte(id.GetOpaqueId()))
_, _ = sha3.Write([]byte(imagenode))
_, _ = sha3.Write([]byte(readmeNode))
h := make([]byte, 64)
_, _ = sha3.Read(h)
return fmt.Sprintf("%x", h)
}

type specialDriveItemEntry struct {
specialDriveItems []libregraph.DriveItem
rootMtime *types.Timestamp
}

func (g Graph) getSpecialDriveItem(ctx context.Context, ref storageprovider.Reference, itemName string, baseURL *url.URL, space *storageprovider.StorageSpace) *libregraph.DriveItem {
var spaceItem *libregraph.DriveItem
if id.SpaceId == "" && id.OpaqueId == "" {
if ref.GetResourceId().GetSpaceId() == "" && ref.GetResourceId().GetOpaqueId() == "" {
return nil
}

spaceItem, err := g.getDriveItem(ctx, id)
// FIXME we should send a fieldmask 'path' and return it as the Path property to save an additional call to the storage.
// To do that we need to align the useg of ResourceInfo.Name vs ResourceInfo.Path. By default, only the name should be set
// and Path should always be relative to the space root OR the resource the current user can access ...
spaceItem, err := g.getDriveItem(ctx, ref)
if err != nil {
g.logger.Error().Err(err).Str("ID", id.OpaqueId).Str("name", itemName).Msg("Could not get item info")
g.logger.Debug().Err(err).Str("ID", ref.GetResourceId().GetOpaqueId()).Str("name", itemName).Msg("Could not get item info")
return nil
}
itemPath, err := g.getPathForResource(ctx, id)
if err != nil {
g.logger.Error().Err(err).Str("ID", id.OpaqueId).Str("name", itemName).Msg("Could not get item path")
return nil
itemPath := ref.Path
if itemPath == "" {
// lookup by id
itemPath, err = g.getPathForResource(ctx, *ref.ResourceId)
if err != nil {
g.logger.Debug().Err(err).Str("ID", ref.GetResourceId().GetOpaqueId()).Str("name", itemName).Msg("Could not get item path")
return nil
}
}
spaceItem.SpecialFolder = &libregraph.SpecialFolder{Name: libregraph.PtrString(itemName)}
webdavURL := *baseURL
Expand Down
2 changes: 1 addition & 1 deletion services/graph/pkg/service/v0/drives.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, storageSpaces

// can't access disabled space
if utils.ReadPlainFromOpaque(storageSpace.Opaque, "trashed") != "trashed" {
res.Special = g.getExtendedSpaceProperties(ctx, baseURL, storageSpace)
res.Special = g.getSpecialDriveItems(ctx, baseURL, storageSpace)
quota, err := g.getDriveQuota(ctx, storageSpace)
res.Quota = &quota
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion services/graph/pkg/service/v0/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ type Graph struct {
gatewayClient gateway.GatewayAPIClient
roleService RoleService
permissionsService Permissions
spacePropertiesCache *ttlcache.Cache[string, interface{}]
specialDriveItemsCache *ttlcache.Cache[string, interface{}]
usersCache *ttlcache.Cache[string, libregraph.User]
groupsCache *ttlcache.Cache[string, libregraph.Group]
eventsPublisher events.Publisher
Expand Down
38 changes: 29 additions & 9 deletions services/graph/pkg/service/v0/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ var _ = Describe("Graph", func() {
gatewayClient.On("GetQuota", mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
Status: status.NewUnimplemented(ctx, fmt.Errorf("not supported"), "not supported"),
}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{
Status: status.NewNotFound(ctx, "no special files found"),
}, nil)

r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives?$orderby=name%20asc", nil)
r = r.WithContext(ctx)
Expand Down Expand Up @@ -891,16 +894,30 @@ var _ = Describe("Graph", func() {
Status: status.NewOK(ctx),
Path: "thepath",
}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{
Status: status.NewOK(ctx),
Info: &provider.ResourceInfo{
Id: &provider.ResourceId{
StorageId: "pro-1",
SpaceId: "spaceID",
OpaqueId: "specialID",
// no stat for the image
gatewayClient.On("Stat",
mock.Anything,
mock.MatchedBy(
func(req *provider.StatRequest) bool {
return req.Ref.Path == "/.space/logo.png"
})).
Return(&provider.StatResponse{
Status: status.NewNotFound(ctx, "not found"),
}, nil)
// mock readme stats
gatewayClient.On("Stat",
mock.Anything,
mock.Anything).
Return(&provider.StatResponse{
Status: status.NewOK(ctx),
Info: &provider.ResourceInfo{
Id: &provider.ResourceId{
StorageId: "pro-1",
SpaceId: "spaceID",
OpaqueId: "specialID",
},
},
},
}, nil)
}, nil)
gatewayClient.On("ListStorageSpaces",
mock.Anything,
mock.MatchedBy(
Expand Down Expand Up @@ -994,6 +1011,9 @@ var _ = Describe("Graph", func() {
StorageSpace: req.StorageSpace,
}
}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{
Status: status.NewNotFound(ctx, "no special files found"),
}, nil)

r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/drives/{driveID}/", bytes.NewBuffer(driveJson))
rctx := chi.NewRouteContext()
Expand Down
2 changes: 1 addition & 1 deletion services/graph/pkg/service/v0/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func NewService(opts ...Option) (Graph, error) {
config: options.Config,
mux: m,
logger: &options.Logger,
spacePropertiesCache: spacePropertiesCache,
specialDriveItemsCache: spacePropertiesCache,
usersCache: usersCache,
groupsCache: groupsCache,
eventsPublisher: options.EventsPublisher,
Expand Down

0 comments on commit b354e79

Please sign in to comment.