Repository: rclone/rclone
File: backend/pcloud/pcloud.go (Go)

Code before:
	var q api.UserInfo
	err = f.pacer.Call(func() (bool, error) {
		resp, err = f.srv.CallJSON(ctx, &opts, nil, &q)
		err = q.Error.Update(err)
		return shouldRetry(ctx, resp, err)
	})
	if err != nil {
		return nil, err
	}
	free := max(q.Quota-q.UsedQuota, 0)
	usage = &fs.Usage{
		Total: fs.NewUsageValue(q.Quota),     // quota of bytes that can be used
		Used:  fs.NewUsageValue(q.UsedQuota), // bytes in use
		Free:  fs.NewUsageValue(free),        // bytes which can be uploaded before reaching the quota
	}
	return usage, nil
}

// Shutdown shutdown the fs
func (f *Fs) Shutdown(ctx context.Context) error {
	f.tokenRenewer.Shutdown()
	return nil
}

// changeNotify implements fs.Features.ChangeNotify
func (f *Fs) changeNotify(ctx context.Context, notify func(string, fs.EntryType), ch <-chan time.Duration) {
	// Start long-poll loop in background
	go f.changeNotifyLoop(ctx, notify, ch)
}

// changeNotifyLoop contains the blocking long-poll logic.
func (f *Fs) changeNotifyLoop(ctx context.Context, notify func(string, fs.EntryType), ch <-chan time.Duration) {
	// Standard polling interval
	interval := 30 * time.Second

	// Start with diffID = 0 to get the current state
	var diffID int64 = 0

	// Cache to map FolderID (int64) -> Rclone Path (string)
	// This local cache is essential because the API provides events by ID,
	// but rclone needs paths. We cannot rely on rclone's internal dirCache
	// here as it is optimized for Path->ID, not ID->Path.
	folderPathCache := make(map[int64]string)
	folderPathCache[0] = "" // Initialize with Root

	// Safety: Limit cache size to prevent memory leaks on long-running mounts
	const maxCacheSize = 10000

	// Forward declaration for recursion
	var resolvePath func(int64) string


Review comment (suggestion):
This should be called `ChangeNotify`

and here

https://github.com/rclone/rclone/blob/976aa6b41680211e06b2821e67ef92de10c376e2/backend/pcloud/pcloud.go#L1403

Add an interface assertion for the ChangeNotify method

Code after:
		resp, err = f.srv.CallJSON(ctx, &opts, nil, &q)
		err = q.Error.Update(err)
		return shouldRetry(ctx, resp, err)
	})
	if err != nil {
		return nil, err
	}
	free := max(q.Quota-q.UsedQuota, 0)
	usage = &fs.Usage{
		Total: fs.NewUsageValue(q.Quota),     // quota of bytes that can be used
		Used:  fs.NewUsageValue(q.UsedQuota), // bytes in use
		Free:  fs.NewUsageValue(free),        // bytes which can be uploaded before reaching the quota
	}
	return usage, nil
}

// Shutdown shutdown the fs
func (f *Fs) Shutdown(ctx context.Context) error {
	f.tokenRenewer.Shutdown()
	return nil
}

// ChangeNotify implements fs.Features.ChangeNotify
func (f *Fs) ChangeNotify(ctx context.Context, notify func(string, fs.EntryType), ch <-chan time.Duration) {
	// Start long-poll loop in background
	go f.changeNotifyLoop(ctx, notify, ch)
}

// changeNotifyLoop contains the blocking long-poll logic.
func (f *Fs) changeNotifyLoop(ctx context.Context, notify func(string, fs.EntryType), ch <-chan time.Duration) {
	// Standard polling interval
	interval := 30 * time.Second

	// Start with diffID = 0 to get the current state
	var diffID int64

	// Helper to process changes from the diff API
	handleChanges := func(entries []map[string]any) {
		notifiedPaths := make(map[string]bool)

		for _, entry := range entries {
			meta, ok := entry["metadata"].(map[string]any)
			if !ok {
				continue
			}

			// Robust extraction of ParentFolderID
			var pid int64
			if val, ok := meta["parentfolderid"]; ok {
				switch v := val.(type) {
				case float64: