mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-07-14 01:22:09 -04:00
That should help to keep up with the stream of messages and prevent services from being flagged as slow consumers.
340 lines
10 KiB
Go
340 lines
10 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sync/atomic"
|
|
|
|
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
|
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
|
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
|
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"github.com/opencloud-eu/reva/v2/pkg/events"
|
|
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
|
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
|
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
|
|
|
"github.com/opencloud-eu/opencloud/pkg/log"
|
|
"github.com/opencloud-eu/opencloud/services/clientlog/pkg/config"
|
|
)
|
|
|
|
// ClientlogService is the service responsible for user activities
|
|
type ClientlogService struct {
|
|
log log.Logger
|
|
cfg *config.Config
|
|
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
|
registeredEvents map[string]events.Unmarshaller // ?
|
|
tp trace.TracerProvider
|
|
tracer trace.Tracer
|
|
publisher events.Publisher
|
|
ch <-chan events.Event
|
|
stopCh chan struct{}
|
|
stopped atomic.Bool
|
|
}
|
|
|
|
// NewClientlogService returns a clientlog service
|
|
func NewClientlogService(opts ...Option) (*ClientlogService, error) {
|
|
o := &Options{}
|
|
for _, opt := range opts {
|
|
opt(o)
|
|
}
|
|
|
|
if o.Stream == nil {
|
|
return nil, fmt.Errorf("need non nil stream (%v) to work properly", o.Stream)
|
|
}
|
|
|
|
ch, err := events.Consume(o.Stream, "clientlog", o.RegisteredEvents...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cl := &ClientlogService{
|
|
log: o.Logger,
|
|
cfg: o.Config,
|
|
gatewaySelector: o.GatewaySelector,
|
|
registeredEvents: make(map[string]events.Unmarshaller),
|
|
tp: o.TraceProvider,
|
|
tracer: o.TraceProvider.Tracer("github.com/opencloud-eu/opencloud/services/clientlog/pkg/service"),
|
|
publisher: o.Stream,
|
|
ch: ch,
|
|
stopCh: make(chan struct{}, 1),
|
|
}
|
|
|
|
for _, e := range o.RegisteredEvents {
|
|
typ := reflect.TypeOf(e)
|
|
cl.registeredEvents[typ.String()] = e
|
|
}
|
|
|
|
return cl, nil
|
|
}
|
|
|
|
// Run runs the service
|
|
func (cl *ClientlogService) Run() error {
|
|
EventLoop:
|
|
for {
|
|
select {
|
|
case event, ok := <-cl.ch:
|
|
if !ok {
|
|
break EventLoop
|
|
}
|
|
|
|
go cl.processEvent(event)
|
|
|
|
if cl.stopped.Load() {
|
|
break EventLoop
|
|
}
|
|
case <-cl.stopCh:
|
|
break EventLoop
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (cl *ClientlogService) Close() {
|
|
if cl.stopped.CompareAndSwap(false, true) {
|
|
close(cl.stopCh)
|
|
}
|
|
}
|
|
|
|
func (cl *ClientlogService) processEvent(event events.Event) {
|
|
gwc, err := cl.gatewaySelector.Next()
|
|
if err != nil {
|
|
cl.log.Error().Err(err).Interface("event", event).Msg("error getting gateway client")
|
|
return
|
|
}
|
|
|
|
ctx, err := utils.GetServiceUserContextWithContext(context.Background(), gwc, cl.cfg.ServiceAccount.ServiceAccountID, cl.cfg.ServiceAccount.ServiceAccountSecret)
|
|
if err != nil {
|
|
cl.log.Error().Err(err).Interface("event", event).Msg("error authenticating service user")
|
|
return
|
|
}
|
|
|
|
var (
|
|
users []string
|
|
evType string
|
|
data any
|
|
)
|
|
|
|
fileEv := func(typ string, ref *provider.Reference) {
|
|
evType = typ
|
|
users, data, err = processFileEvent(ctx, ref, gwc, event.InitiatorID)
|
|
}
|
|
|
|
shareEv := func(typ string, ref *provider.Reference, uid *user.UserId, gid *group.GroupId) {
|
|
evType = typ
|
|
users, data, err = processShareEvent(ctx, ref, gwc, event.InitiatorID, uid, gid)
|
|
}
|
|
|
|
spaceEv := func(typ string, id *provider.StorageSpaceId, uids []string) {
|
|
evType = typ
|
|
users, data, err = processSpaceEvent(ctx, id, gwc, event.InitiatorID, uids)
|
|
}
|
|
|
|
switch e := event.Event.(type) {
|
|
default:
|
|
err = errors.New("unhandled event")
|
|
case events.UploadReady:
|
|
if e.Failed {
|
|
// we don't inform about failed uploads yet
|
|
return
|
|
}
|
|
fileEv("postprocessing-finished", e.FileRef)
|
|
case events.ItemTrashed:
|
|
evType = "item-trashed"
|
|
users, data, err = processItemTrashedEvent(ctx, e.Ref, gwc, event.InitiatorID, e.ID)
|
|
case events.ItemRestored:
|
|
fileEv("item-restored", e.Ref)
|
|
case events.ContainerCreated:
|
|
fileEv("folder-created", e.Ref)
|
|
case events.ItemMoved:
|
|
// we send a dedicated event in case the item was only renamed
|
|
if isRename(e.OldReference, e.Ref) {
|
|
fileEv("item-renamed", e.Ref)
|
|
} else {
|
|
fileEv("item-moved", e.Ref)
|
|
}
|
|
case events.FileLocked:
|
|
fileEv("file-locked", e.Ref)
|
|
case events.FileUnlocked:
|
|
fileEv("file-unlocked", e.Ref)
|
|
case events.FileTouched:
|
|
fileEv("file-touched", e.Ref)
|
|
case events.SpaceCreated:
|
|
spaceEv("space-created", e.ID, []string{e.Executant.GetOpaqueId()})
|
|
case events.SpaceDisabled:
|
|
uids := []string{}
|
|
for k := range e.Members {
|
|
uids = append(uids, k)
|
|
}
|
|
spaceEv("space-disabled", e.ID, uids)
|
|
case events.SpaceDeleted:
|
|
uids := []string{}
|
|
for k := range e.FinalMembers {
|
|
uids = append(uids, k)
|
|
}
|
|
spaceEv("space-deleted", e.ID, uids)
|
|
case events.SpaceEnabled:
|
|
uids := []string{}
|
|
for k := range e.Members {
|
|
uids = append(uids, k)
|
|
}
|
|
spaceEv("space-enabled", e.ID, uids)
|
|
case events.SpaceShared:
|
|
r, _ := storagespace.ParseReference(e.ID.GetOpaqueId())
|
|
shareEv("space-member-added", &r, e.GranteeUserID, e.GranteeGroupID)
|
|
case events.SpaceShareUpdated:
|
|
r, _ := storagespace.ParseReference(e.ID.GetOpaqueId())
|
|
shareEv("space-share-updated", &r, e.GranteeUserID, e.GranteeGroupID)
|
|
case events.SpaceUnshared:
|
|
r, _ := storagespace.ParseReference(e.ID.GetOpaqueId())
|
|
shareEv("space-member-removed", &r, e.GranteeUserID, e.GranteeGroupID)
|
|
case events.ShareCreated:
|
|
shareEv("share-created", &provider.Reference{ResourceId: e.ItemID}, e.GranteeUserID, e.GranteeGroupID)
|
|
case events.ShareUpdated:
|
|
shareEv("share-updated", &provider.Reference{ResourceId: e.ItemID}, e.GranteeUserID, e.GranteeGroupID)
|
|
case events.ShareRemoved:
|
|
shareEv("share-removed", &provider.Reference{ResourceId: e.ItemID}, e.GranteeUserID, e.GranteeGroupID)
|
|
case events.LinkCreated:
|
|
fileEv("link-created", &provider.Reference{ResourceId: e.ItemID})
|
|
case events.LinkUpdated:
|
|
fileEv("link-updated", &provider.Reference{ResourceId: e.ItemID})
|
|
case events.LinkRemoved:
|
|
fileEv("link-removed", &provider.Reference{ResourceId: e.ItemID})
|
|
case events.BackchannelLogout:
|
|
evType, users, data = backchannelLogoutEvent(e)
|
|
}
|
|
|
|
if err != nil {
|
|
cl.log.Error().Err(err).Interface("event", event).Msg("error gathering members for event")
|
|
return
|
|
}
|
|
|
|
// II) instruct sse service to send the information
|
|
if err := cl.sendSSE(users, evType, data); err != nil {
|
|
cl.log.Error().Err(err).Interface("userIDs", users).Str("eventid", event.ID).Msg("failed to store event for user")
|
|
return
|
|
}
|
|
}
|
|
|
|
func (cl *ClientlogService) sendSSE(userIDs []string, evType string, data any) error {
|
|
b, err := json.Marshal(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return events.Publish(context.Background(), cl.publisher, events.SendSSE{
|
|
UserIDs: userIDs,
|
|
Type: evType,
|
|
Message: b,
|
|
})
|
|
}
|
|
|
|
// process file related events
|
|
func processFileEvent(ctx context.Context, ref *provider.Reference, gwc gateway.GatewayAPIClient, initiatorid string) ([]string, FileEvent, error) {
|
|
info, err := utils.GetResource(ctx, ref, gwc)
|
|
if err != nil {
|
|
return nil, FileEvent{}, err
|
|
}
|
|
|
|
data := FileEvent{
|
|
ParentItemID: storagespace.FormatResourceID(info.GetParentId()),
|
|
ItemID: storagespace.FormatResourceID(info.GetId()),
|
|
SpaceID: storagespace.FormatStorageID(info.GetSpace().GetRoot().GetStorageId(), info.GetSpace().GetRoot().GetSpaceId()),
|
|
InitiatorID: initiatorid,
|
|
Etag: info.GetEtag(),
|
|
}
|
|
|
|
users, err := utils.GetSpaceMembers(ctx, info.GetSpace().GetId().GetOpaqueId(), gwc, utils.ViewerRole)
|
|
return users, data, err
|
|
}
|
|
|
|
// process share related events
|
|
func processShareEvent(ctx context.Context, ref *provider.Reference, gwc gateway.GatewayAPIClient, initiatorid string, shareeID *user.UserId, shareeGroupID *group.GroupId) ([]string, FileEvent, error) {
|
|
users, data, err := processFileEvent(ctx, ref, gwc, initiatorid)
|
|
if err != nil {
|
|
return users, data, err
|
|
}
|
|
|
|
return addShareeData(ctx, gwc, data, users, shareeID, shareeGroupID)
|
|
}
|
|
|
|
func processSpaceEvent(ctx context.Context, id *provider.StorageSpaceId, gwc gateway.GatewayAPIClient, initiatorid string, uids []string) ([]string, FileEvent, error) {
|
|
data := FileEvent{
|
|
SpaceID: id.GetOpaqueId(),
|
|
InitiatorID: initiatorid,
|
|
}
|
|
return uids, data, nil
|
|
}
|
|
|
|
// custom logic for item trashed event
|
|
func processItemTrashedEvent(ctx context.Context, ref *provider.Reference, gwc gateway.GatewayAPIClient, initiatorid string, itemID *provider.ResourceId) ([]string, FileEvent, error) {
|
|
data := FileEvent{
|
|
ItemID: storagespace.FormatResourceID(itemID),
|
|
// TODO: check with web if parentID is needed
|
|
// ParentItemID: storagespace.FormatResourceID(*item.GetRef().GetResourceId()),
|
|
SpaceID: storagespace.FormatStorageID(itemID.GetStorageId(), itemID.GetSpaceId()),
|
|
InitiatorID: initiatorid,
|
|
}
|
|
|
|
users, err := utils.GetSpaceMembers(ctx, itemID.GetSpaceId(), gwc, utils.ViewerRole)
|
|
return users, data, err
|
|
}
|
|
|
|
// adds share related data to the FileEvent
|
|
func addShareeData(ctx context.Context, gwc gateway.GatewayAPIClient, fe FileEvent, users []string, shareeID *user.UserId, shareeGroupID *group.GroupId) ([]string, FileEvent, error) {
|
|
us, err := resolveID(ctx, gwc, shareeID, shareeGroupID)
|
|
if err != nil {
|
|
return users, fe, err
|
|
}
|
|
|
|
fe.AffectedUserIDs = us
|
|
|
|
// TODO: this list can get long. Should we add a limit? If yes, how big?
|
|
for _, u := range us {
|
|
users = appendUnique(users, u)
|
|
}
|
|
return users, fe, nil
|
|
}
|
|
|
|
// returns the user or the members of the affected group
|
|
func resolveID(ctx context.Context, gwc gateway.GatewayAPIClient, uid *user.UserId, gid *group.GroupId) ([]string, error) {
|
|
if uid != nil {
|
|
return []string{uid.GetOpaqueId()}, nil
|
|
}
|
|
return utils.GetGroupMembers(ctx, gid.GetOpaqueId(), gwc)
|
|
}
|
|
|
|
// returns users or append(users, user)
|
|
func appendUnique(users []string, user string) []string {
|
|
for _, u := range users {
|
|
if u == user {
|
|
return users
|
|
}
|
|
}
|
|
return append(users, user)
|
|
}
|
|
|
|
// returns true if this is just a rename
|
|
func isRename(o, n *provider.Reference) bool {
|
|
// if resourceids are different we assume it is a move
|
|
if !utils.ResourceIDEqual(o.GetResourceId(), n.GetResourceId()) {
|
|
return false
|
|
}
|
|
return filepath.Base(o.GetPath()) != filepath.Base(n.GetPath())
|
|
}
|
|
|
|
func backchannelLogoutEvent(e events.BackchannelLogout) (string, []string, BackchannelLogout) {
|
|
return "backchannel-logout", []string{e.Executant.GetOpaqueId()}, BackchannelLogout{
|
|
UserID: e.Executant.GetOpaqueId(),
|
|
Timestamp: e.Timestamp.String(),
|
|
SessionID: e.SessionId,
|
|
}
|
|
}
|