From c10a4d465c8a9dbf87dd90b6350cc6c308349265 Mon Sep 17 00:00:00 2001 From: masrlinu Date: Wed, 17 Dec 2025 16:13:25 +0100 Subject: [PATCH] pcloud: add support for real-time updates in mount Co-authored-by: masrlinu <5259918+masrlinu@users.noreply.github.com> --- backend/pcloud/api/types.go | 8 +++ backend/pcloud/pcloud.go | 133 ++++++++++++++++++++++++++++++++++++ docs/content/pcloud.md | 25 +++++++ 3 files changed, 166 insertions(+) diff --git a/backend/pcloud/api/types.go b/backend/pcloud/api/types.go index 6f2d6361b..d567d49af 100644 --- a/backend/pcloud/api/types.go +++ b/backend/pcloud/api/types.go @@ -222,3 +222,11 @@ type UserInfo struct { } `json:"steps"` } `json:"journey"` } + +// DiffResult is the response from /diff +type DiffResult struct { + Result int `json:"result"` + DiffID int64 `json:"diffid"` + Entries []map[string]any `json:"entries"` + Error string `json:"error"` +} diff --git a/backend/pcloud/pcloud.go b/backend/pcloud/pcloud.go index ff7fa1a68..afbc421ef 100644 --- a/backend/pcloud/pcloud.go +++ b/backend/pcloud/pcloud.go @@ -171,6 +171,7 @@ type Fs struct { dirCache *dircache.DirCache // Map of directory path to directory id pacer *fs.Pacer // pacer for API calls tokenRenewer *oauthutil.Renew // renew the token on expiry + lastDiffID int64 // change tracking state for diff long-polling } // Object describes a pcloud object @@ -1033,6 +1034,137 @@ func (f *Fs) Shutdown(ctx context.Context) error { 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: + pid = int64(v) + case int64: + pid = v + case int: + pid = int64(v) + } + } + + // Resolve the path using dirCache.GetInv + // pCloud uses "d" prefix for directory IDs in cache, but API returns numbers + dirID := fmt.Sprintf("d%d", pid) + parentPath, ok := f.dirCache.GetInv(dirID) + + if !ok { + // Parent not in cache, so we can ignore this change as it is outside + // of what the mount has seen or cares about. + continue + } + + name, _ := meta["name"].(string) + fullPath := path.Join(parentPath, name) + + // Determine EntryType (File or Directory) + entryType := fs.EntryObject + if isFolder, ok := meta["isfolder"].(bool); ok && isFolder { + entryType = fs.EntryDirectory + } + + // Deduplicate notifications for this batch + if !notifiedPaths[fullPath] { + fs.Debugf(f, "ChangeNotify: detected change in %q (type: %v)", fullPath, entryType) + notify(fullPath, entryType) + notifiedPaths[fullPath] = true + } + } + } + + for { + // Check context and channel + select { + case <-ctx.Done(): + return + case newInterval, ok := <-ch: + if !ok { + return + } + interval = newInterval + default: + } + + // Setup /diff Request + opts := rest.Opts{ + Method: "GET", + Path: "/diff", + Parameters: url.Values{}, + } + + if diffID != 0 { + opts.Parameters.Set("diffid", strconv.FormatInt(diffID, 10)) + opts.Parameters.Set("block", "1") + } else { + opts.Parameters.Set("last", "0") + } + + // Perform Long-Poll + // Timeout set to 90s (server usually blocks for 60s max) + reqCtx, cancel := context.WithTimeout(ctx, 90*time.Second) + var result api.DiffResult + + _, err := f.srv.CallJSON(reqCtx, &opts, nil, &result) + cancel() + + if err != nil { + if errors.Is(err, context.Canceled) { + return + } + // Ignore timeout errors as they are normal for long-polling + if !errors.Is(err, context.DeadlineExceeded) { + fs.Infof(f, "ChangeNotify: polling error: %v. Waiting %v.", err, interval) + time.Sleep(interval) + } + continue + } + + // If result is not 0, reset DiffID to resync + if result.Result != 0 { + diffID = 0 + time.Sleep(2 * time.Second) + continue + } + + if result.DiffID != 0 { + diffID = result.DiffID + f.lastDiffID = diffID + } + + if len(result.Entries) > 0 { + handleChanges(result.Entries) + } + } +} + // Hashes returns the supported hash sets. func (f *Fs) Hashes() hash.Set { // EU region supports SHA1 and SHA256 (but rclone doesn't @@ -1401,6 +1533,7 @@ var ( _ fs.ListPer = (*Fs)(nil) _ fs.Abouter = (*Fs)(nil) _ fs.Shutdowner = (*Fs)(nil) + _ fs.ChangeNotifier = (*Fs)(nil) _ fs.Object = (*Object)(nil) _ fs.IDer = (*Object)(nil) ) diff --git a/docs/content/pcloud.md b/docs/content/pcloud.md index 8f8167e34..90e16b380 100644 --- a/docs/content/pcloud.md +++ b/docs/content/pcloud.md @@ -173,6 +173,31 @@ So if the folder you want rclone to use your is "My Music/", then use the return id from ```rclone lsf``` command (ex. `dxxxxxxxx2`) as the `root_folder_id` variable value in the config file. +### Change notifications and mounts + +The pCloud backend supports real‑time updates for rclone mounts via change +notifications. rclone uses pCloud’s diff long‑polling API to detect changes and +will automatically refresh directory listings in the mounted filesystem when +changes occur. + +Notes and behavior: + +- Works automatically when using `rclone mount` and requires no additional + configuration. +- Notifications are directory‑scoped: when rclone detects a change, it refreshes + the affected directory so new/removed/renamed files become visible promptly. +- Updates are near real‑time. The backend uses a long‑poll with short fallback + polling intervals, so you should see changes appear quickly without manual + refreshes. + +If you want to debug or verify notifications, you can use the helper command: + +```bash +rclone test changenotify remote: +``` + +This will log incoming change notifications for the given remote. + ### Standard options