iclouddrive (photos): names containing "/" can be listed but not addressed (FromStandardName re-encodes the slash)
Before you start
- I have searched the forum and the existing issues for this problem.
- I have tested with the latest beta or stable release and can still reproduce the problem.
- This is a reproducible bug, not a usage question (questions belong on the forum).
Associated forum post URL
No response
What is the problem you are having with rclone?
On the iclouddrive backend with service = photos, rclone renames colliding filenames by
appending the photo's record id. CloudKit record names are base64 and can contain /, so the
generated filename contains a path separator. The file is listed normally, with the slash
shown as the fullwidth solidus / (U+FF0F), but it cannot then be addressed by that name:
operations/copyfile srcFs=icphotos:PrimarySync/<album>
srcRemote=lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic
-> {"error": "object not found", "status": 404}
The same file copies fine through a sync, which never resolves a name. A sync copies the objects the listing yields:
sync/copy srcFs=icphotos:PrimarySync/<album> dstFs=/tmp/slashtest
_filter={"IncludeRule": ["/lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic"]}
-> success, 1 transfer, 22467 bytes, 0 errors
In one album, 2,195 of 9,933 files are affected, and every one of them is unreachable by name.
This is the same root cause as #9464 (albums with a slash in their name), which is still open. That issue reports it for directories; this one reports it for files, where the slash is introduced by rclone itself rather than by the user's album naming.
The deployment drives rclone through rcd, so most of the evidence here is from the rc API.
Listing shows the file:
{"Path": "lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic",
"Name": "lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic",
"Size": 22467, "MimeType": "image/heif",
"ModTime": "2024-05-17T15:12:12+02:00", "IsDir": false}
operations/copyfile on that exact name, with the destination pointed at local disk so
no second remote is involved:
{"error": "object not found",
"input": {"srcFs": "icphotos:PrimarySync/<album>",
"srcRemote": "lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic",
"dstFs": "/tmp/slashtest",
"dstRemote": "lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic"},
"path": "operations/copyfile", "status": 404}
Same result whether the album is part of the Fs root (icphotos:PrimarySync/<album> plus
the leaf name) or part of the remote path (icphotos: plus PrimarySync/<album>/<name>).
Expected behaviour: a name that appears in a listing can be used to address that object.
rclone version
First observed on Linux:
rclone v1.75.1
- os/version: ubuntu 24.04 (64 bit)
- os/kernel: 6.8.0-138-generic (x86_64)
- os/type: linux
- os/arch: amd64
- go/version: go1.26.8
The -vv log below is from a second reproduction on macOS, on a different album and driven
from the CLI rather than through rcd:
rclone v1.74.3
- os/version: darwin 15.7.7 (64 bit)
- os/kernel: 24.6.0 (arm64)
- os/type: darwin
- os/arch: arm64 (ARMv8 compatible)
- go/version: go1.26.4
- go/linking: dynamic
- go/tags: cmount
The relevant code is unchanged on master (812e693).
Operating system
Linux (also reproduced on macOS, log below)
Which cloud storage system are you using?
iCloud Photos, using the iclouddrive backend with service = photos.
Output of rclone config redacted
[icphotos]
type = iclouddrive
service = photos
apple_id = XXX
_auth_session =
password = XXX
cookies = XXX
trust_token = XXX
The command you were trying to run
rclone lsf "icphotos:PrimarySync/Live" | grep '/' | head -1
rclone -vv copy "icphotos:PrimarySync/Live/FullSizeRender_AS9ueJXBbemIxo0zLZ5s/HtDNEmd.heic" /tmp/slashtest
rclone -vv copy "icphotos:PrimarySync/Live" /tmp/slashtest --include "/FullSizeRender_AS9ueJXBbemIxo0zLZ5s/HtDNEmd.heic"
In that Live album, rclone lsf ... | grep -c '/' reports 780 affected
files out of 9,372 listed.
A log from the command with the -vv flag
Addressing the file by name fails. The NewObject debug line shows the lookup being made
with the fullwidth solidus still in place, which is the bug:
$ rclone -vv copy "icphotos:PrimarySync/Live/FullSizeRender_AS9ueJXBbemIxo0zLZ5s/HtDNEmd.heic" /tmp/slashtest
DEBUG : iclouddrive photos: zone PrimarySync unchanged, using cached listings
DEBUG : iclouddrive photos: 9372 items from cache for "Live"
DEBUG : iCloud Photos root 'PrimarySync/Live': NewObject(FullSizeRender_AS9ueJXBbemIxo0zLZ5s/HtDNEmd.heic): photo "FullSizeRender_AS9ueJXBbemIxo0zLZ5s/HtDNEmd.heic" not found in album "Live"
DEBUG : Creating backend with remote "/tmp/slashtest"
ERROR : iCloud Photos root 'PrimarySync/Live/FullSizeRender_AS9ueJXBbemIxo0zLZ5s/HtDNEmd.heic': error reading source root directory: directory not found
ERROR : Attempt 1/3 failed with 1 errors and: directory not found
ERROR : Attempt 2/3 failed with 1 errors and: directory not found
ERROR : Attempt 3/3 failed with 1 errors and: directory not found
NOTICE: Failed to copy: directory not found
The same file through a listing, with a filter instead of a name lookup, transfers fine:
$ rclone -vv copy "icphotos:PrimarySync/Live" /tmp/slashtest --include "/FullSizeRender_AS9ueJXBbemIxo0zLZ5s/HtDNEmd.heic"
DEBUG : FullSizeRender_AS9ueJXBbemIxo0zLZ5s/HtDNEmd.heic: Need to transfer - File not found at Destination
DEBUG : FullSizeRender_AS9ueJXBbemIxo0zLZ5s/HtDNEmd.heic.f15bb91a.partial: size = 1031139 OK
INFO : FullSizeRender_AS9ueJXBbemIxo0zLZ5s/HtDNEmd.heic: Copied (new)
Transferred: 1006.972 KiB / 1006.972 KiB, 100%, 0 B/s, ETA -
Checks: 0 / 0, -, Listed 9372
Transferred: 1 / 1, 100%
Anything else?
The slash is the trigger, not the renaming
Renamed files are fine as long as their record id has no slash in it. From the same album, same listing, same run:
| file | result |
|---|---|
lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic (id contains /) |
object not found |
lp_image_ASSF+TOD9orbqwbHjoX+8epk+jGc.heic (id, no /) |
copied OK |
lp_image_AaLMxhsv8uIaBhtjMHzyBo3FA+1j.heic (id, no /) |
copied OK |
Image_Ab9RCvKZf8Ze+u2NmyOdpCIFoQxK.png (id, no /) |
copied OK |
Image 17-6-2026 at 9.40 pm.jpg (ordinary name) |
copied OK |
So this is not about the renaming, and not about + or other base64 characters. Only the
slash matters.
Where the round trip breaks
ToStandardName and FromStandardName are not inverses when the backend's raw name contains
a character that the backend's own encoder encodes. From lib/encoder/encoder.go:
func FromStandardName(e Encoder, s string) string { return e.Encode(Standard.Decode(s)) }
func ToStandardName(e Encoder, s string) string { return Standard.Encode(e.Decode(s)) }
The iclouddrive default encoding is Display | EncodeBackSlash | EncodeSlash | EncodeInvalidUtf8 (backend/iclouddrive/icloud.go), and Display == Standard, which already
contains EncodeSlash. So for a raw name holding a literal /:
ToStandardName:e.Decodeleaves the literal/alone,Standard.Encodeturns it into/. The listing shows/. Correct.FromStandardName:Standard.Decodeturns/back into/, and thene.Encode, which hasEncodeSlashset, immediately turns it into/again.
So PhotosFs.NewObject looks up lp_image_…/….heic while album.photoCache is keyed on the
raw lp_image_…/….heic, and the lookup misses. List never does the reverse conversion,
which is why sync works.
A standalone test in lib/encoder reproduces it with no iCloud account:
func TestSlashRoundTrip(t *testing.T) {
raw := "lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic"
for _, tc := range []struct {
name string
e MultiEncoder
}{
{"current default", MultiEncoder(Display | EncodeBackSlash | EncodeSlash | EncodeInvalidUtf8)},
{"minus EncodeSlash", MultiEncoder((Display | EncodeBackSlash | EncodeInvalidUtf8) &^ EncodeSlash)},
} {
listed := tc.e.ToStandardName(raw)
back := tc.e.FromStandardName(listed)
t.Logf("%-18s listed=%q back=%q roundtrip_ok=%v", tc.name, listed, back, back == raw)
}
}
current default listed="lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic" back="lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic" roundtrip_ok=false
minus EncodeSlash listed="lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic" back="lp_image_ATOMfgPib/j3YAcQ+gVIIrNhkzpB.heic" roundtrip_ok=true
Note that the listed name is identical in both cases, so dropping EncodeSlash from the
backend default would not change any name rclone shows or writes. It only restores the
reverse direction. Since Display already implies EncodeSlash, clearing it needs &^, not
just removing the explicit term.
EncodeBackSlash has the same defect in the other direction: a raw name containing \ is
listed as \ (because Standard does not encode backslash) but looked up as \.
Two places this could be fixed
- The encoding default in
backend/iclouddrive/icloud.go. This is what #9464 needs, and it also fixes the file case here. deduplicateFilenamesinbackend/iclouddrive/api/photos.gosynthesises the name asbase + "_" + p.ID + ext, wherep.IDismaster.RecordName. Even with the encoder fixed, it seems worth not minting a name containing a path separator in the first place. I have not proposed a specific sanitisation because it has to stay stable and collision-free.
I am happy to send a PR for whichever of these you prefer.
Supporting data on the renaming
All 2,195 affected names parse as <base>_<id><ext>; none of them still has a bare
<base><ext> present in the listing (consistent with every colliding copy being renamed); and
1,082 of the 1,109 distinct ids are shared by exactly two files differing only in extension:
lp_image_<id>.heic and lp_image_<id>.MOV, the two components of one Live Photo. The album
holds roughly eleven hundred Live Photos all delivered as lp_image.heic / lp_image.MOV,
which is what triggers the mass rename.
Note: operations/stat succeeds and is misleading
operations/stat on the failing path returns the item happily. That is not evidence the
lookup works: StatJSON in fs/operations/lsjson.go tries NewObject first and then falls
back to listing the parent and matching the leaf name, so it answers from the listing.
How I ruled out stale state
fscache/clear before the run, and fscache/entries reported 0 both before and after; the
album was then listed fresh (4.6 s) and the copy failed on that fresh listing. I did not clear
the on-disk cache under ~/.cache/rclone/iclouddrive-photos/, but staleness cannot explain the
result either way: within a single listing, files split cleanly by whether their id contains a
slash.
Source: rclone/rclone