refactor: move wopi operation into connector and change logging

This commit is contained in:
Juan Pablo Villafáñez
2024-03-20 10:14:32 +01:00
parent 8f4806f1d4
commit aa58caef63
16 changed files with 1066 additions and 1165 deletions

View File

@@ -7,11 +7,12 @@ import (
"github.com/oklog/run"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config/parser"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/connector"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/cs3wopiserver"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/logging"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/server/grpc"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/server/http"
"github.com/urfave/cli/v2"
@@ -27,7 +28,13 @@ func Server(cfg *config.Config) *cli.Command {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
logger := log.NewLogger(
log.Name(cfg.Service.Name),
log.Level(cfg.Log.Level),
log.Pretty(cfg.Log.Pretty),
log.Color(cfg.Log.Color),
log.File(cfg.Log.File),
)
traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name)
if err != nil {
return err
@@ -92,7 +99,7 @@ func Server(cfg *config.Config) *cli.Command {
})
*/
server, err := http.Server(
http.App(app),
http.Adapter(connector.NewHttpAdapter(app.GetGwc(), cfg)),
http.Logger(logger),
http.Config(cfg),
http.Context(ctx),

View File

@@ -0,0 +1,37 @@
package connector
type ConnectorError struct {
HttpCodeOut int
Msg string
}
func (e *ConnectorError) Error() string {
return e.Msg
}
func NewConnectorError(code int, msg string) *ConnectorError {
return &ConnectorError{
HttpCodeOut: code,
Msg: msg,
}
}
type Connector struct {
fileConnector *FileConnector
contentConnector *ContentConnector
}
func NewConnector(fc *FileConnector, cc *ContentConnector) *Connector {
return &Connector{
fileConnector: fc,
contentConnector: cc,
}
}
func (c *Connector) GetFileConnector() *FileConnector {
return c.fileConnector
}
func (c *Connector) GetContentConnector() *ContentConnector {
return c.contentConnector
}

View File

@@ -0,0 +1,312 @@
package connector
import (
"bytes"
"context"
"crypto/tls"
"io"
"net/http"
"strconv"
"time"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/app"
"github.com/rs/zerolog"
)
type ContentConnector struct {
gwc gatewayv1beta1.GatewayAPIClient
cfg *config.Config
}
func NewContentConnector(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config) *ContentConnector {
return &ContentConnector{
gwc: gwc,
cfg: cfg,
}
}
// GetFile downloads the file from the storage
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/getfile
func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error {
wopiContext, err := app.WopiContextFromCtx(ctx)
if err != nil {
return err
}
logger := zerolog.Ctx(ctx)
// Initiate download request
req := &providerv1beta1.InitiateFileDownloadRequest{
Ref: &wopiContext.FileReference,
}
resp, err := c.gwc.InitiateFileDownload(ctx, req)
if err != nil {
logger.Error().Err(err).Msg("GetFile: InitiateFileDownload failed")
return err
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("GetFile: InitiateFileDownload failed with wrong status")
return NewConnectorError(500, resp.Status.GetCode().String()+" "+resp.Status.GetMessage())
}
// Figure out the download endpoint and download token
downloadEndpoint := ""
downloadToken := ""
hasDownloadToken := false
for _, proto := range resp.Protocols {
if proto.Protocol == "simple" || proto.Protocol == "spaces" {
downloadEndpoint = proto.DownloadEndpoint
downloadToken = proto.Token
hasDownloadToken = proto.Token != ""
break
}
}
if downloadEndpoint == "" {
logger.Error().
Str("Endpoint", downloadEndpoint).
Bool("HasDownloadToken", hasDownloadToken).
Msg("GetFile: Download endpoint or token is missing")
return NewConnectorError(500, "GetFile: Download endpoint is missing")
}
httpClient := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: c.cfg.CS3Api.DataGateway.Insecure,
},
},
}
// Prepare the request to download the file
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadEndpoint, bytes.NewReader([]byte("")))
if err != nil {
logger.Error().
Err(err).
Str("Endpoint", downloadEndpoint).
Bool("HasDownloadToken", hasDownloadToken).
Msg("GetFile: Could not create the request to the endpoint")
return err
}
if downloadToken != "" {
// public link downloads have the token in the download endpoint
httpReq.Header.Add("X-Reva-Transfer", downloadToken)
}
// TODO: the access token shouldn't be needed
httpReq.Header.Add("X-Access-Token", wopiContext.AccessToken)
httpResp, err := httpClient.Do(httpReq)
if err != nil {
logger.Error().
Err(err).
Str("Endpoint", downloadEndpoint).
Bool("HasDownloadToken", hasDownloadToken).
Msg("GetFile: Get request to the download endpoint failed")
return err
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
logger.Error().
Err(err).
Int("HttpCode", httpResp.StatusCode).
Msg("GetFile: downloading the file failed")
return NewConnectorError(500, "GetFile: Downloading the file failed")
}
// Copy the download into the writer
_, err = io.Copy(writer, httpResp.Body)
if err != nil {
logger.Error().Msg("GetFile: copying the file content to the response body failed")
return err
}
logger.Debug().Msg("GetFile: success")
return nil
}
// PutFile uploads the file to the storage
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/putfile
func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, streamLength int64, lockID string) (string, error) {
wopiContext, err := app.WopiContextFromCtx(ctx)
if err != nil {
return "", err
}
logger := zerolog.Ctx(ctx).With().
Str("RequestedLockID", lockID).
Int64("UploadLength", streamLength).
Logger()
// We need a stat call on the target file in order to get both the lock
// (if any) and the current size of the file
statRes, err := c.gwc.Stat(ctx, &providerv1beta1.StatRequest{
Ref: &wopiContext.FileReference,
})
if err != nil {
logger.Error().Err(err).Msg("PutFile: stat failed")
return "", err
}
if statRes.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("StatusCode", statRes.Status.Code.String()).
Str("StatusMsg", statRes.Status.Message).
Msg("PutFile: stat failed with unexpected status")
return "", NewConnectorError(500, statRes.Status.GetCode().String()+" "+statRes.Status.GetMessage())
}
// If there is a lock and it mismatches, return 409
if statRes.Info.Lock != nil && statRes.Info.Lock.LockId != lockID {
logger.Error().
Str("LockID", statRes.Info.Lock.LockId).
Msg("PutFile: wrong lock")
// onlyoffice says it's required to send the current lockId, MS doesn't say anything
return statRes.Info.Lock.LockId, NewConnectorError(409, "Wrong lock")
}
// only unlocked uploads can go through if the target file is empty,
// otherwise the X-WOPI-Lock header is required even if there is no lock on the file
// This is part of the onlyoffice documentation (https://api.onlyoffice.com/editors/wopi/restapi/putfile)
// Wopivalidator fails some tests if we don't also check for the X-WOPI-Lock header.
if lockID == "" && statRes.Info.Lock == nil && statRes.Info.Size > 0 {
logger.Error().Msg("PutFile: file must be locked first")
// onlyoffice says to send an empty string if the file is unlocked, MS doesn't say anything
return "", NewConnectorError(409, "File must be locked first")
}
// Prepare the data to initiate the upload
opaque := &types.Opaque{
Map: make(map[string]*types.OpaqueEntry),
}
if streamLength >= 0 {
opaque.Map["Upload-Length"] = &types.OpaqueEntry{
Decoder: "plain",
Value: []byte(strconv.FormatInt(streamLength, 10)),
}
}
req := &providerv1beta1.InitiateFileUploadRequest{
Opaque: opaque,
Ref: &wopiContext.FileReference,
LockId: lockID,
// TODO: if-match
//Options: &providerv1beta1.InitiateFileUploadRequest_IfMatch{
// IfMatch: "",
//},
}
// Initiate the upload request
resp, err := c.gwc.InitiateFileUpload(ctx, req)
if err != nil {
logger.Error().Err(err).Msg("UploadHelper: InitiateFileUpload failed")
return "", err
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("UploadHelper: InitiateFileUpload failed with wrong status")
return "", NewConnectorError(500, resp.Status.GetCode().String()+" "+resp.Status.GetMessage())
}
// if the content length is greater than 0, we need to upload the content to the
// target endpoint, otherwise we're done
if streamLength > 0 {
uploadEndpoint := ""
uploadToken := ""
hasUploadToken := false
for _, proto := range resp.Protocols {
if proto.Protocol == "simple" || proto.Protocol == "spaces" {
uploadEndpoint = proto.UploadEndpoint
uploadToken = proto.Token
hasUploadToken = proto.Token != ""
break
}
}
if uploadEndpoint == "" {
logger.Error().
Str("Endpoint", uploadEndpoint).
Bool("HasUploadToken", hasUploadToken).
Msg("UploadHelper: Upload endpoint or token is missing")
return "", NewConnectorError(500, "upload endpoint or token is missing")
}
httpClient := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: c.cfg.CS3Api.DataGateway.Insecure,
},
},
Timeout: 10 * time.Second,
}
// prepare the request to upload the contents to the upload endpoint
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadEndpoint, stream)
if err != nil {
logger.Error().
Err(err).
Str("Endpoint", uploadEndpoint).
Bool("HasUploadToken", hasUploadToken).
Msg("UploadHelper: Could not create the request to the endpoint")
return "", err
}
// "stream" is an *http.body and doesn't fill the httpReq.ContentLength automatically
// we need to fill the ContentLength ourselves, and must match the stream length in order
// to prevent issues
httpReq.ContentLength = streamLength
if uploadToken != "" {
// public link uploads have the token in the upload endpoint
httpReq.Header.Add("X-Reva-Transfer", uploadToken)
}
// TODO: the access token shouldn't be needed
httpReq.Header.Add("X-Access-Token", wopiContext.AccessToken)
httpReq.Header.Add("X-Lock-Id", lockID)
// TODO: better mechanism for the upload while locked, relies on patch in REVA
//if lockID, ok := ctxpkg.ContextGetLockID(ctx); ok {
// httpReq.Header.Add("X-Lock-Id", lockID)
//}
httpResp, err := httpClient.Do(httpReq)
if err != nil {
logger.Error().
Err(err).
Str("Endpoint", uploadEndpoint).
Bool("HasUploadToken", hasUploadToken).
Msg("UploadHelper: Put request to the upload endpoint failed")
return "", err
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
logger.Error().
Str("Endpoint", uploadEndpoint).
Bool("HasUploadToken", hasUploadToken).
Int("HttpCode", httpResp.StatusCode).
Msg("UploadHelper: Put request to the upload endpoint failed with unexpected status")
return "", NewConnectorError(500, "PutFile: Uploading the file failed")
}
}
logger.Debug().Msg("PutFile: success")
return "", nil
}

View File

@@ -0,0 +1,480 @@
package connector
import (
"context"
"encoding/hex"
"path"
"time"
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/google/uuid"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/app"
"github.com/rs/zerolog"
)
const (
// WOPI Locks generally have a lock duration of 30 minutes and will be refreshed before expiration if needed
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/concepts#lock
lockDuration time.Duration = 30 * time.Minute
)
type FileConnector struct {
gwc gatewayv1beta1.GatewayAPIClient
cfg *config.Config
}
func NewFileConnector(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config) *FileConnector {
return &FileConnector{
gwc: gwc,
cfg: cfg,
}
}
// GetLock returns a lock or an empty string if no lock exists
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/getlock
func (f *FileConnector) GetLock(ctx context.Context) (string, error) {
wopiContext, err := app.WopiContextFromCtx(ctx)
if err != nil {
return "", err
}
logger := zerolog.Ctx(ctx)
req := &providerv1beta1.GetLockRequest{
Ref: &wopiContext.FileReference,
}
resp, err := f.gwc.GetLock(ctx, req)
if err != nil {
logger.Error().Err(err).Msg("GetLock failed")
return "", err
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("StatusCode", resp.Status.GetCode().String()).
Str("StatusMsg", resp.Status.GetMessage()).
Msg("GetLock failed with unexpected status")
return "", NewConnectorError(404, resp.Status.GetCode().String()+" "+resp.Status.GetMessage())
}
lockID := ""
if resp.Lock != nil {
lockID = resp.Lock.LockId
}
// log the success at debug level
logger.Debug().
Str("LockID", lockID).
Msg("GetLock success")
return lockID, nil
}
// Lock returns a WOPI lock or performs an unlock and relock
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/lock
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/unlockandrelock
func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (string, error) {
wopiContext, err := app.WopiContextFromCtx(ctx)
if err != nil {
return "", err
}
logger := zerolog.Ctx(ctx).With().
Str("RequestedLockID", lockID).
Str("RequestedOldLockID", oldLockID).
Logger()
if lockID == "" {
logger.Error().Msg("Lock failed due to empty lockID")
return "", NewConnectorError(400, "Requested lockID is empty")
}
var setOrRefreshStatus *rpcv1beta1.Status
if oldLockID == "" {
// If the oldLockID is empty, this is a "LOCK" request
req := &providerv1beta1.SetLockRequest{
Ref: &wopiContext.FileReference,
Lock: &providerv1beta1.Lock{
LockId: lockID,
AppName: f.cfg.App.LockName,
Type: providerv1beta1.LockType_LOCK_TYPE_WRITE,
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(time.Now().Add(lockDuration).Unix()),
},
},
}
resp, err := f.gwc.SetLock(ctx, req)
if err != nil {
logger.Error().Err(err).Msg("SetLock failed")
return "", err
}
setOrRefreshStatus = resp.Status
} else {
// If the oldLockID isn't empty, this is a "UnlockAndRelock" request. We'll
// do a "RefreshLock" in reva and provide the old lock
req := &providerv1beta1.RefreshLockRequest{
Ref: &wopiContext.FileReference,
Lock: &providerv1beta1.Lock{
LockId: lockID,
AppName: f.cfg.App.LockName,
Type: providerv1beta1.LockType_LOCK_TYPE_WRITE,
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(time.Now().Add(lockDuration).Unix()),
},
},
ExistingLockId: oldLockID,
}
resp, err := f.gwc.RefreshLock(ctx, req)
if err != nil {
logger.Error().Err(err).Msg("UnlockAndRefresh failed")
return "", err
}
setOrRefreshStatus = resp.Status
}
// we're checking the status of either the "SetLock" or "RefreshLock" operations
switch setOrRefreshStatus.Code {
case rpcv1beta1.Code_CODE_OK:
logger.Debug().Msg("SetLock successful")
return "", nil
case rpcv1beta1.Code_CODE_FAILED_PRECONDITION, rpcv1beta1.Code_CODE_ABORTED:
// Code_CODE_FAILED_PRECONDITION -> Lock operation mismatched lock
// Code_CODE_ABORTED -> UnlockAndRelock operation mismatched lock
// In both cases, we need to get the current lock to return it in a
// 409 response if needed
req := &providerv1beta1.GetLockRequest{
Ref: &wopiContext.FileReference,
}
resp, err := f.gwc.GetLock(ctx, req)
if err != nil {
logger.Error().Err(err).Msg("SetLock failed, fallback to GetLock failed too")
return "", err
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("SetLock failed, fallback to GetLock failed with unexpected status")
}
if resp.Lock != nil {
if resp.Lock.LockId != lockID {
// lockId is different -> return 409 with the current lockId
logger.Warn().
Str("LockID", resp.Lock.LockId).
Msg("SetLock conflict")
return resp.Lock.LockId, NewConnectorError(409, "Lock conflict")
}
// TODO: according to the spec we need to treat this as a RefreshLock
// There was a problem with the lock, but the file has the same lockId now.
// This should never happen unless there are race conditions.
// Since the lockId matches now, we'll assume success for now.
// As said in the todo, we probably should send a "RefreshLock" request here.
logger.Warn().
Str("LockID", resp.Lock.LockId).
Msg("SetLock lock refreshed instead")
return resp.Lock.LockId, nil
}
// TODO: Is this the right error code?
logger.Error().Msg("SetLock failed and could not refresh")
return "", NewConnectorError(404, "Could not refresh the lock")
default:
logger.Error().
Str("StatusCode", setOrRefreshStatus.Code.String()).
Str("StatusMsg", setOrRefreshStatus.Message).
Msg("SetLock failed with unexpected status")
return "", NewConnectorError(500, setOrRefreshStatus.GetCode().String()+" "+setOrRefreshStatus.GetMessage())
}
}
// RefreshLock refreshes a provided lock for 30 minutes
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/refreshlock
func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (string, error) {
wopiContext, err := app.WopiContextFromCtx(ctx)
if err != nil {
return "", err
}
logger := zerolog.Ctx(ctx).With().
Str("RequestedLockID", lockID).
Logger()
if lockID == "" {
logger.Error().Msg("RefreshLock failed due to empty lockID")
return "", NewConnectorError(400, "Requested lockID is empty")
}
req := &providerv1beta1.RefreshLockRequest{
Ref: &wopiContext.FileReference,
Lock: &providerv1beta1.Lock{
LockId: lockID,
AppName: f.cfg.App.LockName,
Type: providerv1beta1.LockType_LOCK_TYPE_WRITE,
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(time.Now().Add(lockDuration).Unix()),
},
},
}
resp, err := f.gwc.RefreshLock(ctx, req)
if err != nil {
logger.Error().Err(err).Msg("RefreshLock failed")
return "", err
}
switch resp.Status.Code {
case rpcv1beta1.Code_CODE_OK:
logger.Debug().Msg("RefreshLock successful")
return "", nil
case rpcv1beta1.Code_CODE_NOT_FOUND:
logger.Error().
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed, file reference not found")
return "", NewConnectorError(404, "File reference not found")
case rpcv1beta1.Code_CODE_ABORTED:
logger.Error().
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed, lock mismatch")
// Either the file is unlocked or there is no lock
// We need to return 409 with the current lock
req := &providerv1beta1.GetLockRequest{
Ref: &wopiContext.FileReference,
}
resp, err := f.gwc.GetLock(ctx, req)
if err != nil {
logger.Error().Err(err).Msg("RefreshLock failed trying to get the current lock")
return "", err
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed, tried to get the current lock failed with unexpected status")
return "", NewConnectorError(500, resp.Status.GetCode().String()+" "+resp.Status.GetMessage())
}
if resp.Lock == nil {
logger.Error().
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed, no lock on file")
return "", NewConnectorError(409, "No lock on file")
} else {
// lock is different than the one requested, otherwise we wouldn't reached this point
logger.Error().
Str("LockID", resp.Lock.LockId).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed, lock mismatch")
return resp.Lock.LockId, NewConnectorError(409, "Lock mismatch")
}
default:
logger.Error().
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed with unexpected status")
return "", NewConnectorError(500, resp.Status.GetCode().String()+" "+resp.Status.GetMessage())
}
}
// UnLock removes a given lock from a file
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/unlock
func (f *FileConnector) UnLock(ctx context.Context, lockID string) (string, error) {
wopiContext, err := app.WopiContextFromCtx(ctx)
if err != nil {
return "", err
}
logger := zerolog.Ctx(ctx).With().
Str("RequestedLockID", lockID).
Logger()
if lockID == "" {
logger.Error().Msg("Unlock failed due to empty lockID")
return "", NewConnectorError(400, "Requested lockID is empty")
}
req := &providerv1beta1.UnlockRequest{
Ref: &wopiContext.FileReference,
Lock: &providerv1beta1.Lock{
LockId: lockID,
AppName: f.cfg.App.LockName,
},
}
resp, err := f.gwc.Unlock(ctx, req)
if err != nil {
logger.Error().Err(err).Msg("Unlock failed")
return "", err
}
switch resp.Status.Code {
case rpcv1beta1.Code_CODE_OK:
logger.Debug().Msg("Unlock successful")
return "", nil
case rpcv1beta1.Code_CODE_ABORTED:
// File isn't locked. Need to return 409 with empty lock
logger.Error().Err(err).Msg("Unlock failed, file isn't locked")
return "", NewConnectorError(409, "File is not locked")
case rpcv1beta1.Code_CODE_LOCKED:
// We need to return 409 with the current lock
req := &providerv1beta1.GetLockRequest{
Ref: &wopiContext.FileReference,
}
resp, err := f.gwc.GetLock(ctx, req)
if err != nil {
logger.Error().Err(err).Msg("Unlock failed trying to get the current lock")
return "", err
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("Unlock failed, tried to get the current lock failed with unexpected status")
return "", NewConnectorError(500, resp.Status.GetCode().String()+" "+resp.Status.GetMessage())
}
var outLockId string
if resp.Lock == nil {
logger.Error().
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("Unlock failed, no lock on file")
outLockId = ""
} else {
// lock is different than the one requested, otherwise we wouldn't reached this point
logger.Error().
Str("LockID", resp.Lock.LockId).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("Unlock failed, lock mismatch")
outLockId = resp.Lock.LockId
}
return outLockId, NewConnectorError(409, "Lock mismatch")
default:
logger.Error().
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("Unlock failed with unexpected status")
return "", NewConnectorError(500, resp.Status.GetCode().String()+" "+resp.Status.GetMessage())
}
}
// CheckFileInfo returns information about the requested file and capabilities of the wopi server
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo
func (f *FileConnector) CheckFileInfo(ctx context.Context) (app.FileInfo, error) {
wopiContext, err := app.WopiContextFromCtx(ctx)
if err != nil {
return app.FileInfo{}, err
}
logger := zerolog.Ctx(ctx)
statRes, err := f.gwc.Stat(ctx, &providerv1beta1.StatRequest{
Ref: &wopiContext.FileReference,
})
if err != nil {
logger.Error().Err(err).Msg("CheckFileInfo: stat failed")
return app.FileInfo{}, err
}
if statRes.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("StatusCode", statRes.Status.Code.String()).
Str("StatusMsg", statRes.Status.Message).
Msg("CheckFileInfo: stat failed with unexpected status")
return app.FileInfo{}, NewConnectorError(500, statRes.Status.GetCode().String()+" "+statRes.Status.GetMessage())
}
fileInfo := app.FileInfo{
// OwnerID must use only alphanumeric chars (https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo/checkfileinfo-response#requirements-for-user-identity-properties)
OwnerID: hex.EncodeToString([]byte(statRes.Info.Owner.OpaqueId + "@" + statRes.Info.Owner.Idp)),
Size: int64(statRes.Info.Size),
Version: statRes.Info.Mtime.String(),
BaseFileName: path.Base(statRes.Info.Path),
BreadcrumbDocName: path.Base(statRes.Info.Path),
// to get the folder we actually need to do a GetPath() request
//BreadcrumbFolderName: path.Dir(statRes.Info.Path),
UserCanNotWriteRelative: true,
HostViewUrl: wopiContext.ViewAppUrl,
HostEditUrl: wopiContext.EditAppUrl,
//EnableOwnerTermination: true, // enable only for collabora? wopivalidator is complaining
EnableOwnerTermination: false,
SupportsExtendedLockLength: true,
SupportsGetLock: true,
SupportsLocks: true,
}
switch wopiContext.ViewMode {
case appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE:
fileInfo.SupportsUpdate = true
fileInfo.UserCanWrite = true
case appproviderv1beta1.ViewMode_VIEW_MODE_READ_ONLY:
// nothing special to do here for now
case appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY:
fileInfo.DisableExport = true
fileInfo.DisableCopy = true
fileInfo.DisablePrint = true
}
// user logic from reva wopi driver #TODO: refactor
var isPublicShare bool = false
if wopiContext.User != nil {
// UserID must use only alphanumeric chars (https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo/checkfileinfo-response#requirements-for-user-identity-properties)
if wopiContext.User.Id.Type == userv1beta1.UserType_USER_TYPE_LIGHTWEIGHT {
fileInfo.UserID = hex.EncodeToString([]byte(statRes.Info.Owner.OpaqueId + "@" + statRes.Info.Owner.Idp))
} else {
fileInfo.UserID = hex.EncodeToString([]byte(wopiContext.User.Id.OpaqueId + "@" + wopiContext.User.Id.Idp))
}
if wopiContext.User.Opaque != nil {
if _, ok := wopiContext.User.Opaque.Map["public-share-role"]; ok {
isPublicShare = true
}
}
if !isPublicShare {
fileInfo.UserFriendlyName = wopiContext.User.Username
fileInfo.UserID = hex.EncodeToString([]byte(wopiContext.User.Id.OpaqueId + "@" + wopiContext.User.Id.Idp))
}
}
if wopiContext.User == nil || isPublicShare {
randomID, _ := uuid.NewUUID()
fileInfo.UserID = hex.EncodeToString([]byte("guest-" + randomID.String()))
fileInfo.UserFriendlyName = "Guest " + randomID.String()
fileInfo.IsAnonymousUser = true
}
logger.Debug().Msg("CheckFileInfo: success")
return fileInfo, nil
}

View File

@@ -0,0 +1,178 @@
package connector
import (
"encoding/json"
"errors"
"net/http"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
"github.com/rs/zerolog"
)
const (
HeaderWopiLock string = "X-WOPI-Lock"
HeaderWopiOldLock string = "X-WOPI-OldLock"
)
type HttpAdapter struct {
con *Connector
}
func NewHttpAdapter(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config) *HttpAdapter {
return &HttpAdapter{
con: NewConnector(
NewFileConnector(gwc, cfg),
NewContentConnector(gwc, cfg),
),
}
}
func (h *HttpAdapter) GetLock(w http.ResponseWriter, r *http.Request) {
fileCon := h.con.GetFileConnector()
lockID, err := fileCon.GetLock(r.Context())
if err != nil {
var conError *ConnectorError
if errors.As(err, &conError) {
http.Error(w, http.StatusText(conError.HttpCodeOut), conError.HttpCodeOut)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
w.Header().Set(HeaderWopiLock, lockID)
}
func (h *HttpAdapter) Lock(w http.ResponseWriter, r *http.Request) {
oldLockID := r.Header.Get(HeaderWopiOldLock)
lockID := r.Header.Get(HeaderWopiLock)
fileCon := h.con.GetFileConnector()
newLockID, err := fileCon.Lock(r.Context(), lockID, oldLockID)
if err != nil {
var conError *ConnectorError
if errors.As(err, &conError) {
if conError.HttpCodeOut == 409 {
w.Header().Set(HeaderWopiLock, newLockID)
}
http.Error(w, http.StatusText(conError.HttpCodeOut), conError.HttpCodeOut)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
// If no error, a HTTP 200 should be sent automatically.
// X-WOPI-Lock header isn't needed on HTTP 200
}
func (h *HttpAdapter) RefreshLock(w http.ResponseWriter, r *http.Request) {
lockID := r.Header.Get(HeaderWopiLock)
fileCon := h.con.GetFileConnector()
newLockID, err := fileCon.RefreshLock(r.Context(), lockID)
if err != nil {
var conError *ConnectorError
if errors.As(err, &conError) {
if conError.HttpCodeOut == 409 {
w.Header().Set(HeaderWopiLock, newLockID)
}
http.Error(w, http.StatusText(conError.HttpCodeOut), conError.HttpCodeOut)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
// If no error, a HTTP 200 should be sent automatically.
// X-WOPI-Lock header isn't needed on HTTP 200
}
func (h *HttpAdapter) UnLock(w http.ResponseWriter, r *http.Request) {
lockID := r.Header.Get(HeaderWopiLock)
fileCon := h.con.GetFileConnector()
newLockID, err := fileCon.UnLock(r.Context(), lockID)
if err != nil {
var conError *ConnectorError
if errors.As(err, &conError) {
if conError.HttpCodeOut == 409 {
w.Header().Set(HeaderWopiLock, newLockID)
}
http.Error(w, http.StatusText(conError.HttpCodeOut), conError.HttpCodeOut)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
// If no error, a HTTP 200 should be sent automatically.
// X-WOPI-Lock header isn't needed on HTTP 200
}
func (h *HttpAdapter) CheckFileInfo(w http.ResponseWriter, r *http.Request) {
fileCon := h.con.GetFileConnector()
fileInfo, err := fileCon.CheckFileInfo(r.Context())
if err != nil {
var conError *ConnectorError
if errors.As(err, &conError) {
http.Error(w, http.StatusText(conError.HttpCodeOut), conError.HttpCodeOut)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
logger := zerolog.Ctx(r.Context())
jsonFileInfo, err := json.Marshal(fileInfo)
if err != nil {
logger.Error().Err(err).Msg("CheckFileInfo: failed to marshal fileinfo")
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
bytes, err := w.Write(jsonFileInfo)
if err != nil {
logger.Error().
Err(err).
Int("TotalBytes", len(jsonFileInfo)).
Int("WrittenBytes", bytes).
Msg("CheckFileInfo: failed to write contents in the HTTP response")
}
}
func (h *HttpAdapter) GetFile(w http.ResponseWriter, r *http.Request) {
contentCon := h.con.GetContentConnector()
err := contentCon.GetFile(r.Context(), w)
if err != nil {
var conError *ConnectorError
if errors.As(err, &conError) {
http.Error(w, http.StatusText(conError.HttpCodeOut), conError.HttpCodeOut)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}
}
func (h *HttpAdapter) PutFile(w http.ResponseWriter, r *http.Request) {
lockID := r.Header.Get(HeaderWopiLock)
contentCon := h.con.GetContentConnector()
newLockID, err := contentCon.PutFile(r.Context(), r.Body, r.ContentLength, lockID)
if err != nil {
var conError *ConnectorError
if errors.As(err, &conError) {
if conError.HttpCodeOut == 409 {
w.Header().Set(HeaderWopiLock, newLockID)
}
http.Error(w, http.StatusText(conError.HttpCodeOut), conError.HttpCodeOut)
} else {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
// If no error, a HTTP 200 should be sent automatically.
// X-WOPI-Lock header isn't needed on HTTP 200
}

View File

@@ -44,6 +44,10 @@ func New(cfg *config.Config, logger log.Logger) (*DemoApp, error) {
return app, nil
}
func (app *DemoApp) GetGwc() gatewayv1beta1.GatewayAPIClient {
return app.gwc
}
func (app *DemoApp) GetCS3apiClient() error {
// establish a connection to the cs3 api endpoint
// in this case a REVA gateway, started by oCIS

View File

@@ -1,6 +0,0 @@
package app
const (
HeaderWopiLock string = "X-WOPI-Lock"
HeaderWopiOldLock string = "X-WOPI-OldLock"
)

View File

@@ -11,6 +11,7 @@ import (
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/golang-jwt/jwt/v4"
"github.com/rs/zerolog"
"google.golang.org/grpc/metadata"
)
@@ -29,7 +30,7 @@ type WopiContext struct {
ViewAppUrl string
}
func WopiContextAuthMiddleware(app *DemoApp, next http.Handler) http.Handler {
func WopiContextAuthMiddleware(jwtSecret string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
accessToken := r.URL.Query().Get("access_token")
if accessToken == "" {
@@ -44,7 +45,7 @@ func WopiContextAuthMiddleware(app *DemoApp, next http.Handler) http.Handler {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(app.Config.JWTSecret), nil
return []byte(jwtSecret), nil
})
if err != nil {
@@ -59,7 +60,7 @@ func WopiContextAuthMiddleware(app *DemoApp, next http.Handler) http.Handler {
ctx := r.Context()
wopiContextAccessToken, err := DecryptAES([]byte(app.Config.JWTSecret), claims.WopiContext.AccessToken)
wopiContextAccessToken, err := DecryptAES([]byte(jwtSecret), claims.WopiContext.AccessToken)
if err != nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
@@ -70,6 +71,17 @@ func WopiContextAuthMiddleware(app *DemoApp, next http.Handler) http.Handler {
// authentication for the CS3 api
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, claims.WopiContext.AccessToken)
// include additional info in the context's logger
// we might need to check https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/common-headers
// although some headers might not be sent depending on the client.
logger := zerolog.Ctx(ctx)
ctx = logger.With().
Str("WopiOverride", r.Header.Get("X-WOPI-Override")).
Str("FileReference", claims.WopiContext.FileReference.String()).
Str("ViewMode", claims.WopiContext.ViewMode.String()).
Str("Requester", claims.WopiContext.User.GetId().String()).
Logger().WithContext(ctx)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -1,169 +0,0 @@
package app
import (
"io"
"net/http"
"strconv"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/helpers"
)
// GetFile downloads the file from the storage
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/getfile
func GetFile(app *DemoApp, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
wopiContext, _ := WopiContextFromCtx(ctx)
// download the file
resp, err := helpers.DownloadFile(
ctx,
&wopiContext.FileReference,
app.gwc,
wopiContext.AccessToken,
app.Config.CS3Api.DataGateway.Insecure,
app.Logger,
)
if err != nil || resp.StatusCode != http.StatusOK {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Int("HttpCode", resp.StatusCode).
Msg("GetFile: downloading the file failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// read the file from the body
defer resp.Body.Close()
_, err = io.Copy(w, resp.Body)
if err != nil {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("GetFile: copying the file content to the response body failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
app.Logger.Debug().
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("GetFile: success")
}
// PutFile uploads the file to the storage
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/putfile
func PutFile(app *DemoApp, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
wopiContext, _ := WopiContextFromCtx(ctx)
// read the file from the body
defer r.Body.Close()
// We need a stat call on the target file in order to get both the lock
// (if any) and the current size of the file
statRes, err := app.gwc.Stat(ctx, &providerv1beta1.StatRequest{
Ref: &wopiContext.FileReference,
})
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", r.Header.Get(HeaderWopiLock)).
Str("UploadLength", strconv.FormatInt(r.ContentLength, 10)).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("PutFile: stat failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if statRes.Status.Code != rpcv1beta1.Code_CODE_OK {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", r.Header.Get(HeaderWopiLock)).
Str("UploadLength", strconv.FormatInt(r.ContentLength, 10)).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", statRes.Status.Code.String()).
Str("StatusMsg", statRes.Status.Message).
Msg("PutFile: stat failed with unexpected status")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// If there is a lock and it mismatches, return 409
if statRes.Info.Lock != nil && statRes.Info.Lock.LockId != r.Header.Get(HeaderWopiLock) {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", r.Header.Get(HeaderWopiLock)).
Str("UploadLength", strconv.FormatInt(r.ContentLength, 10)).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("LockID", statRes.Info.Lock.LockId).
Msg("PutFile: wrong lock")
// onlyoffice says it's required to send the current lockId, MS doesn't say anything
w.Header().Add(HeaderWopiLock, statRes.Info.Lock.LockId)
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
return
}
// only unlocked uploads can go through if the target file is empty,
// otherwise the X-WOPI-Lock header is required even if there is no lock on the file
// This is part of the onlyoffice documentation (https://api.onlyoffice.com/editors/wopi/restapi/putfile)
// Wopivalidator fails some tests if we don't also check for the X-WOPI-Lock header.
if r.Header.Get(HeaderWopiLock) == "" && statRes.Info.Lock == nil && statRes.Info.Size > 0 {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", r.Header.Get(HeaderWopiLock)).
Str("UploadLength", strconv.FormatInt(r.ContentLength, 10)).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("PutFile: file must be locked first")
// onlyoffice says to send an empty string if the file is unlocked, MS doesn't say anything
w.Header().Add(HeaderWopiLock, "")
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
return
}
// upload the file
err = helpers.UploadFile(
ctx,
r.Body,
r.ContentLength,
&wopiContext.FileReference,
app.gwc,
wopiContext.AccessToken,
r.Header.Get(HeaderWopiLock),
app.Config.CS3Api.DataGateway.Insecure,
app.Logger,
)
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", r.Header.Get(HeaderWopiLock)).
Str("UploadLength", strconv.FormatInt(r.ContentLength, 10)).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("PutFile: uploading the file failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
app.Logger.Debug().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", r.Header.Get(HeaderWopiLock)).
Str("UploadLength", strconv.FormatInt(r.ContentLength, 10)).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("PutFile: success")
}

View File

@@ -1,150 +0,0 @@
package app
import (
"encoding/hex"
"encoding/json"
"net/http"
"path"
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/google/uuid"
)
func WopiInfoHandler(app *DemoApp, w http.ResponseWriter, r *http.Request) {
// Logs for this endpoint will be covered by the access log. We can't extract
// more info
http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
}
// CheckFileInfo returns information about the requested file and capabilities of the wopi server
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo
func CheckFileInfo(app *DemoApp, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
wopiContext, _ := WopiContextFromCtx(ctx)
statRes, err := app.gwc.Stat(ctx, &providerv1beta1.StatRequest{
Ref: &wopiContext.FileReference,
})
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("CheckFileInfo: stat failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if statRes.Status.Code != rpcv1beta1.Code_CODE_OK {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", statRes.Status.Code.String()).
Str("StatusMsg", statRes.Status.Message).
Msg("CheckFileInfo: stat failed with unexpected status")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
fileInfo := FileInfo{
// OwnerID must use only alphanumeric chars (https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo/checkfileinfo-response#requirements-for-user-identity-properties)
OwnerID: hex.EncodeToString([]byte(statRes.Info.Owner.OpaqueId + "@" + statRes.Info.Owner.Idp)),
Size: int64(statRes.Info.Size),
Version: statRes.Info.Mtime.String(),
BaseFileName: path.Base(statRes.Info.Path),
BreadcrumbDocName: path.Base(statRes.Info.Path),
// to get the folder we actually need to do a GetPath() request
//BreadcrumbFolderName: path.Dir(statRes.Info.Path),
UserCanNotWriteRelative: true,
HostViewUrl: wopiContext.ViewAppUrl,
HostEditUrl: wopiContext.EditAppUrl,
//EnableOwnerTermination: true, // enable only for collabora? wopivalidator is complaining
EnableOwnerTermination: false,
SupportsExtendedLockLength: true,
SupportsGetLock: true,
SupportsLocks: true,
}
switch wopiContext.ViewMode {
case appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE:
fileInfo.SupportsUpdate = true
fileInfo.UserCanWrite = true
case appproviderv1beta1.ViewMode_VIEW_MODE_READ_ONLY:
// nothing special to do here for now
case appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY:
fileInfo.DisableExport = true
fileInfo.DisableCopy = true
fileInfo.DisablePrint = true
}
// user logic from reva wopi driver #TODO: refactor
var isPublicShare bool = false
if wopiContext.User != nil {
// UserID must use only alphanumeric chars (https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo/checkfileinfo-response#requirements-for-user-identity-properties)
if wopiContext.User.Id.Type == userv1beta1.UserType_USER_TYPE_LIGHTWEIGHT {
fileInfo.UserID = hex.EncodeToString([]byte(statRes.Info.Owner.OpaqueId + "@" + statRes.Info.Owner.Idp))
} else {
fileInfo.UserID = hex.EncodeToString([]byte(wopiContext.User.Id.OpaqueId + "@" + wopiContext.User.Id.Idp))
}
if wopiContext.User.Opaque != nil {
if _, ok := wopiContext.User.Opaque.Map["public-share-role"]; ok {
isPublicShare = true
}
}
if !isPublicShare {
fileInfo.UserFriendlyName = wopiContext.User.Username
fileInfo.UserID = hex.EncodeToString([]byte(wopiContext.User.Id.OpaqueId + "@" + wopiContext.User.Id.Idp))
}
}
if wopiContext.User == nil || isPublicShare {
randomID, _ := uuid.NewUUID()
fileInfo.UserID = hex.EncodeToString([]byte("guest-" + randomID.String()))
fileInfo.UserFriendlyName = "Guest " + randomID.String()
fileInfo.IsAnonymousUser = true
}
jsonFileInfo, err := json.Marshal(fileInfo)
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("CheckFileInfo: failed to marshal fileinfo")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
app.Logger.Debug().
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("CheckFileInfo: success")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
bytes, err := w.Write(jsonFileInfo)
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Int("TotalBytes", len(jsonFileInfo)).
Int("WrittenBytes", bytes).
Msg("CheckFileInfo: failed to write contents in the HTTP response")
}
}

View File

@@ -1,524 +0,0 @@
package app
import (
"net/http"
"time"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
const (
// WOPI Locks generally have a lock duration of 30 minutes and will be refreshed before expiration if needed
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/concepts#lock
lockDuration time.Duration = 30 * time.Minute
)
// GetLock returns a lock or an empty string if no lock exists
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/getlock
func GetLock(app *DemoApp, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
wopiContext, _ := WopiContextFromCtx(ctx)
req := &providerv1beta1.GetLockRequest{
Ref: &wopiContext.FileReference,
}
resp, err := app.gwc.GetLock(ctx, req)
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("GetLock failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("GetLock failed with unexpected status")
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
lockID := ""
if resp.Lock != nil {
lockID = resp.Lock.LockId
}
// log the success at debug level
app.Logger.Debug().
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", resp.Status.Code.String()).
Str("LockID", lockID).
Msg("GetLock success")
w.Header().Set(HeaderWopiLock, lockID)
}
// Lock returns a WOPI lock or performs an unlock and relock
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/lock
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/unlockandrelock
func Lock(app *DemoApp, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
wopiContext, _ := WopiContextFromCtx(ctx)
oldLockID := r.Header.Get(HeaderWopiOldLock)
lockID := r.Header.Get(HeaderWopiLock)
if lockID == "" {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("Lock failed due to empty lockID")
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
var setOrRefreshStatus *rpcv1beta1.Status
if oldLockID == "" {
// If the oldLockID is empty, this is a "LOCK" request
req := &providerv1beta1.SetLockRequest{
Ref: &wopiContext.FileReference,
Lock: &providerv1beta1.Lock{
LockId: lockID,
AppName: app.Config.App.LockName,
Type: providerv1beta1.LockType_LOCK_TYPE_WRITE,
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(time.Now().Add(lockDuration).Unix()),
},
},
}
resp, err := app.gwc.SetLock(ctx, req)
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("SetLock failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
setOrRefreshStatus = resp.Status
} else {
// If the oldLockID isn't empty, this is a "UnlockAndRelock" request. We'll
// do a "RefreshLock" in reva and provide the old lock
req := &providerv1beta1.RefreshLockRequest{
Ref: &wopiContext.FileReference,
Lock: &providerv1beta1.Lock{
LockId: lockID,
AppName: app.Config.App.LockName,
Type: providerv1beta1.LockType_LOCK_TYPE_WRITE,
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(time.Now().Add(lockDuration).Unix()),
},
},
ExistingLockId: oldLockID,
}
resp, err := app.gwc.RefreshLock(ctx, req)
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("RequestedOldLockID", oldLockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("UnlockAndRefresh failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
setOrRefreshStatus = resp.Status
}
// we're checking the status of either the "SetLock" or "RefreshLock" operations
switch setOrRefreshStatus.Code {
case rpcv1beta1.Code_CODE_OK:
app.Logger.Debug().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("SetLock successful")
return
case rpcv1beta1.Code_CODE_FAILED_PRECONDITION, rpcv1beta1.Code_CODE_ABORTED:
// Code_CODE_FAILED_PRECONDITION -> Lock operation mismatched lock
// Code_CODE_ABORTED -> UnlockAndRelock operation mismatched lock
// In both cases, we need to get the current lock to return it in a
// 409 response if needed
req := &providerv1beta1.GetLockRequest{
Ref: &wopiContext.FileReference,
}
resp, err := app.gwc.GetLock(ctx, req)
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("SetLock failed, fallback to GetLock failed too")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("SetLock failed, fallback to GetLock failed with unexpected status")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
if resp.Lock != nil {
if resp.Lock.LockId != lockID {
// lockId is different -> return 409 with the current lockId
app.Logger.Warn().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("LockID", resp.Lock.LockId).
Msg("SetLock conflict")
w.Header().Set(HeaderWopiLock, resp.Lock.LockId)
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
return
}
// TODO: according to the spec we need to treat this as a RefreshLock
// There was a problem with the lock, but the file has the same lockId now.
// This should never happen unless there are race conditions.
// Since the lockId matches now, we'll assume success for now.
// As said in the todo, we probably should send a "RefreshLock" request here.
app.Logger.Warn().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("LockID", resp.Lock.LockId).
Msg("SetLock lock refreshed instead")
return
}
// TODO: Is this the right error code?
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("SetLock failed and could not refresh")
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
default:
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", setOrRefreshStatus.Code.String()).
Str("StatusMsg", setOrRefreshStatus.Message).
Msg("SetLock failed with unexpected status")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
// RefreshLock refreshes a provided lock for 30 minutes
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/refreshlock
func RefreshLock(app *DemoApp, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
wopiContext, _ := WopiContextFromCtx(ctx)
lockID := r.Header.Get(HeaderWopiLock)
if lockID == "" {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("RefreshLock failed due to empty lockID")
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
req := &providerv1beta1.RefreshLockRequest{
Ref: &wopiContext.FileReference,
Lock: &providerv1beta1.Lock{
LockId: lockID,
AppName: app.Config.App.LockName,
Type: providerv1beta1.LockType_LOCK_TYPE_WRITE,
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(time.Now().Add(lockDuration).Unix()),
},
},
}
resp, err := app.gwc.RefreshLock(ctx, req)
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("RefreshLock failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
switch resp.Status.Code {
case rpcv1beta1.Code_CODE_OK:
app.Logger.Debug().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("RefreshLock successful")
return
case rpcv1beta1.Code_CODE_NOT_FOUND:
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed, file reference not found")
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
case rpcv1beta1.Code_CODE_ABORTED:
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed, lock mismatch")
// Either the file is unlocked or there is no lock
// We need to return 409 with the current lock
req := &providerv1beta1.GetLockRequest{
Ref: &wopiContext.FileReference,
}
resp, err := app.gwc.GetLock(ctx, req)
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("RefreshLock failed trying to get the current lock")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed, tried to get the current lock failed with unexpected status")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if resp.Lock == nil {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed, no lock on file")
w.Header().Set(HeaderWopiLock, "")
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
return
} else {
// lock is different than the one requested, otherwise we wouldn't reached this point
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("LockID", resp.Lock.LockId).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed, lock mismatch")
w.Header().Set(HeaderWopiLock, resp.Lock.LockId)
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
return
}
default:
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("RefreshLock failed with unexpected status")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
// UnLock removes a given lock from a file
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/unlock
func UnLock(app *DemoApp, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
wopiContext, _ := WopiContextFromCtx(ctx)
lockID := r.Header.Get(HeaderWopiLock)
if lockID == "" {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("Unlock failed due to empty lockID")
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
req := &providerv1beta1.UnlockRequest{
Ref: &wopiContext.FileReference,
Lock: &providerv1beta1.Lock{
LockId: lockID,
AppName: app.Config.App.LockName,
},
}
resp, err := app.gwc.Unlock(ctx, req)
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("Unlock failed")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
switch resp.Status.Code {
case rpcv1beta1.Code_CODE_OK:
app.Logger.Debug().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("Unlock successful")
return
case rpcv1beta1.Code_CODE_ABORTED:
// File isn't locked. Need to return 409 with empty lock
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("Unlock failed, file isn't locked")
w.Header().Set(HeaderWopiLock, "")
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
return
case rpcv1beta1.Code_CODE_LOCKED:
// We need to return 409 with the current lock
req := &providerv1beta1.GetLockRequest{
Ref: &wopiContext.FileReference,
}
resp, err := app.gwc.GetLock(ctx, req)
if err != nil {
app.Logger.Error().
Err(err).
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Msg("Unlock failed trying to get the current lock")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("Unlock failed, tried to get the current lock failed with unexpected status")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if resp.Lock == nil {
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("Unlock failed, no lock on file")
w.Header().Set(HeaderWopiLock, "")
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
} else {
// lock is different than the one requested, otherwise we wouldn't reached this point
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("LockID", resp.Lock.LockId).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("Unlock failed, lock mismatch")
w.Header().Set(HeaderWopiLock, resp.Lock.LockId)
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
}
return
default:
app.Logger.Error().
Str("FileReference", wopiContext.FileReference.String()).
Str("RequestedLockID", lockID).
Str("ViewMode", wopiContext.ViewMode.String()).
Str("Requester", wopiContext.User.GetId().String()).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("Unlock failed with unexpected status")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}

View File

@@ -1,109 +0,0 @@
package helpers
import (
"bytes"
"context"
"crypto/tls"
"errors"
"net/http"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
func DownloadFile(
ctx context.Context,
ref *providerv1beta1.Reference,
gwc gatewayv1beta1.GatewayAPIClient,
token string,
insecure bool,
logger log.Logger,
) (http.Response, error) {
req := &providerv1beta1.InitiateFileDownloadRequest{
Ref: ref,
}
resp, err := gwc.InitiateFileDownload(ctx, req)
if err != nil {
logger.Error().
Err(err).
Str("FileReference", ref.String()).
Msg("DownloadHelper: InitiateFileDownload failed")
return http.Response{}, err
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("FileReference", ref.String()).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("DownloadHelper: InitiateFileDownload failed with wrong status")
return http.Response{}, errors.New("InitiateFileDownload failed with status " + resp.Status.Code.String())
}
downloadEndpoint := ""
downloadToken := ""
hasDownloadToken := false
for _, proto := range resp.Protocols {
if proto.Protocol == "simple" || proto.Protocol == "spaces" {
downloadEndpoint = proto.DownloadEndpoint
downloadToken = proto.Token
hasDownloadToken = proto.Token != ""
break
}
}
if downloadEndpoint == "" {
logger.Error().
Str("FileReference", ref.String()).
Str("Endpoint", downloadEndpoint).
Bool("HasDownloadToken", hasDownloadToken).
Msg("DownloadHelper: Download endpoint or token is missing")
return http.Response{}, errors.New("download endpoint is missing")
}
httpClient := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
},
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadEndpoint, bytes.NewReader([]byte("")))
if err != nil {
logger.Error().
Err(err).
Str("FileReference", ref.String()).
Str("Endpoint", downloadEndpoint).
Bool("HasDownloadToken", hasDownloadToken).
Msg("DownloadHelper: Could not create the request to the endpoint")
return http.Response{}, err
}
if downloadToken != "" {
// public link downloads have the token in the download endpoint
httpReq.Header.Add("X-Reva-Transfer", downloadToken)
}
// TODO: the access token shouldn't be needed
httpReq.Header.Add("X-Access-Token", token)
// TODO: this needs a refactor to comply with the "bodyclose" linter
// response body is closed in the caller method for now
httpResp, err := httpClient.Do(httpReq) //nolint:bodyclose
if err != nil {
logger.Error().
Err(err).
Str("FileReference", ref.String()).
Str("Endpoint", downloadEndpoint).
Bool("HasDownloadToken", hasDownloadToken).
Msg("DownloadHelper: Get request to the download endpoint failed")
return http.Response{}, err
}
return *httpResp, nil
}

View File

@@ -1,169 +0,0 @@
package helpers
import (
"context"
"crypto/tls"
"errors"
"io"
"net/http"
"strconv"
"time"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
func UploadFile(
ctx context.Context,
content io.Reader, // content won't be closed inside the method
contentLength int64,
ref *providerv1beta1.Reference,
gwc gatewayv1beta1.GatewayAPIClient,
token string,
lockID string,
insecure bool,
logger log.Logger,
) error {
opaque := &types.Opaque{
Map: make(map[string]*types.OpaqueEntry),
}
strContentLength := strconv.FormatInt(contentLength, 10)
if contentLength >= 0 {
opaque.Map["Upload-Length"] = &types.OpaqueEntry{
Decoder: "plain",
Value: []byte(strContentLength),
}
}
req := &providerv1beta1.InitiateFileUploadRequest{
Opaque: opaque,
Ref: ref,
LockId: lockID,
// TODO: if-match
//Options: &providerv1beta1.InitiateFileUploadRequest_IfMatch{
// IfMatch: "",
//},
}
resp, err := gwc.InitiateFileUpload(ctx, req)
if err != nil {
logger.Error().
Err(err).
Str("FileReference", ref.String()).
Str("RequestedLockID", lockID).
Str("UploadLength", strContentLength).
Msg("UploadHelper: InitiateFileUpload failed")
return err
}
if resp.Status.Code != rpcv1beta1.Code_CODE_OK {
logger.Error().
Str("FileReference", ref.String()).
Str("RequestedLockID", lockID).
Str("UploadLength", strContentLength).
Str("StatusCode", resp.Status.Code.String()).
Str("StatusMsg", resp.Status.Message).
Msg("UploadHelper: InitiateFileUpload failed with wrong status")
return errors.New("InitiateFileUpload failed with status " + resp.Status.Code.String())
}
// if content length is 0, we're done. We don't upload anything to the target endpoint
if contentLength == 0 {
return nil
}
uploadEndpoint := ""
uploadToken := ""
hasUploadToken := false
for _, proto := range resp.Protocols {
if proto.Protocol == "simple" || proto.Protocol == "spaces" {
uploadEndpoint = proto.UploadEndpoint
uploadToken = proto.Token
hasUploadToken = proto.Token != ""
break
}
}
if uploadEndpoint == "" {
logger.Error().
Str("FileReference", ref.String()).
Str("RequestedLockID", lockID).
Str("UploadLength", strContentLength).
Str("Endpoint", uploadEndpoint).
Bool("HasUploadToken", hasUploadToken).
Msg("UploadHelper: Upload endpoint or token is missing")
return errors.New("upload endpoint or token is missing")
}
httpClient := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
},
Timeout: 10 * time.Second,
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadEndpoint, content)
if err != nil {
logger.Error().
Err(err).
Str("FileReference", ref.String()).
Str("RequestedLockID", lockID).
Str("UploadLength", strContentLength).
Str("Endpoint", uploadEndpoint).
Bool("HasUploadToken", hasUploadToken).
Msg("UploadHelper: Could not create the request to the endpoint")
return err
}
// "content" is an *http.body and doesn't fill the httpReq.ContentLength automatically
// we need to fill the ContentLength ourselves, and must match the stream length in order
// to prevent issues
httpReq.ContentLength = contentLength
if uploadToken != "" {
// public link uploads have the token in the upload endpoint
httpReq.Header.Add("X-Reva-Transfer", uploadToken)
}
// TODO: the access token shouldn't be needed
httpReq.Header.Add("X-Access-Token", token)
httpReq.Header.Add("X-Lock-Id", lockID)
// TODO: better mechanism for the upload while locked, relies on patch in REVA
//if lockID, ok := ctxpkg.ContextGetLockID(ctx); ok {
// httpReq.Header.Add("X-Lock-Id", lockID)
//}
httpResp, err := httpClient.Do(httpReq)
if err != nil {
logger.Error().
Err(err).
Str("FileReference", ref.String()).
Str("RequestedLockID", lockID).
Str("UploadLength", strContentLength).
Str("Endpoint", uploadEndpoint).
Bool("HasUploadToken", hasUploadToken).
Msg("UploadHelper: Put request to the upload endpoint failed")
return err
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
logger.Error().
Str("FileReference", ref.String()).
Str("RequestedLockID", lockID).
Str("UploadLength", strContentLength).
Str("Endpoint", uploadEndpoint).
Bool("HasUploadToken", hasUploadToken).
Int("HttpCode", httpResp.StatusCode).
Msg("UploadHelper: Put request to the upload endpoint failed with unexpected status")
return errors.New("Put request failed with status " + strconv.Itoa(httpResp.StatusCode))
}
return nil
}

View File

@@ -1,17 +0,0 @@
package logging
import (
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
)
// LoggerFromConfig initializes a service-specific logger instance.
func Configure(name string, cfg *config.Log) log.Logger {
return log.NewLogger(
log.Name(name),
log.Level(cfg.Level),
log.Pretty(cfg.Pretty),
log.Color(cfg.Color),
log.File(cfg.File),
)
}

View File

@@ -5,7 +5,7 @@ import (
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/internal/app"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/connector"
"go.opentelemetry.io/otel/trace"
)
@@ -14,7 +14,7 @@ type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
App *app.DemoApp
Adapter *connector.HttpAdapter
Logger log.Logger
Context context.Context
Config *config.Config
@@ -33,9 +33,9 @@ func newOptions(opts ...Option) Options {
}
// App provides a function to set the logger option.
func App(val *app.DemoApp) Option {
func Adapter(val *connector.HttpAdapter) Option {
return func(o *Options) {
o.App = val
o.Adapter = val
}
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/owncloud/ocis/v2/ocis-pkg/account"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
"github.com/owncloud/ocis/v2/ocis-pkg/service/http"
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
@@ -76,7 +77,7 @@ func Server(opts ...Option) (http.Service, error) {
),
)
prepareRoutes(mux, options.App)
prepareRoutes(mux, options)
if err := micro.RegisterHandler(service.Server(), mux); err != nil {
return http.Service{}, err
@@ -85,23 +86,37 @@ func Server(opts ...Option) (http.Service, error) {
return service, nil
}
func prepareRoutes(r *chi.Mux, demoapp *app.DemoApp) {
func prepareRoutes(r *chi.Mux, options Options) {
adapter := options.Adapter
logger := options.Logger
// prepare basic logger for the request
r.Use(func(h stdhttp.Handler) stdhttp.Handler {
return stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
ctx := logger.With().
Str(log.RequestIDString, r.Header.Get("X-Request-ID")).
Str("proto", r.Proto).
Str("method", r.Method).
Str("path", r.URL.Path).
Logger().WithContext(r.Context())
h.ServeHTTP(w, r.WithContext(ctx))
})
})
r.Route("/wopi", func(r chi.Router) {
r.Get("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
app.WopiInfoHandler(demoapp, w, r)
stdhttp.Error(w, stdhttp.StatusText(stdhttp.StatusTeapot), stdhttp.StatusTeapot)
})
r.Route("/files/{fileid}", func(r chi.Router) {
r.Use(func(h stdhttp.Handler) stdhttp.Handler {
// authentication and wopi context
return app.WopiContextAuthMiddleware(demoapp, h)
return app.WopiContextAuthMiddleware(options.Config.JWTSecret, h)
},
)
r.Get("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
app.CheckFileInfo(demoapp, w, r)
adapter.CheckFileInfo(w, r)
})
r.Post("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
@@ -110,13 +125,13 @@ func prepareRoutes(r *chi.Mux, demoapp *app.DemoApp) {
case "LOCK":
// "UnlockAndRelock" operation goes through here
app.Lock(demoapp, w, r)
adapter.Lock(w, r)
case "GET_LOCK":
app.GetLock(demoapp, w, r)
adapter.GetLock(w, r)
case "REFRESH_LOCK":
app.RefreshLock(demoapp, w, r)
adapter.RefreshLock(w, r)
case "UNLOCK":
app.UnLock(demoapp, w, r)
adapter.UnLock(w, r)
case "PUT_USER_INFO":
// https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/putuserinfo
@@ -138,7 +153,7 @@ func prepareRoutes(r *chi.Mux, demoapp *app.DemoApp) {
r.Route("/contents", func(r chi.Router) {
r.Get("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
app.GetFile(demoapp, w, r)
adapter.GetFile(w, r)
})
r.Post("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
@@ -146,7 +161,7 @@ func prepareRoutes(r *chi.Mux, demoapp *app.DemoApp) {
switch action {
case "PUT":
app.PutFile(demoapp, w, r)
adapter.PutFile(w, r)
default:
stdhttp.Error(w, stdhttp.StatusText(stdhttp.StatusInternalServerError), stdhttp.StatusInternalServerError)