Skip to content
Draft
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ require (
github.com/onsi/gomega v1.42.1
github.com/open-policy-agent/opa v1.19.0
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260805104427-6f6c31d4e3fd
github.com/opencloud-eu/reva/v2 v2.48.0
github.com/opensearch-project/opensearch-go/v4 v4.6.0
github.com/orcaman/concurrent-map v1.0.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,8 @@ github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+l
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d h1:JcqGDiyrcaQwVyV861TUyQgO7uEmsjkhfm7aQd84dOw=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260805104427-6f6c31d4e3fd h1:lHtemzGLCFwOFIKKjWO/IaOEkGHjl/6c5u+qH/dsGKg=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260805104427-6f6c31d4e3fd/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU=
github.com/opencloud-eu/reva/v2 v2.48.0 h1:G/4Jbv0DWWOfA5u5DtV0CB75pi9Wwtj7JkJQOEBvErs=
github.com/opencloud-eu/reva/v2 v2.48.0/go.mod h1:ZCo/xQM6if+upZa7rJCmdifZ/Y5XHLCrscHytC39yI4=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
Expand Down
1 change: 0 additions & 1 deletion services/collaboration/pkg/connector/fileconnector.go
Original file line number Diff line number Diff line change
Expand Up @@ -1578,7 +1578,6 @@ func (f *FileConnector) GetAvatar(ctx context.Context, userID string) (*Connecto
}
return nil, NewConnectorError(http.StatusBadGateway, "failed to fetch avatar")
}
defer photoFile.Close()

data, err := io.ReadAll(photoFile)
if err != nil {
Expand Down
63 changes: 63 additions & 0 deletions services/graph/mocks/base_graph_provider.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion services/graph/pkg/service/v0/api_driveitem_permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,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, s.publicBaseURL, statResponse.GetInfo())
item, err := s.cs3ResourceToDriveItem(statResponse.GetInfo())
if err != nil {
return collectionOfPermissions, err
}
Expand Down
22 changes: 13 additions & 9 deletions services/graph/pkg/service/v0/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/share"
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"

Expand Down Expand Up @@ -50,6 +51,16 @@ type BaseGraphService struct {
config *config.Config
availableRoles []*libregraph.UnifiedRoleDefinition
publicBaseURL *url.URL
downloadSigner signedurl.Signer
}

// webURLForResource returns the public web URL pointing at the given resource
// (e.g. https://cloud.example.com/f/<resource-id>), using the pre-parsed
// publicBaseURL held by the service.
func (g BaseGraphService) webURLForResource(rid *storageprovider.ResourceId) *string {
u := *g.publicBaseURL
u.Path = path.Join(u.Path, "f", storagespace.FormatResourceID(rid))
return libregraph.PtrString(u.String())
}

func (g BaseGraphService) getDriveItem(ctx context.Context, ref *storageprovider.Reference) (*libregraph.DriveItem, error) {
Expand All @@ -66,7 +77,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, g.publicBaseURL, res.GetInfo())
return g.cs3ResourceToDriveItem(res.GetInfo())
}

func (g BaseGraphService) CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error) {
Expand Down Expand Up @@ -217,14 +228,6 @@ func (g BaseGraphService) cs3SpacePermissionsToLibreGraph(ctx context.Context, s
}

func (g BaseGraphService) libreGraphPermissionFromCS3PublicShare(createdLink *link.PublicShare) (*libregraph.Permission, error) {
webURL, err := url.Parse(g.config.Spaces.WebDavBase)
if err != nil {
g.logger.Error().
Err(err).
Str("url", g.config.Spaces.WebDavBase).
Msg("failed to parse webURL base url")
return nil, err
}
lt, actions := linktype.SharingLinkTypeFromCS3Permissions(createdLink.GetPermissions())
perm := libregraph.NewPermission()
perm.Id = libregraph.PtrString(createdLink.GetId().GetOpaqueId())
Expand All @@ -235,6 +238,7 @@ func (g BaseGraphService) libreGraphPermissionFromCS3PublicShare(createdLink *li
LibreGraphQuickLink: libregraph.PtrBool(createdLink.GetQuicklink()),
}
perm.LibreGraphPermissionsActions = actions
webURL := *g.publicBaseURL
webURL.Path = path.Join(webURL.Path, "s", createdLink.GetToken())
perm.Link.SetWebUrl(webURL.String())

Expand Down
116 changes: 116 additions & 0 deletions services/graph/pkg/service/v0/driveitem_download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package svc

import (
"errors"
"net/http"
"path"
"strings"
"time"

cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"

"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
)

func (g Graph) GetDriveItemContent(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

driveID, err := parseIDParam(r, "driveID")
if err != nil {
errorcode.RenderError(w, r, err)
return
}
itemID, err := parseIDParam(r, "itemID")
if err != nil {
errorcode.RenderError(w, r, err)
return
}
if driveID.GetStorageId() != itemID.GetStorageId() || driveID.GetSpaceId() != itemID.GetSpaceId() {
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item does not exist")
return
}

user, ok := revactx.ContextGetUser(ctx)
if !ok {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context")
return
}

gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
stat, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &itemID}})
switch {
case err != nil:
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_OK:
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_NOT_FOUND:
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, stat.GetStatus().GetMessage())
return
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_PERMISSION_DENIED:
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, stat.GetStatus().GetMessage())
return
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_UNAUTHENTICATED:
errorcode.Unauthenticated.Render(w, r, http.StatusUnauthorized, stat.GetStatus().GetMessage())
return
default:
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, stat.GetStatus().GetMessage())
return
}

downloadURL, err := g.signedDownloadURL(&itemID, user.GetId().GetOpaqueId())
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}

http.Redirect(w, r, downloadURL, http.StatusFound)
}

func (g BaseGraphService) signedDownloadURL(id *storageprovider.ResourceId, userID string) (string, error) {
if g.downloadSigner == nil {
return "", errors.New("download url signing is not configured")
}
base, err := g.getWebDavBaseURL()
if err != nil {
return "", err
}
base.Path = path.Join(base.Path, storagespace.FormatResourceID(id))
return g.downloadSigner.Sign(base.String(), userID, 30*time.Minute)
}

func shouldSelect(r *http.Request, property string) bool {
for _, v := range strings.Split(r.URL.Query().Get("$select"), ",") {
if strings.TrimSpace(v) == property {
return true
}
}
return false
}

func (g Graph) setDriveItemsDownloadURL(r *http.Request, items []*libregraph.DriveItem, infos []*storageprovider.ResourceInfo) {
if g.downloadSigner == nil || !shouldSelect(r, "@microsoft.graph.downloadUrl") {
return
}
user, ok := revactx.ContextGetUser(r.Context())
if !ok {
return
}
for i := range items {
if items[i].File == nil {
continue
}
u, err := g.signedDownloadURL(infos[i].GetId(), user.GetId().GetOpaqueId())
if err != nil {
continue
}
items[i].MicrosoftGraphDownloadUrl = &u
}
}
32 changes: 16 additions & 16 deletions services/graph/pkg/service/v0/driveitems.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,13 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
return
}

files, err := formatDriveItems(g.logger, g.publicBaseURL, lRes.GetInfos())
files, err := g.formatDriveItems(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())
return
}
g.setDriveItemsDownloadURL(r, files, lRes.GetInfos())

render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: files})
Expand Down Expand Up @@ -269,11 +270,12 @@ 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, g.publicBaseURL, res.GetInfo())
driveItem, err := g.cs3ResourceToDriveItem(res.GetInfo())
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
g.setDriveItemsDownloadURL(r, []*libregraph.DriveItem{driveItem}, []*storageprovider.ResourceInfo{res.GetInfo()})

render.Status(r, http.StatusOK)
render.JSON(w, r, &driveItem)
Expand Down Expand Up @@ -337,11 +339,12 @@ func (g Graph) GetDriveItemChildren(w http.ResponseWriter, r *http.Request) {
return
}

files, err := formatDriveItems(g.logger, g.publicBaseURL, res.GetInfos())
files, err := g.formatDriveItems(res.GetInfos())
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
g.setDriveItemsDownloadURL(r, files, res.GetInfos())

render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: files})
Expand Down Expand Up @@ -385,10 +388,10 @@ func (g Graph) getRemoteItem(ctx context.Context, root *storageprovider.Resource
return item, nil
}

func formatDriveItems(logger *log.Logger, publicBaseURL *url.URL, mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) {
func (g BaseGraphService) formatDriveItems(mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) {
responses := make([]*libregraph.DriveItem, 0, len(mds))
for i := range mds {
res, err := cs3ResourceToDriveItem(logger, publicBaseURL, mds[i])
res, err := g.cs3ResourceToDriveItem(mds[i])
if err != nil {
return nil, err
}
Expand All @@ -402,19 +405,16 @@ func cs3TimestampToTime(t *types.Timestamp) time.Time {
return time.Unix(int64(t.GetSeconds()), int64(t.GetNanos()))
}

func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) {
func (g BaseGraphService) cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) {
size := new(int64)
*size = int64(res.GetSize()) // TODO lurking overflow: make size of libregraph drive item use uint64

driveItem := &libregraph.DriveItem{
Id: libregraph.PtrString(storagespace.FormatResourceID(res.GetId())),
Size: size,
Id: libregraph.PtrString(storagespace.FormatResourceID(res.GetId())),
Size: size,
WebUrl: g.webURLForResource(res.GetId()),
}

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
}
Expand Down Expand Up @@ -453,10 +453,10 @@ func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *sto
}

if res.GetArbitraryMetadata() != nil {
driveItem.Audio = cs3ResourceToDriveItemAudioFacet(logger, res)
driveItem.Image = cs3ResourceToDriveItemImageFacet(logger, res)
driveItem.Location = cs3ResourceToDriveItemLocationFacet(logger, res)
driveItem.Photo = cs3ResourceToDriveItemPhotoFacet(logger, res)
driveItem.Audio = cs3ResourceToDriveItemAudioFacet(g.logger, res)
driveItem.Image = cs3ResourceToDriveItemImageFacet(g.logger, res)
driveItem.Location = cs3ResourceToDriveItemLocationFacet(g.logger, res)
driveItem.Photo = cs3ResourceToDriveItemPhotoFacet(g.logger, res)
}

return driveItem, nil
Expand Down
Loading