Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,19 @@
package command
import (
"github.com/qsfera/server/services/clientlog/pkg/config"
"github.com/spf13/cobra"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "health",
Short: "Check health status",
RunE: func(cmd *cobra.Command, args []string) error {
// Not implemented
return nil
},
}
}
@@ -0,0 +1,36 @@
package command
import (
"os"
"github.com/qsfera/server/pkg/clihelper"
"github.com/qsfera/server/services/clientlog/pkg/config"
"github.com/spf13/cobra"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) []*cobra.Command {
return []*cobra.Command{
// start this service
Server(cfg),
// interaction with this service
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the clientlog command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cobra.Command{
Use: "clientlog",
Short: "starts clientlog service",
})
app.AddCommand(GetCommands(cfg)...)
app.SetArgs(os.Args[1:])
return app.ExecuteContext(cfg.Context)
}
@@ -0,0 +1,143 @@
package command
import (
"context"
"fmt"
"os/signal"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/generators"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/runner"
"github.com/qsfera/server/pkg/tracing"
"github.com/qsfera/server/pkg/version"
"github.com/qsfera/server/services/clientlog/pkg/config"
"github.com/qsfera/server/services/clientlog/pkg/config/parser"
"github.com/qsfera/server/services/clientlog/pkg/metrics"
"github.com/qsfera/server/services/clientlog/pkg/server/debug"
"github.com/qsfera/server/services/clientlog/pkg/service"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/spf13/cobra"
)
// all events we care about
var _registeredEvents = []events.Unmarshaller{
events.UploadReady{},
events.ItemTrashed{},
events.ItemRestored{},
events.ItemMoved{},
events.ContainerCreated{},
events.FileLocked{},
events.FileUnlocked{},
events.FileTouched{},
events.SpaceShared{},
events.SpaceShareUpdated{},
events.SpaceUnshared{},
events.ShareCreated{},
events.ShareRemoved{},
events.ShareUpdated{},
events.LinkCreated{},
events.LinkUpdated{},
events.LinkRemoved{},
events.BackchannelLogout{},
}
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "server",
Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
tracerProvider, err := tracing.GetTraceProvider(cmd.Context(), cfg.Commons.TracesExporter, cfg.Service.Name)
if err != nil {
return err
}
var cancel context.CancelFunc
if cfg.Context == nil {
cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...)
defer cancel()
}
ctx := cfg.Context
mtrcs := metrics.New()
mtrcs.BuildInfo.WithLabelValues(version.GetString()).Set(1)
connName := generators.GenerateConnectionName(cfg.Service.Name, generators.NTypeBus)
s, err := stream.NatsFromConfig(connName, false, stream.NatsConfig(cfg.Events))
if err != nil {
return err
}
tm, err := pool.StringToTLSMode(cfg.GRPCClientTLS.Mode)
if err != nil {
return err
}
gatewaySelector, err := pool.GatewaySelector(
cfg.RevaGateway,
pool.WithTLSCACert(cfg.GRPCClientTLS.CACert),
pool.WithTLSMode(tm),
pool.WithRegistry(registry.GetRegistry()),
pool.WithTracerProvider(tracerProvider),
)
if err != nil {
return fmt.Errorf("could not get reva client selector: %s", err)
}
gr := runner.NewGroup()
{
svc, err := service.NewClientlogService(
service.Logger(logger),
service.Config(cfg),
service.Stream(s),
service.GatewaySelector(gatewaySelector),
service.RegisteredEvents(_registeredEvents),
service.TraceProvider(tracerProvider),
)
if err != nil {
logger.Info().Err(err).Str("transport", "http").Msg("Failed to initialize server")
return err
}
gr.Add(runner.New(cfg.Service.Name+".svc", func() error {
return svc.Run()
}, func() {
svc.Close()
}))
}
{
debugServer, err := debug.Server(
debug.Logger(logger),
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
logger.Info().Err(err).Str("server", "debug").Msg("Failed to initialize server")
return err
}
gr.Add(runner.NewGolangHttpServerRunner(cfg.Service.Name+".debug", debugServer))
}
grResults := gr.Run(ctx)
// return the first non-nil error found in the results
for _, grResult := range grResults {
if grResult.RunnerError != nil {
return grResult.RunnerError
}
}
return nil
},
}
}
@@ -0,0 +1,19 @@
package command
import (
"github.com/qsfera/server/services/clientlog/pkg/config"
"github.com/spf13/cobra"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "print the version of this binary and the running service instances",
RunE: func(cmd *cobra.Command, args []string) error {
// not implemented
return nil
},
}
}
@@ -0,0 +1,51 @@
package config
import (
"context"
"github.com/qsfera/server/pkg/shared"
)
// Config combines all available configuration parts.
type Config struct {
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
Service Service `yaml:"-"`
LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;CLIENTLOG_USERLOG_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"`
Debug Debug `yaml:"debug"`
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
TokenManager *TokenManager `yaml:"token_manager"`
RevaGateway string `yaml:"reva_gateway" env:"OC_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" introductionVersion:"1.0.0"`
Events Events `yaml:"events"`
ServiceAccount ServiceAccount `yaml:"service_account"`
Context context.Context `yaml:"-"`
}
// Events combines the configuration options for the event bus.
type Events struct {
Endpoint string `yaml:"endpoint" env:"OC_EVENTS_ENDPOINT;CLIENTLOG_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture." introductionVersion:"1.0.0"`
Cluster string `yaml:"cluster" env:"OC_EVENTS_CLUSTER;CLIENTLOG_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Mandatory when using NATS as event system." introductionVersion:"1.0.0"`
TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_EVENTS_TLS_INSECURE;CLIENTLOG_EVENTS_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"1.0.0"`
TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_EVENTS_TLS_ROOT_CA_CERTIFICATE;CLIENTLOG_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided NOTIFICATIONS_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"1.0.0"`
EnableTLS bool `yaml:"enable_tls" env:"OC_EVENTS_ENABLE_TLS;CLIENTLOG_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the КуСфера service which receives and delivers events between the services." introductionVersion:"1.0.0"`
AuthUsername string `yaml:"username" env:"OC_EVENTS_AUTH_USERNAME;CLIENTLOG_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the КуСфера service which receives and delivers events between the services." introductionVersion:"1.0.0"`
AuthPassword string `yaml:"password" env:"OC_EVENTS_AUTH_PASSWORD;CLIENTLOG_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the КуСфера service which receives and delivers events between the services." introductionVersion:"1.0.0"`
}
// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;CLIENTLOG_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"`
}
// ServiceAccount is the configuration for the used service account
type ServiceAccount struct {
ServiceAccountID string `yaml:"service_account_id" env:"OC_SERVICE_ACCOUNT_ID;CLIENTLOG_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"1.0.0"`
ServiceAccountSecret string `yaml:"service_account_secret" env:"OC_SERVICE_ACCOUNT_SECRET;CLIENTLOG_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,9 @@
package config
// Debug defines the available debug configuration.
type Debug struct {
Addr string `yaml:"addr" env:"CLIENTLOG_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"`
Token string `yaml:"token" env:"CLIENTLOG_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"`
Pprof bool `yaml:"pprof" env:"CLIENTLOG_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"`
Zpages bool `yaml:"zpages" env:"CLIENTLOG_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,61 @@
package defaults
import (
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/pkg/structs"
"github.com/qsfera/server/services/clientlog/pkg/config"
)
// FullDefaultConfig returns the full default config
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
// DefaultConfig return the default configuration
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9260",
Token: "",
Pprof: false,
Zpages: false,
},
Service: config.Service{
Name: "clientlog",
},
Events: config.Events{
Endpoint: "127.0.0.1:9233",
Cluster: "qsfera-cluster",
EnableTLS: false,
},
RevaGateway: shared.DefaultRevaConfig().Address,
}
}
// EnsureDefaults ensures the config contains default values
func EnsureDefaults(cfg *config.Config) {
if cfg.LogLevel == "" {
cfg.LogLevel = "error"
}
if cfg.GRPCClientTLS == nil && cfg.Commons != nil {
cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
}
if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil {
cfg.TokenManager = &config.TokenManager{
JWTSecret: cfg.Commons.TokenManager.JWTSecret,
}
} else if cfg.TokenManager == nil {
cfg.TokenManager = &config.TokenManager{}
}
}
// Sanitize sanitizes the config
func Sanitize(cfg *config.Config) {
// sanitize config
}
@@ -0,0 +1,50 @@
package parser
import (
"errors"
occfg "github.com/qsfera/server/pkg/config"
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/services/clientlog/pkg/config"
"github.com/qsfera/server/services/clientlog/pkg/config/defaults"
"github.com/qsfera/server/pkg/config/envdecode"
)
// ParseConfig loads configuration from known paths.
func ParseConfig(cfg *config.Config) error {
err := occfg.BindSourcesToStructs(cfg.Service.Name, cfg)
if err != nil {
return err
}
defaults.EnsureDefaults(cfg)
// load all env variables relevant to the config in the current context.
if err := envdecode.Decode(cfg); err != nil {
// no environment variable set for this config is an expected "error"
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
return err
}
}
defaults.Sanitize(cfg)
return Validate(cfg)
}
// Validate validates the config
func Validate(cfg *config.Config) error {
if cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
if cfg.ServiceAccount.ServiceAccountID == "" {
return shared.MissingServiceAccountID(cfg.Service.Name)
}
if cfg.ServiceAccount.ServiceAccountSecret == "" {
return shared.MissingServiceAccountSecret(cfg.Service.Name)
}
return nil
}
@@ -0,0 +1,6 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string `yaml:"-"`
}
@@ -0,0 +1,35 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
var (
// Namespace defines the namespace for the defines metrics.
Namespace = "qsfera"
// Subsystem defines the subsystem for the defines metrics.
Subsystem = "clientlog"
)
// Metrics defines the available metrics of this service.
type Metrics struct {
BuildInfo *prometheus.GaugeVec
}
// New initializes the available metrics.
func New() *Metrics {
m := &Metrics{
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "build_info",
Help: "Build information",
}, []string{"version"}),
}
_ = prometheus.Register(
m.BuildInfo,
)
// TODO: implement metrics
return m
}
@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/clientlog/pkg/config"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Context context.Context
Config *config.Config
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
@@ -0,0 +1,30 @@
package debug
import (
"net/http"
"github.com/qsfera/server/pkg/checks"
"github.com/qsfera/server/pkg/handlers"
"github.com/qsfera/server/pkg/service/debug"
"github.com/qsfera/server/pkg/version"
)
// Server initializes the debug service and server.
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
readyHandlerConfiguration := handlers.NewCheckHandlerConfiguration().
WithLogger(options.Logger).
WithCheck("nats reachability", checks.NewNatsCheck(options.Config.Events.Endpoint))
return debug.NewService(
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.GetString()),
debug.Address(options.Config.Debug.Addr),
debug.Token(options.Config.Debug.Token),
debug.Pprof(options.Config.Debug.Pprof),
debug.Zpages(options.Config.Debug.Zpages),
debug.Ready(handlers.NewCheckHandler(readyHandlerConfiguration)),
), nil
}
@@ -0,0 +1,20 @@
package service
// FileEvent is emitted when a file is uploaded/renamed/deleted/...
type FileEvent struct {
ParentItemID string `json:"parentitemid"`
ItemID string `json:"itemid"`
SpaceID string `json:"spaceid"`
InitiatorID string `json:"initiatorid"`
Etag string `json:"etag"`
// Only in case of sharing (refactor this into separate struct when more fields are needed)
AffectedUserIDs []string `json:"affecteduserids"`
}
// BackchannelLogout is emitted when the callback revived from the identity provider
type BackchannelLogout struct {
UserID string `json:"userid"`
Timestamp string `json:"timestamp"`
SessionID string `json:"sessionid"`
}
@@ -0,0 +1,65 @@
package service
import (
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/clientlog/pkg/config"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"go.opentelemetry.io/otel/trace"
)
// Option for the clientlog service
type Option func(*Options)
// Options for the clientlog service
type Options struct {
Logger log.Logger
Stream events.Stream
Config *config.Config
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
RegisteredEvents []events.Unmarshaller
TraceProvider trace.TracerProvider
}
// Logger configures a logger for the clientlog service
func Logger(log log.Logger) Option {
return func(o *Options) {
o.Logger = log
}
}
// Stream configures an event stream for the clientlog service
func Stream(s events.Stream) Option {
return func(o *Options) {
o.Stream = s
}
}
// Config adds the config for the clientlog service
func Config(c *config.Config) Option {
return func(o *Options) {
o.Config = c
}
}
// GatewaySelector adds a grpc client selector for the gateway service
func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) Option {
return func(o *Options) {
o.GatewaySelector = gatewaySelector
}
}
// RegisteredEvents registers the events the service should listen to
func RegisteredEvents(e []events.Unmarshaller) Option {
return func(o *Options) {
o.RegisteredEvents = e
}
}
// TraceProvider adds a tracer provider for the clientlog service
func TraceProvider(tp trace.TracerProvider) Option {
return func(o *Options) {
o.TraceProvider = tp
}
}
@@ -0,0 +1,305 @@
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/qsfera/server/pkg/log"
"github.com/qsfera/server/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/qsfera/server/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
}
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)
}
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.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)
}
// 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,
}
}