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

Code before:
	// 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

	// Helper: Recursively resolve pCloud folder ID to rclone relative path
	resolvePath = func(folderID int64) string {
		// 0. Base case: Root
		if folderID == 0 {
			return ""
		}

		// 1. Check local cache first
		if p, ok := folderPathCache[folderID]; ok {
			return p
		}

		// 2. API Call: Fetch metadata (Name & ParentID)
		// We MUST call the API here because the changeNotify loop is isolated
		// and doesn't know about folders listed by the user in the mount.
		// "nostats=1" keeps the call lightweight.
		opts := rest.Opts{
			Method: "GET",
			Path:   "/listfolder",
			Parameters: url.Values{
				"folderid": {strconv.FormatInt(folderID, 10)},
				"nostats":  {"1"},
			},
		}

		var res api.ItemResult

Review comment (performance):
Factor this helper into a new method on Fs as it is a bit long.

The drive backend just uses f.dirCache.GetInv for this - I think this should work here.

Note that any directories that need flushing for `rclone mount` will already be in the directory cache so this should always have a mapping.

So maybe this helper isn't needed at all.

Code after:
}

// 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