Skip to content

Commit

Permalink
merge: branch 'feat/events'
Browse files Browse the repository at this point in the history
  • Loading branch information
ichenhe committed Oct 31, 2023
2 parents dc6dea2 + 7f80376 commit 6699c6f
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 40 deletions.
40 changes: 32 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,38 @@ For example:

# Events

> In active development, all [Syncthing native events](https://docs.syncthing.net/dev/events.html#event-types) will be
> supported soon.
| Event | Description |
|-------------------------------------|-------------------------------------------------------------------|
| st:FolderCompletion | [doc](https://docs.syncthing.net/events/foldercompletion.html) |
| st:LocalChangeDetected | [doc](https://docs.syncthing.net/events/localchangedetected.html) |
| ex:LocalFolderContentChangeDetected | Based on `st:LocalChangeDetected`, with file path matcher. |
| Event | Description |
|-------------------------------------|---------------------------------------------------------------------------------|
| st:ConfigSaved | [Syncthing doc](https://docs.syncthing.net/events/configsaved.html) |
| st:DeviceConnected | [Syncthing doc](https://docs.syncthing.net/events/deviceconnected.html) |
| st:DeviceDisconnected | [Syncthing doc](https://docs.syncthing.net/events/devicedisconnected.html) |
| st:DeviceDiscovered | [Syncthing doc](https://docs.syncthing.net/events/devicediscovered.html) |
| st:DevicePaused | [Syncthing doc](https://docs.syncthing.net/events/devicepaused.html) |
| st:DeviceResumed | [Syncthing doc](https://docs.syncthing.net/events/deviceresumed.html) |
| st:DownloadProgress | [Syncthing doc](https://docs.syncthing.net/events/downloadprogress.html) |
| st:Failure | [Syncthing doc](https://docs.syncthing.net/events/failure.html) |
| st:FolderCompletion | [Syncthing doc](https://docs.syncthing.net/events/foldercompletion.html) |
| st:FolderErrors | [Syncthing doc](https://docs.syncthing.net/events/foldererrors.html) |
| st:FolderPaused | [Syncthing doc](https://docs.syncthing.net/events/folderpaused.html) |
| st:FolderResumed | [Syncthing doc](https://docs.syncthing.net/events/folderresumed.html) |
| st:FolderScanProgress | [Syncthing doc](https://docs.syncthing.net/events/folderscanprogress.html) |
| st:FolderSummary | [Syncthing doc](https://docs.syncthing.net/events/foldersummary.html) |
| st:FolderWatchStateChanged | [Syncthing doc](https://docs.syncthing.net/events/folderwatchstatechanged.html) |
| st:ItemFinished | [Syncthing doc](https://docs.syncthing.net/events/itemfinished.html) |
| st:ItemStarted | [Syncthing doc](https://docs.syncthing.net/events/itemstarted.html) |
| st:ListenAddressesChanged | [Syncthing doc](https://docs.syncthing.net/events/listenaddresseschanged.html) |
| st:LocalChangeDetected | [Syncthing doc](https://docs.syncthing.net/events/localchangedetected.html) |
| st:LocalIndexUpdated | [Syncthing doc](https://docs.syncthing.net/events/localindexupdated.html) |
| st:LoginAttempt | [Syncthing doc](https://docs.syncthing.net/events/loginattempt.html) |
| st:PendingDevicesChanged | [Syncthing doc](https://docs.syncthing.net/events/pendingdeviceschanged.html) |
| st:PendingFoldersChanged | [Syncthing doc](https://docs.syncthing.net/events/pendingfolderschanged.html) |
| st:RemoteChangeDetected | [Syncthing doc](https://docs.syncthing.net/events/remotechangedetected.html) |
| st:RemoteDownloadProgress | [Syncthing doc](https://docs.syncthing.net/events/remotedownloadprogress.html) |
| st:RemoteIndexUpdated | [Syncthing doc](https://docs.syncthing.net/events/remoteindexupdated.html) |
| st:Starting | [Syncthing doc](https://docs.syncthing.net/events/starting.html) |
| st:StartupComplete | [Syncthing doc](https://docs.syncthing.net/events/startupcomplete.html) |
| st:StateChanged | [Syncthing doc](https://docs.syncthing.net/events/statechanged.html) |
| ex:LocalFolderContentChangeDetected | Based on `st:LocalChangeDetected`, with file path matcher. |

## Common Parameters

Expand Down
2 changes: 1 addition & 1 deletion cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func main() {
for i, h := range appProfile.Hooks {
hookDef := domain.NewHookDefinition(h.Name, i)
if err := hookManager.RegisterHook(&h, hookDef); err != nil {
logger.Errorf("failed to regiter hook: %s", err)
hookDef.AddToLogger(logger).Errorf("failed to regiter hook: %s", err)
}
}
logger.Infof("%d hook loaded", len(appProfile.Hooks))
Expand Down
125 changes: 98 additions & 27 deletions domain/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"github.com/syncthing/syncthing/lib/events"
"strings"
"time"
)

Expand All @@ -18,60 +17,132 @@ func (e *Event) String() string {
return fmt.Sprintf("Event <time: %s, type: %s>", e.Time.String(), e.Type)
}

func NewEventFromStEvent(event events.Event) *Event {
func NewEventFromStEvent(event events.Event) (*Event, error) {
ev := convertFromStEventType(event.Type)
if ev == UnknownEventType {
return nil, errors.New("unknown syncthing event type")
}
return &Event{
Time: event.Time,
Type: convertFromStEventType(event.Type),
Type: ev,
Data: event.Data,
}
}, nil
}

// convertFromStEventType converts a Syncthing event type to local one. Returns UnknownEventType if error.
func convertFromStEventType(eventType events.EventType) EventType {
switch eventType {
case events.FolderCompletion:
return FolderCompletion
case events.LocalChangeDetected:
return LocalChangeDetected
if locEv, ex := mapToLocalType[eventType]; ex {
return locEv
}
panic(errors.New(fmt.Sprintf("unknown st exevent type '%d'", eventType)))
return UnknownEventType
}

type EventType string

var ErrNotValidNativeEventType = errors.New("not a valid native event type")

// Supported event types. Each type except UnknownEventType MUST be registered in mapToStType.
const (
UnknownEventType EventType = ""
FolderCompletion EventType = "st:FolderCompletion"
LocalChangeDetected EventType = "st:LocalChangeDetected"
UnknownEventType EventType = ""

ConfigSaved EventType = "st:ConfigSaved"
DeviceConnected EventType = "st:DeviceConnected"
DeviceDisconnected EventType = "st:DeviceDisconnected"
DeviceDiscovered EventType = "st:DeviceDiscovered"
DevicePaused EventType = "st:DevicePaused"
DeviceResumed EventType = "st:DeviceResumed"
DownloadProgress EventType = "st:DownloadProgress"
Failure EventType = "st:Failure"
FolderCompletion EventType = "st:FolderCompletion"
FolderErrors EventType = "st:FolderErrors"
FolderPaused EventType = "st:FolderPaused"
FolderResumed EventType = "st:FolderResumed"
FolderScanProgress EventType = "st:FolderScanProgress"
FolderSummary EventType = "st:FolderSummary"
FolderWatchStateChanged EventType = "st:FolderWatchStateChanged"
ItemFinished EventType = "st:ItemFinished"
ItemStarted EventType = "st:ItemStarted"
ListenAddressesChanged EventType = "st:ListenAddressesChanged"
LocalChangeDetected EventType = "st:LocalChangeDetected"
LocalIndexUpdated EventType = "st:LocalIndexUpdated"
LoginAttempt EventType = "st:LoginAttempt"
PendingDevicesChanged EventType = "st:PendingDevicesChanged"
PendingFoldersChanged EventType = "st:PendingFoldersChanged"
RemoteChangeDetected EventType = "st:RemoteChangeDetected"
RemoteDownloadProgress EventType = "st:RemoteDownloadProgress"
RemoteIndexUpdated EventType = "st:RemoteIndexUpdated"
Starting EventType = "st:Starting"
StartupComplete EventType = "st:StartupComplete"
StateChanged EventType = "st:StateChanged"

LocalFolderContentChangeDetected EventType = "ex:LocalFolderContentChangeDetected"
)

// mapToStType records ALL events and corresponding native events, 0 if no corresponding event type.
var mapToStType = map[EventType]events.EventType{
ConfigSaved: events.ConfigSaved,
DeviceConnected: events.DeviceConnected,
DeviceDisconnected: events.DeviceDisconnected,
DeviceDiscovered: events.DeviceDiscovered,
DevicePaused: events.DevicePaused,
DeviceResumed: events.DeviceResumed,
DownloadProgress: events.DownloadProgress,
Failure: events.Failure,
FolderCompletion: events.FolderCompletion,
FolderErrors: events.FolderErrors,
FolderPaused: events.FolderPaused,
FolderResumed: events.FolderResumed,
FolderScanProgress: events.FolderScanProgress,
FolderSummary: events.FolderSummary,
FolderWatchStateChanged: events.FolderWatchStateChanged,
ItemFinished: events.ItemFinished,
ItemStarted: events.ItemStarted,
ListenAddressesChanged: events.ListenAddressesChanged,
LocalChangeDetected: events.LocalChangeDetected,
LocalIndexUpdated: events.LocalIndexUpdated,
LoginAttempt: events.LoginAttempt,
PendingDevicesChanged: events.PendingDevicesChanged,
PendingFoldersChanged: events.PendingFoldersChanged,
RemoteChangeDetected: events.RemoteChangeDetected,
RemoteDownloadProgress: events.RemoteDownloadProgress,
RemoteIndexUpdated: events.RemoteIndexUpdated,
Starting: events.Starting,
StartupComplete: events.StartupComplete,
StateChanged: events.StateChanged,

LocalFolderContentChangeDetected: events.EventType(0),
}

var mapToLocalType map[events.EventType]EventType

func init() {
// build reverse map
mapToLocalType = make(map[events.EventType]EventType, len(mapToStType))
for cu, st := range mapToStType {
if st != 0 {
mapToLocalType[st] = cu
}
}
}

func UnmarshalEventType(evType string) EventType {
switch evType {
case string(FolderCompletion):
return FolderCompletion
case string(LocalFolderContentChangeDetected):
return LocalFolderContentChangeDetected
default:
t := EventType(evType)
if _, ex := mapToStType[t]; !ex {
return UnknownEventType
}
return t
}

func (t EventType) IsNativeEvent() bool {
return strings.HasPrefix(string(t), "st:")
e, ex := mapToStType[t]
return ex && e != 0
}

// ConvertToNative converts current event type to syncthing native type. Returns ErrNotValidNativeEventType if failed.
func (t EventType) ConvertToNative() (events.EventType, error) {
if !t.IsNativeEvent() {
return 0, ErrNotValidNativeEventType
}
native := events.UnmarshalEventType(string(t)[3:])
if native == 0 {
n, ex := mapToStType[t]
if !ex || n == 0 {
return 0, ErrNotValidNativeEventType
} else {
return native, nil
}
return n, nil
}
94 changes: 94 additions & 0 deletions domain/event_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package domain

import (
"errors"
"github.com/syncthing/syncthing/lib/events"
"testing"
)

func TestEventType_ConvertToNative(t *testing.T) {
tests := []struct {
name string
t EventType
want events.EventType
wantErr bool
}{
{name: "native-wrapper event", t: LocalChangeDetected, want: events.LocalChangeDetected, wantErr: false},
{name: "non-native event", t: LocalFolderContentChangeDetected, want: events.EventType(-716), wantErr: true},
{name: "invalid input", t: EventType("abc"), want: events.EventType(-716), wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.t.ConvertToNative()
if tt.wantErr && !errors.Is(err, ErrNotValidNativeEventType) {
t.Errorf("ConvertToNative() error = %v, want %v", err, ErrNotValidNativeEventType)
return
}
if !tt.wantErr {
if err != nil {
t.Errorf("ConvertToNative() error = %v, want success", err)
return
}
if got != tt.want {
t.Errorf("ConvertToNative() got = %v, want %v", got, tt.want)
}
}
})
}
}

func TestEventType_IsNativeEvent(t *testing.T) {
tests := []struct {
name string
t EventType
want bool
}{
{name: "native event", t: FolderCompletion, want: true},
{name: "non-native event", t: LocalFolderContentChangeDetected, want: false},
{name: "invalid input", t: EventType("abc"), want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.t.IsNativeEvent(); got != tt.want {
t.Errorf("IsNativeEvent() = %v, want %v", got, tt.want)
}
})
}
}

func TestUnmarshalEventType(t *testing.T) {
tests := []struct {
name string
str string
want EventType
}{
{name: "native event", str: "st:FolderCompletion", want: FolderCompletion},
{name: "ex event", str: "ex:LocalFolderContentChangeDetected", want: LocalFolderContentChangeDetected},
{name: "invalid input", str: "st:invalid", want: UnknownEventType},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := UnmarshalEventType(tt.str); got != tt.want {
t.Errorf("UnmarshalEventType() = %v, want %v", got, tt.want)
}
})
}
}

func Test_convertFromStEventType(t *testing.T) {
tests := []struct {
name string
stEv events.EventType
want EventType
}{
{name: "normal", stEv: events.LocalChangeDetected, want: LocalChangeDetected},
{name: "unknown st event type", stEv: events.EventType(-716), want: UnknownEventType},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := convertFromStEventType(tt.stEv); got != tt.want {
t.Errorf("convertFromStEventType() = %v, want %v", got, tt.want)
}
})
}
}
8 changes: 7 additions & 1 deletion domain/evsource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ type EventCh = <-chan *Event

// ConvertNativeEventChannel converts native event channel into custom event channel. The returned channel will be
// closed automatically once the given native event channel is closed.
//
// Events that failed to convert to local event will be ignored.
func ConvertNativeEventChannel(nativeEventCh <-chan events.Event) EventCh {
ch := make(chan *Event)
go func() {
for nativeEv := range nativeEventCh {
ch <- NewEventFromStEvent(nativeEv)
if ev, err := NewEventFromStEvent(nativeEv); err != nil {
continue
} else {
ch <- ev
}
}
close(ch)
}()
Expand Down
4 changes: 2 additions & 2 deletions exevent/evsource.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (s *EventSource) Subscribe(eventType domain.EventType, params *domain.HookP
if eventType.IsNativeEvent() {
nativeType, err := eventType.ConvertToNative()
if err != nil {
return nil, err
return nil, fmt.Errorf("can not convert %s to st event type: %w", eventType, err)
}
nativeCh := s.stClient.SubscribeEvent([]events.EventType{nativeType}, -1)
eventCh = domain.ConvertNativeEventChannel(nativeCh)
Expand All @@ -69,7 +69,7 @@ func (s *EventSource) Subscribe(eventType domain.EventType, params *domain.HookP
eventUnsub = unsub
}
default:
return nil, domain.NewIllegalEventParamError(fmt.Sprintf("unknown eventType: %s", eventType))
return nil, domain.NewIllegalEventParamError(fmt.Sprintf("internal error, recognized event type but can not register: %s", eventType))
}
}

Expand Down
6 changes: 5 additions & 1 deletion hook/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ func (m *Manager) RegisterHook(hook *domain.Hook, hookDef *domain.HookDefinition
m.mutex.Lock()
defer m.mutex.Unlock()

eventCh, err := m.eventSource.Subscribe(domain.UnmarshalEventType(hook.EventType), &hook.Parameter, nil)
evType := domain.UnmarshalEventType(hook.EventType)
if evType == domain.UnknownEventType {
return fmt.Errorf("failed to subscribe event: unknown event type: %s", hook.EventType)
}
eventCh, err := m.eventSource.Subscribe(evType, &hook.Parameter, nil)
if err != nil {
return fmt.Errorf("failed to subscribe exevent: %w", err)
}
Expand Down

0 comments on commit 6699c6f

Please sign in to comment.