diff --git a/services/collaboration/pkg/connector/connector.go b/services/collaboration/pkg/connector/connector.go index c23c2a4f77..f87a09f5fc 100644 --- a/services/collaboration/pkg/connector/connector.go +++ b/services/collaboration/pkg/connector/connector.go @@ -1,5 +1,9 @@ package connector +// ConnectorError defines an error in the connector. It contains an error code +// and a message. +// For convenience, the error code can be used as HTTP error code, although +// the connector shouldn't know anything about HTTP. type ConnectorError struct { HttpCodeOut int Msg string @@ -16,6 +20,15 @@ func NewConnectorError(code int, msg string) *ConnectorError { } } +// Connector will implement the WOPI operations. +// For convenience, the connector splits the operations based on the +// WOPI endpoints, so you'll need to get the specific connector first. +// +// Available endpoints: +// * "Files" -> GetFileConnector() +// * "File contents" -> GetContentConnector() +// +// Other endpoints aren't available for now. type Connector struct { fileConnector *FileConnector contentConnector *ContentConnector diff --git a/services/collaboration/pkg/connector/contentconnector.go b/services/collaboration/pkg/connector/contentconnector.go index f639c1f725..1559e9d314 100644 --- a/services/collaboration/pkg/connector/contentconnector.go +++ b/services/collaboration/pkg/connector/contentconnector.go @@ -18,6 +18,10 @@ import ( "github.com/rs/zerolog" ) +// ContentConnector implements the "File contents" endpoint. +// Basically, the ContentConnector handles downloads (GetFile) and +// uploads (PutFile) +// Note that operations might return any kind of error, not just ConnectorError type ContentConnector struct { gwc gatewayv1beta1.GatewayAPIClient cfg *config.Config @@ -32,6 +36,13 @@ func NewContentConnector(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config // GetFile downloads the file from the storage // https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/getfile +// +// The context MUST have a WOPI context, otherwise an error will be returned. +// You can pass a pre-configured zerologger instance through the context that +// will be used to log messages. +// +// The contents of the file will be written directly into the writer passed as +// parameter. func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error { wopiContext, err := middleware.WopiContextFromCtx(ctx) if err != nil { @@ -138,6 +149,24 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error // PutFile uploads the file to the storage // https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/putfile +// +// The context MUST have a WOPI context, otherwise an error will be returned. +// You can pass a pre-configured zerologger instance through the context that +// will be used to log messages. +// +// The contents of the file will be read from the stream. The full stream +// length must be provided in order to upload the file. +// +// A lock ID must be provided for the upload (which must match the lock in the +// file). The only case where an empty lock ID can be used is if the target +// file has 0 size. +// +// This method will return the lock ID that should be returned in case of a +// conflict, otherwise it will return an empty string. This means that if the +// method returns a ConnectorError with code 409, the returned string is the +// lock ID that should be used in the X-WOPI-Lock header. In other error +// cases or if the method is successful, an empty string will be returned +// (check for err != nil to know if something went wrong) func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, streamLength int64, lockID string) (string, error) { wopiContext, err := middleware.WopiContextFromCtx(ctx) if err != nil { diff --git a/services/collaboration/pkg/connector/fileconnector.go b/services/collaboration/pkg/connector/fileconnector.go index 534fb2fa8b..2366c4a43d 100644 --- a/services/collaboration/pkg/connector/fileconnector.go +++ b/services/collaboration/pkg/connector/fileconnector.go @@ -38,6 +38,14 @@ func NewFileConnector(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config) * // 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 +// +// The context MUST have a WOPI context, otherwise an error will be returned. +// You can pass a pre-configured zerologger instance through the context that +// will be used to log messages. +// +// The lock ID applied to the file reference in the context will be returned +// (if any). An error will be returned if something goes wrong. The error +// could be a ConnectorError func (f *FileConnector) GetLock(ctx context.Context) (string, error) { wopiContext, err := middleware.WopiContextFromCtx(ctx) if err != nil { @@ -80,6 +88,21 @@ func (f *FileConnector) GetLock(ctx context.Context) (string, error) { // 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 +// +// The context MUST have a WOPI context, otherwise an error will be returned. +// You can pass a pre-configured zerologger instance through the context that +// will be used to log messages. +// +// Lock the file reference contained in the context with the provided lockID. +// The oldLockID is only used for the "unlock and relock" operation. The "lock" +// operation doesn't use the oldLockID and needs to be empty in this case. +// +// For the "lock" operation, if the operation is successful, an empty lock id +// will be returned without any error. In case of conflict, the current lock +// id will be returned along with a 409 ConnectorError. For any other error, +// the method will return an empty lock id. +// +// For the "unlock and relock" operation, the behavior will be the same. func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (string, error) { wopiContext, err := middleware.WopiContextFromCtx(ctx) if err != nil { @@ -207,6 +230,17 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (str // RefreshLock refreshes a provided lock for 30 minutes // https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/refreshlock +// +// The context MUST have a WOPI context, otherwise an error will be returned. +// You can pass a pre-configured zerologger instance through the context that +// will be used to log messages. +// +// If the operation is successful, an empty lock id will be returned without +// any error. In case of conflict, the current lock id will be returned +// along with a 409 ConnectorError. For any other error, the method will +// return an empty lock id. +// The conflict happens if the provided lockID doesn't match the one actually +// applied in the target file. func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (string, error) { wopiContext, err := middleware.WopiContextFromCtx(ctx) if err != nil { @@ -304,6 +338,17 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (string, // UnLock removes a given lock from a file // https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/unlock +// +// The context MUST have a WOPI context, otherwise an error will be returned. +// You can pass a pre-configured zerologger instance through the context that +// will be used to log messages. +// +// If the operation is successful, an empty lock id will be returned without +// any error. In case of conflict, the current lock id will be returned +// along with a 409 ConnectorError. For any other error, the method will +// return an empty lock id. +// The conflict happens if the provided lockID doesn't match the one actually +// applied in the target file. func (f *FileConnector) UnLock(ctx context.Context, lockID string) (string, error) { wopiContext, err := middleware.WopiContextFromCtx(ctx) if err != nil { @@ -389,6 +434,13 @@ func (f *FileConnector) UnLock(ctx context.Context, lockID string) (string, erro // 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 +// +// The context MUST have a WOPI context, otherwise an error will be returned. +// You can pass a pre-configured zerologger instance through the context that +// will be used to log messages. +// +// If the operation is successful, a "FileInfo" instance will be returned, +// otherwise the "FileInfo" will be empty and an error will be returned. func (f *FileConnector) CheckFileInfo(ctx context.Context) (FileInfo, error) { wopiContext, err := middleware.WopiContextFromCtx(ctx) if err != nil { diff --git a/services/collaboration/pkg/connector/fileinfo.go b/services/collaboration/pkg/connector/fileinfo.go index e9f715ca91..9466568406 100644 --- a/services/collaboration/pkg/connector/fileinfo.go +++ b/services/collaboration/pkg/connector/fileinfo.go @@ -1,5 +1,11 @@ package connector +// FileInfo contains the properties of the file. +// Some properties refer to capabilities in the WOPI client, and capabilities +// that the WOPI server has. +// +// For now, the FileInfo contains data for Microsoft, Collabora and OnlyOffice. +// Not all the properties are supported by every system. type FileInfo struct { // ------------ // Microsoft WOPI check file info specification: diff --git a/services/collaboration/pkg/connector/httpadapter.go b/services/collaboration/pkg/connector/httpadapter.go index ff94a6d479..8eda845cfd 100644 --- a/services/collaboration/pkg/connector/httpadapter.go +++ b/services/collaboration/pkg/connector/httpadapter.go @@ -15,6 +15,14 @@ const ( HeaderWopiOldLock string = "X-WOPI-OldLock" ) +// HttpAdapter will adapt the responses from the connector to HTTP. +// +// The adapter will use the request's context for the connector operations, +// this means that the request MUST have a valid WOPI context and a +// pre-configured logger. This should have been prepared in the routing. +// +// All operations are expected to follow the definitions found in +// https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/endpoints type HttpAdapter struct { con *Connector } diff --git a/services/collaboration/pkg/helpers/cs3.go b/services/collaboration/pkg/helpers/cs3.go index a1cb1cfac3..8b3f21f469 100644 --- a/services/collaboration/pkg/helpers/cs3.go +++ b/services/collaboration/pkg/helpers/cs3.go @@ -8,6 +8,10 @@ import ( var commonCS3ApiClient gatewayv1beta1.GatewayAPIClient +// GatewayAPIClient gets an instance based on the provided configuration. +// The instance will be cached and returned if possible, unless the "forceNew" +// parameter is set to true. In this case, the old instance will be replaced +// with the new one if there is no error. func GetCS3apiClient(cfg *config.Config, forceNew bool) (gatewayv1beta1.GatewayAPIClient, error) { // establish a connection to the cs3 api endpoint // in this case a REVA gateway, started by oCIS diff --git a/services/collaboration/pkg/helpers/registration.go b/services/collaboration/pkg/helpers/registration.go index 1908c76e64..49fbfab28b 100644 --- a/services/collaboration/pkg/helpers/registration.go +++ b/services/collaboration/pkg/helpers/registration.go @@ -14,11 +14,20 @@ import ( "github.com/owncloud/ocis/v2/services/collaboration/pkg/config" ) +// RegisterOcisService will register this service. +// There are no explicit requirements for the context, and it will be passed +// without changes to the underlying RegisterService method. func RegisterOcisService(ctx context.Context, cfg *config.Config, logger log.Logger) error { svc := registry.BuildGRPCService(cfg.Service.Name, uuid.Must(uuid.NewV4()).String(), cfg.GRPC.Addr, "0.0.0") return registry.RegisterService(ctx, svc, logger) } +// RegisterAppProvider will register this service as app provider in REVA. +// The GatewayAPIClient is expected to be provided via `helpers.GetCS3apiClient`. +// The appUrls are expected to be provided via `helpers.GetAppURLs` +// +// Note that this method doesn't provide a re-registration mechanism, so it +// will register the service once func RegisterAppProvider( ctx context.Context, cfg *config.Config, diff --git a/services/collaboration/pkg/service/grpc/v0/option.go b/services/collaboration/pkg/service/grpc/v0/option.go index a107fd66e2..0ad88e8826 100644 --- a/services/collaboration/pkg/service/grpc/v0/option.go +++ b/services/collaboration/pkg/service/grpc/v0/option.go @@ -39,7 +39,7 @@ func Config(val *config.Config) Option { } } -// ViewUrl provides a function to set the ViewUrl option. +// AppURLs provides a function to set the AppURLs option. func AppURLs(val map[string]map[string]string) Option { return func(o *Options) { o.AppURLs = val diff --git a/services/collaboration/pkg/service/grpc/v0/service.go b/services/collaboration/pkg/service/grpc/v0/service.go index 6edee2d8ec..78de1bd56e 100644 --- a/services/collaboration/pkg/service/grpc/v0/service.go +++ b/services/collaboration/pkg/service/grpc/v0/service.go @@ -21,6 +21,7 @@ import ( "github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware" ) +// NewHandler creates a new grpc service implementing the OpenInApp interface func NewHandler(opts ...Option) (*Service, func(), error) { teardown := func() {} options := newOptions(opts...) @@ -39,7 +40,7 @@ func NewHandler(opts ...Option) (*Service, func(), error) { }, teardown, nil } -// Service implements the searchServiceHandler interface +// Service implements the OpenInApp interface type Service struct { id string appURLs map[string]map[string]string