gdnotify

package module
v0.5.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 17, 2025 License: MIT Imports: 44 Imported by: 0

README

gdnotify

Documentation Latest GitHub release Github Actions test License

gdnotify is a Google Drive change notifier for AWS.

Changes that occur in Google Drive are notified through Amazon EventBridge.

Install

Homebrew (macOS and Linux)
$ brew install mashiike/tap/awstee
Binary packages

Releases

Usage

$ gdnotify -h
Usage: gdnotify <command> [flags]

gdnotify is a tool for managing notification channels for Google Drive.

Flags:
  -h, --help                                         Show context-sensitive help.
      --log-level="info"                             log level ($GDNOTIFY_LOG_LEVEL)
      --log-format="text"                            log format ($GDNOTIFY_LOG_FORMAT)
      --[no-]log-color                               enable color output ($GDNOTIFY_LOG_COLOR)
      --version                                      show version
      --storage-type="dynamodb"                      storage type ($GDNOTIFY_STORAGE_TYPE)
      --storage-table-name="gdnotify"                dynamodb table name ($GDNOTIFY_DDB_TABLE_NAME)
      --[no-]storage-auto-create                     auto create dynamodb table ($GDNOTIFY_DDB_AUTO_CREATE)
      --storage-dynamo-db-endpoint=STRING            dynamodb endpoint ($GDNOTIFY_DDB_ENDPOINT)
      --storage-data-file="gdnotify.dat"             file storage data file ($GDNOTIFY_FILE_STORAGE_DATA_FILE)
      --storage-lock-file="gdnotify.lock"            file storage lock file ($GDNOTIFY_FILE_STORAGE_LOCK_FILE)
      --notification-type="eventbridge"              notification type ($GDNOTIFY_NOTIFICATION_TYPE)
      --notification-event-bus="default"             event bus name (eventbridge type only) ($GDNOTIFY_EVENTBRIDGE_EVENT_BUS)
      --notification-event-file="gdnotify.json"      event file path (file type only) ($GDNOTIFY_EVENT_FILE)
      --webhook=""                                   webhook address ($GDNOTIFY_WEBHOOK)
      --expiration=168h                              channel expiration ($GDNOTIFY_EXPIRATION)
      --within-modified-time=WITHIN-MODIFIED-TIME    within modified time, If the edit time is not within this time, notifications will not be sent ($GDNOTIFY_WITHIN_MODIFIED_TIME).

Commands:
  list [flags]
    list notification channels

  serve [flags]
    serve webhook server

  cleanup [flags]
    remove all notification channels

  sync [flags]
    force sync notification channels; re-register expired notification channels,register new unregistered channels and get all new notification

Run "gdnotify <command> --help" for more information on a command.

Refer to the following document to prepare the permissions for Google Cloud. Please enable the Google Drive API v3 in the corresponding Google Cloud in advance. https://cloud.google.com/docs/authentication/application-default-credentials

Start the server with gdnotify as follows.

$ go run cmd/gdnotify/main.go --storage-auto-create                  
time=2025-03-13T19:29:17.799+09:00 level=INFO msg="check describe dynamodb table" table_name=gdnotify
time=2025-03-13T19:29:18.379+09:00 level=INFO msg="starting up with local httpd :25254"

By default, the server will start at :25254. By specifying the --storage-auto-create option, the DynamoDB table will be created automatically.

Here, use the Tonnel function of VSCode, etc., to make it accessible from the outside. If it becomes accessible at an address like https://xxxxxxxx-25254.asse.devtunnels.ms/, access it as follows.

$ curl -X POST https://xxxxxxxx-25254.asse.devtunnels.ms/sync

Then, notifications to EventBridge will start. The following diagram illustrates what happens.

sequenceDiagram
  autonumber
  gdnotify [/sync]->>+Google API: GET /drive/v3/changes/startPageToken
  Google API-->>-gdnotify [/sync]: PageToken
  gdnotify [/sync]->>+Google API: POST /drive/v3/changes/watch
  Google API [push]--)gdnotify [/]:  sync request
  Google API-->>-gdnotify [/sync]: response
  gdnotify [/sync]->>+DynamoDB: put item
  DynamoDB-->>-gdnotify [/sync]: response
  loop
    Google API [push]->>+gdnotify [/]: change request
    gdnotify [/]->>+Google API: GET /drive/v3/changes
    Google API-->>- gdnotify [/]: changes info
     gdnotify [/]->>+EventBridge: Put Events
    EventBridge-->>- gdnotify [/]: response
     gdnotify [/]->>+DynamoDB: update item
    DynamoDB-->>- gdnotify [/]: response
    gdnotify [/]->>+Google API [push]: Status Ok
  end

Usage with AWS Lambda

gdnotify can run as a Lambda runtime. Therefore, by deploying the binary as a Lambda function, you can easily receive Google Drive change notifications via EventBridge.

Let's solidify the Lambda package with the following configuration (runtime provided.al2023)

lambda.zip
└── bootstrap    # build binary

For more details, refer to Lambda Example.

The IAM permissions required are as follows, in addition to AWSLambdaBasicExecutionRole:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Webhook",
            "Effect": "Allow",
            "Action": [
                "events:PutEvents",
                "dynamodb:DescribeTable",
                "dynamodb:GetItem",
                "dynamodb:UpdateItem",
                "dynamodb:CreateTable",
                "dynamodb:PutItem",
                "dynamodb:DeleteItem",
                "dynamodb:Scan"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

A related document is https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html

When the Lambda function is invoked directly, it behaves as if a request was made to the /sync endpoint. It is recommended to periodically invoke the Lambda function using EventBridge Scheduler, etc. The /sync endpoint performs notification channel rotation and forced change notification synchronization.

To receive change notifications from the Google Drive API, you need to set up a Lambda Function URL, API Gateway, ALB, etc. The simplest is the Lambda Function URL, so here is a reference link.

lambda function URLs document is https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html

EventBridge Event Payload

Finally, the notified event is notified with the following detail.

{
  "subject": "File gdnotify (XXXXXXXXXX) changed by hoge [[email protected]] at 2022-06-15T00:03:45.843Z",
  "entity": {
    "id": "XXXXXXXXXX",
    "kind": "drive#file",
    "name": "gdnotify",
    "createdTime": ""
  },
  "actor": {
    "displayName": "hoge",
    "emailAddress": "[email protected]",
    "kind": "drive#user"
  },
  "change": {
    "changeType": "file",
    "file": {
      "id": "XXXXXXXXXX",
      "kind": "drive#file",
      "lastModifyingUser": {
        "displayName": "hoge",
        "emailAddress": "[email protected]",
        "kind": "drive#user"
      },
      "mimeType": "application/vnd.google-apps.spreadsheet",
      "modifiedTime": "2022-06-15T00:03:45.843Z",
      "name": "gdnotify",
      "size": "1500",
      "version": "20"
    },
    "fileId": "XXXXXXXXXX",
    "kind": "drive#change",
    "time": "2022-06-15T00:03:55.849Z"
  }
}

For example, if you set the following event pattern, all events will trigger the rule.

{
    "source" : [{
    "prefix" : "oss.gdnotify"
    }]
}

If you specify the following event rule, you can narrow it down to only file-related notifications.

{
    "source": [{
        "prefix":"oss.gdnotify"
    }],
    "detail":{
        "subject":[{
            "prefix": "File"
        }]
    }
}

Set any EventBridge rules and connect to the subsequent processing.

LICENSE

MIT License

Copyright (c) 2022 IKEDA Masashi

Documentation

Index

Constants

View Source
const (
	DefaultDriveID   = "__default__"
	DefaultDriveName = "My Drive and Individual Files"
)
View Source
const (
	DetailTypeFileRemoved  = "File Removed"
	DetailTypeFileTrashed  = "File Move to trash"
	DetailTypeFileChanged  = "File Changed"
	DetailTypeDriveRemoved = "Shared Drive Removed"
	DetailTypeDriveChanged = "Drive Status Changed"
)

Variables

View Source
var Version = "v0.5.3"

Functions

func GetAttributeValueAs

func GetAttributeValueAs[T types.AttributeValue](key string, values map[string]types.AttributeValue) (T, bool)

func IterMap added in v0.5.0

func IterMap[E, F any](seq iter.Seq[E], fn func(E) F) iter.Seq[F]

func KeyValues added in v0.5.0

func KeyValues[E, V any, K comparable](s []E, fn func(E) (K, V)) map[K]V

func Map added in v0.5.0

func Map[E, F any](s []E, fn func(E) F) []F

func SetAWSConfig added in v0.5.0

func SetAWSConfig(cfg aws.Config)

Types

type App

type App struct {
	// contains filtered or unexported fields
}

func New

func New(cfg AppOption, storage Storage, notification Notification, gcpOpts ...option.ClientOption) (*App, error)

func (*App) ChangesList

func (app *App) ChangesList(ctx context.Context, channelID string) ([]*drive.Change, *ChannelItem, error)

func (*App) Cleanup added in v0.5.0

func (app *App) Cleanup(ctx context.Context, _ CleanupOption) error

func (*App) Close

func (app *App) Close() error

func (*App) CreateChannel

func (app *App) CreateChannel(ctx context.Context, driveID string) error

func (*App) DeleteChannel

func (app *App) DeleteChannel(ctx context.Context, item *ChannelItem) error

func (*App) DriveIDs added in v0.4.0

func (app *App) DriveIDs(ctx context.Context) ([]string, error)

func (*App) Drives added in v0.5.0

func (app *App) Drives(ctx context.Context) ([]*drive.Drive, error)

func (*App) List added in v0.5.0

func (app *App) List(ctx context.Context, o ListOption) error

func (*App) RotateChannel

func (app *App) RotateChannel(ctx context.Context, item *ChannelItem) error

func (*App) SendNotification added in v0.3.0

func (app *App) SendNotification(ctx context.Context, item *ChannelItem, changes []*drive.Change) error

func (*App) Serve added in v0.5.0

func (app *App) Serve(ctx context.Context, o ServeOption) error

func (*App) ServeHTTP

func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*App) Sync added in v0.5.0

func (app *App) Sync(ctx context.Context, _ SyncOption) error

type AppOption added in v0.5.0

type AppOption struct {
	Webhook            string         `help:"webhook address" default:"" env:"GDNOTIFY_WEBHOOK"`
	Expiration         time.Duration  `help:"channel expiration" default:"168h" env:"GDNOTIFY_EXPIRATION"`
	WithinModifiedTime *time.Duration `` /* 138-byte string literal not displayed */
}

type CLI added in v0.5.0

type CLI struct {
	LogLevel      string             `help:"log level" default:"info" env:"GDNOTIFY_LOG_LEVEL"`
	LogFormat     string             `help:"log format" default:"text" enum:"text,json" env:"GDNOTIFY_LOG_FORMAT"`
	LogColor      bool               `help:"enable color output" default:"true" env:"GDNOTIFY_LOG_COLOR" negatable:""`
	Version       kong.VersionFlag   `help:"show version"`
	Storage       StorageOption      `embed:"" prefix:"storage-"`
	Nootification NotificationOption `embed:"" prefix:"notification-"`
	AppOption     `embed:""`

	List    ListOption    `cmd:"" help:"list notification channels"`
	Serve   ServeOption   `cmd:"" help:"serve webhook server" default:"true"`
	Cleanup CleanupOption `cmd:"" help:"remove all notification channels"`
	Sync    SyncOption    `` /* 153-byte string literal not displayed */
}

func (*CLI) Run added in v0.5.0

func (c *CLI) Run(ctx context.Context) int

type ChangeEventDetail added in v0.2.0

type ChangeEventDetail struct {
	Subject string        `json:"subject"`
	Entity  *TargetEntity `json:"entity"`
	Actor   *drive.User   `json:"actor"`
	Change  *drive.Change `json:"change"`
}

func (*ChangeEventDetail) DetailType added in v0.2.0

func (e *ChangeEventDetail) DetailType() string

func (*ChangeEventDetail) MarshalJSON added in v0.2.0

func (e *ChangeEventDetail) MarshalJSON() ([]byte, error)

func (*ChangeEventDetail) Source added in v0.2.0

func (e *ChangeEventDetail) Source(sourcePrefix string) string

type ChannelAlreadyExists

type ChannelAlreadyExists struct {
	ChannelID string
}

func (*ChannelAlreadyExists) Error

func (err *ChannelAlreadyExists) Error() string

type ChannelItem

type ChannelItem struct {
	ChannelID          string
	Expiration         time.Time
	PageToken          string
	ResourceID         string
	DriveID            string
	PageTokenFetchedAt time.Time
	CreatedAt          time.Time
	UpdatedAt          time.Time
}

func NewChannelItemWithDynamoDBAttributeValues

func NewChannelItemWithDynamoDBAttributeValues(values map[string]types.AttributeValue) *ChannelItem

func (*ChannelItem) IsAboutToExpired

func (item *ChannelItem) IsAboutToExpired(ctx context.Context, remaining time.Duration) bool

func (*ChannelItem) ToDynamoDBAttributeValues

func (item *ChannelItem) ToDynamoDBAttributeValues() map[string]types.AttributeValue

type ChannelNotFoundError added in v0.5.0

type ChannelNotFoundError struct {
	ChannelID string
}

func (*ChannelNotFoundError) Error added in v0.5.0

func (err *ChannelNotFoundError) Error() string

type CleanupOption added in v0.5.0

type CleanupOption struct {
}

type DynamoDBStorage

type DynamoDBStorage struct {
	// contains filtered or unexported fields
}

func NewDynamoDBStorage

func NewDynamoDBStorage(ctx context.Context, cfg StorageOption) (*DynamoDBStorage, error)

func (*DynamoDBStorage) DeleteChannel

func (s *DynamoDBStorage) DeleteChannel(ctx context.Context, target *ChannelItem) error

func (*DynamoDBStorage) FindAllChannels

func (s *DynamoDBStorage) FindAllChannels(ctx context.Context) (<-chan []*ChannelItem, error)

func (*DynamoDBStorage) FindOneByChannelID

func (s *DynamoDBStorage) FindOneByChannelID(ctx context.Context, channelID string) (*ChannelItem, error)

func (*DynamoDBStorage) SaveChannel

func (s *DynamoDBStorage) SaveChannel(ctx context.Context, item *ChannelItem) error

func (*DynamoDBStorage) UpdatePageToken

func (s *DynamoDBStorage) UpdatePageToken(ctx context.Context, target *ChannelItem) error

type EventBridgeClient

type EventBridgeClient interface {
	PutEvents(ctx context.Context, params *eventbridge.PutEventsInput, optFns ...func(*eventbridge.Options)) (*eventbridge.PutEventsOutput, error)
}

type EventBridgeNotification

type EventBridgeNotification struct {
	// contains filtered or unexported fields
}

func (*EventBridgeNotification) SendChanges

func (n *EventBridgeNotification) SendChanges(ctx context.Context, item *ChannelItem, changes []*drive.Change) error

type FileNotification

type FileNotification struct {
	// contains filtered or unexported fields
}

func NewFileNotification

func NewFileNotification(_ context.Context, cfg NotificationOption) (*FileNotification, error)

func (*FileNotification) SendChanges

func (n *FileNotification) SendChanges(ctx context.Context, _ *ChannelItem, changes []*drive.Change) error

type FileStorage

type FileStorage struct {
	Items []*ChannelItem

	LockFile string
	FilePath string
	// contains filtered or unexported fields
}

func NewFileStorage

func NewFileStorage(_ context.Context, cfg StorageOption) (*FileStorage, error)

func (*FileStorage) DeleteChannel

func (s *FileStorage) DeleteChannel(ctx context.Context, target *ChannelItem) error

func (*FileStorage) FindAllChannels

func (s *FileStorage) FindAllChannels(ctx context.Context) (<-chan []*ChannelItem, error)

func (*FileStorage) FindOneByChannelID

func (s *FileStorage) FindOneByChannelID(ctx context.Context, channelID string) (*ChannelItem, error)

func (*FileStorage) SaveChannel

func (s *FileStorage) SaveChannel(ctx context.Context, item *ChannelItem) error

func (*FileStorage) UpdatePageToken

func (s *FileStorage) UpdatePageToken(ctx context.Context, target *ChannelItem) error

type ListOption added in v0.5.0

type ListOption struct {
	Output io.Writer `kong:"-"`
}

type Notification

type Notification interface {
	SendChanges(context.Context, *ChannelItem, []*drive.Change) error
}

func NewEventBridgeNotification

func NewEventBridgeNotification(_ context.Context, cfg NotificationOption) (Notification, error)

func NewNotification

func NewNotification(ctx context.Context, cfg NotificationOption) (Notification, error)

type NotificationOption added in v0.5.0

type NotificationOption struct {
	Type      string `help:"notification type" default:"eventbridge" enum:"eventbridge,file" env:"GDNOTIFY_NOTIFICATION_TYPE"`
	EventBus  string `help:"event bus name (eventbridge type only)" default:"default" env:"GDNOTIFY_EVENTBRIDGE_EVENT_BUS"`
	EventFile string `help:"event file path (file type only)" default:"gdnotify.json" env:"GDNOTIFY_EVENT_FILE"`
}

type ServeOption added in v0.5.0

type ServeOption struct {
	Port int `help:"webhook httpd port" default:"25254" env:"GDNOTIFY_PORT"`
}

type Storage

type Storage interface {
	FindAllChannels(context.Context) (<-chan []*ChannelItem, error)
	FindOneByChannelID(context.Context, string) (*ChannelItem, error)
	UpdatePageToken(context.Context, *ChannelItem) error
	SaveChannel(context.Context, *ChannelItem) error
	DeleteChannel(context.Context, *ChannelItem) error
}

func NewStorage

func NewStorage(ctx context.Context, cfg StorageOption) (Storage, error)

type StorageOption added in v0.5.0

type StorageOption struct {
	Type             string `help:"storage type" default:"dynamodb" enum:"dynamodb,file" env:"GDNOTIFY_STORAGE_TYPE"`
	TableName        string `help:"dynamodb table name" default:"gdnotify" env:"GDNOTIFY_DDB_TABLE_NAME"`
	AutoCreate       bool   `help:"auto create dynamodb table" default:"false" env:"GDNOTIFY_DDB_AUTO_CREATE" negatable:""`
	DynamoDBEndpoint string `help:"dynamodb endpoint" env:"GDNOTIFY_DDB_ENDPOINT"`
	DataFile         string `help:"file storage data file" default:"gdnotify.dat" env:"GDNOTIFY_FILE_STORAGE_DATA_FILE"`
	LockFile         string `help:"file storage lock file" default:"gdnotify.lock" env:"GDNOTIFY_FILE_STORAGE_LOCK_FILE"`
}

type SyncOption added in v0.5.0

type SyncOption struct {
}

type TargetEntity added in v0.2.0

type TargetEntity struct {
	Id          string `json:"id"`
	Kind        string `json:"kind"`
	Name        string `json:"name"`
	CreatedTime string `json:"createdTime"`
}

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL