mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-04-18 14:27:50 -04:00
refactor: move implementation to fileconnector
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user