mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-18 03:18:52 -06:00
refactor: move wopi operation into connector and change logging
This commit is contained in:
@@ -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),
|
||||
|
||||
37
services/collaboration/pkg/connector/connector.go
Normal file
37
services/collaboration/pkg/connector/connector.go
Normal 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
|
||||
}
|
||||
312
services/collaboration/pkg/connector/contentconnector.go
Normal file
312
services/collaboration/pkg/connector/contentconnector.go
Normal 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
|
||||
}
|
||||
480
services/collaboration/pkg/connector/fileconnector.go
Normal file
480
services/collaboration/pkg/connector/fileconnector.go
Normal 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
|
||||
}
|
||||
178
services/collaboration/pkg/connector/httpadapter.go
Normal file
178
services/collaboration/pkg/connector/httpadapter.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package app
|
||||
|
||||
const (
|
||||
HeaderWopiLock string = "X-WOPI-Lock"
|
||||
HeaderWopiOldLock string = "X-WOPI-OldLock"
|
||||
)
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user