diff --git a/backend/iclouddrive/api/drive.go b/backend/iclouddrive/api/drive.go index 1fe9d907a..73d23e39a 100644 --- a/backend/iclouddrive/api/drive.go +++ b/backend/iclouddrive/api/drive.go @@ -498,7 +498,7 @@ func (d *DriveService) CopyDocByItemID(ctx context.Context, itemID string) (*Dri } // CreateUpload creates an url for an upload. -func (d *DriveService) CreateUpload(ctx context.Context, size int64, name string) (*UploadResponse, *http.Response, error) { +func (d *DriveService) CreateUpload(ctx context.Context, size int64, name, zone string) (*UploadResponse, *http.Response, error) { // first we need to request an upload url values := map[string]any{ "filename": name, @@ -513,7 +513,7 @@ func (d *DriveService) CreateUpload(ctx context.Context, size int64, name string opts := rest.Opts{ Method: "POST", - Path: "/ws/" + defaultZone + "/upload/web", + Path: "/ws/" + zone + "/upload/web", ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}), RootURL: d.docsEndpoint, Body: body, @@ -552,14 +552,14 @@ func (d *DriveService) Upload(ctx context.Context, in io.Reader, size int64, nam // ctx: the context.Context object for the request. // r: a pointer to the UpdateFileInfo struct containing the information for the file update. // Returns a pointer to the DriveItem struct representing the updated file, the http.Response object, and an error if any. -func (d *DriveService) UpdateFile(ctx context.Context, r *UpdateFileInfo) (*DriveItem, *http.Response, error) { +func (d *DriveService) UpdateFile(ctx context.Context, r *UpdateFileInfo, zone string) (*DriveItem, *http.Response, error) { body, err := IntoReader(r) if err != nil { return nil, nil, err } opts := rest.Opts{ Method: "POST", - Path: "/ws/" + defaultZone + "/update/documents", + Path: "/ws/" + zone + "/update/documents", ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}), RootURL: d.docsEndpoint, Body: body, @@ -572,7 +572,7 @@ func (d *DriveService) UpdateFile(ctx context.Context, r *UpdateFileInfo) (*Driv doc := responseInfo.Results[0].Document item := DriveItem{ - Drivewsid: "FILE::com.apple.CloudDocs::" + doc.DocumentID, + Drivewsid: ConstructDriveID(doc.DocumentID, zone, "FILE"), Docwsid: doc.DocumentID, Itemid: doc.ItemID, Etag: doc.Etag, @@ -892,6 +892,18 @@ func DeconstructDriveID(id string) (docType, zone, docid string) { return split[0], split[1], split[2] } +// ZoneFromDriveID returns the zone a drive ID belongs to, or the default zone +// for an ID that carries none. Items in an app container -- Obsidian, Pages, +// Shortcuts -- live in that app's zone rather than com.apple.CloudDocs, and a +// write addressed to the wrong zone cannot resolve its parent. +func ZoneFromDriveID(id string) string { + _, zone, _ := DeconstructDriveID(id) + if zone == "" { + return defaultZone + } + return zone +} + // ConstructDriveID constructs a drive ID from the given components. func ConstructDriveID(id string, zone string, t string) string { return strings.Join([]string{t, zone, id}, "::") diff --git a/backend/iclouddrive/api/drive_test.go b/backend/iclouddrive/api/drive_test.go new file mode 100644 index 000000000..112f3f765 --- /dev/null +++ b/backend/iclouddrive/api/drive_test.go @@ -0,0 +1,32 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestZoneFromDriveID(t *testing.T) { + for _, test := range []struct { + name string + id string + want string + }{ + {"app container", "FOLDER::iCloud.md.obsidian::documents#o2v", "iCloud.md.obsidian"}, + {"another app container", "FOLDER::com.apple.Pages::documents#7qt", "com.apple.Pages"}, + {"ordinary folder", "FOLDER::com.apple.CloudDocs::B847FE2D", "com.apple.CloudDocs"}, + {"file in a container", "FILE::iCloud.md.obsidian::abc123", "iCloud.md.obsidian"}, + {"no zone", "root", defaultZone}, + {"empty", "", defaultZone}, + } { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.want, ZoneFromDriveID(test.id)) + }) + } +} + +func TestZoneFromDriveIDRoundTrip(t *testing.T) { + id := ConstructDriveID("abc123", "iCloud.md.obsidian", "FILE") + assert.Equal(t, "FILE::iCloud.md.obsidian::abc123", id) + assert.Equal(t, "iCloud.md.obsidian", ZoneFromDriveID(id)) +} diff --git a/backend/iclouddrive/iclouddrive.go b/backend/iclouddrive/iclouddrive.go index 90be7a712..3c78e3bce 100644 --- a/backend/iclouddrive/iclouddrive.go +++ b/backend/iclouddrive/iclouddrive.go @@ -343,7 +343,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, var item *api.DriveItem if err = f.pacer.Call(func() (bool, error) { - item, resp, err = f.service.UpdateFile(ctx, &r) + item, resp, err = f.service.UpdateFile(ctx, &r, api.ZoneFromDriveID(pathID)) return retryResultUnknown(ctx, resp, err) }); err != nil { return nil, err @@ -949,7 +949,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op // Create document var uploadInfo *api.UploadResponse if err = o.fs.pacer.Call(func() (bool, error) { - uploadInfo, resp, err = o.fs.service.CreateUpload(ctx, size, name) + uploadInfo, resp, err = o.fs.service.CreateUpload(ctx, size, name, api.ZoneFromDriveID(dirID)) return ignoreResultUnknown(ctx, resp, err) }); err != nil { return err @@ -988,7 +988,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op // Update metadata var item *api.DriveItem if err = o.fs.pacer.Call(func() (bool, error) { - item, resp, err = o.fs.service.UpdateFile(ctx, &r) + item, resp, err = o.fs.service.UpdateFile(ctx, &r, api.ZoneFromDriveID(dirID)) return ignoreResultUnknown(ctx, resp, err) }); err != nil { return err