refactor: move implementation to fileconnector

This commit is contained in:
Michael Barz
2026-04-14 17:46:45 +02:00
parent 864e20028f
commit d05db011e5
4 changed files with 155 additions and 47 deletions

View File

@@ -3,5 +3,3 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT
go 1.23.4
require github.com/gosexy/gettext v0.0.0-20160830220431-74466a0a0c4a // go-xgettext
require github.com/jessevdk/go-flags v1.6.1 // indirect

View File

@@ -169,6 +169,74 @@ func (_c *FileConnectorService_DeleteFile_Call) RunAndReturn(run func(ctx contex
return _c
}
// GetAvatar provides a mock function for the type FileConnectorService
func (_mock *FileConnectorService) GetAvatar(ctx context.Context, userID string) (*connector.ConnectorResponse, error) {
ret := _mock.Called(ctx, userID)
if len(ret) == 0 {
panic("no return value specified for GetAvatar")
}
var r0 *connector.ConnectorResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*connector.ConnectorResponse, error)); ok {
return returnFunc(ctx, userID)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) *connector.ConnectorResponse); ok {
r0 = returnFunc(ctx, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connector.ConnectorResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FileConnectorService_GetAvatar_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAvatar'
type FileConnectorService_GetAvatar_Call struct {
*mock.Call
}
// GetAvatar is a helper method to define mock.On call
// - ctx context.Context
// - userID string
func (_e *FileConnectorService_Expecter) GetAvatar(ctx interface{}, userID interface{}) *FileConnectorService_GetAvatar_Call {
return &FileConnectorService_GetAvatar_Call{Call: _e.mock.On("GetAvatar", ctx, userID)}
}
func (_c *FileConnectorService_GetAvatar_Call) Run(run func(ctx context.Context, userID string)) *FileConnectorService_GetAvatar_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *FileConnectorService_GetAvatar_Call) Return(connectorResponse *connector.ConnectorResponse, err error) *FileConnectorService_GetAvatar_Call {
_c.Call.Return(connectorResponse, err)
return _c
}
func (_c *FileConnectorService_GetAvatar_Call) RunAndReturn(run func(ctx context.Context, userID string) (*connector.ConnectorResponse, error)) *FileConnectorService_GetAvatar_Call {
_c.Call.Return(run)
return _c
}
// GetLock provides a mock function for the type FileConnectorService
func (_mock *FileConnectorService) GetLock(ctx context.Context) (*connector.ConnectorResponse, error) {
ret := _mock.Called(ctx)

View File

@@ -8,6 +8,7 @@ import (
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
@@ -94,6 +95,10 @@ type FileConnectorService interface {
// In case of conflict, this method will return the actual lockId in
// the file as second return value.
RenameFile(ctx context.Context, lockID, target string) (*ConnectorResponse, error)
// GetAvatar fetches the user's avatar image from the Graph API.
// The response Body contains the raw image bytes ([]byte) and the
// Headers contain Content-Type and Cache-Control.
GetAvatar(ctx context.Context, userID string) (*ConnectorResponse, error)
}
// FileConnector implements the "File" endpoint.
@@ -1539,3 +1544,61 @@ func (f *FileConnector) getScopeByKeyPrefix(scopes map[string]*auth.Scope, keyPr
}
return fmt.Errorf("scope %s not found", keyPrefix)
}
// GetAvatar fetches the user's avatar image from the Graph API.
// The returned ConnectorResponse carries the raw image bytes in Body ([]byte)
// and Content-Type / Cache-Control in Headers.
func (f *FileConnector) GetAvatar(ctx context.Context, userID string) (*ConnectorResponse, error) {
logger := zerolog.Ctx(ctx)
wopiContext, err := middleware.WopiContextFromCtx(ctx)
if err != nil {
logger.Error().Err(err).Msg("GetAvatar: missing WOPI context")
return nil, NewConnectorError(http.StatusUnauthorized, "missing WOPI context")
}
avatarURL := f.cfg.CS3Api.GraphEndpoint + "/v1.0/users/" + userID + "/photo/$value"
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, avatarURL, nil)
if err != nil {
logger.Error().Err(err).Msg("GetAvatar: failed to create request")
return nil, NewConnectorError(http.StatusInternalServerError, "failed to create request")
}
httpReq.Header.Set(ctxpkg.TokenHeader, wopiContext.AccessToken)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
logger.Error().Err(err).Msg("GetAvatar: failed to fetch avatar from Graph API")
return nil, NewConnectorError(http.StatusBadGateway, "failed to fetch avatar")
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
logger.Warn().Int("status", httpResp.StatusCode).Msg("GetAvatar: Graph API returned non-200")
return nil, NewConnectorError(httpResp.StatusCode, http.StatusText(httpResp.StatusCode))
}
data, err := io.ReadAll(httpResp.Body)
if err != nil {
logger.Error().Err(err).Msg("GetAvatar: failed to read avatar body")
return nil, NewConnectorError(http.StatusInternalServerError, "failed to read avatar body")
}
headers := map[string]string{
"Cache-Control": "public, max-age=300",
}
if ct := httpResp.Header.Get("Content-Type"); ct != "" {
headers["Content-Type"] = ct
}
if cl := httpResp.Header.Get("Content-Length"); cl != "" {
headers["Content-Length"] = cl
}
return &ConnectorResponse{
Status: http.StatusOK,
Headers: headers,
Body: data,
}, nil
}

View File

@@ -3,7 +3,6 @@ package connector
import (
"encoding/json"
"errors"
"io"
"net/http"
"strconv"
@@ -13,8 +12,6 @@ import (
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/config"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/connector/utf7"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/locks"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/middleware"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/rs/zerolog"
microstore "go-micro.dev/v4/store"
@@ -46,14 +43,12 @@ const (
type HttpAdapter struct {
con ConnectorService
locks locks.LockParser
cfg *config.Config
}
// NewHttpAdapter will create a new HTTP adapter. A new connector using the
// provided gateway API client and configuration will be used in the adapter
func NewHttpAdapter(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config, st microstore.Store) *HttpAdapter {
httpAdapter := &HttpAdapter{
cfg: cfg,
con: NewConnector(
NewFileConnector(gws, cfg, st),
NewContentConnector(gws, cfg),
@@ -312,11 +307,6 @@ func (h *HttpAdapter) RenameFile(w http.ResponseWriter, r *http.Request) {
// The WOPI token in the query string provides authentication (validated by
// the WopiContextAuthMiddleware). Collabora loads avatars via img.src which
// is a plain browser GET — it cannot send auth headers.
//
// Internally, the handler calls the Graph service directly (bypassing the
// proxy) and authenticates with the reva access token via the x-access-token
// header — the same mechanism the proxy uses when forwarding requests to
// backend services.
func (h *HttpAdapter) GetAvatar(w http.ResponseWriter, r *http.Request) {
logger := zerolog.Ctx(r.Context())
userID := chi.URLParam(r, "userID")
@@ -325,47 +315,36 @@ func (h *HttpAdapter) GetAvatar(w http.ResponseWriter, r *http.Request) {
return
}
wopiContext, err := middleware.WopiContextFromCtx(r.Context())
fileCon := h.con.GetFileConnector()
response, err := fileCon.GetAvatar(r.Context(), userID)
if err != nil {
logger.Error().Err(err).Msg("GetAvatar: missing WOPI context")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
var connErr *ConnectorError
if errors.As(err, &connErr) {
http.Error(w, http.StatusText(connErr.HttpCodeOut), connErr.HttpCodeOut)
} else {
logger.Error().Err(err).Msg("GetAvatar: failed to fetch avatar")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
h.writeConnectorAvatarResponse(w, r, response)
}
// Build the internal Graph API URL for the user's photo.
// We call the Graph service directly (not through the proxy) so we can
// authenticate with the reva token via x-access-token.
graphEndpoint := h.cfg.CS3Api.GraphEndpoint
avatarURL := graphEndpoint + "/v1.0/users/" + userID + "/photo/$value"
httpReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, avatarURL, nil)
func (h *HttpAdapter) writeConnectorAvatarResponse(w http.ResponseWriter, r *http.Request, response *ConnectorResponse) {
data, _ := response.Body.([]byte)
for key, value := range response.Headers {
w.Header().Set(key, value)
}
w.WriteHeader(response.Status)
written, err := w.Write(data)
if err != nil {
logger.Error().Err(err).Msg("GetAvatar: failed to create request")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
logger := zerolog.Ctx(r.Context())
logger.Error().
Err(err).
Int("TotalBytes", len(data)).
Int("WrittenBytes", written).
Msg("failed to write avatar contents in the HTTP response")
}
httpReq.Header.Set(revactx.TokenHeader, wopiContext.AccessToken)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
logger.Error().Err(err).Msg("GetAvatar: failed to fetch avatar from Graph API")
http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
logger.Warn().Int("status", httpResp.StatusCode).Msg("GetAvatar: Graph API returned non-200")
http.Error(w, http.StatusText(httpResp.StatusCode), httpResp.StatusCode)
return
}
if ct := httpResp.Header.Get("Content-Type"); ct != "" {
w.Header().Set("Content-Type", ct)
}
w.Header().Set("Cache-Control", "public, max-age=300")
w.WriteHeader(http.StatusOK)
io.Copy(w, httpResp.Body)
}
func (h *HttpAdapter) writeConnectorResponse(w http.ResponseWriter, r *http.Request, response *ConnectorResponse) {