Merge pull request #2744 from opencloud-eu/feat/driveitem-weburl

feat(graph): populate driveItem.webUrl per Libre Graph spec
This commit is contained in:
Dominik Schmidt
2026-05-12 20:50:11 +02:00
committed by GitHub
6 changed files with 71 additions and 9 deletions

View File

@@ -3,6 +3,7 @@ package svc
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"slices"
@@ -91,6 +92,10 @@ type ListPermissionsQueryOptions struct {
// NewDriveItemPermissionsService creates a new DriveItemPermissionsService
func NewDriveItemPermissionsService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], identityCache cache.IdentityCache, config *config.Config) (DriveItemPermissionsService, error) {
publicBaseURL, err := url.Parse(config.Spaces.WebDavBase)
if err != nil {
return DriveItemPermissionsService{}, fmt.Errorf("could not parse graph.spaces.webdav_base: %w", err)
}
return DriveItemPermissionsService{
BaseGraphService: BaseGraphService{
logger: &log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemService").Logger()},
@@ -98,6 +103,7 @@ func NewDriveItemPermissionsService(logger log.Logger, gatewaySelector pool.Sele
identityCache: identityCache,
config: config,
availableRoles: unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(config.UnifiedRoles.AvailableRoles...)),
publicBaseURL: publicBaseURL,
},
}, nil
}
@@ -405,7 +411,7 @@ func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID
driveItems := make(driveItemsByResourceID, 1)
// we can use the statResponse to build the drive item before fetching the shares
item, err := cs3ResourceToDriveItem(s.logger, statResponse.GetInfo())
item, err := cs3ResourceToDriveItem(s.logger, s.publicBaseURL, statResponse.GetInfo())
if err != nil {
return collectionOfPermissions, err
}

View File

@@ -49,6 +49,7 @@ type BaseGraphService struct {
identityCache cache.IdentityCache
config *config.Config
availableRoles []*libregraph.UnifiedRoleDefinition
publicBaseURL *url.URL
}
func (g BaseGraphService) getSpaceRootPermissions(ctx context.Context, spaceID *storageprovider.StorageSpaceId, countOnly bool) ([]libregraph.Permission, int, error) {
@@ -81,7 +82,7 @@ func (g BaseGraphService) getDriveItem(ctx context.Context, ref *storageprovider
refStr, _ := storagespace.FormatReference(ref)
return nil, fmt.Errorf("could not stat %s: %s", refStr, res.GetStatus().GetMessage())
}
return cs3ResourceToDriveItem(g.logger, res.GetInfo())
return cs3ResourceToDriveItem(g.logger, g.publicBaseURL, res.GetInfo())
}
func (g BaseGraphService) CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error) {

View File

@@ -206,7 +206,7 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
return
}
files, err := formatDriveItems(g.logger, lRes.GetInfos())
files, err := formatDriveItems(g.logger, g.publicBaseURL, lRes.GetInfos())
if err != nil {
g.logger.Error().Err(err).Msg("error encoding response as json")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
@@ -271,7 +271,7 @@ func (g Graph) GetDriveItem(w http.ResponseWriter, r *http.Request) {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.GetStatus().GetMessage())
return
}
driveItem, err := cs3ResourceToDriveItem(g.logger, res.GetInfo())
driveItem, err := cs3ResourceToDriveItem(g.logger, g.publicBaseURL, res.GetInfo())
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
@@ -339,7 +339,7 @@ func (g Graph) GetDriveItemChildren(w http.ResponseWriter, r *http.Request) {
return
}
files, err := formatDriveItems(g.logger, res.GetInfos())
files, err := formatDriveItems(g.logger, g.publicBaseURL, res.GetInfos())
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
@@ -416,10 +416,10 @@ func (g Graph) getRemoteItem(ctx context.Context, root *storageprovider.Resource
return item, nil
}
func formatDriveItems(logger *log.Logger, mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) {
func formatDriveItems(logger *log.Logger, publicBaseURL *url.URL, mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) {
responses := make([]*libregraph.DriveItem, 0, len(mds))
for i := range mds {
res, err := cs3ResourceToDriveItem(logger, mds[i])
res, err := cs3ResourceToDriveItem(logger, publicBaseURL, mds[i])
if err != nil {
return nil, err
}
@@ -433,7 +433,7 @@ func cs3TimestampToTime(t *types.Timestamp) time.Time {
return time.Unix(int64(t.GetSeconds()), int64(t.GetNanos()))
}
func cs3ResourceToDriveItem(logger *log.Logger, res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) {
func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) {
size := new(int64)
*size = int64(res.GetSize()) // TODO lurking overflow: make size of libregraph drive item use uint64
@@ -442,6 +442,10 @@ func cs3ResourceToDriveItem(logger *log.Logger, res *storageprovider.ResourceInf
Size: size,
}
webURL := *publicBaseURL
webURL.Path = path.Join(webURL.Path, "f", storagespace.FormatResourceID(res.GetId()))
driveItem.WebUrl = libregraph.PtrString(webURL.String())
if name := path.Base(res.GetPath()); name != "" {
driveItem.Name = &name
}

View File

@@ -0,0 +1,44 @@
package svc
import (
"net/url"
"testing"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/opencloud-eu/opencloud/pkg/log"
)
func TestCS3ResourceToDriveItemPopulatesWebUrl(t *testing.T) {
logger := log.NewLogger()
res := &provider.ResourceInfo{
Id: &provider.ResourceId{
StorageId: "storage-1",
SpaceId: "space-1",
OpaqueId: "item-1",
},
Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER,
}
t.Run("public base URL without path", func(t *testing.T) {
base, err := url.Parse("https://example.com")
require.NoError(t, err)
item, err := cs3ResourceToDriveItem(&logger, base, res)
require.NoError(t, err)
require.NotNil(t, item.WebUrl)
assert.Equal(t, "https://example.com/f/storage-1$space-1%21item-1", *item.WebUrl)
})
t.Run("public base URL with path prefix", func(t *testing.T) {
base, err := url.Parse("https://example.com/cloud")
require.NoError(t, err)
item, err := cs3ResourceToDriveItem(&logger, base, res)
require.NoError(t, err)
require.NotNil(t, item.WebUrl)
assert.Equal(t, "https://example.com/cloud/f/storage-1$space-1%21item-1", *item.WebUrl)
})
}

View File

@@ -94,7 +94,7 @@ func (g Graph) FollowDriveItem(w http.ResponseWriter, r *http.Request) {
}
}
driveItem, err := cs3ResourceToDriveItem(g.logger, statRes.GetInfo())
driveItem, err := cs3ResourceToDriveItem(g.logger, g.publicBaseURL, statRes.GetInfo())
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return

View File

@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"time"
@@ -154,12 +155,18 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
cache.IdentityCacheWithGroupsTTL(time.Duration(options.Config.Spaces.GroupsCacheTTL)),
)
publicBaseURL, err := url.Parse(options.Config.Spaces.WebDavBase)
if err != nil {
return Graph{}, fmt.Errorf("could not parse graph.spaces.webdav_base: %w", err)
}
baseGraphService := BaseGraphService{
logger: &options.Logger,
identityCache: identityCache,
gatewaySelector: options.GatewaySelector,
config: options.Config,
availableRoles: unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(options.Config.UnifiedRoles.AvailableRoles...)),
publicBaseURL: publicBaseURL,
}
drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector)