From 882689549f1a93cdc508fe57369c22504d714c55 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Tue, 11 Jun 2024 16:42:52 +0200 Subject: [PATCH 01/13] feat(activitylog): add api Signed-off-by: jkoberg --- .../6.0.0_2024-06-19/activity-service.md | 1 + services/activitylog/pkg/command/server.go | 32 +++-- services/activitylog/pkg/config/config.go | 25 ++++ .../pkg/config/defaults/defaultconfig.go | 30 +++- .../activitylog/pkg/server/http/option.go | 130 ++++++++++++++++++ .../activitylog/pkg/server/http/server.go | 100 ++++++++++++++ services/activitylog/pkg/service/http.go | 114 +++++++++++++++ services/activitylog/pkg/service/options.go | 18 +++ services/activitylog/pkg/service/response.go | 67 +++++++++ services/activitylog/pkg/service/service.go | 55 +++++--- .../pkg/config/defaults/defaultconfig.go | 5 +- .../pkg/config/defaults/defaultconfig.go | 11 ++ 12 files changed, 557 insertions(+), 31 deletions(-) create mode 100644 services/activitylog/pkg/server/http/option.go create mode 100644 services/activitylog/pkg/server/http/server.go create mode 100644 services/activitylog/pkg/service/http.go create mode 100644 services/activitylog/pkg/service/response.go diff --git a/changelog/6.0.0_2024-06-19/activity-service.md b/changelog/6.0.0_2024-06-19/activity-service.md index 1058280a061..8f82f56fe79 100644 --- a/changelog/6.0.0_2024-06-19/activity-service.md +++ b/changelog/6.0.0_2024-06-19/activity-service.md @@ -2,4 +2,5 @@ Enhancement: Activitylog Service Adds a new service `activitylog` which stores events (activities) per resource. This data can be retrieved by clients to show item activities +https://github.com/owncloud/ocis/pull/9360 https://github.com/owncloud/ocis/pull/9327 diff --git a/services/activitylog/pkg/command/server.go b/services/activitylog/pkg/command/server.go index ed07a722a07..f80c7b4b84c 100644 --- a/services/activitylog/pkg/command/server.go +++ b/services/activitylog/pkg/command/server.go @@ -14,13 +14,15 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/handlers" "github.com/owncloud/ocis/v2/ocis-pkg/registry" "github.com/owncloud/ocis/v2/ocis-pkg/service/debug" + ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" "github.com/owncloud/ocis/v2/ocis-pkg/tracing" "github.com/owncloud/ocis/v2/ocis-pkg/version" + ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" "github.com/owncloud/ocis/v2/services/activitylog/pkg/config" "github.com/owncloud/ocis/v2/services/activitylog/pkg/config/parser" "github.com/owncloud/ocis/v2/services/activitylog/pkg/logging" "github.com/owncloud/ocis/v2/services/activitylog/pkg/metrics" - "github.com/owncloud/ocis/v2/services/activitylog/pkg/service" + "github.com/owncloud/ocis/v2/services/activitylog/pkg/server/http" "github.com/urfave/cli/v2" microstore "go-micro.dev/v4/store" ) @@ -109,15 +111,27 @@ func Server(cfg *config.Config) *cli.Command { return fmt.Errorf("could not get reva client selector: %s", err) } + grpcClient, err := ogrpc.NewClient( + append(ogrpc.GetClientOptions(cfg.GRPCClientTLS), ogrpc.WithTraceProvider(tracerProvider))..., + ) + if err != nil { + return err + } + + hClient := ehsvc.NewEventHistoryService("com.owncloud.api.eventhistory", grpcClient) + { - svc, err := service.New( - service.Logger(logger), - service.Config(cfg), - service.TraceProvider(tracerProvider), - service.Stream(evStream), - service.RegisteredEvents(_registeredEvents), - service.Store(evStore), - service.GatewaySelector(gatewaySelector), + svc, err := http.Server( + http.Logger(logger), + http.Config(cfg), + http.Context(ctx), // NOTE: not passing this "option" leads to a panic in go-micro + http.TraceProvider(tracerProvider), + http.Stream(evStream), + http.RegisteredEvents(_registeredEvents), + http.Store(evStore), + http.GatewaySelector(gatewaySelector), + http.History(hClient), + http.RegisteredEvents(_registeredEvents), ) if err != nil { diff --git a/services/activitylog/pkg/config/config.go b/services/activitylog/pkg/config/config.go index 2a7f24137a9..580ca381ac6 100644 --- a/services/activitylog/pkg/config/config.go +++ b/services/activitylog/pkg/config/config.go @@ -23,6 +23,9 @@ type Config struct { RevaGateway string `yaml:"reva_gateway" env:"OCIS_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" introductionVersion:"5.0"` GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` + HTTP HTTP `yaml:"http"` + TokenManager *TokenManager `yaml:"token_manager"` + ServiceAccount ServiceAccount `yaml:"service_account"` Context context.Context `yaml:"-"` @@ -56,3 +59,25 @@ type ServiceAccount struct { ServiceAccountID string `yaml:"service_account_id" env:"OCIS_SERVICE_ACCOUNT_ID;ACTIVITYLOG_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"5.0"` ServiceAccountSecret string `yaml:"service_account_secret" env:"OCIS_SERVICE_ACCOUNT_SECRET;ACTIVITYOG_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"5.0"` } + +// CORS defines the available cors configuration. +type CORS struct { + AllowedOrigins []string `yaml:"allow_origins" env:"OCIS_CORS_ALLOW_ORIGINS;ACTIVITYLOG_CORS_ALLOW_ORIGINS" desc:"A list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"` + AllowedMethods []string `yaml:"allow_methods" env:"OCIS_CORS_ALLOW_METHODS;ACTIVITYLOG_CORS_ALLOW_METHODS" desc:"A list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"` + AllowedHeaders []string `yaml:"allow_headers" env:"OCIS_CORS_ALLOW_HEADERS;ACTIVITYLOG_CORS_ALLOW_HEADERS" desc:"A list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"` + AllowCredentials bool `yaml:"allow_credentials" env:"OCIS_CORS_ALLOW_CREDENTIALS;ACTIVITYLOG_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials." introductionVersion:"pre5.0"` +} + +// HTTP defines the available http configuration. +type HTTP struct { + Addr string `yaml:"addr" env:"ACTIVITYLOG_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"pre5.0"` + Namespace string `yaml:"-"` + Root string `yaml:"root" env:"ACTIVITYLOG_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"pre5.0"` + CORS CORS `yaml:"cors"` + TLS shared.HTTPServiceTLS `yaml:"tls"` +} + +// TokenManager is the config for using the reva token manager +type TokenManager struct { + JWTSecret string `yaml:"jwt_secret" env:"OCIS_JWT_SECRET;ACTIVITYLOG_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"pre5.0"` +} diff --git a/services/activitylog/pkg/config/defaults/defaultconfig.go b/services/activitylog/pkg/config/defaults/defaultconfig.go index 84edc80fd3c..00103af6be1 100644 --- a/services/activitylog/pkg/config/defaults/defaultconfig.go +++ b/services/activitylog/pkg/config/defaults/defaultconfig.go @@ -38,6 +38,17 @@ func DefaultConfig() *config.Config { Table: "", }, RevaGateway: shared.DefaultRevaConfig().Address, + HTTP: config.HTTP{ + Addr: "127.0.0.1:0", + Root: "/", + Namespace: "com.owncloud.web", + CORS: config.CORS{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET"}, + AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Ocs-Apirequest"}, + AllowCredentials: true, + }, + }, } } @@ -55,6 +66,22 @@ func EnsureDefaults(cfg *config.Config) { cfg.Log = &config.Log{} } + 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{} + } + + if cfg.Commons != nil { + cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS + } + // provide with defaults for shared tracing, since we need a valid destination address for "envdecode". if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil { cfg.Tracing = &config.Tracing{ @@ -67,9 +94,6 @@ func EnsureDefaults(cfg *config.Config) { cfg.Tracing = &config.Tracing{} } - if cfg.GRPCClientTLS == nil && cfg.Commons != nil { - cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS) - } } // Sanitize sanitizes the config diff --git a/services/activitylog/pkg/server/http/option.go b/services/activitylog/pkg/server/http/option.go new file mode 100644 index 00000000000..5d415be3f63 --- /dev/null +++ b/services/activitylog/pkg/server/http/option.go @@ -0,0 +1,130 @@ +package http + +import ( + "context" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + "github.com/cs3org/reva/v2/pkg/events" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" + "github.com/owncloud/ocis/v2/services/activitylog/pkg/config" + "github.com/owncloud/ocis/v2/services/activitylog/pkg/metrics" + "github.com/urfave/cli/v2" + "go-micro.dev/v4/store" + "go.opentelemetry.io/otel/trace" +) + +// 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 + Metrics *metrics.Metrics + Flags []cli.Flag + Namespace string + Store store.Store + Stream events.Stream + GatewaySelector pool.Selectable[gateway.GatewayAPIClient] + TraceProvider trace.TracerProvider + HistoryClient ehsvc.EventHistoryService + RegisteredEvents []events.Unmarshaller +} + +// 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 + } +} + +// Metrics provides a function to set the metrics option. +func Metrics(val *metrics.Metrics) Option { + return func(o *Options) { + o.Metrics = val + } +} + +// Flags provides a function to set the flags option. +func Flags(val []cli.Flag) Option { + return func(o *Options) { + o.Flags = append(o.Flags, val...) + } +} + +// Namespace provides a function to set the Namespace option. +func Namespace(val string) Option { + return func(o *Options) { + o.Namespace = val + } +} + +// Store provides a function to configure the store +func Store(store store.Store) Option { + return func(o *Options) { + o.Store = store + } +} + +// Stream provides a function to configure the stream +func Stream(stream events.Stream) Option { + return func(o *Options) { + o.Stream = stream + } +} + +// GatewaySelector provides a function to configure the gateway client selector +func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) Option { + return func(o *Options) { + o.GatewaySelector = gatewaySelector + } +} + +// History provides a function to configure the event history client +func History(h ehsvc.EventHistoryService) Option { + return func(o *Options) { + o.HistoryClient = h + } +} + +// RegisteredEvents provides a function to register events +func RegisteredEvents(evs []events.Unmarshaller) Option { + return func(o *Options) { + o.RegisteredEvents = evs + } +} + +// TraceProvider provides a function to set the TracerProvider option +func TraceProvider(val trace.TracerProvider) Option { + return func(o *Options) { + o.TraceProvider = val + } +} diff --git a/services/activitylog/pkg/server/http/server.go b/services/activitylog/pkg/server/http/server.go new file mode 100644 index 00000000000..455f2125c68 --- /dev/null +++ b/services/activitylog/pkg/server/http/server.go @@ -0,0 +1,100 @@ +package http + +import ( + "fmt" + + stdhttp "net/http" + + "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/cors" + "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" + "github.com/owncloud/ocis/v2/ocis-pkg/version" + svc "github.com/owncloud/ocis/v2/services/activitylog/pkg/service" + "github.com/riandyrn/otelchi" + "go-micro.dev/v4" +) + +// Service is the service interface +type Service interface{} + +// Server initializes the http service and server. +func Server(opts ...Option) (http.Service, error) { + options := newOptions(opts...) + + service, err := http.NewService( + http.TLSConfig(options.Config.HTTP.TLS), + http.Logger(options.Logger), + http.Namespace(options.Config.HTTP.Namespace), + http.Name(options.Config.Service.Name), + http.Version(version.GetString()), + http.Address(options.Config.HTTP.Addr), + http.Context(options.Context), + http.Flags(options.Flags...), + http.TraceProvider(options.TraceProvider), + ) + if err != nil { + options.Logger.Error(). + Err(err). + Msg("Error initializing http service") + return http.Service{}, fmt.Errorf("could not initialize http service: %w", err) + } + + middlewares := []func(stdhttp.Handler) stdhttp.Handler{ + chimiddleware.RequestID, + middleware.Version( + options.Config.Service.Name, + version.GetString(), + ), + middleware.Logger( + options.Logger, + ), + middleware.ExtractAccountUUID( + account.Logger(options.Logger), + account.JWTSecret(options.Config.TokenManager.JWTSecret), + ), + middleware.Cors( + cors.Logger(options.Logger), + cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins), + cors.AllowedMethods(options.Config.HTTP.CORS.AllowedMethods), + cors.AllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders), + cors.AllowCredentials(options.Config.HTTP.CORS.AllowCredentials), + ), + } + + mux := chi.NewMux() + mux.Use(middlewares...) + + mux.Use( + otelchi.Middleware( + "actitivylog", + otelchi.WithChiRoutes(mux), + otelchi.WithTracerProvider(options.TraceProvider), + otelchi.WithPropagators(tracing.GetPropagator()), + ), + ) + + handle, err := svc.New( + svc.Logger(options.Logger), + svc.Stream(options.Stream), + svc.Mux(mux), + svc.Store(options.Store), + svc.Config(options.Config), + svc.GatewaySelector(options.GatewaySelector), + svc.TraceProvider(options.TraceProvider), + svc.HistoryClient(options.HistoryClient), + svc.RegisteredEvents(options.RegisteredEvents), + ) + if err != nil { + return http.Service{}, err + } + + if err := micro.RegisterHandler(service.Server(), handle); err != nil { + return http.Service{}, err + } + + return service, nil +} diff --git a/services/activitylog/pkg/service/http.go b/services/activitylog/pkg/service/http.go new file mode 100644 index 00000000000..5636ec24322 --- /dev/null +++ b/services/activitylog/pkg/service/http.go @@ -0,0 +1,114 @@ +package service + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/v2/pkg/events" + "github.com/cs3org/reva/v2/pkg/storagespace" + "github.com/go-chi/chi/v5" + ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" + ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" + "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" +) + +// ServeHTTP implements the http.Handler interface. +func (s *ActivitylogService) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.mux.ServeHTTP(w, r) +} + +// HandleGetItemActivities handles the request to get the activities of an item. +func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *http.Request) { + // TODO: Compare driveid with itemid to avoid bad requests + rid, err := parseIDParam(r, "item-id") + if err != nil { + s.log.Info().Err(err).Msg("invalid resource id") + w.WriteHeader(http.StatusBadRequest) + return + } + + raw, err := s.Activities(&rid) + if err != nil { + s.log.Error().Err(err).Msg("error getting activities") + w.WriteHeader(http.StatusInternalServerError) + return + } + + ids := make([]string, 0, len(raw)) + for _, a := range raw { + // TODO: Filter by depth and timestamp + ids = append(ids, a.EventID) + } + + fmt.Println("IDS:", ids) + + evRes, err := s.evHistory.GetEvents(r.Context(), &ehsvc.GetEventsRequest{Ids: ids}) + if err != nil { + s.log.Error().Err(err).Msg("error getting events") + w.WriteHeader(http.StatusInternalServerError) + return + } + + // TODO: compare returned events with initial list and remove missing ones + + fmt.Println("EVENTS:", evRes.GetEvents()) + + var acts []Activity + for _, e := range evRes.GetEvents() { + // FIXME: Should all users get all events? If not we can filter here + + switch ev := s.unwrapEvent(e).(type) { + case nil: + // error already logged in unwrapEvent + continue + case events.UploadReady: + act := UploadReady(e.Id, ev) + acts = append(acts, act) + } + } + + fmt.Println("ACTIVITIES:", acts) + + b, err := json.Marshal(acts) + if err != nil { + s.log.Error().Err(err).Msg("error marshalling activities") + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(b) + w.WriteHeader(http.StatusOK) +} + +func (s *ActivitylogService) unwrapEvent(e *ehmsg.Event) interface{} { + etype, ok := s.registeredEvents[e.GetType()] + if !ok { + s.log.Error().Str("eventid", e.GetId()).Str("eventtype", e.GetType()).Msg("event not registered") + return nil + } + + einterface, err := etype.Unmarshal(e.GetEvent()) + if err != nil { + s.log.Error().Str("eventid", e.GetId()).Str("eventtype", e.GetType()).Msg("failed to umarshal event") + return nil + } + + return einterface +} + +// TODO: I found this on graph service. We should move it to `utils` pkg so both services can use it. +func parseIDParam(r *http.Request, param string) (provider.ResourceId, error) { + driveID, err := url.PathUnescape(chi.URLParam(r, param)) + if err != nil { + return provider.ResourceId{}, errorcode.New(errorcode.InvalidRequest, err.Error()) + } + + id, err := storagespace.ParseID(driveID) + if err != nil { + return provider.ResourceId{}, errorcode.New(errorcode.InvalidRequest, err.Error()) + } + return id, nil +} diff --git a/services/activitylog/pkg/service/options.go b/services/activitylog/pkg/service/options.go index cc96d160348..8c9fef3fe3e 100644 --- a/services/activitylog/pkg/service/options.go +++ b/services/activitylog/pkg/service/options.go @@ -4,7 +4,9 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" "github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/go-chi/chi/v5" "github.com/owncloud/ocis/v2/ocis-pkg/log" + ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" "github.com/owncloud/ocis/v2/services/activitylog/pkg/config" microstore "go-micro.dev/v4/store" "go.opentelemetry.io/otel/trace" @@ -22,6 +24,8 @@ type Options struct { RegisteredEvents []events.Unmarshaller Store microstore.Store GatewaySelector pool.Selectable[gateway.GatewayAPIClient] + Mux *chi.Mux + HistoryClient ehsvc.EventHistoryService } // Logger configures a logger for the activitylog service @@ -72,3 +76,17 @@ func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) o.GatewaySelector = gatewaySelector } } + +// Mux defines the muxer for the service +func Mux(m *chi.Mux) Option { + return func(o *Options) { + o.Mux = m + } +} + +// HistoryClient adds a grpc client for the eventhistory service +func HistoryClient(hc ehsvc.EventHistoryService) Option { + return func(o *Options) { + o.HistoryClient = hc + } +} diff --git a/services/activitylog/pkg/service/response.go b/services/activitylog/pkg/service/response.go new file mode 100644 index 00000000000..f09d6cfff13 --- /dev/null +++ b/services/activitylog/pkg/service/response.go @@ -0,0 +1,67 @@ +package service + +import ( + "time" + + "github.com/cs3org/reva/v2/pkg/events" + "github.com/cs3org/reva/v2/pkg/storagespace" +) + +// GetActivitiesResponse is the response on GET activities requests +type GetActivitiesResponse struct { + Activities []Activity `json:"value"` +} + +// Activity represents an activity as it is returned to the client +type Activity struct { + ID string `json:"id"` + + // TODO: Implement these + Action interface{} `json:"action"` + DriveItem Resource `json:"driveItem"` + Actor Actor `json:"actor"` + Times Times `json:"times"` + + Template Template `json:"template"` +} + +// Resource represents an item such as a file or folder +type Resource struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// Actor represents the user who performed the Action +type Actor struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` +} + +// Times represents the timestamps of the Activity +type Times struct { + RecordedTime time.Time `json:"recordedTime"` +} + +// Template contains activity details +type Template struct { + Message string `json:"message"` + Variables map[string]interface{} `json:"variables"` +} + +// UploadReady converts a UploadReady events to an Activity +func UploadReady(eid string, e events.UploadReady) Activity { + rid, _ := storagespace.FormatReference(e.FileRef) + res := Resource{ + ID: rid, + Name: e.Filename, + } + return Activity{ + ID: eid, + Template: Template{ + Message: "file created", + Variables: map[string]interface{}{ + "resource": res, + }, + }, + } +} diff --git a/services/activitylog/pkg/service/service.go b/services/activitylog/pkg/service/service.go index 4f2c0cf23db..00b50c81465 100644 --- a/services/activitylog/pkg/service/service.go +++ b/services/activitylog/pkg/service/service.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "path/filepath" + "reflect" "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -13,13 +14,15 @@ import ( "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" + "github.com/go-chi/chi/v5" "github.com/owncloud/ocis/v2/ocis-pkg/log" + ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" "github.com/owncloud/ocis/v2/services/activitylog/pkg/config" microstore "go-micro.dev/v4/store" ) -// Activity represents an activity -type Activity struct { +// RawActivity represents an activity as it is stored in the activitylog store +type RawActivity struct { EventID string `json:"event_id"` Depth int `json:"depth"` Timestamp time.Time `json:"timestamp"` @@ -27,11 +30,15 @@ type Activity struct { // ActivitylogService logs events per resource type ActivitylogService struct { - cfg *config.Config - log log.Logger - events <-chan events.Event - store microstore.Store - gws pool.Selectable[gateway.GatewayAPIClient] + cfg *config.Config + log log.Logger + events <-chan events.Event + store microstore.Store + gws pool.Selectable[gateway.GatewayAPIClient] + mux *chi.Mux + evHistory ehsvc.EventHistoryService + + registeredEvents map[string]events.Unmarshaller } // New creates a new ActivitylogService @@ -55,13 +62,27 @@ func New(opts ...Option) (*ActivitylogService, error) { } s := &ActivitylogService{ - log: o.Logger, - cfg: o.Config, - events: ch, - store: o.Store, - gws: o.GatewaySelector, + log: o.Logger, + cfg: o.Config, + events: ch, + store: o.Store, + gws: o.GatewaySelector, + mux: o.Mux, + evHistory: o.HistoryClient, + registeredEvents: make(map[string]events.Unmarshaller), } + s.mux.Route("/graph/v1.0/drives/{drive-id}", func(r chi.Router) { + r.Get("/items/{item-id}/activities", s.HandleGetItemActivities) + }) + + for _, e := range o.RegisteredEvents { + typ := reflect.TypeOf(e) + s.registeredEvents[typ.String()] = e + } + + go s.Run() + return s, nil } @@ -155,7 +176,7 @@ func (a *ActivitylogService) AddActivityTrashed(resourceID *provider.ResourceId, } // Activities returns the activities for the given resource -func (a *ActivitylogService) Activities(rid *provider.ResourceId) ([]Activity, error) { +func (a *ActivitylogService) Activities(rid *provider.ResourceId) ([]RawActivity, error) { resourceID := storagespace.FormatResourceID(*rid) records, err := a.store.Read(resourceID) @@ -164,10 +185,10 @@ func (a *ActivitylogService) Activities(rid *provider.ResourceId) ([]Activity, e } if len(records) == 0 { - return []Activity{}, nil + return []RawActivity{}, nil } - var activities []Activity + var activities []RawActivity if err := json.Unmarshal(records[0].Value, &activities); err != nil { return nil, fmt.Errorf("could not unmarshal activities: %w", err) } @@ -214,7 +235,7 @@ func (a *ActivitylogService) storeActivity(rid *provider.ResourceId, eventID str return err } - var activities []Activity + var activities []RawActivity if len(records) > 0 { if err := json.Unmarshal(records[0].Value, &activities); err != nil { return err @@ -222,7 +243,7 @@ func (a *ActivitylogService) storeActivity(rid *provider.ResourceId, eventID str } // TODO: max len check? - activities = append(activities, Activity{ + activities = append(activities, RawActivity{ EventID: eventID, Depth: depth, Timestamp: timestamp, diff --git a/services/eventhistory/pkg/config/defaults/defaultconfig.go b/services/eventhistory/pkg/config/defaults/defaultconfig.go index 141817568d3..fbcb7da441a 100644 --- a/services/eventhistory/pkg/config/defaults/defaultconfig.go +++ b/services/eventhistory/pkg/config/defaults/defaultconfig.go @@ -33,9 +33,10 @@ func DefaultConfig() *config.Config { EnableTLS: false, }, Store: config.Store{ - Store: "memory", + Store: "nats-js-kv", + Nodes: []string{"127.0.0.1:9233"}, Database: "eventhistory", - Table: "events", + Table: "", TTL: 336 * time.Hour, }, GRPC: config.GRPCConfig{ diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index c638012ac47..e73fe14e9ba 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -236,6 +236,17 @@ func DefaultPolicies() []config.Policy { Endpoint: "/app/", // /app or /apps? ocdav only handles /apps Service: "com.owncloud.web.frontend", }, + // reroute activities endpoint to activitylog service + // { + // Type: config.RegexRoute, + // Endpoint: "/graph/v1.0/drives/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/items/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/activities", + // Service: "com.owncloud.web.activitylog", + // }, + { + Type: config.RegexRoute, + Endpoint: "/graph/v1.0/drives/[^/]+/items/[^/]+/activities", + Service: "com.owncloud.web.activitylog", + }, { Endpoint: "/graph/v1.0/invitations", Service: "com.owncloud.web.invitations", From 1fb31178b620a1e4659ed57ea64980d6606af184 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Wed, 12 Jun 2024 15:19:09 +0200 Subject: [PATCH 02/13] feat(activitylog): adjust response format Signed-off-by: jkoberg --- services/activitylog/pkg/service/http.go | 30 +++++--- services/activitylog/pkg/service/response.go | 75 +++++++++++++++----- services/eventhistory/pkg/service/service.go | 4 ++ 3 files changed, 80 insertions(+), 29 deletions(-) diff --git a/services/activitylog/pkg/service/http.go b/services/activitylog/pkg/service/http.go index 5636ec24322..81548b3ff9a 100644 --- a/services/activitylog/pkg/service/http.go +++ b/services/activitylog/pkg/service/http.go @@ -2,13 +2,13 @@ package service import ( "encoding/json" - "fmt" "net/http" "net/url" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/storagespace" + "github.com/cs3org/reva/v2/pkg/utils" "github.com/go-chi/chi/v5" ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" @@ -43,8 +43,6 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h ids = append(ids, a.EventID) } - fmt.Println("IDS:", ids) - evRes, err := s.evHistory.GetEvents(r.Context(), &ehsvc.GetEventsRequest{Ids: ids}) if err != nil { s.log.Error().Err(err).Msg("error getting events") @@ -52,25 +50,35 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h return } - // TODO: compare returned events with initial list and remove missing ones - - fmt.Println("EVENTS:", evRes.GetEvents()) - var acts []Activity for _, e := range evRes.GetEvents() { + // TODO: compare returned events with initial list and remove missing ones + // FIXME: Should all users get all events? If not we can filter here + var ( + message string + res Resource + act Actor + ts Times + ) + switch ev := s.unwrapEvent(e).(type) { case nil: // error already logged in unwrapEvent continue case events.UploadReady: - act := UploadReady(e.Id, ev) - acts = append(acts, act) + message = "{user} created {resource}" + res, act, ts, err = s.ResponseData(ev.FileRef, ev.ExecutingUser.GetId(), ev.ExecutingUser.GetDisplayName(), utils.TSToTime(ev.Timestamp)) } - } - fmt.Println("ACTIVITIES:", acts) + if err != nil { + s.log.Error().Err(err).Msg("error getting response data") + continue + } + + acts = append(acts, NewActivity(message, res, act, ts.RecordedTime, e.GetId())) + } b, err := json.Marshal(acts) if err != nil { diff --git a/services/activitylog/pkg/service/response.go b/services/activitylog/pkg/service/response.go index f09d6cfff13..9b24564ee4c 100644 --- a/services/activitylog/pkg/service/response.go +++ b/services/activitylog/pkg/service/response.go @@ -3,8 +3,10 @@ package service import ( "time" - "github.com/cs3org/reva/v2/pkg/events" + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v2/pkg/storagespace" + "github.com/cs3org/reva/v2/pkg/utils" ) // GetActivitiesResponse is the response on GET activities requests @@ -14,15 +16,14 @@ type GetActivitiesResponse struct { // Activity represents an activity as it is returned to the client type Activity struct { - ID string `json:"id"` + ID string `json:"id"` + DriveItem Resource `json:"driveItem"` + Actor Actor `json:"actor"` + Times Times `json:"times"` + Template Template `json:"template"` - // TODO: Implement these - Action interface{} `json:"action"` - DriveItem Resource `json:"driveItem"` - Actor Actor `json:"actor"` - Times Times `json:"times"` - - Template Template `json:"template"` + // TODO: Implement + Action interface{} `json:"action"` } // Resource represents an item such as a file or folder @@ -48,20 +49,58 @@ type Template struct { Variables map[string]interface{} `json:"variables"` } -// UploadReady converts a UploadReady events to an Activity -func UploadReady(eid string, e events.UploadReady) Activity { - rid, _ := storagespace.FormatReference(e.FileRef) - res := Resource{ - ID: rid, - Name: e.Filename, - } +// NewActivity creates a new activity +func NewActivity(message string, res Resource, user Actor, ts time.Time, eventID string) Activity { return Activity{ - ID: eid, + ID: eventID, + Times: Times{ + RecordedTime: ts, + }, + DriveItem: res, + Actor: user, Template: Template{ - Message: "file created", + Message: message, Variables: map[string]interface{}{ "resource": res, + "user": user, }, }, } } + +// ResponseData returns the relevant response data for the activity +func (s *ActivitylogService) ResponseData(ref *provider.Reference, uid *user.UserId, username string, ts time.Time) (Resource, Actor, Times, error) { + gwc, err := s.gws.Next() + if err != nil { + return Resource{}, Actor{}, Times{}, err + } + + ctx, err := utils.GetServiceUserContext(s.cfg.ServiceAccount.ServiceAccountID, gwc, s.cfg.ServiceAccount.ServiceAccountSecret) + if err != nil { + return Resource{}, Actor{}, Times{}, err + } + + info, err := utils.GetResource(ctx, ref, gwc) + if err != nil { + return Resource{}, Actor{}, Times{}, err + } + + if username == "" { + u, err := utils.GetUser(uid, gwc) + if err != nil { + return Resource{}, Actor{}, Times{}, err + } + username = u.GetUsername() + } + + return Resource{ + ID: storagespace.FormatResourceID(*info.Id), + Name: info.Path, + }, Actor{ + ID: uid.GetOpaqueId(), + DisplayName: username, + }, Times{ + RecordedTime: ts, + }, nil + +} diff --git a/services/eventhistory/pkg/service/service.go b/services/eventhistory/pkg/service/service.go index 848fc4d9b48..ba2a492986e 100644 --- a/services/eventhistory/pkg/service/service.go +++ b/services/eventhistory/pkg/service/service.go @@ -130,6 +130,10 @@ func (eh *EventHistoryService) getEvent(id string) (*ehmsg.Event, error) { return nil, err } + if len(evs) == 0 { + return nil, store.ErrNotFound + } + var ev StoreEvent if err := json.Unmarshal(evs[0].Value, &ev); err != nil { eh.log.Error().Err(err).Str("eventid", id).Msg("could not unmarshal event") From 72f4dc90601fecab564fa14f373f811ad52ad887 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Thu, 13 Jun 2024 16:02:05 +0200 Subject: [PATCH 03/13] feat(activitylog): add missing events Signed-off-by: jkoberg --- services/activitylog/pkg/service/http.go | 54 +++++++++++++++++-- services/activitylog/pkg/service/response.go | 13 ++--- .../activitylog/pkg/service/service_test.go | 18 +++---- 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/services/activitylog/pkg/service/http.go b/services/activitylog/pkg/service/http.go index 81548b3ff9a..a39b11645da 100644 --- a/services/activitylog/pkg/service/http.go +++ b/services/activitylog/pkg/service/http.go @@ -50,7 +50,7 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h return } - var acts []Activity + var resp GetActivitiesResponse for _, e := range evRes.GetEvents() { // TODO: compare returned events with initial list and remove missing ones @@ -70,6 +70,48 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h case events.UploadReady: message = "{user} created {resource}" res, act, ts, err = s.ResponseData(ev.FileRef, ev.ExecutingUser.GetId(), ev.ExecutingUser.GetDisplayName(), utils.TSToTime(ev.Timestamp)) + case events.FileTouched: + message = "{user} created {resource}" + res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) + case events.ContainerCreated: + message = "{user} created {resource}" + res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) + case events.ItemTrashed: + message = "{user} trashed {resource}" + res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) + case events.ItemPurged: + message = "{user} purged {resource}" + res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) + case events.ItemMoved: + message = "{user} moved {resource}" + res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) + case events.ShareCreated: + message = "{user} shared {resource}" + res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) + case events.ShareUpdated: + message = "{user} updated share of {resource}" + res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.MTime)) + case events.ShareRemoved: + message = "{user} removed share of {resource}" + res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", ev.Timestamp) + case events.LinkCreated: + message = "{user} created link to {resource}" + res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) + case events.LinkUpdated: + message = "{user} updated link to {resource}" + res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) + case events.LinkRemoved: + message = "{user} removed link to {resource}" + res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.Timestamp)) + case events.SpaceShared: + message = "{user} shared space {resource}" + res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) + case events.SpaceShareUpdated: + message = "{user} updated share of space {resource}" + res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) + case events.SpaceUnshared: + message = "{user} unshared space {resource}" + res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) } if err != nil { @@ -77,17 +119,21 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h continue } - acts = append(acts, NewActivity(message, res, act, ts.RecordedTime, e.GetId())) + resp.Activities = append(resp.Activities, NewActivity(message, res, act, ts.RecordedTime, e.GetId())) } - b, err := json.Marshal(acts) + b, err := json.Marshal(resp) if err != nil { s.log.Error().Err(err).Msg("error marshalling activities") w.WriteHeader(http.StatusInternalServerError) return } - w.Write(b) + if _, err := w.Write(b); err != nil { + s.log.Error().Err(err).Msg("error writing response") + w.WriteHeader(http.StatusInternalServerError) + return + } w.WriteHeader(http.StatusOK) } diff --git a/services/activitylog/pkg/service/response.go b/services/activitylog/pkg/service/response.go index 9b24564ee4c..44273145e7d 100644 --- a/services/activitylog/pkg/service/response.go +++ b/services/activitylog/pkg/service/response.go @@ -16,14 +16,9 @@ type GetActivitiesResponse struct { // Activity represents an activity as it is returned to the client type Activity struct { - ID string `json:"id"` - DriveItem Resource `json:"driveItem"` - Actor Actor `json:"actor"` - Times Times `json:"times"` - Template Template `json:"template"` - - // TODO: Implement - Action interface{} `json:"action"` + ID string `json:"id"` + Times Times `json:"times"` + Template Template `json:"template"` } // Resource represents an item such as a file or folder @@ -56,8 +51,6 @@ func NewActivity(message string, res Resource, user Actor, ts time.Time, eventID Times: Times{ RecordedTime: ts, }, - DriveItem: res, - Actor: user, Template: Template{ Message: message, Variables: map[string]interface{}{ diff --git a/services/activitylog/pkg/service/service_test.go b/services/activitylog/pkg/service/service_test.go index 55b2f62716f..2aa9b92a211 100644 --- a/services/activitylog/pkg/service/service_test.go +++ b/services/activitylog/pkg/service/service_test.go @@ -14,7 +14,7 @@ func TestAddActivity(t *testing.T) { Name string Tree map[string]*provider.ResourceInfo Activities map[string]string - Expected map[string][]Activity + Expected map[string][]RawActivity }{ { Name: "simple", @@ -26,7 +26,7 @@ func TestAddActivity(t *testing.T) { Activities: map[string]string{ "activity": "base", }, - Expected: map[string][]Activity{ + Expected: map[string][]RawActivity{ "base": activitites("activity", 0), "parent": activitites("activity", 1), "spaceid": activitites("activity", 2), @@ -43,7 +43,7 @@ func TestAddActivity(t *testing.T) { "activity1": "base", "activity2": "base", }, - Expected: map[string][]Activity{ + Expected: map[string][]RawActivity{ "base": activitites("activity1", 0, "activity2", 0), "parent": activitites("activity1", 1, "activity2", 1), "spaceid": activitites("activity1", 2, "activity2", 2), @@ -61,7 +61,7 @@ func TestAddActivity(t *testing.T) { "activity1": "base1", "activity2": "base2", }, - Expected: map[string][]Activity{ + Expected: map[string][]RawActivity{ "base1": activitites("activity1", 0), "base2": activitites("activity2", 0), "parent": activitites("activity1", 1, "activity2", 1), @@ -83,7 +83,7 @@ func TestAddActivity(t *testing.T) { "activity2": "base2", "activity3": "base3", }, - Expected: map[string][]Activity{ + Expected: map[string][]RawActivity{ "base1": activitites("activity1", 0), "base2": activitites("activity2", 0), "base3": activitites("activity3", 0), @@ -109,7 +109,7 @@ func TestAddActivity(t *testing.T) { "activity3": "base3", "activity4": "parent2", }, - Expected: map[string][]Activity{ + Expected: map[string][]RawActivity{ "base1": activitites("activity1", 0), "base2": activitites("activity2", 0), "base3": activitites("activity3", 0), @@ -143,9 +143,9 @@ func TestAddActivity(t *testing.T) { } } -func activitites(acts ...interface{}) []Activity { - var activities []Activity - act := Activity{} +func activitites(acts ...interface{}) []RawActivity { + var activities []RawActivity + act := RawActivity{} for _, a := range acts { switch v := a.(type) { case string: From 5249cbc1388fa138e2c42bdfb85da727dfd17747 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Fri, 14 Jun 2024 15:44:42 +0200 Subject: [PATCH 04/13] feat(activitylog): translations Signed-off-by: jkoberg --- ocis-pkg/l10n/l10n.go | 3 + services/activitylog/Makefile | 24 ++++++++ services/activitylog/pkg/command/server.go | 5 +- .../activitylog/pkg/server/http/option.go | 13 ++++- .../activitylog/pkg/server/http/server.go | 1 + services/activitylog/pkg/service/http.go | 56 +++++++++++++------ .../pkg/service/l10n/locale/tmp.txt | 0 .../activitylog/pkg/service/l10n/userlog.pot | 22 ++++++++ services/activitylog/pkg/service/options.go | 9 +++ services/activitylog/pkg/service/response.go | 26 +++++++-- services/activitylog/pkg/service/service.go | 17 +++--- 11 files changed, 145 insertions(+), 31 deletions(-) create mode 100644 services/activitylog/pkg/service/l10n/locale/tmp.txt create mode 100644 services/activitylog/pkg/service/l10n/userlog.pot diff --git a/ocis-pkg/l10n/l10n.go b/ocis-pkg/l10n/l10n.go index f9b89528f1a..5c99c399a7d 100644 --- a/ocis-pkg/l10n/l10n.go +++ b/ocis-pkg/l10n/l10n.go @@ -14,6 +14,9 @@ import ( micrometadata "go-micro.dev/v4/metadata" ) +// HeaderAcceptLanguage is the header key for the accept-language header +var HeaderAcceptLanguage = "Accept-Language" + // Template marks a string as translatable func Template(s string) string { return s } diff --git a/services/activitylog/Makefile b/services/activitylog/Makefile index dd41e6af723..8d4cc0e1582 100644 --- a/services/activitylog/Makefile +++ b/services/activitylog/Makefile @@ -1,6 +1,10 @@ SHELL := bash NAME := activitylog +# Where to write the files generated by this makefile. +OUTPUT_DIR = ./pkg/service/l10n +TEMPLATE_FILE = ./pkg/service/l10n/activitylog.pot + include ../../.make/recursion.mk ############ tooling ############ @@ -29,6 +33,26 @@ ci-go-generate: # CI runs ci-node-generate automatically before this target .PHONY: ci-node-generate ci-node-generate: +############ translations ######## +.PHONY: l10n-pull +l10n-pull: + cd $(OUTPUT_DIR) && tx pull --all --force --skip --minimum-perc=75 + +.PHONY: l10n-push +l10n-push: + cd $(OUTPUT_DIR) && tx push -s --skip + +.PHONY: l10n-read +l10n-read: $(GO_XGETTEXT) + go-xgettext -o $(OUTPUT_DIR)/userlog.pot --keyword=l10n.Template -s pkg/service/response.go + +.PHONY: l10n-write +l10n-write: + +.PHONY: l10n-clean +l10n-clean: + rm -f $(TEMPLATE_FILE); + ############ licenses ############ .PHONY: ci-node-check-licenses ci-node-check-licenses: diff --git a/services/activitylog/pkg/command/server.go b/services/activitylog/pkg/command/server.go index f80c7b4b84c..50e4a05d6f2 100644 --- a/services/activitylog/pkg/command/server.go +++ b/services/activitylog/pkg/command/server.go @@ -18,6 +18,7 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/tracing" "github.com/owncloud/ocis/v2/ocis-pkg/version" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" + settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/v2/services/activitylog/pkg/config" "github.com/owncloud/ocis/v2/services/activitylog/pkg/config/parser" "github.com/owncloud/ocis/v2/services/activitylog/pkg/logging" @@ -119,6 +120,7 @@ func Server(cfg *config.Config) *cli.Command { } hClient := ehsvc.NewEventHistoryService("com.owncloud.api.eventhistory", grpcClient) + vClient := settingssvc.NewValueService("com.owncloud.api.settings", grpcClient) { svc, err := http.Server( @@ -130,7 +132,8 @@ func Server(cfg *config.Config) *cli.Command { http.RegisteredEvents(_registeredEvents), http.Store(evStore), http.GatewaySelector(gatewaySelector), - http.History(hClient), + http.HistoryClient(hClient), + http.ValueClient(vClient), http.RegisteredEvents(_registeredEvents), ) diff --git a/services/activitylog/pkg/server/http/option.go b/services/activitylog/pkg/server/http/option.go index 5d415be3f63..760cb131b3a 100644 --- a/services/activitylog/pkg/server/http/option.go +++ b/services/activitylog/pkg/server/http/option.go @@ -8,6 +8,7 @@ import ( "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/owncloud/ocis/v2/ocis-pkg/log" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" + settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/v2/services/activitylog/pkg/config" "github.com/owncloud/ocis/v2/services/activitylog/pkg/metrics" "github.com/urfave/cli/v2" @@ -31,6 +32,7 @@ type Options struct { GatewaySelector pool.Selectable[gateway.GatewayAPIClient] TraceProvider trace.TracerProvider HistoryClient ehsvc.EventHistoryService + ValueClient settingssvc.ValueService RegisteredEvents []events.Unmarshaller } @@ -108,8 +110,8 @@ func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) } } -// History provides a function to configure the event history client -func History(h ehsvc.EventHistoryService) Option { +// HistoryClient provides a function to configure the event history client +func HistoryClient(h ehsvc.EventHistoryService) Option { return func(o *Options) { o.HistoryClient = h } @@ -128,3 +130,10 @@ func TraceProvider(val trace.TracerProvider) Option { o.TraceProvider = val } } + +// ValueClient provides a function to set the ValueClient options +func ValueClient(val settingssvc.ValueService) Option { + return func(o *Options) { + o.ValueClient = val + } +} diff --git a/services/activitylog/pkg/server/http/server.go b/services/activitylog/pkg/server/http/server.go index 455f2125c68..86a239b2873 100644 --- a/services/activitylog/pkg/server/http/server.go +++ b/services/activitylog/pkg/server/http/server.go @@ -86,6 +86,7 @@ func Server(opts ...Option) (http.Service, error) { svc.GatewaySelector(options.GatewaySelector), svc.TraceProvider(options.TraceProvider), svc.HistoryClient(options.HistoryClient), + svc.ValueClient(options.ValueClient), svc.RegisteredEvents(options.RegisteredEvents), ) if err != nil { diff --git a/services/activitylog/pkg/service/http.go b/services/activitylog/pkg/service/http.go index a39b11645da..6cf8ba551d1 100644 --- a/services/activitylog/pkg/service/http.go +++ b/services/activitylog/pkg/service/http.go @@ -1,20 +1,34 @@ package service import ( + "embed" "encoding/json" "net/http" "net/url" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + revactx "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" "github.com/go-chi/chi/v5" + "github.com/owncloud/ocis/v2/ocis-pkg/l10n" ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" ) +var ( + //go:embed l10n/locale + _localeFS embed.FS + + // subfolder where the translation files are stored + _localeSubPath = "l10n/locale" + + // domain of the activitylog service (transifex) + _domain = "activitylog" +) + // ServeHTTP implements the http.Handler interface. func (s *ActivitylogService) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) @@ -22,6 +36,12 @@ func (s *ActivitylogService) ServeHTTP(w http.ResponseWriter, r *http.Request) { // HandleGetItemActivities handles the request to get the activities of an item. func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *http.Request) { + activeUser, ok := revactx.ContextGetUser(r.Context()) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + // TODO: Compare driveid with itemid to avoid bad requests rid, err := parseIDParam(r, "item-id") if err != nil { @@ -68,49 +88,49 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h // error already logged in unwrapEvent continue case events.UploadReady: - message = "{user} created {resource}" + message = MessageResourceCreated res, act, ts, err = s.ResponseData(ev.FileRef, ev.ExecutingUser.GetId(), ev.ExecutingUser.GetDisplayName(), utils.TSToTime(ev.Timestamp)) case events.FileTouched: - message = "{user} created {resource}" + message = MessageResourceCreated res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.ContainerCreated: - message = "{user} created {resource}" + message = MessageResourceCreated res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.ItemTrashed: - message = "{user} trashed {resource}" + message = MessageResourceTrashed res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.ItemPurged: - message = "{user} purged {resource}" + message = MessageResourcePurged res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.ItemMoved: - message = "{user} moved {resource}" + message = MessageResourceMoved res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.ShareCreated: - message = "{user} shared {resource}" + message = MessageShareCreated res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) case events.ShareUpdated: - message = "{user} updated share of {resource}" + message = MessageShareUpdated res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.MTime)) case events.ShareRemoved: - message = "{user} removed share of {resource}" + message = MessageShareDeleted res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", ev.Timestamp) case events.LinkCreated: - message = "{user} created link to {resource}" + message = MessageLinkCreated res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) case events.LinkUpdated: - message = "{user} updated link to {resource}" + message = MessageLinkUpdated res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) case events.LinkRemoved: - message = "{user} removed link to {resource}" + message = MessageLinkDeleted res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.Timestamp)) case events.SpaceShared: - message = "{user} shared space {resource}" + message = MessageSpaceShared res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) case events.SpaceShareUpdated: - message = "{user} updated share of space {resource}" + message = MessageSpaceShareUpdated res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) case events.SpaceUnshared: - message = "{user} unshared space {resource}" + message = MessageSpaceUnshared res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) } @@ -119,7 +139,11 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h continue } - resp.Activities = append(resp.Activities, NewActivity(message, res, act, ts.RecordedTime, e.GetId())) + // todo: configurable default locale? + loc := l10n.MustGetUserLocale(r.Context(), activeUser.GetId().GetOpaqueId(), r.Header.Get(l10n.HeaderAcceptLanguage), s.valService) + t := l10n.NewTranslatorFromCommonConfig("en", _domain, "", _localeFS, _localeSubPath) + + resp.Activities = append(resp.Activities, NewActivity(t.Translate(message, loc), res, act, ts, e.GetId())) } b, err := json.Marshal(resp) diff --git a/services/activitylog/pkg/service/l10n/locale/tmp.txt b/services/activitylog/pkg/service/l10n/locale/tmp.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/activitylog/pkg/service/l10n/userlog.pot b/services/activitylog/pkg/service/l10n/userlog.pot new file mode 100644 index 00000000000..606d31510c8 --- /dev/null +++ b/services/activitylog/pkg/service/l10n/userlog.pot @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "Project-Id-Version: \n" + "Report-Msgid-Bugs-To: EMAIL\n" + "POT-Creation-Date: 2024-06-14 15:29+0200\n" + "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" + "Last-Translator: FULL NAME \n" + "Language-Team: LANGUAGE \n" + "Language: \n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=CHARSET\n" + "Content-Transfer-Encoding: 8bit\n" + +#: pkg/service/response.go:15 +msgid "{user} created {resource}" +msgstr "" + diff --git a/services/activitylog/pkg/service/options.go b/services/activitylog/pkg/service/options.go index 8c9fef3fe3e..0aa97abfc56 100644 --- a/services/activitylog/pkg/service/options.go +++ b/services/activitylog/pkg/service/options.go @@ -7,6 +7,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/owncloud/ocis/v2/ocis-pkg/log" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" + settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/v2/services/activitylog/pkg/config" microstore "go-micro.dev/v4/store" "go.opentelemetry.io/otel/trace" @@ -26,6 +27,7 @@ type Options struct { GatewaySelector pool.Selectable[gateway.GatewayAPIClient] Mux *chi.Mux HistoryClient ehsvc.EventHistoryService + ValueClient settingssvc.ValueService } // Logger configures a logger for the activitylog service @@ -90,3 +92,10 @@ func HistoryClient(hc ehsvc.EventHistoryService) Option { o.HistoryClient = hc } } + +// ValueClient adds a grpc client for the value service +func ValueClient(vs settingssvc.ValueService) Option { + return func(o *Options) { + o.ValueClient = vs + } +} diff --git a/services/activitylog/pkg/service/response.go b/services/activitylog/pkg/service/response.go index 44273145e7d..ce185cc2064 100644 --- a/services/activitylog/pkg/service/response.go +++ b/services/activitylog/pkg/service/response.go @@ -7,6 +7,24 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" + "github.com/owncloud/ocis/v2/ocis-pkg/l10n" +) + +// Translations +var ( + MessageResourceCreated = l10n.Template("{user} created {resource}") + MessageResourceTrashed = l10n.Template("{user} trashed {resource}") + MessageResourcePurged = l10n.Template("{user} purged {resource}") + MessageResourceMoved = l10n.Template("{user} moved {resource}") + MessageShareCreated = l10n.Template("{user} shared {resource}") + MessageShareUpdated = l10n.Template("{user} updated share of {resource}") + MessageShareDeleted = l10n.Template("{user} deleted share of {resource}") + MessageLinkCreated = l10n.Template("{user} created link to {resource}") + MessageLinkUpdated = l10n.Template("{user} updated link to {resource}") + MessageLinkDeleted = l10n.Template("{user} deleted link to {resource}") + MessageSpaceShared = l10n.Template("{user} shared space {resource}") + MessageSpaceShareUpdated = l10n.Template("{user} updated share of space {resource}") + MessageSpaceUnshared = l10n.Template("{user} unshared space {resource}") ) // GetActivitiesResponse is the response on GET activities requests @@ -45,12 +63,10 @@ type Template struct { } // NewActivity creates a new activity -func NewActivity(message string, res Resource, user Actor, ts time.Time, eventID string) Activity { +func NewActivity(message string, res Resource, user Actor, ts Times, eventID string) Activity { return Activity{ - ID: eventID, - Times: Times{ - RecordedTime: ts, - }, + ID: eventID, + Times: ts, Template: Template{ Message: message, Variables: map[string]interface{}{ diff --git a/services/activitylog/pkg/service/service.go b/services/activitylog/pkg/service/service.go index 00b50c81465..9cfbb7ff28f 100644 --- a/services/activitylog/pkg/service/service.go +++ b/services/activitylog/pkg/service/service.go @@ -17,6 +17,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/owncloud/ocis/v2/ocis-pkg/log" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" + settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/v2/services/activitylog/pkg/config" microstore "go-micro.dev/v4/store" ) @@ -30,13 +31,14 @@ type RawActivity struct { // ActivitylogService logs events per resource type ActivitylogService struct { - cfg *config.Config - log log.Logger - events <-chan events.Event - store microstore.Store - gws pool.Selectable[gateway.GatewayAPIClient] - mux *chi.Mux - evHistory ehsvc.EventHistoryService + cfg *config.Config + log log.Logger + events <-chan events.Event + store microstore.Store + gws pool.Selectable[gateway.GatewayAPIClient] + mux *chi.Mux + evHistory ehsvc.EventHistoryService + valService settingssvc.ValueService registeredEvents map[string]events.Unmarshaller } @@ -69,6 +71,7 @@ func New(opts ...Option) (*ActivitylogService, error) { gws: o.GatewaySelector, mux: o.Mux, evHistory: o.HistoryClient, + valService: o.ValueClient, registeredEvents: make(map[string]events.Unmarshaller), } From 949c5d08484f61a72a2debf5bf709eb126d3cdaf Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Tue, 18 Jun 2024 12:13:13 +0200 Subject: [PATCH 05/13] enhancement(activitylog): enhance activitylog graph endpoint - make use of libregraph artifacts - add a basic activity kql ast parser --- services/activitylog/pkg/service/http.go | 36 ++++++++++++++-- services/activitylog/pkg/service/response.go | 42 ++++++------------- services/activitylog/pkg/service/service.go | 7 ++-- .../pkg/config/defaults/defaultconfig.go | 9 +--- 4 files changed, 50 insertions(+), 44 deletions(-) diff --git a/services/activitylog/pkg/service/http.go b/services/activitylog/pkg/service/http.go index 6cf8ba551d1..f574bfea857 100644 --- a/services/activitylog/pkg/service/http.go +++ b/services/activitylog/pkg/service/http.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "net/url" + "strings" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" revactx "github.com/cs3org/reva/v2/pkg/ctx" @@ -12,10 +13,14 @@ import ( "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" "github.com/go-chi/chi/v5" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/ocis-pkg/l10n" ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" + "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" + "github.com/owncloud/ocis/v2/services/search/pkg/query/kql" ) var ( @@ -42,8 +47,33 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h return } - // TODO: Compare driveid with itemid to avoid bad requests - rid, err := parseIDParam(r, "item-id") + qraw := r.URL.Query().Get("kql") + if qraw == "" { + w.WriteHeader(http.StatusBadRequest) + } + + qBuilder := kql.Builder{} + qast, err := qBuilder.Build(qraw) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + } + + var itemID string + + for _, n := range qast.Nodes { + v, ok := n.(*ast.StringNode) + if !ok { + continue + } + + if strings.ToLower(v.Key) != "itemid" { + continue + } + + itemID = v.Value + } + + rid, err := storagespace.ParseID(itemID) if err != nil { s.log.Info().Err(err).Msg("invalid resource id") w.WriteHeader(http.StatusBadRequest) @@ -80,7 +110,7 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h message string res Resource act Actor - ts Times + ts libregraph.ActivityTimes ) switch ev := s.unwrapEvent(e).(type) { diff --git a/services/activitylog/pkg/service/response.go b/services/activitylog/pkg/service/response.go index ce185cc2064..9186d1d3e82 100644 --- a/services/activitylog/pkg/service/response.go +++ b/services/activitylog/pkg/service/response.go @@ -7,6 +7,8 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/ocis-pkg/l10n" ) @@ -29,14 +31,7 @@ var ( // GetActivitiesResponse is the response on GET activities requests type GetActivitiesResponse struct { - Activities []Activity `json:"value"` -} - -// Activity represents an activity as it is returned to the client -type Activity struct { - ID string `json:"id"` - Times Times `json:"times"` - Template Template `json:"template"` + Activities []libregraph.Activity `json:"value"` } // Resource represents an item such as a file or folder @@ -51,23 +46,12 @@ type Actor struct { DisplayName string `json:"displayName"` } -// Times represents the timestamps of the Activity -type Times struct { - RecordedTime time.Time `json:"recordedTime"` -} - -// Template contains activity details -type Template struct { - Message string `json:"message"` - Variables map[string]interface{} `json:"variables"` -} - // NewActivity creates a new activity -func NewActivity(message string, res Resource, user Actor, ts Times, eventID string) Activity { - return Activity{ - ID: eventID, +func NewActivity(message string, res Resource, user Actor, ts libregraph.ActivityTimes, eventID string) libregraph.Activity { + return libregraph.Activity{ + Id: eventID, Times: ts, - Template: Template{ + Template: libregraph.ActivityTemplate{ Message: message, Variables: map[string]interface{}{ "resource": res, @@ -78,26 +62,26 @@ func NewActivity(message string, res Resource, user Actor, ts Times, eventID str } // ResponseData returns the relevant response data for the activity -func (s *ActivitylogService) ResponseData(ref *provider.Reference, uid *user.UserId, username string, ts time.Time) (Resource, Actor, Times, error) { +func (s *ActivitylogService) ResponseData(ref *provider.Reference, uid *user.UserId, username string, ts time.Time) (Resource, Actor, libregraph.ActivityTimes, error) { gwc, err := s.gws.Next() if err != nil { - return Resource{}, Actor{}, Times{}, err + return Resource{}, Actor{}, libregraph.ActivityTimes{}, err } ctx, err := utils.GetServiceUserContext(s.cfg.ServiceAccount.ServiceAccountID, gwc, s.cfg.ServiceAccount.ServiceAccountSecret) if err != nil { - return Resource{}, Actor{}, Times{}, err + return Resource{}, Actor{}, libregraph.ActivityTimes{}, err } info, err := utils.GetResource(ctx, ref, gwc) if err != nil { - return Resource{}, Actor{}, Times{}, err + return Resource{}, Actor{}, libregraph.ActivityTimes{}, err } if username == "" { u, err := utils.GetUser(uid, gwc) if err != nil { - return Resource{}, Actor{}, Times{}, err + return Resource{}, Actor{}, libregraph.ActivityTimes{}, err } username = u.GetUsername() } @@ -108,7 +92,7 @@ func (s *ActivitylogService) ResponseData(ref *provider.Reference, uid *user.Use }, Actor{ ID: uid.GetOpaqueId(), DisplayName: username, - }, Times{ + }, libregraph.ActivityTimes{ RecordedTime: ts, }, nil diff --git a/services/activitylog/pkg/service/service.go b/services/activitylog/pkg/service/service.go index 9cfbb7ff28f..03624740593 100644 --- a/services/activitylog/pkg/service/service.go +++ b/services/activitylog/pkg/service/service.go @@ -15,11 +15,12 @@ import ( "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" "github.com/go-chi/chi/v5" + microstore "go-micro.dev/v4/store" + "github.com/owncloud/ocis/v2/ocis-pkg/log" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/v2/services/activitylog/pkg/config" - microstore "go-micro.dev/v4/store" ) // RawActivity represents an activity as it is stored in the activitylog store @@ -75,9 +76,7 @@ func New(opts ...Option) (*ActivitylogService, error) { registeredEvents: make(map[string]events.Unmarshaller), } - s.mux.Route("/graph/v1.0/drives/{drive-id}", func(r chi.Router) { - r.Get("/items/{item-id}/activities", s.HandleGetItemActivities) - }) + s.mux.Get("/graph/v1beta1/extensions/org.libregraph/activities", s.HandleGetItemActivities) for _, e := range o.RegisteredEvents { typ := reflect.TypeOf(e) diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index e73fe14e9ba..af2093522b3 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -236,15 +236,8 @@ func DefaultPolicies() []config.Policy { Endpoint: "/app/", // /app or /apps? ocdav only handles /apps Service: "com.owncloud.web.frontend", }, - // reroute activities endpoint to activitylog service - // { - // Type: config.RegexRoute, - // Endpoint: "/graph/v1.0/drives/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/items/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/activities", - // Service: "com.owncloud.web.activitylog", - // }, { - Type: config.RegexRoute, - Endpoint: "/graph/v1.0/drives/[^/]+/items/[^/]+/activities", + Endpoint: "/graph/v1beta1/extensions/org.libregraph/activities", Service: "com.owncloud.web.activitylog", }, { From ca9192cf36aad2d5495c1b5eb870b7fb22fde5bc Mon Sep 17 00:00:00 2001 From: jkoberg Date: Tue, 18 Jun 2024 15:41:18 +0200 Subject: [PATCH 06/13] feat(activitylog): allow filtering activities Signed-off-by: jkoberg --- services/activitylog/pkg/config/debug.go | 8 +- services/activitylog/pkg/config/log.go | 8 +- services/activitylog/pkg/config/tracing.go | 8 +- services/activitylog/pkg/service/http.go | 144 ++++++++++++++------ services/activitylog/pkg/service/service.go | 28 +++- 5 files changed, 139 insertions(+), 57 deletions(-) diff --git a/services/activitylog/pkg/config/debug.go b/services/activitylog/pkg/config/debug.go index 4f7fc486c6f..b1435d9d261 100644 --- a/services/activitylog/pkg/config/debug.go +++ b/services/activitylog/pkg/config/debug.go @@ -2,8 +2,8 @@ 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:"5.0"` - Token string `yaml:"token" env:"CLIENTLOG_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"5.0"` - Pprof bool `yaml:"pprof" env:"CLIENTLOG_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"5.0"` - Zpages bool `yaml:"zpages" env:"CLIENTLOG_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"5.0"` + Addr string `yaml:"addr" env:"ACTIVITYLOG_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"5.0"` + Token string `yaml:"token" env:"ACTIVITYLOG_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"5.0"` + Pprof bool `yaml:"pprof" env:"ACTIVITYLOG_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"5.0"` + Zpages bool `yaml:"zpages" env:"ACTIVITYLOG_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"5.0"` } diff --git a/services/activitylog/pkg/config/log.go b/services/activitylog/pkg/config/log.go index 6661076218f..fef52d10789 100644 --- a/services/activitylog/pkg/config/log.go +++ b/services/activitylog/pkg/config/log.go @@ -2,8 +2,8 @@ package config // Log defines the available log configuration. type Log struct { - Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;CLIENTLOG_USERLOG_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"5.0"` - Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;CLIENTLOG_USERLOG_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"5.0"` - Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;CLIENTLOG_USERLOG_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"5.0"` - File string `mapstructure:"file" env:"OCIS_LOG_FILE;CLIENTLOG_USERLOG_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"5.0"` + Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;ACTIVITYLOG_USERLOG_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"5.0"` + Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;ACTIVITYLOG_USERLOG_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"5.0"` + Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;ACTIVITYLOG_USERLOG_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"5.0"` + File string `mapstructure:"file" env:"OCIS_LOG_FILE;ACTIVITYLOG_USERLOG_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"5.0"` } diff --git a/services/activitylog/pkg/config/tracing.go b/services/activitylog/pkg/config/tracing.go index f4320a0860a..f39dabd38ac 100644 --- a/services/activitylog/pkg/config/tracing.go +++ b/services/activitylog/pkg/config/tracing.go @@ -4,10 +4,10 @@ import "github.com/owncloud/ocis/v2/ocis-pkg/tracing" // Tracing defines the available tracing configuration. type Tracing struct { - Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;CLIENTLOG_TRACING_ENABLED" desc:"Activates tracing." introductionVersion:"5.0"` - Type string `yaml:"type" env:"OCIS_TRACING_TYPE;CLIENTLOG_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now." introductionVersion:"5.0"` - Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;CLIENTLOG_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent." introductionVersion:"5.0"` - Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;CLIENTLOG_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset." introductionVersion:"5.0"` + Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;ACTIVITYLOG_TRACING_ENABLED" desc:"Activates tracing." introductionVersion:"5.0"` + Type string `yaml:"type" env:"OCIS_TRACING_TYPE;ACTIVITYLOG_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now." introductionVersion:"5.0"` + Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;ACTIVITYLOG_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent." introductionVersion:"5.0"` + Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;ACTIVITYLOG_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset." introductionVersion:"5.0"` } // Convert Tracing to the tracing package's Config struct. diff --git a/services/activitylog/pkg/service/http.go b/services/activitylog/pkg/service/http.go index f574bfea857..39dbbb26839 100644 --- a/services/activitylog/pkg/service/http.go +++ b/services/activitylog/pkg/service/http.go @@ -3,8 +3,9 @@ package service import ( "embed" "encoding/json" + "errors" "net/http" - "net/url" + "strconv" "strings" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" @@ -12,13 +13,11 @@ import ( "github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" - "github.com/go-chi/chi/v5" libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/v2/ocis-pkg/l10n" ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" - "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" "github.com/owncloud/ocis/v2/services/search/pkg/query/kql" ) @@ -47,40 +46,15 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h return } - qraw := r.URL.Query().Get("kql") - if qraw == "" { - w.WriteHeader(http.StatusBadRequest) - } - - qBuilder := kql.Builder{} - qast, err := qBuilder.Build(qraw) + rid, limit, rawActivityAccepted, activityAccepted, err := s.getFilters(r.URL.Query().Get("kql")) if err != nil { - w.WriteHeader(http.StatusBadRequest) - } - - var itemID string - - for _, n := range qast.Nodes { - v, ok := n.(*ast.StringNode) - if !ok { - continue - } - - if strings.ToLower(v.Key) != "itemid" { - continue - } - - itemID = v.Value - } - - rid, err := storagespace.ParseID(itemID) - if err != nil { - s.log.Info().Err(err).Msg("invalid resource id") + s.log.Info().Str("query", r.URL.Query().Get("kql")).Err(err).Msg("error getting filters") + _, _ = w.Write([]byte(err.Error())) w.WriteHeader(http.StatusBadRequest) return } - raw, err := s.Activities(&rid) + raw, err := s.Activities(rid) if err != nil { s.log.Error().Err(err).Msg("error getting activities") w.WriteHeader(http.StatusInternalServerError) @@ -88,9 +62,13 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h } ids := make([]string, 0, len(raw)) + toDelete := make(map[string]struct{}, len(raw)) for _, a := range raw { - // TODO: Filter by depth and timestamp + if !rawActivityAccepted(a) { + continue + } ids = append(ids, a.EventID) + toDelete[a.EventID] = struct{}{} } evRes, err := s.evHistory.GetEvents(r.Context(), &ehsvc.GetEventsRequest{Ids: ids}) @@ -102,9 +80,15 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h var resp GetActivitiesResponse for _, e := range evRes.GetEvents() { - // TODO: compare returned events with initial list and remove missing ones + delete(toDelete, e.GetId()) - // FIXME: Should all users get all events? If not we can filter here + if limit != 0 && len(resp.Activities) >= limit { + continue + } + + if !activityAccepted(e) { + continue + } var ( message string @@ -169,13 +153,23 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h continue } - // todo: configurable default locale? + // FIXME: configurable default locale? loc := l10n.MustGetUserLocale(r.Context(), activeUser.GetId().GetOpaqueId(), r.Header.Get(l10n.HeaderAcceptLanguage), s.valService) t := l10n.NewTranslatorFromCommonConfig("en", _domain, "", _localeFS, _localeSubPath) resp.Activities = append(resp.Activities, NewActivity(t.Translate(message, loc), res, act, ts, e.GetId())) } + // delete activities in separate go routine + if len(toDelete) > 0 { + go func() { + err := s.RemoveActivities(rid, toDelete) + if err != nil { + s.log.Error().Err(err).Msg("error removing activities") + } + }() + } + b, err := json.Marshal(resp) if err != nil { s.log.Error().Err(err).Msg("error marshalling activities") @@ -207,16 +201,80 @@ func (s *ActivitylogService) unwrapEvent(e *ehmsg.Event) interface{} { return einterface } -// TODO: I found this on graph service. We should move it to `utils` pkg so both services can use it. -func parseIDParam(r *http.Request, param string) (provider.ResourceId, error) { - driveID, err := url.PathUnescape(chi.URLParam(r, param)) +func (s *ActivitylogService) getFilters(query string) (*provider.ResourceId, int, func(RawActivity) bool, func(*ehmsg.Event) bool, error) { + qast, err := kql.Builder{}.Build(query) if err != nil { - return provider.ResourceId{}, errorcode.New(errorcode.InvalidRequest, err.Error()) + return nil, 0, nil, nil, err } - id, err := storagespace.ParseID(driveID) + prefilters := make([]func(RawActivity) bool, 0) + postfilters := make([]func(*ehmsg.Event) bool, 0) + + var ( + itemID string + limit int + ) + + for _, n := range qast.Nodes { + switch v := n.(type) { + case *ast.StringNode: + switch strings.ToLower(v.Key) { + case "itemid": + itemID = v.Value + case "depth": + depth, err := strconv.Atoi(v.Value) + if err != nil { + return nil, limit, nil, nil, err + } + + prefilters = append(prefilters, func(a RawActivity) bool { + return a.Depth <= depth + }) + case "limit": + l, err := strconv.Atoi(v.Value) + if err != nil { + return nil, limit, nil, nil, err + } + + limit = l + } + case *ast.DateTimeNode: + switch v.Operator.Value { + case "<", "<=": + prefilters = append(prefilters, func(a RawActivity) bool { + return a.Timestamp.Before(v.Value) + }) + case ">", ">=": + prefilters = append(prefilters, func(a RawActivity) bool { + return a.Timestamp.After(v.Value) + }) + } + case *ast.OperatorNode: + if v.Value != "AND" { + return nil, limit, nil, nil, errors.New("only AND operator is supported") + } + } + } + + rid, err := storagespace.ParseID(itemID) if err != nil { - return provider.ResourceId{}, errorcode.New(errorcode.InvalidRequest, err.Error()) + return nil, limit, nil, nil, err + } + pref := func(a RawActivity) bool { + for _, f := range prefilters { + if !f(a) { + return false + } + } + return true + } + postf := func(e *ehmsg.Event) bool { + for _, f := range postfilters { + if !f(e) { + return false + } + } + return true } - return id, nil + return &rid, limit, pref, postf, nil } diff --git a/services/activitylog/pkg/service/service.go b/services/activitylog/pkg/service/service.go index 03624740593..2908b636d85 100644 --- a/services/activitylog/pkg/service/service.go +++ b/services/activitylog/pkg/service/service.go @@ -89,7 +89,7 @@ func New(opts ...Option) (*ActivitylogService, error) { } // Run runs the service -func (a *ActivitylogService) Run() error { +func (a *ActivitylogService) Run() { for e := range a.events { var err error switch ev := e.Event.(type) { @@ -129,7 +129,6 @@ func (a *ActivitylogService) Run() error { a.log.Error().Err(err).Interface("event", e).Msg("could not process event") } } - return nil } // AddActivity adds the activity to the given resource and all its parents @@ -198,6 +197,31 @@ func (a *ActivitylogService) Activities(rid *provider.ResourceId) ([]RawActivity return activities, nil } +// RemoveActivities removes the activities from the given resource +func (a *ActivitylogService) RemoveActivities(rid *provider.ResourceId, toDelete map[string]struct{}) error { + curActivities, err := a.Activities(rid) + if err != nil { + return err + } + + var acts []RawActivity + for _, a := range curActivities { + if _, ok := toDelete[a.EventID]; !ok { + acts = append(acts, a) + } + } + + b, err := json.Marshal(acts) + if err != nil { + return err + } + + return a.store.Write(µstore.Record{ + Key: storagespace.FormatResourceID(*rid), + Value: b, + }) +} + // note: getResource is abstracted to allow unit testing, in general this will just be utils.GetResource func (a *ActivitylogService) addActivity(initRef *provider.Reference, eventID string, timestamp time.Time, getResource func(*provider.Reference) (*provider.ResourceInfo, error)) error { var ( From 0d604dfb9b515146a749c03c21f2d25f5467b675 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Wed, 19 Jun 2024 10:43:25 +0200 Subject: [PATCH 07/13] feat(ocis): bump libre-graph-api-go Signed-off-by: jkoberg --- go.mod | 2 +- go.sum | 4 +- .../owncloud/libre-graph-api-go/README.md | 5 + .../libre-graph-api-go/api_activities.go | 137 +++++++++++ .../owncloud/libre-graph-api-go/client.go | 3 + .../libre-graph-api-go/model_activity.go | 213 ++++++++++++++++++ .../model_activity_template.go | 194 ++++++++++++++++ .../model_activity_times.go | 158 +++++++++++++ .../model_collection_of_activities.go | 124 ++++++++++ vendor/modules.txt | 2 +- 10 files changed, 838 insertions(+), 4 deletions(-) create mode 100644 vendor/github.com/owncloud/libre-graph-api-go/api_activities.go create mode 100644 vendor/github.com/owncloud/libre-graph-api-go/model_activity.go create mode 100644 vendor/github.com/owncloud/libre-graph-api-go/model_activity_template.go create mode 100644 vendor/github.com/owncloud/libre-graph-api-go/model_activity_times.go create mode 100644 vendor/github.com/owncloud/libre-graph-api-go/model_collection_of_activities.go diff --git a/go.mod b/go.mod index b8792fbb062..20a0e946ca8 100644 --- a/go.mod +++ b/go.mod @@ -72,7 +72,7 @@ require ( github.com/onsi/gomega v1.33.1 github.com/open-policy-agent/opa v0.65.0 github.com/orcaman/concurrent-map v1.0.0 - github.com/owncloud/libre-graph-api-go v1.0.5-0.20240529101512-a631b9eeddb3 + github.com/owncloud/libre-graph-api-go v1.0.5-0.20240618162722-2298241331d1 github.com/pkg/errors v0.9.1 github.com/pkg/xattr v0.4.9 github.com/prometheus/client_golang v1.19.1 diff --git a/go.sum b/go.sum index 99a2eec780f..a016609a2ed 100644 --- a/go.sum +++ b/go.sum @@ -1805,8 +1805,8 @@ github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35uk github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA= -github.com/owncloud/libre-graph-api-go v1.0.5-0.20240529101512-a631b9eeddb3 h1:2pMI03tU/IWc+Y4p1doTaTE1zrAR55RYOzYrt57/aPA= -github.com/owncloud/libre-graph-api-go v1.0.5-0.20240529101512-a631b9eeddb3/go.mod h1:yXI+rmE8yYx+ZsGVrnCpprw/gZMcxjwntnX2y2+VKxY= +github.com/owncloud/libre-graph-api-go v1.0.5-0.20240618162722-2298241331d1 h1:w1OhLyFevK8NCYn50TEsDpInk/T6qVk/v1LDca6Zx8Y= +github.com/owncloud/libre-graph-api-go v1.0.5-0.20240618162722-2298241331d1/go.mod h1:yXI+rmE8yYx+ZsGVrnCpprw/gZMcxjwntnX2y2+VKxY= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pablodz/inotifywaitgo v0.0.6 h1:BTjQfnixXwG7oYmlIiyhWA6iyO9BtxatB3YgiibOTFc= diff --git a/vendor/github.com/owncloud/libre-graph-api-go/README.md b/vendor/github.com/owncloud/libre-graph-api-go/README.md index d1e630879d9..c6ceee69464 100644 --- a/vendor/github.com/owncloud/libre-graph-api-go/README.md +++ b/vendor/github.com/owncloud/libre-graph-api-go/README.md @@ -78,6 +78,7 @@ All URIs are relative to *https://ocis.ocis-traefik.latest.owncloud.works/graph* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- +*ActivitiesApi* | [**GetActivities**](docs/ActivitiesApi.md#getactivities) | **Get** /v1beta1/extensions/org.libregraph/activities | Get activities *ApplicationsApi* | [**GetApplication**](docs/ApplicationsApi.md#getapplication) | **Get** /v1.0/applications/{application-id} | Get application by id *ApplicationsApi* | [**ListApplications**](docs/ApplicationsApi.md#listapplications) | **Get** /v1.0/applications | Get all applications *DriveItemApi* | [**DeleteDriveItem**](docs/DriveItemApi.md#deletedriveitem) | **Delete** /v1beta1/drives/{drive-id}/items/{item-id} | Delete a DriveItem. @@ -168,6 +169,9 @@ Class | Method | HTTP request | Description ## Documentation For Models + - [Activity](docs/Activity.md) + - [ActivityTemplate](docs/ActivityTemplate.md) + - [ActivityTimes](docs/ActivityTimes.md) - [AppRole](docs/AppRole.md) - [AppRoleAssignment](docs/AppRoleAssignment.md) - [Application](docs/Application.md) @@ -175,6 +179,7 @@ Class | Method | HTTP request | Description - [ClassMemberReference](docs/ClassMemberReference.md) - [ClassReference](docs/ClassReference.md) - [ClassTeacherReference](docs/ClassTeacherReference.md) + - [CollectionOfActivities](docs/CollectionOfActivities.md) - [CollectionOfAppRoleAssignments](docs/CollectionOfAppRoleAssignments.md) - [CollectionOfApplications](docs/CollectionOfApplications.md) - [CollectionOfClass](docs/CollectionOfClass.md) diff --git a/vendor/github.com/owncloud/libre-graph-api-go/api_activities.go b/vendor/github.com/owncloud/libre-graph-api-go/api_activities.go new file mode 100644 index 00000000000..7b859a7b25e --- /dev/null +++ b/vendor/github.com/owncloud/libre-graph-api-go/api_activities.go @@ -0,0 +1,137 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.4 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" +) + +// ActivitiesApiService ActivitiesApi service +type ActivitiesApiService service + +type ApiGetActivitiesRequest struct { + ctx context.Context + ApiService *ActivitiesApiService + kql *string +} + +func (r ApiGetActivitiesRequest) Kql(kql string) ApiGetActivitiesRequest { + r.kql = &kql + return r +} + +func (r ApiGetActivitiesRequest) Execute() (*CollectionOfActivities, *http.Response, error) { + return r.ApiService.GetActivitiesExecute(r) +} + +/* +GetActivities Get activities + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiGetActivitiesRequest +*/ +func (a *ActivitiesApiService) GetActivities(ctx context.Context) ApiGetActivitiesRequest { + return ApiGetActivitiesRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return CollectionOfActivities +func (a *ActivitiesApiService) GetActivitiesExecute(r ApiGetActivitiesRequest) (*CollectionOfActivities, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *CollectionOfActivities + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ActivitiesApiService.GetActivities") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1beta1/extensions/org.libregraph/activities" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.kql != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "kql", r.kql, "") + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v OdataError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/vendor/github.com/owncloud/libre-graph-api-go/client.go b/vendor/github.com/owncloud/libre-graph-api-go/client.go index fcfbb7b7842..285f371c16b 100644 --- a/vendor/github.com/owncloud/libre-graph-api-go/client.go +++ b/vendor/github.com/owncloud/libre-graph-api-go/client.go @@ -48,6 +48,8 @@ type APIClient struct { // API Services + ActivitiesApi *ActivitiesApiService + ApplicationsApi *ApplicationsApiService DriveItemApi *DriveItemApiService @@ -111,6 +113,7 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.common.client = c // API Services + c.ActivitiesApi = (*ActivitiesApiService)(&c.common) c.ApplicationsApi = (*ApplicationsApiService)(&c.common) c.DriveItemApi = (*DriveItemApiService)(&c.common) c.DrivesApi = (*DrivesApiService)(&c.common) diff --git a/vendor/github.com/owncloud/libre-graph-api-go/model_activity.go b/vendor/github.com/owncloud/libre-graph-api-go/model_activity.go new file mode 100644 index 00000000000..74d4b8590a0 --- /dev/null +++ b/vendor/github.com/owncloud/libre-graph-api-go/model_activity.go @@ -0,0 +1,213 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.4 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the Activity type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Activity{} + +// Activity Represents activity. +type Activity struct { + // Activity ID. + Id string `json:"id"` + Times ActivityTimes `json:"times"` + Template ActivityTemplate `json:"template"` +} + +type _Activity Activity + +// NewActivity instantiates a new Activity object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewActivity(id string, times ActivityTimes, template ActivityTemplate) *Activity { + this := Activity{} + this.Id = id + this.Times = times + this.Template = template + return &this +} + +// NewActivityWithDefaults instantiates a new Activity object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewActivityWithDefaults() *Activity { + this := Activity{} + return &this +} + +// GetId returns the Id field value +func (o *Activity) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *Activity) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *Activity) SetId(v string) { + o.Id = v +} + +// GetTimes returns the Times field value +func (o *Activity) GetTimes() ActivityTimes { + if o == nil { + var ret ActivityTimes + return ret + } + + return o.Times +} + +// GetTimesOk returns a tuple with the Times field value +// and a boolean to check if the value has been set. +func (o *Activity) GetTimesOk() (*ActivityTimes, bool) { + if o == nil { + return nil, false + } + return &o.Times, true +} + +// SetTimes sets field value +func (o *Activity) SetTimes(v ActivityTimes) { + o.Times = v +} + +// GetTemplate returns the Template field value +func (o *Activity) GetTemplate() ActivityTemplate { + if o == nil { + var ret ActivityTemplate + return ret + } + + return o.Template +} + +// GetTemplateOk returns a tuple with the Template field value +// and a boolean to check if the value has been set. +func (o *Activity) GetTemplateOk() (*ActivityTemplate, bool) { + if o == nil { + return nil, false + } + return &o.Template, true +} + +// SetTemplate sets field value +func (o *Activity) SetTemplate(v ActivityTemplate) { + o.Template = v +} + +func (o Activity) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Activity) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + toSerialize["times"] = o.Times + toSerialize["template"] = o.Template + return toSerialize, nil +} + +func (o *Activity) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "times", + "template", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varActivity := _Activity{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varActivity) + + if err != nil { + return err + } + + *o = Activity(varActivity) + + return err +} + +type NullableActivity struct { + value *Activity + isSet bool +} + +func (v NullableActivity) Get() *Activity { + return v.value +} + +func (v *NullableActivity) Set(val *Activity) { + v.value = val + v.isSet = true +} + +func (v NullableActivity) IsSet() bool { + return v.isSet +} + +func (v *NullableActivity) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableActivity(val *Activity) *NullableActivity { + return &NullableActivity{value: val, isSet: true} +} + +func (v NullableActivity) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableActivity) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/vendor/github.com/owncloud/libre-graph-api-go/model_activity_template.go b/vendor/github.com/owncloud/libre-graph-api-go/model_activity_template.go new file mode 100644 index 00000000000..75bcadb899b --- /dev/null +++ b/vendor/github.com/owncloud/libre-graph-api-go/model_activity_template.go @@ -0,0 +1,194 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.4 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the ActivityTemplate type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ActivityTemplate{} + +// ActivityTemplate struct for ActivityTemplate +type ActivityTemplate struct { + // Activity description. + Message string `json:"message"` + // Activity description variables. + Variables map[string]interface{} `json:"variables,omitempty"` +} + +type _ActivityTemplate ActivityTemplate + +// NewActivityTemplate instantiates a new ActivityTemplate object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewActivityTemplate(message string) *ActivityTemplate { + this := ActivityTemplate{} + this.Message = message + return &this +} + +// NewActivityTemplateWithDefaults instantiates a new ActivityTemplate object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewActivityTemplateWithDefaults() *ActivityTemplate { + this := ActivityTemplate{} + return &this +} + +// GetMessage returns the Message field value +func (o *ActivityTemplate) GetMessage() string { + if o == nil { + var ret string + return ret + } + + return o.Message +} + +// GetMessageOk returns a tuple with the Message field value +// and a boolean to check if the value has been set. +func (o *ActivityTemplate) GetMessageOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Message, true +} + +// SetMessage sets field value +func (o *ActivityTemplate) SetMessage(v string) { + o.Message = v +} + +// GetVariables returns the Variables field value if set, zero value otherwise. +func (o *ActivityTemplate) GetVariables() map[string]interface{} { + if o == nil || IsNil(o.Variables) { + var ret map[string]interface{} + return ret + } + return o.Variables +} + +// GetVariablesOk returns a tuple with the Variables field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ActivityTemplate) GetVariablesOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Variables) { + return map[string]interface{}{}, false + } + return o.Variables, true +} + +// HasVariables returns a boolean if a field has been set. +func (o *ActivityTemplate) HasVariables() bool { + if o != nil && !IsNil(o.Variables) { + return true + } + + return false +} + +// SetVariables gets a reference to the given map[string]interface{} and assigns it to the Variables field. +func (o *ActivityTemplate) SetVariables(v map[string]interface{}) { + o.Variables = v +} + +func (o ActivityTemplate) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ActivityTemplate) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["message"] = o.Message + if !IsNil(o.Variables) { + toSerialize["variables"] = o.Variables + } + return toSerialize, nil +} + +func (o *ActivityTemplate) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "message", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varActivityTemplate := _ActivityTemplate{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varActivityTemplate) + + if err != nil { + return err + } + + *o = ActivityTemplate(varActivityTemplate) + + return err +} + +type NullableActivityTemplate struct { + value *ActivityTemplate + isSet bool +} + +func (v NullableActivityTemplate) Get() *ActivityTemplate { + return v.value +} + +func (v *NullableActivityTemplate) Set(val *ActivityTemplate) { + v.value = val + v.isSet = true +} + +func (v NullableActivityTemplate) IsSet() bool { + return v.isSet +} + +func (v *NullableActivityTemplate) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableActivityTemplate(val *ActivityTemplate) *NullableActivityTemplate { + return &NullableActivityTemplate{value: val, isSet: true} +} + +func (v NullableActivityTemplate) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableActivityTemplate) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/vendor/github.com/owncloud/libre-graph-api-go/model_activity_times.go b/vendor/github.com/owncloud/libre-graph-api-go/model_activity_times.go new file mode 100644 index 00000000000..d6970a37559 --- /dev/null +++ b/vendor/github.com/owncloud/libre-graph-api-go/model_activity_times.go @@ -0,0 +1,158 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.4 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "bytes" + "encoding/json" + "fmt" + "time" +) + +// checks if the ActivityTimes type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ActivityTimes{} + +// ActivityTimes struct for ActivityTimes +type ActivityTimes struct { + // Timestamp of the activity. + RecordedTime time.Time `json:"recordedTime"` +} + +type _ActivityTimes ActivityTimes + +// NewActivityTimes instantiates a new ActivityTimes object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewActivityTimes(recordedTime time.Time) *ActivityTimes { + this := ActivityTimes{} + this.RecordedTime = recordedTime + return &this +} + +// NewActivityTimesWithDefaults instantiates a new ActivityTimes object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewActivityTimesWithDefaults() *ActivityTimes { + this := ActivityTimes{} + return &this +} + +// GetRecordedTime returns the RecordedTime field value +func (o *ActivityTimes) GetRecordedTime() time.Time { + if o == nil { + var ret time.Time + return ret + } + + return o.RecordedTime +} + +// GetRecordedTimeOk returns a tuple with the RecordedTime field value +// and a boolean to check if the value has been set. +func (o *ActivityTimes) GetRecordedTimeOk() (*time.Time, bool) { + if o == nil { + return nil, false + } + return &o.RecordedTime, true +} + +// SetRecordedTime sets field value +func (o *ActivityTimes) SetRecordedTime(v time.Time) { + o.RecordedTime = v +} + +func (o ActivityTimes) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ActivityTimes) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["recordedTime"] = o.RecordedTime + return toSerialize, nil +} + +func (o *ActivityTimes) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "recordedTime", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varActivityTimes := _ActivityTimes{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varActivityTimes) + + if err != nil { + return err + } + + *o = ActivityTimes(varActivityTimes) + + return err +} + +type NullableActivityTimes struct { + value *ActivityTimes + isSet bool +} + +func (v NullableActivityTimes) Get() *ActivityTimes { + return v.value +} + +func (v *NullableActivityTimes) Set(val *ActivityTimes) { + v.value = val + v.isSet = true +} + +func (v NullableActivityTimes) IsSet() bool { + return v.isSet +} + +func (v *NullableActivityTimes) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableActivityTimes(val *ActivityTimes) *NullableActivityTimes { + return &NullableActivityTimes{value: val, isSet: true} +} + +func (v NullableActivityTimes) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableActivityTimes) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/vendor/github.com/owncloud/libre-graph-api-go/model_collection_of_activities.go b/vendor/github.com/owncloud/libre-graph-api-go/model_collection_of_activities.go new file mode 100644 index 00000000000..3ebfd31fbb7 --- /dev/null +++ b/vendor/github.com/owncloud/libre-graph-api-go/model_collection_of_activities.go @@ -0,0 +1,124 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.4 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the CollectionOfActivities type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CollectionOfActivities{} + +// CollectionOfActivities struct for CollectionOfActivities +type CollectionOfActivities struct { + Value []Activity `json:"value,omitempty"` +} + +// NewCollectionOfActivities instantiates a new CollectionOfActivities object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewCollectionOfActivities() *CollectionOfActivities { + this := CollectionOfActivities{} + return &this +} + +// NewCollectionOfActivitiesWithDefaults instantiates a new CollectionOfActivities object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewCollectionOfActivitiesWithDefaults() *CollectionOfActivities { + this := CollectionOfActivities{} + return &this +} + +// GetValue returns the Value field value if set, zero value otherwise. +func (o *CollectionOfActivities) GetValue() []Activity { + if o == nil || IsNil(o.Value) { + var ret []Activity + return ret + } + return o.Value +} + +// GetValueOk returns a tuple with the Value field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CollectionOfActivities) GetValueOk() ([]Activity, bool) { + if o == nil || IsNil(o.Value) { + return nil, false + } + return o.Value, true +} + +// HasValue returns a boolean if a field has been set. +func (o *CollectionOfActivities) HasValue() bool { + if o != nil && !IsNil(o.Value) { + return true + } + + return false +} + +// SetValue gets a reference to the given []Activity and assigns it to the Value field. +func (o *CollectionOfActivities) SetValue(v []Activity) { + o.Value = v +} + +func (o CollectionOfActivities) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CollectionOfActivities) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Value) { + toSerialize["value"] = o.Value + } + return toSerialize, nil +} + +type NullableCollectionOfActivities struct { + value *CollectionOfActivities + isSet bool +} + +func (v NullableCollectionOfActivities) Get() *CollectionOfActivities { + return v.value +} + +func (v *NullableCollectionOfActivities) Set(val *CollectionOfActivities) { + v.value = val + v.isSet = true +} + +func (v NullableCollectionOfActivities) IsSet() bool { + return v.isSet +} + +func (v *NullableCollectionOfActivities) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCollectionOfActivities(val *CollectionOfActivities) *NullableCollectionOfActivities { + return &NullableCollectionOfActivities{value: val, isSet: true} +} + +func (v NullableCollectionOfActivities) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCollectionOfActivities) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index f94a8a8e08b..eee2a2cc1cb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1623,7 +1623,7 @@ github.com/opentracing/opentracing-go/log # github.com/orcaman/concurrent-map v1.0.0 ## explicit github.com/orcaman/concurrent-map -# github.com/owncloud/libre-graph-api-go v1.0.5-0.20240529101512-a631b9eeddb3 +# github.com/owncloud/libre-graph-api-go v1.0.5-0.20240618162722-2298241331d1 ## explicit; go 1.18 github.com/owncloud/libre-graph-api-go # github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c From 7a819412c272060f5342a135c2c7dca2aea3a6c9 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Wed, 19 Jun 2024 12:01:02 +0200 Subject: [PATCH 08/13] feat(ocis): move ast and kql pkg to ocis-pkg Signed-off-by: jkoberg --- {services/search/pkg/query => ocis-pkg}/ast/ast.go | 0 {services/search/pkg/query => ocis-pkg}/ast/test/test.go | 3 +-- {services/search/pkg/query => ocis-pkg}/kql/cast.go | 3 +-- {services/search/pkg/query => ocis-pkg}/kql/connect.go | 2 +- .../search/pkg/query => ocis-pkg}/kql/dictionary.peg | 0 .../search/pkg/query => ocis-pkg}/kql/dictionary_gen.go | 0 .../search/pkg/query => ocis-pkg}/kql/dictionary_test.go | 7 +++---- {services/search/pkg/query => ocis-pkg}/kql/doc.go | 0 .../pkg/query => ocis-pkg}/kql/engine_suite_test.go | 0 {services/search/pkg/query => ocis-pkg}/kql/factory.go | 2 +- {services/search/pkg/query => ocis-pkg}/kql/gen.go | 0 {services/search/pkg/query => ocis-pkg}/kql/kql.go | 2 +- {services/search/pkg/query => ocis-pkg}/kql/kql_test.go | 5 ++--- {services/search/pkg/query => ocis-pkg}/kql/validate.go | 2 +- services/activitylog/Makefile | 2 +- services/activitylog/pkg/config/log.go | 8 ++++---- services/activitylog/pkg/service/http.go | 4 ++-- services/search/Makefile | 2 +- services/search/pkg/query/bleve/bleve.go | 2 +- services/search/pkg/query/bleve/compiler.go | 4 ++-- services/search/pkg/query/bleve/compiler_test.go | 3 +-- services/search/pkg/query/error.go | 2 +- services/search/pkg/query/query.go | 4 +--- 23 files changed, 25 insertions(+), 32 deletions(-) rename {services/search/pkg/query => ocis-pkg}/ast/ast.go (100%) rename {services/search/pkg/query => ocis-pkg}/ast/test/test.go (92%) rename {services/search/pkg/query => ocis-pkg}/kql/cast.go (97%) rename {services/search/pkg/query => ocis-pkg}/kql/connect.go (98%) rename {services/search/pkg/query => ocis-pkg}/kql/dictionary.peg (100%) rename {services/search/pkg/query => ocis-pkg}/kql/dictionary_gen.go (100%) rename {services/search/pkg/query => ocis-pkg}/kql/dictionary_test.go (99%) rename {services/search/pkg/query => ocis-pkg}/kql/doc.go (100%) rename {services/search/pkg/query => ocis-pkg}/kql/engine_suite_test.go (100%) rename {services/search/pkg/query => ocis-pkg}/kql/factory.go (98%) rename {services/search/pkg/query => ocis-pkg}/kql/gen.go (100%) rename {services/search/pkg/query => ocis-pkg}/kql/kql.go (94%) rename {services/search/pkg/query => ocis-pkg}/kql/kql_test.go (88%) rename {services/search/pkg/query => ocis-pkg}/kql/validate.go (91%) diff --git a/services/search/pkg/query/ast/ast.go b/ocis-pkg/ast/ast.go similarity index 100% rename from services/search/pkg/query/ast/ast.go rename to ocis-pkg/ast/ast.go diff --git a/services/search/pkg/query/ast/test/test.go b/ocis-pkg/ast/test/test.go similarity index 92% rename from services/search/pkg/query/ast/test/test.go rename to ocis-pkg/ast/test/test.go index 01a260de0ea..946856f8b30 100644 --- a/services/search/pkg/query/ast/test/test.go +++ b/ocis-pkg/ast/test/test.go @@ -4,8 +4,7 @@ package test import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" + "github.com/owncloud/ocis/v2/ocis-pkg/ast" ) // DiffAst returns a human-readable report of the differences between two values diff --git a/services/search/pkg/query/kql/cast.go b/ocis-pkg/kql/cast.go similarity index 97% rename from services/search/pkg/query/kql/cast.go rename to ocis-pkg/kql/cast.go index c6ca30541ac..55033eb1ff0 100644 --- a/services/search/pkg/query/kql/cast.go +++ b/ocis-pkg/kql/cast.go @@ -5,9 +5,8 @@ import ( "time" "github.com/jinzhu/now" + "github.com/owncloud/ocis/v2/ocis-pkg/ast" "github.com/owncloud/ocis/v2/services/search/pkg/query" - - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" ) func toNode[T ast.Node](in interface{}) (T, error) { diff --git a/services/search/pkg/query/kql/connect.go b/ocis-pkg/kql/connect.go similarity index 98% rename from services/search/pkg/query/kql/connect.go rename to ocis-pkg/kql/connect.go index 3c926d688ce..674a9156183 100644 --- a/services/search/pkg/query/kql/connect.go +++ b/ocis-pkg/kql/connect.go @@ -3,7 +3,7 @@ package kql import ( "strings" - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" + "github.com/owncloud/ocis/v2/ocis-pkg/ast" ) // connectNodes connects given nodes diff --git a/services/search/pkg/query/kql/dictionary.peg b/ocis-pkg/kql/dictionary.peg similarity index 100% rename from services/search/pkg/query/kql/dictionary.peg rename to ocis-pkg/kql/dictionary.peg diff --git a/services/search/pkg/query/kql/dictionary_gen.go b/ocis-pkg/kql/dictionary_gen.go similarity index 100% rename from services/search/pkg/query/kql/dictionary_gen.go rename to ocis-pkg/kql/dictionary_gen.go diff --git a/services/search/pkg/query/kql/dictionary_test.go b/ocis-pkg/kql/dictionary_test.go similarity index 99% rename from services/search/pkg/query/kql/dictionary_test.go rename to ocis-pkg/kql/dictionary_test.go index adf4bd2d45e..812f831df79 100644 --- a/services/search/pkg/query/kql/dictionary_test.go +++ b/ocis-pkg/kql/dictionary_test.go @@ -6,12 +6,11 @@ import ( "time" "github.com/jinzhu/now" + "github.com/owncloud/ocis/v2/ocis-pkg/ast" + "github.com/owncloud/ocis/v2/ocis-pkg/ast/test" + "github.com/owncloud/ocis/v2/ocis-pkg/kql" "github.com/owncloud/ocis/v2/services/search/pkg/query" tAssert "github.com/stretchr/testify/assert" - - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast/test" - "github.com/owncloud/ocis/v2/services/search/pkg/query/kql" ) func TestParse_Spec(t *testing.T) { diff --git a/services/search/pkg/query/kql/doc.go b/ocis-pkg/kql/doc.go similarity index 100% rename from services/search/pkg/query/kql/doc.go rename to ocis-pkg/kql/doc.go diff --git a/services/search/pkg/query/kql/engine_suite_test.go b/ocis-pkg/kql/engine_suite_test.go similarity index 100% rename from services/search/pkg/query/kql/engine_suite_test.go rename to ocis-pkg/kql/engine_suite_test.go diff --git a/services/search/pkg/query/kql/factory.go b/ocis-pkg/kql/factory.go similarity index 98% rename from services/search/pkg/query/kql/factory.go rename to ocis-pkg/kql/factory.go index 0eb858dd97a..99deef825d8 100644 --- a/services/search/pkg/query/kql/factory.go +++ b/ocis-pkg/kql/factory.go @@ -3,7 +3,7 @@ package kql import ( "strings" - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" + "github.com/owncloud/ocis/v2/ocis-pkg/ast" ) func base(text []byte, pos position) (*ast.Base, error) { diff --git a/services/search/pkg/query/kql/gen.go b/ocis-pkg/kql/gen.go similarity index 100% rename from services/search/pkg/query/kql/gen.go rename to ocis-pkg/kql/gen.go diff --git a/services/search/pkg/query/kql/kql.go b/ocis-pkg/kql/kql.go similarity index 94% rename from services/search/pkg/query/kql/kql.go rename to ocis-pkg/kql/kql.go index 2cf03a7f066..6e14cc7b6f5 100644 --- a/services/search/pkg/query/kql/kql.go +++ b/ocis-pkg/kql/kql.go @@ -5,7 +5,7 @@ import ( "errors" "time" - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" + "github.com/owncloud/ocis/v2/ocis-pkg/ast" ) // The operator node value definition diff --git a/services/search/pkg/query/kql/kql_test.go b/ocis-pkg/kql/kql_test.go similarity index 88% rename from services/search/pkg/query/kql/kql_test.go rename to ocis-pkg/kql/kql_test.go index c130f755820..09f1249c5bb 100644 --- a/services/search/pkg/query/kql/kql_test.go +++ b/ocis-pkg/kql/kql_test.go @@ -3,11 +3,10 @@ package kql_test import ( "testing" + "github.com/owncloud/ocis/v2/ocis-pkg/ast" + "github.com/owncloud/ocis/v2/ocis-pkg/kql" "github.com/owncloud/ocis/v2/services/search/pkg/query" tAssert "github.com/stretchr/testify/assert" - - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" - "github.com/owncloud/ocis/v2/services/search/pkg/query/kql" ) func TestNewAST(t *testing.T) { diff --git a/services/search/pkg/query/kql/validate.go b/ocis-pkg/kql/validate.go similarity index 91% rename from services/search/pkg/query/kql/validate.go rename to ocis-pkg/kql/validate.go index 96c6761ab17..0712a1a7ecc 100644 --- a/services/search/pkg/query/kql/validate.go +++ b/ocis-pkg/kql/validate.go @@ -1,8 +1,8 @@ package kql import ( + "github.com/owncloud/ocis/v2/ocis-pkg/ast" "github.com/owncloud/ocis/v2/services/search/pkg/query" - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" ) func validateAst(a *ast.Ast) error { diff --git a/services/activitylog/Makefile b/services/activitylog/Makefile index 8d4cc0e1582..f57e85e66fa 100644 --- a/services/activitylog/Makefile +++ b/services/activitylog/Makefile @@ -44,7 +44,7 @@ l10n-push: .PHONY: l10n-read l10n-read: $(GO_XGETTEXT) - go-xgettext -o $(OUTPUT_DIR)/userlog.pot --keyword=l10n.Template -s pkg/service/response.go + go-xgettext -o $(OUTPUT_DIR)/activitylog.pot --keyword=l10n.Template -s pkg/service/response.go .PHONY: l10n-write l10n-write: diff --git a/services/activitylog/pkg/config/log.go b/services/activitylog/pkg/config/log.go index fef52d10789..6b19a620411 100644 --- a/services/activitylog/pkg/config/log.go +++ b/services/activitylog/pkg/config/log.go @@ -2,8 +2,8 @@ package config // Log defines the available log configuration. type Log struct { - Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;ACTIVITYLOG_USERLOG_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"5.0"` - Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;ACTIVITYLOG_USERLOG_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"5.0"` - Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;ACTIVITYLOG_USERLOG_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"5.0"` - File string `mapstructure:"file" env:"OCIS_LOG_FILE;ACTIVITYLOG_USERLOG_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"5.0"` + Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;ACTIVITYLOG_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"5.0"` + Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;ACTIVITYLOG_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"5.0"` + Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;ACTIVITYLOG_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"5.0"` + File string `mapstructure:"file" env:"OCIS_LOG_FILE;ACTIVITYLOG_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"5.0"` } diff --git a/services/activitylog/pkg/service/http.go b/services/activitylog/pkg/service/http.go index 39dbbb26839..73aeda0ebf4 100644 --- a/services/activitylog/pkg/service/http.go +++ b/services/activitylog/pkg/service/http.go @@ -15,11 +15,11 @@ import ( "github.com/cs3org/reva/v2/pkg/utils" libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/ocis-pkg/ast" + "github.com/owncloud/ocis/v2/ocis-pkg/kql" "github.com/owncloud/ocis/v2/ocis-pkg/l10n" ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0" ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0" - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" - "github.com/owncloud/ocis/v2/services/search/pkg/query/kql" ) var ( diff --git a/services/search/Makefile b/services/search/Makefile index bc58a2eb79f..a55dd6fed7f 100644 --- a/services/search/Makefile +++ b/services/search/Makefile @@ -26,7 +26,7 @@ include ../../.make/generate.mk .PHONY: ci-go-generate ci-go-generate: $(PIGEON) $(MOCKERY) # CI runs ci-node-generate automatically before this target $(MOCKERY) - $(PIGEON) -optimize-grammar -optimize-parser -o pkg/query/kql/dictionary_gen.go pkg/query/kql/dictionary.peg + $(PIGEON) -optimize-grammar -optimize-parser -o ../../ocis-pkg/kql/dictionary_gen.go ../../ocis-pkg/kql/dictionary.peg .PHONY: ci-node-generate ci-node-generate: diff --git a/services/search/pkg/query/bleve/bleve.go b/services/search/pkg/query/bleve/bleve.go index e720b35f70e..b6e825e6eee 100644 --- a/services/search/pkg/query/bleve/bleve.go +++ b/services/search/pkg/query/bleve/bleve.go @@ -4,8 +4,8 @@ package bleve import ( bQuery "github.com/blevesearch/bleve/v2/search/query" + "github.com/owncloud/ocis/v2/ocis-pkg/kql" "github.com/owncloud/ocis/v2/services/search/pkg/query" - "github.com/owncloud/ocis/v2/services/search/pkg/query/kql" ) // Creator is combines a Builder and a Compiler which is used to Create the query. diff --git a/services/search/pkg/query/bleve/compiler.go b/services/search/pkg/query/bleve/compiler.go index 2d0eace100b..7046bee9de3 100644 --- a/services/search/pkg/query/bleve/compiler.go +++ b/services/search/pkg/query/bleve/compiler.go @@ -6,8 +6,8 @@ import ( "github.com/blevesearch/bleve/v2" bleveQuery "github.com/blevesearch/bleve/v2/search/query" - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" - "github.com/owncloud/ocis/v2/services/search/pkg/query/kql" + "github.com/owncloud/ocis/v2/ocis-pkg/ast" + "github.com/owncloud/ocis/v2/ocis-pkg/kql" ) var _fields = map[string]string{ diff --git a/services/search/pkg/query/bleve/compiler_test.go b/services/search/pkg/query/bleve/compiler_test.go index 597ce4c29e5..c1286cab953 100644 --- a/services/search/pkg/query/bleve/compiler_test.go +++ b/services/search/pkg/query/bleve/compiler_test.go @@ -5,9 +5,8 @@ import ( "time" "github.com/blevesearch/bleve/v2/search/query" + "github.com/owncloud/ocis/v2/ocis-pkg/ast" tAssert "github.com/stretchr/testify/assert" - - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" ) var timeMustParse = func(t *testing.T, ts string) time.Time { diff --git a/services/search/pkg/query/error.go b/services/search/pkg/query/error.go index 89f504c8a45..7a3780a38d5 100644 --- a/services/search/pkg/query/error.go +++ b/services/search/pkg/query/error.go @@ -3,7 +3,7 @@ package query import ( "fmt" - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" + "github.com/owncloud/ocis/v2/ocis-pkg/ast" ) // StartsWithBinaryOperatorError records an error and the operation that caused it. diff --git a/services/search/pkg/query/query.go b/services/search/pkg/query/query.go index e49dbecd91f..2b776595b50 100644 --- a/services/search/pkg/query/query.go +++ b/services/search/pkg/query/query.go @@ -1,9 +1,7 @@ // Package query provides functions to work with the different search query flavours. package query -import ( - "github.com/owncloud/ocis/v2/services/search/pkg/query/ast" -) +import "github.com/owncloud/ocis/v2/ocis-pkg/ast" // Builder is the interface that wraps the basic Build method. type Builder interface { From 4b5dca0a13cf62b6e4ffba998a1c30e7e120a3c4 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Thu, 20 Jun 2024 15:24:53 +0200 Subject: [PATCH 09/13] feat(activitylog): finalize translation strings Signed-off-by: jkoberg --- services/activitylog/pkg/command/server.go | 7 - services/activitylog/pkg/service/http.go | 72 +++++--- services/activitylog/pkg/service/response.go | 184 ++++++++++++++----- 3 files changed, 182 insertions(+), 81 deletions(-) diff --git a/services/activitylog/pkg/command/server.go b/services/activitylog/pkg/command/server.go index 50e4a05d6f2..4ed99d10bc1 100644 --- a/services/activitylog/pkg/command/server.go +++ b/services/activitylog/pkg/command/server.go @@ -33,20 +33,13 @@ var _registeredEvents = []events.Unmarshaller{ events.FileTouched{}, events.ContainerCreated{}, events.ItemTrashed{}, - events.ItemPurged{}, events.ItemMoved{}, events.ShareCreated{}, - events.ShareUpdated{}, events.ShareRemoved{}, events.LinkCreated{}, - events.LinkUpdated{}, events.LinkRemoved{}, events.SpaceShared{}, - events.SpaceShareUpdated{}, events.SpaceUnshared{}, - - // TODO: file downloaded only for public links. How to do this? - events.FileDownloaded{}, } // Server is the entrypoint for the server command. diff --git a/services/activitylog/pkg/service/http.go b/services/activitylog/pkg/service/http.go index 73aeda0ebf4..91554b11dfd 100644 --- a/services/activitylog/pkg/service/http.go +++ b/services/activitylog/pkg/service/http.go @@ -5,15 +5,16 @@ import ( "encoding/json" "errors" "net/http" + "path/filepath" "strconv" "strings" + "time" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" revactx "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" - libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/v2/ocis-pkg/ast" "github.com/owncloud/ocis/v2/ocis-pkg/kql" @@ -92,9 +93,8 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h var ( message string - res Resource - act Actor - ts libregraph.ActivityTimes + ts time.Time + vars map[string]interface{} ) switch ev := s.unwrapEvent(e).(type) { @@ -103,49 +103,54 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h continue case events.UploadReady: message = MessageResourceCreated - res, act, ts, err = s.ResponseData(ev.FileRef, ev.ExecutingUser.GetId(), ev.ExecutingUser.GetDisplayName(), utils.TSToTime(ev.Timestamp)) + ts = utils.TSToTime(ev.Timestamp) + vars, err = s.GetVars(WithResource(ev.FileRef, true), WithUser(ev.ExecutingUser.GetId(), ev.ExecutingUser.GetDisplayName())) case events.FileTouched: message = MessageResourceCreated - res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) + ts = utils.TSToTime(ev.Timestamp) + vars, err = s.GetVars(WithResource(ev.Ref, true), WithUser(ev.Executant, "")) case events.ContainerCreated: message = MessageResourceCreated - res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) + ts = utils.TSToTime(ev.Timestamp) + vars, err = s.GetVars(WithResource(ev.Ref, true), WithUser(ev.Executant, "")) case events.ItemTrashed: message = MessageResourceTrashed - res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) - case events.ItemPurged: - message = MessageResourcePurged - res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) + ts = utils.TSToTime(ev.Timestamp) + vars, err = s.GetVars(WithResource(ev.Ref, true), WithUser(ev.Executant, "")) case events.ItemMoved: - message = MessageResourceMoved - res, act, ts, err = s.ResponseData(ev.Ref, ev.Executant, "", utils.TSToTime(ev.Timestamp)) + switch isRename(ev.OldReference, ev.Ref) { + case true: + message = MessageResourceRenamed + vars, err = s.GetVars(WithResource(ev.Ref, false), WithOldResource(ev.OldReference), WithUser(ev.Executant, "")) + case false: + message = MessageResourceMoved + vars, err = s.GetVars(WithResource(ev.Ref, true), WithUser(ev.Executant, "")) + } + ts = utils.TSToTime(ev.Timestamp) case events.ShareCreated: message = MessageShareCreated - res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) - case events.ShareUpdated: - message = MessageShareUpdated - res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.MTime)) + ts = utils.TSToTime(ev.CTime) + vars, err = s.GetVars(WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) case events.ShareRemoved: message = MessageShareDeleted - res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", ev.Timestamp) + ts = ev.Timestamp + vars, err = s.GetVars(WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) case events.LinkCreated: message = MessageLinkCreated - res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) - case events.LinkUpdated: - message = MessageLinkUpdated - res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.CTime)) + ts = utils.TSToTime(ev.CTime) + vars, err = s.GetVars(WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, "")) case events.LinkRemoved: message = MessageLinkDeleted - res, act, ts, err = s.ResponseData(toRef(ev.ItemID), ev.Executant, "", utils.TSToTime(ev.Timestamp)) + ts = utils.TSToTime(ev.Timestamp) + vars, err = s.GetVars(WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, "")) case events.SpaceShared: message = MessageSpaceShared - res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) - case events.SpaceShareUpdated: - message = MessageSpaceShareUpdated - res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) + ts = ev.Timestamp + vars, err = s.GetVars(WithSpace(ev.ID), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) case events.SpaceUnshared: message = MessageSpaceUnshared - res, act, ts, err = s.ResponseData(sToRef(ev.ID), ev.Executant, "", ev.Timestamp) + ts = ev.Timestamp + vars, err = s.GetVars(WithSpace(ev.ID), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) } if err != nil { @@ -157,7 +162,7 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h loc := l10n.MustGetUserLocale(r.Context(), activeUser.GetId().GetOpaqueId(), r.Header.Get(l10n.HeaderAcceptLanguage), s.valService) t := l10n.NewTranslatorFromCommonConfig("en", _domain, "", _localeFS, _localeSubPath) - resp.Activities = append(resp.Activities, NewActivity(t.Translate(message, loc), res, act, ts, e.GetId())) + resp.Activities = append(resp.Activities, NewActivity(t.Translate(message, loc), ts, e.GetId(), vars)) } // delete activities in separate go routine @@ -278,3 +283,12 @@ func (s *ActivitylogService) getFilters(query string) (*provider.ResourceId, int } return &rid, limit, pref, postf, nil } + +// 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()) +} diff --git a/services/activitylog/pkg/service/response.go b/services/activitylog/pkg/service/response.go index 9186d1d3e82..f5ec0f4ad21 100644 --- a/services/activitylog/pkg/service/response.go +++ b/services/activitylog/pkg/service/response.go @@ -1,9 +1,15 @@ package service import ( + "context" + "fmt" + "path/filepath" "time" + 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" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" @@ -14,19 +20,16 @@ import ( // Translations var ( - MessageResourceCreated = l10n.Template("{user} created {resource}") - MessageResourceTrashed = l10n.Template("{user} trashed {resource}") - MessageResourcePurged = l10n.Template("{user} purged {resource}") - MessageResourceMoved = l10n.Template("{user} moved {resource}") - MessageShareCreated = l10n.Template("{user} shared {resource}") - MessageShareUpdated = l10n.Template("{user} updated share of {resource}") - MessageShareDeleted = l10n.Template("{user} deleted share of {resource}") - MessageLinkCreated = l10n.Template("{user} created link to {resource}") - MessageLinkUpdated = l10n.Template("{user} updated link to {resource}") - MessageLinkDeleted = l10n.Template("{user} deleted link to {resource}") - MessageSpaceShared = l10n.Template("{user} shared space {resource}") - MessageSpaceShareUpdated = l10n.Template("{user} updated share of space {resource}") - MessageSpaceUnshared = l10n.Template("{user} unshared space {resource}") + MessageResourceCreated = l10n.Template("{user} added {resource} to {space}") + MessageResourceTrashed = l10n.Template("{user} deleted {resource} from {space}") + MessageResourceMoved = l10n.Template("{user} moved {resource} to {space}") + MessageResourceRenamed = l10n.Template("{user} renamed {oldResource} to {resource}") + MessageShareCreated = l10n.Template("{user} shared {resource} with {sharee}") + MessageShareDeleted = l10n.Template("{user} removed {sharee} from {resource}") + MessageLinkCreated = l10n.Template("{user} shared {resource} via link") + MessageLinkDeleted = l10n.Template("{user} removed link to {resource}") + MessageSpaceShared = l10n.Template("{user} added {sharee} as member of {space}") + MessageSpaceUnshared = l10n.Template("{user} removed {sharee} from {space}") ) // GetActivitiesResponse is the response on GET activities requests @@ -40,60 +43,151 @@ type Resource struct { Name string `json:"name"` } -// Actor represents the user who performed the Action +// Actor represents a user type Actor struct { ID string `json:"id"` DisplayName string `json:"displayName"` } +// ActivityOption allows setting variables for an activity +type ActivityOption func(context.Context, gateway.GatewayAPIClient, map[string]interface{}) error + +// WithResource sets the resource variable for an activity +func WithResource(ref *provider.Reference, addSpace bool) ActivityOption { + return func(ctx context.Context, gwc gateway.GatewayAPIClient, vars map[string]interface{}) error { + info, err := utils.GetResource(ctx, ref, gwc) + if err != nil { + return err + } + + vars["resource"] = Resource{ + ID: storagespace.FormatResourceID(*info.GetId()), + Name: info.GetName(), + } + + if addSpace { + vars["space"] = Resource{ + ID: info.GetSpace().GetId().GetOpaqueId(), + Name: info.GetSpace().GetName(), + } + } + + return nil + } +} + +// WithOldResource sets the oldResource variable for an activity +func WithOldResource(ref *provider.Reference) ActivityOption { + return func(_ context.Context, _ gateway.GatewayAPIClient, vars map[string]interface{}) error { + name := filepath.Base(ref.GetPath()) + vars["oldResource"] = Resource{ + Name: name, + } + return nil + } +} + +// WithUser sets the user variable for an Activity +func WithUser(uid *user.UserId, username string) ActivityOption { + return func(_ context.Context, gwc gateway.GatewayAPIClient, vars map[string]interface{}) error { + if username == "" { + u, err := utils.GetUser(uid, gwc) + if err != nil { + return err + } + username = u.GetUsername() + } + + vars["user"] = Actor{ + ID: uid.GetOpaqueId(), + DisplayName: username, + } + + return nil + } +} + +// WithSharee sets the sharee variable for an activity +func WithSharee(uid *user.UserId, gid *group.GroupId) ActivityOption { + return func(ctx context.Context, gwc gateway.GatewayAPIClient, vars map[string]interface{}) error { + switch { + case uid != nil: + u, err := utils.GetUser(uid, gwc) + if err != nil { + return err + } + + vars["sharee"] = Actor{ + ID: uid.GetOpaqueId(), + DisplayName: u.GetUsername(), + } + case gid != nil: + r, err := gwc.GetGroup(ctx, &group.GetGroupRequest{GroupId: gid}) + if err != nil { + return fmt.Errorf("error getting group: %w", err) + } + + if r.GetStatus().GetCode() != rpc.Code_CODE_OK { + return fmt.Errorf("error getting group: %s", r.GetStatus().GetMessage()) + } + + vars["sharee"] = Actor{ + ID: gid.GetOpaqueId(), + DisplayName: r.GetGroup().GetDisplayName(), + } + + } + + return nil + } +} + +// WithSpace sets the space variable for an activity +func WithSpace(spaceid *provider.StorageSpaceId) ActivityOption { + return func(ctx context.Context, gwc gateway.GatewayAPIClient, vars map[string]interface{}) error { + s, err := utils.GetSpace(ctx, spaceid.GetOpaqueId(), gwc) + if err != nil { + return err + } + vars["space"] = Resource{ + ID: s.GetId().GetOpaqueId(), + Name: s.GetName(), + } + + return nil + } +} + // NewActivity creates a new activity -func NewActivity(message string, res Resource, user Actor, ts libregraph.ActivityTimes, eventID string) libregraph.Activity { +func NewActivity(message string, ts time.Time, eventID string, vars map[string]interface{}) libregraph.Activity { return libregraph.Activity{ Id: eventID, - Times: ts, + Times: libregraph.ActivityTimes{RecordedTime: ts}, Template: libregraph.ActivityTemplate{ - Message: message, - Variables: map[string]interface{}{ - "resource": res, - "user": user, - }, + Message: message, + Variables: vars, }, } } -// ResponseData returns the relevant response data for the activity -func (s *ActivitylogService) ResponseData(ref *provider.Reference, uid *user.UserId, username string, ts time.Time) (Resource, Actor, libregraph.ActivityTimes, error) { +// GetVars calls other service to gather the required data for the activity variables +func (s *ActivitylogService) GetVars(opts ...ActivityOption) (map[string]interface{}, error) { gwc, err := s.gws.Next() if err != nil { - return Resource{}, Actor{}, libregraph.ActivityTimes{}, err + return nil, err } ctx, err := utils.GetServiceUserContext(s.cfg.ServiceAccount.ServiceAccountID, gwc, s.cfg.ServiceAccount.ServiceAccountSecret) if err != nil { - return Resource{}, Actor{}, libregraph.ActivityTimes{}, err + return nil, err } - info, err := utils.GetResource(ctx, ref, gwc) - if err != nil { - return Resource{}, Actor{}, libregraph.ActivityTimes{}, err - } - - if username == "" { - u, err := utils.GetUser(uid, gwc) - if err != nil { - return Resource{}, Actor{}, libregraph.ActivityTimes{}, err + vars := make(map[string]interface{}) + for _, opt := range opts { + if err := opt(ctx, gwc, vars); err != nil { + return nil, err } - username = u.GetUsername() } - return Resource{ - ID: storagespace.FormatResourceID(*info.Id), - Name: info.Path, - }, Actor{ - ID: uid.GetOpaqueId(), - DisplayName: username, - }, libregraph.ActivityTimes{ - RecordedTime: ts, - }, nil - + return vars, nil } From c658c4d8fabbf4834f2b2ef1d6bbb25ff322ed55 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Fri, 21 Jun 2024 10:51:30 +0200 Subject: [PATCH 10/13] feat(activitylog): repair space share events Signed-off-by: jkoberg --- services/activitylog/pkg/service/service.go | 47 +++++++++------------ 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/services/activitylog/pkg/service/service.go b/services/activitylog/pkg/service/service.go index 2908b636d85..32429778ea4 100644 --- a/services/activitylog/pkg/service/service.go +++ b/services/activitylog/pkg/service/service.go @@ -101,28 +101,20 @@ func (a *ActivitylogService) Run() { err = a.AddActivity(ev.Ref, e.ID, utils.TSToTime(ev.Timestamp)) case events.ItemTrashed: err = a.AddActivityTrashed(ev.ID, ev.Ref, e.ID, utils.TSToTime(ev.Timestamp)) - case events.ItemPurged: - err = a.AddActivity(ev.Ref, e.ID, utils.TSToTime(ev.Timestamp)) case events.ItemMoved: err = a.AddActivity(ev.Ref, e.ID, utils.TSToTime(ev.Timestamp)) case events.ShareCreated: err = a.AddActivity(toRef(ev.ItemID), e.ID, utils.TSToTime(ev.CTime)) - case events.ShareUpdated: - err = a.AddActivity(toRef(ev.ItemID), e.ID, utils.TSToTime(ev.MTime)) case events.ShareRemoved: err = a.AddActivity(toRef(ev.ItemID), e.ID, ev.Timestamp) case events.LinkCreated: err = a.AddActivity(toRef(ev.ItemID), e.ID, utils.TSToTime(ev.CTime)) - case events.LinkUpdated: - err = a.AddActivity(toRef(ev.ItemID), e.ID, utils.TSToTime(ev.CTime)) case events.LinkRemoved: err = a.AddActivity(toRef(ev.ItemID), e.ID, utils.TSToTime(ev.Timestamp)) case events.SpaceShared: - err = a.AddActivity(sToRef(ev.ID), e.ID, ev.Timestamp) - case events.SpaceShareUpdated: - err = a.AddActivity(sToRef(ev.ID), e.ID, ev.Timestamp) + err = a.AddSpaceActivity(ev.ID, e.ID, ev.Timestamp) case events.SpaceUnshared: - err = a.AddActivity(sToRef(ev.ID), e.ID, ev.Timestamp) + err = a.AddSpaceActivity(ev.ID, e.ID, ev.Timestamp) } if err != nil { @@ -161,7 +153,7 @@ func (a *ActivitylogService) AddActivityTrashed(resourceID *provider.ResourceId, } // store activity on trashed item - if err := a.storeActivity(resourceID, eventID, 0, timestamp); err != nil { + if err := a.storeActivity(storagespace.FormatResourceID(*resourceID), eventID, 0, timestamp); err != nil { return fmt.Errorf("could not store activity: %w", err) } @@ -176,6 +168,20 @@ func (a *ActivitylogService) AddActivityTrashed(resourceID *provider.ResourceId, }) } +// AddSpaceActivity adds the activity to the given spaceroot +func (a *ActivitylogService) AddSpaceActivity(spaceID *provider.StorageSpaceId, eventID string, timestamp time.Time) error { + // spaceID is in format $ + // activitylog service uses format $! + // lets do some converting, shall we? + rid, err := storagespace.ParseID(spaceID.GetOpaqueId()) + if err != nil { + return fmt.Errorf("could not parse space id: %w", err) + } + rid.OpaqueId = rid.GetSpaceId() + return a.storeActivity(storagespace.FormatResourceID(rid), eventID, 0, timestamp) + +} + // Activities returns the activities for the given resource func (a *ActivitylogService) Activities(rid *provider.ResourceId) ([]RawActivity, error) { resourceID := storagespace.FormatResourceID(*rid) @@ -236,7 +242,7 @@ func (a *ActivitylogService) addActivity(initRef *provider.Reference, eventID st return fmt.Errorf("could not get resource info: %w", err) } - if err := a.storeActivity(info.GetId(), eventID, depth, timestamp); err != nil { + if err := a.storeActivity(storagespace.FormatResourceID(*info.GetId()), eventID, depth, timestamp); err != nil { return fmt.Errorf("could not store activity: %w", err) } @@ -249,13 +255,7 @@ func (a *ActivitylogService) addActivity(initRef *provider.Reference, eventID st } } -func (a *ActivitylogService) storeActivity(rid *provider.ResourceId, eventID string, depth int, timestamp time.Time) error { - if rid == nil { - return errors.New("resource id is required") - } - - resourceID := storagespace.FormatResourceID(*rid) - +func (a *ActivitylogService) storeActivity(resourceID string, eventID string, depth int, timestamp time.Time) error { records, err := a.store.Read(resourceID) if err != nil && err != microstore.ErrNotFound { return err @@ -291,12 +291,3 @@ func toRef(r *provider.ResourceId) *provider.Reference { ResourceId: r, } } - -func sToRef(s *provider.StorageSpaceId) *provider.Reference { - return &provider.Reference{ - ResourceId: &provider.ResourceId{ - OpaqueId: s.GetOpaqueId(), - SpaceId: s.GetOpaqueId(), - }, - } -} From 2f19daa9ef93e5d007a13c19522a13838df4743a Mon Sep 17 00:00:00 2001 From: jkoberg Date: Fri, 21 Jun 2024 11:19:35 +0200 Subject: [PATCH 11/13] feat(activitylog): repair trash events Signed-off-by: jkoberg --- services/activitylog/pkg/service/http.go | 2 +- services/activitylog/pkg/service/response.go | 29 ++++++++++++++++++++ services/activitylog/pkg/service/service.go | 6 ++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/services/activitylog/pkg/service/http.go b/services/activitylog/pkg/service/http.go index 91554b11dfd..9dea4f27ae4 100644 --- a/services/activitylog/pkg/service/http.go +++ b/services/activitylog/pkg/service/http.go @@ -116,7 +116,7 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h case events.ItemTrashed: message = MessageResourceTrashed ts = utils.TSToTime(ev.Timestamp) - vars, err = s.GetVars(WithResource(ev.Ref, true), WithUser(ev.Executant, "")) + vars, err = s.GetVars(WithTrashedResource(ev.Ref, ev.ID), WithUser(ev.Executant, ""), WithSpace(toSpace(ev.Ref))) case events.ItemMoved: switch isRename(ev.OldReference, ev.Ref) { case true: diff --git a/services/activitylog/pkg/service/response.go b/services/activitylog/pkg/service/response.go index f5ec0f4ad21..54ad2f4cf8e 100644 --- a/services/activitylog/pkg/service/response.go +++ b/services/activitylog/pkg/service/response.go @@ -87,6 +87,35 @@ func WithOldResource(ref *provider.Reference) ActivityOption { } } +// WithTrashedResource sets the resource variable if the resource is trashed +func WithTrashedResource(ref *provider.Reference, rid *provider.ResourceId) ActivityOption { + return func(ctx context.Context, gwc gateway.GatewayAPIClient, vars map[string]interface{}) error { + resp, err := gwc.ListRecycle(ctx, &provider.ListRecycleRequest{ + Ref: ref, + }) + if err != nil { + return err + } + if resp.GetStatus().GetCode() != rpc.Code_CODE_OK { + return fmt.Errorf("error listing recycle: %s", resp.GetStatus().GetMessage()) + } + + for _, item := range resp.GetRecycleItems() { + if item.GetKey() == rid.GetOpaqueId() { + + vars["resource"] = Resource{ + ID: storagespace.FormatResourceID(*rid), + Name: filepath.Base(item.GetRef().GetPath()), + } + + return nil + } + } + + return nil + } +} + // WithUser sets the user variable for an Activity func WithUser(uid *user.UserId, username string) ActivityOption { return func(_ context.Context, gwc gateway.GatewayAPIClient, vars map[string]interface{}) error { diff --git a/services/activitylog/pkg/service/service.go b/services/activitylog/pkg/service/service.go index 32429778ea4..7b4cfd3a459 100644 --- a/services/activitylog/pkg/service/service.go +++ b/services/activitylog/pkg/service/service.go @@ -291,3 +291,9 @@ func toRef(r *provider.ResourceId) *provider.Reference { ResourceId: r, } } + +func toSpace(r *provider.Reference) *provider.StorageSpaceId { + return &provider.StorageSpaceId{ + OpaqueId: storagespace.FormatStorageID(r.GetResourceId().GetStorageId(), r.GetResourceId().GetSpaceId()), + } +} From 76ca53bd31e249df7d9348bf046e00c91b7ea48f Mon Sep 17 00:00:00 2001 From: jkoberg Date: Fri, 21 Jun 2024 15:15:36 +0200 Subject: [PATCH 12/13] fix(activitylog): dont use service user context Signed-off-by: jkoberg --- .drone.star | 1 + services/activitylog/pkg/service/http.go | 30 +++++++++++--------- services/activitylog/pkg/service/response.go | 7 +---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.drone.star b/.drone.star index 2924848194b..92eace14e65 100644 --- a/.drone.star +++ b/.drone.star @@ -2039,6 +2039,7 @@ def ocisServer(storage, accounts_hash_difficulty = 4, volumes = [], depends_on = "NATS_NATS_HOST": "0.0.0.0", "NATS_NATS_PORT": 9233, "OCIS_JWT_SECRET": "some-ocis-jwt-secret", + "EVENTHISTORY_STORE": "memory", } if deploy_type == "": diff --git a/services/activitylog/pkg/service/http.go b/services/activitylog/pkg/service/http.go index 9dea4f27ae4..1fbd2fb53d6 100644 --- a/services/activitylog/pkg/service/http.go +++ b/services/activitylog/pkg/service/http.go @@ -15,6 +15,7 @@ import ( "github.com/cs3org/reva/v2/pkg/events" "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" + "google.golang.org/grpc/metadata" "github.com/owncloud/ocis/v2/ocis-pkg/ast" "github.com/owncloud/ocis/v2/ocis-pkg/kql" @@ -41,7 +42,10 @@ func (s *ActivitylogService) ServeHTTP(w http.ResponseWriter, r *http.Request) { // HandleGetItemActivities handles the request to get the activities of an item. func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *http.Request) { - activeUser, ok := revactx.ContextGetUser(r.Context()) + ctx := r.Context() + ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, r.Header.Get("X-Access-Token")) + + activeUser, ok := revactx.ContextGetUser(ctx) if !ok { w.WriteHeader(http.StatusUnauthorized) return @@ -104,53 +108,53 @@ func (s *ActivitylogService) HandleGetItemActivities(w http.ResponseWriter, r *h case events.UploadReady: message = MessageResourceCreated ts = utils.TSToTime(ev.Timestamp) - vars, err = s.GetVars(WithResource(ev.FileRef, true), WithUser(ev.ExecutingUser.GetId(), ev.ExecutingUser.GetDisplayName())) + vars, err = s.GetVars(ctx, WithResource(ev.FileRef, true), WithUser(ev.ExecutingUser.GetId(), ev.ExecutingUser.GetDisplayName())) case events.FileTouched: message = MessageResourceCreated ts = utils.TSToTime(ev.Timestamp) - vars, err = s.GetVars(WithResource(ev.Ref, true), WithUser(ev.Executant, "")) + vars, err = s.GetVars(ctx, WithResource(ev.Ref, true), WithUser(ev.Executant, "")) case events.ContainerCreated: message = MessageResourceCreated ts = utils.TSToTime(ev.Timestamp) - vars, err = s.GetVars(WithResource(ev.Ref, true), WithUser(ev.Executant, "")) + vars, err = s.GetVars(ctx, WithResource(ev.Ref, true), WithUser(ev.Executant, "")) case events.ItemTrashed: message = MessageResourceTrashed ts = utils.TSToTime(ev.Timestamp) - vars, err = s.GetVars(WithTrashedResource(ev.Ref, ev.ID), WithUser(ev.Executant, ""), WithSpace(toSpace(ev.Ref))) + vars, err = s.GetVars(ctx, WithTrashedResource(ev.Ref, ev.ID), WithUser(ev.Executant, ""), WithSpace(toSpace(ev.Ref))) case events.ItemMoved: switch isRename(ev.OldReference, ev.Ref) { case true: message = MessageResourceRenamed - vars, err = s.GetVars(WithResource(ev.Ref, false), WithOldResource(ev.OldReference), WithUser(ev.Executant, "")) + vars, err = s.GetVars(ctx, WithResource(ev.Ref, false), WithOldResource(ev.OldReference), WithUser(ev.Executant, "")) case false: message = MessageResourceMoved - vars, err = s.GetVars(WithResource(ev.Ref, true), WithUser(ev.Executant, "")) + vars, err = s.GetVars(ctx, WithResource(ev.Ref, true), WithUser(ev.Executant, "")) } ts = utils.TSToTime(ev.Timestamp) case events.ShareCreated: message = MessageShareCreated ts = utils.TSToTime(ev.CTime) - vars, err = s.GetVars(WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) + vars, err = s.GetVars(ctx, WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) case events.ShareRemoved: message = MessageShareDeleted ts = ev.Timestamp - vars, err = s.GetVars(WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) + vars, err = s.GetVars(ctx, WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) case events.LinkCreated: message = MessageLinkCreated ts = utils.TSToTime(ev.CTime) - vars, err = s.GetVars(WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, "")) + vars, err = s.GetVars(ctx, WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, "")) case events.LinkRemoved: message = MessageLinkDeleted ts = utils.TSToTime(ev.Timestamp) - vars, err = s.GetVars(WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, "")) + vars, err = s.GetVars(ctx, WithResource(toRef(ev.ItemID), false), WithUser(ev.Executant, "")) case events.SpaceShared: message = MessageSpaceShared ts = ev.Timestamp - vars, err = s.GetVars(WithSpace(ev.ID), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) + vars, err = s.GetVars(ctx, WithSpace(ev.ID), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) case events.SpaceUnshared: message = MessageSpaceUnshared ts = ev.Timestamp - vars, err = s.GetVars(WithSpace(ev.ID), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) + vars, err = s.GetVars(ctx, WithSpace(ev.ID), WithUser(ev.Executant, ""), WithSharee(ev.GranteeUserID, ev.GranteeGroupID)) } if err != nil { diff --git a/services/activitylog/pkg/service/response.go b/services/activitylog/pkg/service/response.go index 54ad2f4cf8e..9a9d710bc2c 100644 --- a/services/activitylog/pkg/service/response.go +++ b/services/activitylog/pkg/service/response.go @@ -200,17 +200,12 @@ func NewActivity(message string, ts time.Time, eventID string, vars map[string]i } // GetVars calls other service to gather the required data for the activity variables -func (s *ActivitylogService) GetVars(opts ...ActivityOption) (map[string]interface{}, error) { +func (s *ActivitylogService) GetVars(ctx context.Context, opts ...ActivityOption) (map[string]interface{}, error) { gwc, err := s.gws.Next() if err != nil { return nil, err } - ctx, err := utils.GetServiceUserContext(s.cfg.ServiceAccount.ServiceAccountID, gwc, s.cfg.ServiceAccount.ServiceAccountSecret) - if err != nil { - return nil, err - } - vars := make(map[string]interface{}) for _, opt := range opts { if err := opt(ctx, gwc, vars); err != nil { From 57a30ecd2a728ecefc3117e70dbc7a674b1ff4cb Mon Sep 17 00:00:00 2001 From: jkoberg Date: Mon, 24 Jun 2024 14:55:29 +0200 Subject: [PATCH 13/13] feat(activitylog): add a mutex to the store Signed-off-by: jkoberg --- .../6.0.0_2024-06-19/activity-service.md | 1 - changelog/unreleased/activity-api.md | 5 ++ services/activitylog/pkg/config/config.go | 2 +- services/activitylog/pkg/service/service.go | 50 ++++++++++++------- 4 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 changelog/unreleased/activity-api.md diff --git a/changelog/6.0.0_2024-06-19/activity-service.md b/changelog/6.0.0_2024-06-19/activity-service.md index 8f82f56fe79..1058280a061 100644 --- a/changelog/6.0.0_2024-06-19/activity-service.md +++ b/changelog/6.0.0_2024-06-19/activity-service.md @@ -2,5 +2,4 @@ Enhancement: Activitylog Service Adds a new service `activitylog` which stores events (activities) per resource. This data can be retrieved by clients to show item activities -https://github.com/owncloud/ocis/pull/9360 https://github.com/owncloud/ocis/pull/9327 diff --git a/changelog/unreleased/activity-api.md b/changelog/unreleased/activity-api.md new file mode 100644 index 00000000000..fd39c2be72b --- /dev/null +++ b/changelog/unreleased/activity-api.md @@ -0,0 +1,5 @@ +Enhancement: Activitylog API + +Adds an api to the `activitylog` service which allows retrieving data by clients to show item activities + +https://github.com/owncloud/ocis/pull/9361 diff --git a/services/activitylog/pkg/config/config.go b/services/activitylog/pkg/config/config.go index 580ca381ac6..7c84b13178d 100644 --- a/services/activitylog/pkg/config/config.go +++ b/services/activitylog/pkg/config/config.go @@ -49,7 +49,7 @@ type Store struct { Database string `yaml:"database" env:"ACTIVITYLOG_STORE_DATABASE" desc:"The database name the configured store should use." introductionVersion:"pre5.0"` Table string `yaml:"table" env:"ACTIVITYLOG_STORE_TABLE" desc:"The database table the store should use." introductionVersion:"pre5.0"` TTL time.Duration `yaml:"ttl" env:"OCIS_PERSISTENT_STORE_TTL;ACTIVITYLOG_STORE_TTL" desc:"Time to live for events in the store. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"` - Size int `yaml:"size" env:"OCIS_PERSISTENT_STORE_SIZE;ACTIVITYLOG_STORE_SIZE" desc:"The maximum quantity of items in the store. Only applies when store type 'ocmem' is configured. Defaults to 512 which is derived from the ocmem package though not exclicitly set as default." introductionVersion:"pre5.0"` + Size int `yaml:"size" env:"OCIS_PERSISTENT_STORE_SIZE;ACTIVITYLOG_STORE_SIZE" desc:"The maximum quantity of items in the store. Only applies when store type 'ocmem' is configured. Defaults to 512 which is derived from the ocmem package though not explicitly set as default." introductionVersion:"pre5.0"` AuthUsername string `yaml:"username" env:"OCIS_PERSISTENT_STORE_AUTH_USERNAME;ACTIVITYLOG_STORE_AUTH_USERNAME" desc:"The username to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"5.0"` AuthPassword string `yaml:"password" env:"OCIS_PERSISTENT_STORE_AUTH_PASSWORD;ACTIVITYLOG_STORE_AUTH_PASSWORD" desc:"The password to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"5.0"` } diff --git a/services/activitylog/pkg/service/service.go b/services/activitylog/pkg/service/service.go index 7b4cfd3a459..2653bd434ba 100644 --- a/services/activitylog/pkg/service/service.go +++ b/services/activitylog/pkg/service/service.go @@ -6,6 +6,7 @@ import ( "fmt" "path/filepath" "reflect" + "sync" "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -40,6 +41,7 @@ type ActivitylogService struct { mux *chi.Mux evHistory ehsvc.EventHistoryService valService settingssvc.ValueService + lock sync.RWMutex registeredEvents map[string]events.Unmarshaller } @@ -73,6 +75,7 @@ func New(opts ...Option) (*ActivitylogService, error) { mux: o.Mux, evHistory: o.HistoryClient, valService: o.ValueClient, + lock: sync.RWMutex{}, registeredEvents: make(map[string]events.Unmarshaller), } @@ -184,28 +187,18 @@ func (a *ActivitylogService) AddSpaceActivity(spaceID *provider.StorageSpaceId, // Activities returns the activities for the given resource func (a *ActivitylogService) Activities(rid *provider.ResourceId) ([]RawActivity, error) { - resourceID := storagespace.FormatResourceID(*rid) - - records, err := a.store.Read(resourceID) - if err != nil && err != microstore.ErrNotFound { - return nil, fmt.Errorf("could not read activities: %w", err) - } - - if len(records) == 0 { - return []RawActivity{}, nil - } + a.lock.RLock() + defer a.lock.RUnlock() - var activities []RawActivity - if err := json.Unmarshal(records[0].Value, &activities); err != nil { - return nil, fmt.Errorf("could not unmarshal activities: %w", err) - } - - return activities, nil + return a.activities(rid) } // RemoveActivities removes the activities from the given resource func (a *ActivitylogService) RemoveActivities(rid *provider.ResourceId, toDelete map[string]struct{}) error { - curActivities, err := a.Activities(rid) + a.lock.Lock() + defer a.lock.Unlock() + + curActivities, err := a.activities(rid) if err != nil { return err } @@ -228,6 +221,26 @@ func (a *ActivitylogService) RemoveActivities(rid *provider.ResourceId, toDelete }) } +func (a *ActivitylogService) activities(rid *provider.ResourceId) ([]RawActivity, error) { + resourceID := storagespace.FormatResourceID(*rid) + + records, err := a.store.Read(resourceID) + if err != nil && err != microstore.ErrNotFound { + return nil, fmt.Errorf("could not read activities: %w", err) + } + + if len(records) == 0 { + return []RawActivity{}, nil + } + + var activities []RawActivity + if err := json.Unmarshal(records[0].Value, &activities); err != nil { + return nil, fmt.Errorf("could not unmarshal activities: %w", err) + } + + return activities, nil +} + // note: getResource is abstracted to allow unit testing, in general this will just be utils.GetResource func (a *ActivitylogService) addActivity(initRef *provider.Reference, eventID string, timestamp time.Time, getResource func(*provider.Reference) (*provider.ResourceInfo, error)) error { var ( @@ -256,6 +269,9 @@ func (a *ActivitylogService) addActivity(initRef *provider.Reference, eventID st } func (a *ActivitylogService) storeActivity(resourceID string, eventID string, depth int, timestamp time.Time) error { + a.lock.Lock() + defer a.lock.Unlock() + records, err := a.store.Read(resourceID) if err != nil && err != microstore.ErrNotFound { return err