Skip to content

Commit c10a4d4

Browse files
authored
pcloud: add support for real-time updates in mount
Co-authored-by: masrlinu <[email protected]>
1 parent 3a6e07a commit c10a4d4

File tree

3 files changed

+166
-0
lines changed

3 files changed

+166
-0
lines changed

backend/pcloud/api/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,11 @@ type UserInfo struct {
222222
} `json:"steps"`
223223
} `json:"journey"`
224224
}
225+
226+
// DiffResult is the response from /diff
227+
type DiffResult struct {
228+
Result int `json:"result"`
229+
DiffID int64 `json:"diffid"`
230+
Entries []map[string]any `json:"entries"`
231+
Error string `json:"error"`
232+
}

backend/pcloud/pcloud.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ type Fs struct {
171171
dirCache *dircache.DirCache // Map of directory path to directory id
172172
pacer *fs.Pacer // pacer for API calls
173173
tokenRenewer *oauthutil.Renew // renew the token on expiry
174+
lastDiffID int64 // change tracking state for diff long-polling
174175
}
175176

176177
// Object describes a pcloud object
@@ -1033,6 +1034,137 @@ func (f *Fs) Shutdown(ctx context.Context) error {
10331034
return nil
10341035
}
10351036

1037+
// ChangeNotify implements fs.Features.ChangeNotify
1038+
func (f *Fs) ChangeNotify(ctx context.Context, notify func(string, fs.EntryType), ch <-chan time.Duration) {
1039+
// Start long-poll loop in background
1040+
go f.changeNotifyLoop(ctx, notify, ch)
1041+
}
1042+
1043+
// changeNotifyLoop contains the blocking long-poll logic.
1044+
func (f *Fs) changeNotifyLoop(ctx context.Context, notify func(string, fs.EntryType), ch <-chan time.Duration) {
1045+
// Standard polling interval
1046+
interval := 30 * time.Second
1047+
1048+
// Start with diffID = 0 to get the current state
1049+
var diffID int64
1050+
1051+
// Helper to process changes from the diff API
1052+
handleChanges := func(entries []map[string]any) {
1053+
notifiedPaths := make(map[string]bool)
1054+
1055+
for _, entry := range entries {
1056+
meta, ok := entry["metadata"].(map[string]any)
1057+
if !ok {
1058+
continue
1059+
}
1060+
1061+
// Robust extraction of ParentFolderID
1062+
var pid int64
1063+
if val, ok := meta["parentfolderid"]; ok {
1064+
switch v := val.(type) {
1065+
case float64:
1066+
pid = int64(v)
1067+
case int64:
1068+
pid = v
1069+
case int:
1070+
pid = int64(v)
1071+
}
1072+
}
1073+
1074+
// Resolve the path using dirCache.GetInv
1075+
// pCloud uses "d" prefix for directory IDs in cache, but API returns numbers
1076+
dirID := fmt.Sprintf("d%d", pid)
1077+
parentPath, ok := f.dirCache.GetInv(dirID)
1078+
1079+
if !ok {
1080+
// Parent not in cache, so we can ignore this change as it is outside
1081+
// of what the mount has seen or cares about.
1082+
continue
1083+
}
1084+
1085+
name, _ := meta["name"].(string)
1086+
fullPath := path.Join(parentPath, name)
1087+
1088+
// Determine EntryType (File or Directory)
1089+
entryType := fs.EntryObject
1090+
if isFolder, ok := meta["isfolder"].(bool); ok && isFolder {
1091+
entryType = fs.EntryDirectory
1092+
}
1093+
1094+
// Deduplicate notifications for this batch
1095+
if !notifiedPaths[fullPath] {
1096+
fs.Debugf(f, "ChangeNotify: detected change in %q (type: %v)", fullPath, entryType)
1097+
notify(fullPath, entryType)
1098+
notifiedPaths[fullPath] = true
1099+
}
1100+
}
1101+
}
1102+
1103+
for {
1104+
// Check context and channel
1105+
select {
1106+
case <-ctx.Done():
1107+
return
1108+
case newInterval, ok := <-ch:
1109+
if !ok {
1110+
return
1111+
}
1112+
interval = newInterval
1113+
default:
1114+
}
1115+
1116+
// Setup /diff Request
1117+
opts := rest.Opts{
1118+
Method: "GET",
1119+
Path: "/diff",
1120+
Parameters: url.Values{},
1121+
}
1122+
1123+
if diffID != 0 {
1124+
opts.Parameters.Set("diffid", strconv.FormatInt(diffID, 10))
1125+
opts.Parameters.Set("block", "1")
1126+
} else {
1127+
opts.Parameters.Set("last", "0")
1128+
}
1129+
1130+
// Perform Long-Poll
1131+
// Timeout set to 90s (server usually blocks for 60s max)
1132+
reqCtx, cancel := context.WithTimeout(ctx, 90*time.Second)
1133+
var result api.DiffResult
1134+
1135+
_, err := f.srv.CallJSON(reqCtx, &opts, nil, &result)
1136+
cancel()
1137+
1138+
if err != nil {
1139+
if errors.Is(err, context.Canceled) {
1140+
return
1141+
}
1142+
// Ignore timeout errors as they are normal for long-polling
1143+
if !errors.Is(err, context.DeadlineExceeded) {
1144+
fs.Infof(f, "ChangeNotify: polling error: %v. Waiting %v.", err, interval)
1145+
time.Sleep(interval)
1146+
}
1147+
continue
1148+
}
1149+
1150+
// If result is not 0, reset DiffID to resync
1151+
if result.Result != 0 {
1152+
diffID = 0
1153+
time.Sleep(2 * time.Second)
1154+
continue
1155+
}
1156+
1157+
if result.DiffID != 0 {
1158+
diffID = result.DiffID
1159+
f.lastDiffID = diffID
1160+
}
1161+
1162+
if len(result.Entries) > 0 {
1163+
handleChanges(result.Entries)
1164+
}
1165+
}
1166+
}
1167+
10361168
// Hashes returns the supported hash sets.
10371169
func (f *Fs) Hashes() hash.Set {
10381170
// EU region supports SHA1 and SHA256 (but rclone doesn't
@@ -1401,6 +1533,7 @@ var (
14011533
_ fs.ListPer = (*Fs)(nil)
14021534
_ fs.Abouter = (*Fs)(nil)
14031535
_ fs.Shutdowner = (*Fs)(nil)
1536+
_ fs.ChangeNotifier = (*Fs)(nil)
14041537
_ fs.Object = (*Object)(nil)
14051538
_ fs.IDer = (*Object)(nil)
14061539
)

docs/content/pcloud.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,31 @@ So if the folder you want rclone to use your is "My Music/", then use the return
173173
id from ```rclone lsf``` command (ex. `dxxxxxxxx2`) as the `root_folder_id` variable
174174
value in the config file.
175175

176+
### Change notifications and mounts
177+
178+
The pCloud backend supports real‑time updates for rclone mounts via change
179+
notifications. rclone uses pCloud’s diff long‑polling API to detect changes and
180+
will automatically refresh directory listings in the mounted filesystem when
181+
changes occur.
182+
183+
Notes and behavior:
184+
185+
- Works automatically when using `rclone mount` and requires no additional
186+
configuration.
187+
- Notifications are directory‑scoped: when rclone detects a change, it refreshes
188+
the affected directory so new/removed/renamed files become visible promptly.
189+
- Updates are near real‑time. The backend uses a long‑poll with short fallback
190+
polling intervals, so you should see changes appear quickly without manual
191+
refreshes.
192+
193+
If you want to debug or verify notifications, you can use the helper command:
194+
195+
```bash
196+
rclone test changenotify remote:
197+
```
198+
199+
This will log incoming change notifications for the given remote.
200+
176201
<!-- autogenerated options start - DO NOT EDIT - instead edit fs.RegInfo in backend/pcloud/pcloud.go and run make backenddocs to verify --> <!-- markdownlint-disable-line line-length -->
177202
### Standard options
178203

0 commit comments

Comments
 (0)