github.com/artpar/rclone@v1.67.3/backend/onedrive/metadata.go (about) 1 package onedrive 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/http" 9 "strings" 10 "time" 11 12 "github.com/artpar/rclone/backend/onedrive/api" 13 "github.com/artpar/rclone/fs" 14 "github.com/artpar/rclone/lib/dircache" 15 "github.com/artpar/rclone/lib/errcount" 16 "golang.org/x/exp/slices" // replace with slices after go1.21 is the minimum version 17 ) 18 19 const ( 20 dirMimeType = "inode/directory" 21 timeFormatIn = time.RFC3339 22 timeFormatOut = "2006-01-02T15:04:05.999Z" // mS for OneDrive Personal, otherwise only S 23 ) 24 25 // system metadata keys which this backend owns 26 var systemMetadataInfo = map[string]fs.MetadataHelp{ 27 "content-type": { 28 Help: "The MIME type of the file.", 29 Type: "string", 30 Example: "text/plain", 31 ReadOnly: true, 32 }, 33 "mtime": { 34 Help: "Time of last modification with S accuracy (mS for OneDrive Personal).", 35 Type: "RFC 3339", 36 Example: "2006-01-02T15:04:05Z", 37 }, 38 "btime": { 39 Help: "Time of file birth (creation) with S accuracy (mS for OneDrive Personal).", 40 Type: "RFC 3339", 41 Example: "2006-01-02T15:04:05Z", 42 }, 43 "utime": { 44 Help: "Time of upload with S accuracy (mS for OneDrive Personal).", 45 Type: "RFC 3339", 46 Example: "2006-01-02T15:04:05Z", 47 ReadOnly: true, 48 }, 49 "created-by-display-name": { 50 Help: "Display name of the user that created the item.", 51 Type: "string", 52 Example: "John Doe", 53 ReadOnly: true, 54 }, 55 "created-by-id": { 56 Help: "ID of the user that created the item.", 57 Type: "string", 58 Example: "48d31887-5fad-4d73-a9f5-3c356e68a038", 59 ReadOnly: true, 60 }, 61 "description": { 62 Help: "A short description of the file. Max 1024 characters. Only supported for OneDrive Personal.", 63 Type: "string", 64 Example: "Contract for signing", 65 }, 66 "id": { 67 Help: "The unique identifier of the item within OneDrive.", 68 Type: "string", 69 Example: "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36K", 70 ReadOnly: true, 71 }, 72 "last-modified-by-display-name": { 73 Help: "Display name of the user that last modified the item.", 74 Type: "string", 75 Example: "John Doe", 76 ReadOnly: true, 77 }, 78 "last-modified-by-id": { 79 Help: "ID of the user that last modified the item.", 80 Type: "string", 81 Example: "48d31887-5fad-4d73-a9f5-3c356e68a038", 82 ReadOnly: true, 83 }, 84 "malware-detected": { 85 Help: "Whether OneDrive has detected that the item contains malware.", 86 Type: "boolean", 87 Example: "true", 88 ReadOnly: true, 89 }, 90 "package-type": { 91 Help: "If present, indicates that this item is a package instead of a folder or file. Packages are treated like files in some contexts and folders in others.", 92 Type: "string", 93 Example: "oneNote", 94 ReadOnly: true, 95 }, 96 "shared-owner-id": { 97 Help: "ID of the owner of the shared item (if shared).", 98 Type: "string", 99 Example: "48d31887-5fad-4d73-a9f5-3c356e68a038", 100 ReadOnly: true, 101 }, 102 "shared-by-id": { 103 Help: "ID of the user that shared the item (if shared).", 104 Type: "string", 105 Example: "48d31887-5fad-4d73-a9f5-3c356e68a038", 106 ReadOnly: true, 107 }, 108 "shared-scope": { 109 Help: "If shared, indicates the scope of how the item is shared: anonymous, organization, or users.", 110 Type: "string", 111 Example: "users", 112 ReadOnly: true, 113 }, 114 "shared-time": { 115 Help: "Time when the item was shared, with S accuracy (mS for OneDrive Personal).", 116 Type: "RFC 3339", 117 Example: "2006-01-02T15:04:05Z", 118 ReadOnly: true, 119 }, 120 "permissions": { 121 Help: "Permissions in a JSON dump of OneDrive format. Enable with --onedrive-metadata-permissions. Properties: id, grantedTo, grantedToIdentities, invitation, inheritedFrom, link, roles, shareId", 122 Type: "JSON", 123 Example: "{}", 124 }, 125 } 126 127 // rwChoices type for fs.Bits 128 type rwChoices struct{} 129 130 func (rwChoices) Choices() []fs.BitsChoicesInfo { 131 return []fs.BitsChoicesInfo{ 132 {Bit: uint64(rwOff), Name: "off"}, 133 {Bit: uint64(rwRead), Name: "read"}, 134 {Bit: uint64(rwWrite), Name: "write"}, 135 } 136 } 137 138 // rwChoice type alias 139 type rwChoice = fs.Bits[rwChoices] 140 141 const ( 142 rwRead rwChoice = 1 << iota 143 rwWrite 144 rwOff rwChoice = 0 145 ) 146 147 // Examples for the options 148 var rwExamples = fs.OptionExamples{{ 149 Value: rwOff.String(), 150 Help: "Do not read or write the value", 151 }, { 152 Value: rwRead.String(), 153 Help: "Read the value only", 154 }, { 155 Value: rwWrite.String(), 156 Help: "Write the value only", 157 }, { 158 Value: (rwRead | rwWrite).String(), 159 Help: "Read and Write the value.", 160 }} 161 162 // Metadata describes metadata properties shared by both Objects and Directories 163 type Metadata struct { 164 fs *Fs // what this object/dir is part of 165 remote string // remote, for convenience when obj/dir not in scope 166 mimeType string // Content-Type of object from server (may not be as uploaded) 167 description string // Provides a user-visible description of the item. Read-write. Only on OneDrive Personal 168 mtime time.Time // Time of last modification with S accuracy. 169 btime time.Time // Time of file birth (creation) with S accuracy. 170 utime time.Time // Time of upload with S accuracy. 171 createdBy api.IdentitySet // user that created the item 172 lastModifiedBy api.IdentitySet // user that last modified the item 173 malwareDetected bool // Whether OneDrive has detected that the item contains malware. 174 packageType string // If present, indicates that this item is a package instead of a folder or file. 175 shared *api.SharedType // information about the shared state of the item, if shared 176 normalizedID string // the normalized ID of the object or dir 177 permissions []*api.PermissionsType // The current set of permissions for the item. Note that to save API calls, this is not guaranteed to be cached on the object. Use m.Get() to refresh. 178 queuedPermissions []*api.PermissionsType // The set of permissions queued to be updated. 179 permsAddOnly bool // Whether to disable "update" and "remove" (for example, during server-side copy when the dst will have new IDs) 180 } 181 182 // Get retrieves the cached metadata and converts it to fs.Metadata. 183 // This is most typically used when OneDrive is the source (as opposed to the dest). 184 // If m.fs.opt.MetadataPermissions includes "read" then this will also include permissions, which requires an API call. 185 // Get does not use an API call otherwise. 186 func (m *Metadata) Get(ctx context.Context) (metadata fs.Metadata, err error) { 187 metadata = make(fs.Metadata, 17) 188 metadata["content-type"] = m.mimeType 189 metadata["mtime"] = m.mtime.Format(timeFormatOut) 190 metadata["btime"] = m.btime.Format(timeFormatOut) 191 metadata["utime"] = m.utime.Format(timeFormatOut) 192 metadata["created-by-display-name"] = m.createdBy.User.DisplayName 193 metadata["created-by-id"] = m.createdBy.User.ID 194 if m.description != "" { 195 metadata["description"] = m.description 196 } 197 metadata["id"] = m.normalizedID 198 metadata["last-modified-by-display-name"] = m.lastModifiedBy.User.DisplayName 199 metadata["last-modified-by-id"] = m.lastModifiedBy.User.ID 200 metadata["malware-detected"] = fmt.Sprint(m.malwareDetected) 201 if m.packageType != "" { 202 metadata["package-type"] = m.packageType 203 } 204 if m.shared != nil { 205 metadata["shared-owner-id"] = m.shared.Owner.User.ID 206 metadata["shared-by-id"] = m.shared.SharedBy.User.ID 207 metadata["shared-scope"] = m.shared.Scope 208 metadata["shared-time"] = time.Time(m.shared.SharedDateTime).Format(timeFormatOut) 209 } 210 if m.fs.opt.MetadataPermissions.IsSet(rwRead) { 211 p, _, err := m.fs.getPermissions(ctx, m.normalizedID) 212 if err != nil { 213 return nil, fmt.Errorf("failed to get permissions: %w", err) 214 } 215 m.permissions = p 216 217 if len(p) > 0 { 218 fs.PrettyPrint(m.permissions, "perms", fs.LogLevelDebug) 219 buf, err := json.Marshal(m.permissions) 220 if err != nil { 221 return nil, fmt.Errorf("failed to marshal permissions: %w", err) 222 } 223 metadata["permissions"] = string(buf) 224 } 225 } 226 return metadata, nil 227 } 228 229 // Set takes fs.Metadata and parses/converts it to cached Metadata. 230 // This is most typically used when OneDrive is the destination (as opposed to the source). 231 // It does not actually update the remote (use Write for that.) 232 // It sets only the writeable metadata properties (i.e. read-only properties are skipped.) 233 // Permissions are included if m.fs.opt.MetadataPermissions includes "write". 234 // It returns errors if writeable properties can't be parsed. 235 // It does not return errors for unsupported properties that may be passed in. 236 // It returns the number of writeable properties set (if it is 0, we can skip the Write API call.) 237 func (m *Metadata) Set(ctx context.Context, metadata fs.Metadata) (numSet int, err error) { 238 numSet = 0 239 for k, v := range metadata { 240 k, v := k, v 241 switch k { 242 case "mtime": 243 t, err := time.Parse(timeFormatIn, v) 244 if err != nil { 245 return numSet, fmt.Errorf("failed to parse metadata %q = %q: %w", k, v, err) 246 } 247 m.mtime = t 248 numSet++ 249 case "btime": 250 t, err := time.Parse(timeFormatIn, v) 251 if err != nil { 252 return numSet, fmt.Errorf("failed to parse metadata %q = %q: %w", k, v, err) 253 } 254 m.btime = t 255 numSet++ 256 case "description": 257 if m.fs.driveType != driveTypePersonal { 258 fs.Debugf(m.remote, "metadata description is only supported for OneDrive Personal -- skipping: %s", v) 259 continue 260 } 261 m.description = v 262 numSet++ 263 case "permissions": 264 if !m.fs.opt.MetadataPermissions.IsSet(rwWrite) { 265 continue 266 } 267 var perms []*api.PermissionsType 268 err := json.Unmarshal([]byte(v), &perms) 269 if err != nil { 270 return numSet, fmt.Errorf("failed to unmarshal permissions: %w", err) 271 } 272 m.queuedPermissions = perms 273 numSet++ 274 default: 275 fs.Debugf(m.remote, "skipping unsupported metadata item: %s: %s", k, v) 276 } 277 } 278 if numSet == 0 { 279 fs.Infof(m.remote, "no writeable metadata found: %v", metadata) 280 } 281 return numSet, nil 282 } 283 284 // toAPIMetadata converts object/dir Metadata to api.Metadata for API calls. 285 // If btime is missing but mtime is present, mtime is also used as the btime, as otherwise it would get overwritten. 286 func (m *Metadata) toAPIMetadata() api.Metadata { 287 update := api.Metadata{ 288 FileSystemInfo: &api.FileSystemInfoFacet{}, 289 } 290 if m.description != "" && m.fs.driveType == driveTypePersonal { 291 update.Description = m.description 292 } 293 if !m.mtime.IsZero() { 294 update.FileSystemInfo.LastModifiedDateTime = api.Timestamp(m.mtime) 295 } 296 if !m.btime.IsZero() { 297 update.FileSystemInfo.CreatedDateTime = api.Timestamp(m.btime) 298 } 299 300 if m.btime.IsZero() && !m.mtime.IsZero() { // use mtime as btime if missing 301 m.btime = m.mtime 302 update.FileSystemInfo.CreatedDateTime = api.Timestamp(m.btime) 303 } 304 return update 305 } 306 307 // Write takes the cached Metadata and sets it on the remote, using API calls. 308 // If m.fs.opt.MetadataPermissions includes "write" and updatePermissions == true, permissions are also set. 309 // Calling Write without any writeable metadata will result in an error. 310 func (m *Metadata) Write(ctx context.Context, updatePermissions bool) (*api.Item, error) { 311 update := m.toAPIMetadata() 312 if update.IsEmpty() { 313 return nil, fmt.Errorf("%v: no writeable metadata found: %v", m.remote, m) 314 } 315 opts := m.fs.newOptsCallWithPath(ctx, m.remote, "PATCH", "") 316 var info *api.Item 317 err := m.fs.pacer.Call(func() (bool, error) { 318 resp, err := m.fs.srv.CallJSON(ctx, &opts, &update, &info) 319 return shouldRetry(ctx, resp, err) 320 }) 321 if err != nil { 322 fs.Debugf(m.remote, "errored metadata: %v", m) 323 return nil, fmt.Errorf("%v: error updating metadata: %v", m.remote, err) 324 } 325 326 if m.fs.opt.MetadataPermissions.IsSet(rwWrite) && updatePermissions { 327 m.normalizedID = info.GetID() 328 err = m.WritePermissions(ctx) 329 if err != nil { 330 fs.Errorf(m.remote, "error writing permissions: %v", err) 331 return info, err 332 } 333 } 334 335 // update the struct since we have fresh info 336 m.fs.setSystemMetadata(info, m, m.remote, m.mimeType) 337 338 return info, err 339 } 340 341 // RefreshPermissions fetches the current permissions from the remote and caches them as Metadata 342 func (m *Metadata) RefreshPermissions(ctx context.Context) (err error) { 343 if m.normalizedID == "" { 344 return errors.New("internal error: normalizedID is missing") 345 } 346 p, _, err := m.fs.getPermissions(ctx, m.normalizedID) 347 if err != nil { 348 return fmt.Errorf("failed to refresh permissions: %w", err) 349 } 350 m.permissions = p 351 return nil 352 } 353 354 // WritePermissions sets the permissions (and no other metadata) on the remote. 355 // m.permissions (the existing perms) and m.queuedPermissions (the new perms to be set) must be set correctly before calling this. 356 // m.permissions == nil will not error, as it is valid to add permissions when there were previously none. 357 // If successful, m.permissions will be set with the new current permissions and m.queuedPermissions will be nil. 358 func (m *Metadata) WritePermissions(ctx context.Context) (err error) { 359 if !m.fs.opt.MetadataPermissions.IsSet(rwWrite) { 360 return errors.New("can't write permissions without --onedrive-metadata-permissions write") 361 } 362 if m.normalizedID == "" { 363 return errors.New("internal error: normalizedID is missing") 364 } 365 366 // compare current to queued and sort into add/update/remove queues 367 add, update, remove := m.sortPermissions() 368 fs.Debugf(m.remote, "metadata permissions: to add: %d to update: %d to remove: %d", len(add), len(update), len(remove)) 369 _, err = m.processPermissions(ctx, add, update, remove) 370 if err != nil { 371 return fmt.Errorf("failed to process permissions: %w", err) 372 } 373 374 err = m.RefreshPermissions(ctx) 375 fs.Debugf(m.remote, "updated permissions (now has %d permissions)", len(m.permissions)) 376 if err != nil { 377 return fmt.Errorf("failed to get permissions: %w", err) 378 } 379 m.queuedPermissions = nil 380 381 return nil 382 } 383 384 // sortPermissions sorts the permissions (to be written) into add, update, and remove queues 385 func (m *Metadata) sortPermissions() (add, update, remove []*api.PermissionsType) { 386 new, old := m.queuedPermissions, m.permissions 387 if len(old) == 0 || m.permsAddOnly { 388 return new, nil, nil // they must all be "add" 389 } 390 391 for _, n := range new { 392 if n == nil { 393 continue 394 } 395 if n.ID != "" { 396 // sanity check: ensure there's a matching "old" id with a non-matching role 397 if !slices.ContainsFunc(old, func(o *api.PermissionsType) bool { 398 return o.ID == n.ID && slices.Compare(o.Roles, n.Roles) != 0 && len(o.Roles) > 0 && len(n.Roles) > 0 399 }) { 400 fs.Debugf(m.remote, "skipping update for invalid roles: %v (perm ID: %v)", n.Roles, n.ID) 401 continue 402 } 403 if m.fs.driveType != driveTypePersonal && n.Link != nil && n.Link.WebURL != "" { 404 // special case to work around API limitation -- can't update a sharing link perm so need to remove + add instead 405 // https://learn.microsoft.com/en-us/answers/questions/986279/why-is-update-permission-graph-api-for-files-not-w 406 // https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/1135 407 fs.Debugf(m.remote, "sortPermissions: can't update due to API limitation, will remove + add instead: %v", n.Roles) 408 remove = append(remove, n) 409 add = append(add, n) 410 continue 411 } 412 fs.Debugf(m.remote, "sortPermissions: will update role to %v", n.Roles) 413 update = append(update, n) 414 } else { 415 fs.Debugf(m.remote, "sortPermissions: will add permission: %v %v", n, n.Roles) 416 add = append(add, n) 417 } 418 } 419 for _, o := range old { 420 newHasOld := slices.ContainsFunc(new, func(n *api.PermissionsType) bool { 421 if n == nil || n.ID == "" { 422 return false // can't remove perms without an ID 423 } 424 return n.ID == o.ID 425 }) 426 if !newHasOld && o.ID != "" && !slices.Contains(add, o) && !slices.Contains(update, o) { 427 fs.Debugf(m.remote, "sortPermissions: will remove permission: %v %v (perm ID: %v)", o, o.Roles, o.ID) 428 remove = append(remove, o) 429 } 430 } 431 return add, update, remove 432 } 433 434 // processPermissions executes the add, update, and remove queues for writing permissions 435 func (m *Metadata) processPermissions(ctx context.Context, add, update, remove []*api.PermissionsType) (newPermissions []*api.PermissionsType, err error) { 436 errs := errcount.New() 437 for _, p := range remove { // remove (need to do these first because of remove + add workaround) 438 _, err := m.removePermission(ctx, p) 439 if err != nil { 440 fs.Errorf(m.remote, "Failed to remove permission: %v", err) 441 errs.Add(err) 442 } 443 } 444 445 for _, p := range add { // add 446 newPs, _, err := m.addPermission(ctx, p) 447 if err != nil { 448 fs.Errorf(m.remote, "Failed to add permission: %v", err) 449 errs.Add(err) 450 continue 451 } 452 newPermissions = append(newPermissions, newPs...) 453 } 454 455 for _, p := range update { // update 456 newP, _, err := m.updatePermission(ctx, p) 457 if err != nil { 458 fs.Errorf(m.remote, "Failed to update permission: %v", err) 459 errs.Add(err) 460 continue 461 } 462 newPermissions = append(newPermissions, newP) 463 } 464 465 return newPermissions, errs.Err("failed to set permissions") 466 } 467 468 // fillRecipients looks for recipients to add from the permission passed in. 469 // It looks for an email address in identity.User.ID and DisplayName, otherwise it uses the identity.User.ID as r.ObjectID. 470 // It considers both "GrantedTo" and "GrantedToIdentities". 471 func fillRecipients(p *api.PermissionsType) (recipients []api.DriveRecipient) { 472 if p == nil { 473 return recipients 474 } 475 ids := make(map[string]struct{}, len(p.GrantedToIdentities)+1) 476 isUnique := func(s string) bool { 477 _, ok := ids[s] 478 return !ok && s != "" 479 } 480 481 addRecipient := func(identity *api.IdentitySet) { 482 r := api.DriveRecipient{} 483 484 id := "" 485 if strings.ContainsRune(identity.User.ID, '@') { 486 id = identity.User.ID 487 r.Email = id 488 } else if strings.ContainsRune(identity.User.DisplayName, '@') { 489 id = identity.User.DisplayName 490 r.Email = id 491 } else { 492 id = identity.User.ID 493 r.ObjectID = id 494 } 495 if !isUnique(id) { 496 return 497 } 498 ids[id] = struct{}{} 499 recipients = append(recipients, r) 500 } 501 for _, identity := range p.GrantedToIdentities { 502 addRecipient(identity) 503 } 504 if p.GrantedTo != nil && p.GrantedTo.User != (api.Identity{}) { 505 addRecipient(p.GrantedTo) 506 } 507 return recipients 508 } 509 510 // addPermission adds new permissions to an object or dir. 511 // if p.Link.Scope == "anonymous" then it will also create a Public Link. 512 func (m *Metadata) addPermission(ctx context.Context, p *api.PermissionsType) (newPs []*api.PermissionsType, resp *http.Response, err error) { 513 opts := m.fs.newOptsCall(m.normalizedID, "POST", "/invite") 514 515 req := &api.AddPermissionsRequest{ 516 Recipients: fillRecipients(p), 517 RequireSignIn: m.fs.driveType != driveTypePersonal, // personal and business have conflicting requirements 518 Roles: p.Roles, 519 } 520 if m.fs.driveType != driveTypePersonal { 521 req.RetainInheritedPermissions = false // not supported for personal 522 } 523 524 if p.Link != nil && p.Link.Scope == api.AnonymousScope { 525 link, err := m.fs.PublicLink(ctx, m.remote, fs.DurationOff, false) 526 if err != nil { 527 return nil, nil, err 528 } 529 p.Link.WebURL = link 530 newPs = append(newPs, p) 531 if len(req.Recipients) == 0 { 532 return newPs, nil, nil 533 } 534 } 535 536 if len(req.Recipients) == 0 { 537 fs.Debugf(m.remote, "skipping add permission -- at least one valid recipient is required") 538 return nil, nil, nil 539 } 540 if len(req.Roles) == 0 { 541 return nil, nil, errors.New("at least one role is required to add a permission (choices: read, write, owner, member)") 542 } 543 if slices.Contains(req.Roles, api.OwnerRole) { 544 fs.Debugf(m.remote, "skipping add permission -- can't invite a user with 'owner' role") 545 return nil, nil, nil 546 } 547 548 newP := &api.PermissionsResponse{} 549 err = m.fs.pacer.Call(func() (bool, error) { 550 resp, err = m.fs.srv.CallJSON(ctx, &opts, &req, &newP) 551 return shouldRetry(ctx, resp, err) 552 }) 553 554 return newP.Value, resp, err 555 } 556 557 // updatePermission updates an existing permission on an object or dir. 558 // This requires the permission ID and a role to update (which will error if it is the same as the existing role.) 559 // Role is the only property that can be updated. 560 func (m *Metadata) updatePermission(ctx context.Context, p *api.PermissionsType) (newP *api.PermissionsType, resp *http.Response, err error) { 561 opts := m.fs.newOptsCall(m.normalizedID, "PATCH", "/permissions/"+p.ID) 562 req := api.UpdatePermissionsRequest{Roles: p.Roles} // roles is the only property that can be updated 563 564 if len(req.Roles) == 0 { 565 return nil, nil, errors.New("at least one role is required to update a permission (choices: read, write, owner, member)") 566 } 567 568 newP = &api.PermissionsType{} 569 err = m.fs.pacer.Call(func() (bool, error) { 570 resp, err = m.fs.srv.CallJSON(ctx, &opts, &req, &newP) 571 return shouldRetry(ctx, resp, err) 572 }) 573 574 return newP, resp, err 575 } 576 577 // removePermission removes an existing permission on an object or dir. 578 // This requires the permission ID. 579 func (m *Metadata) removePermission(ctx context.Context, p *api.PermissionsType) (resp *http.Response, err error) { 580 opts := m.fs.newOptsCall(m.normalizedID, "DELETE", "/permissions/"+p.ID) 581 opts.NoResponse = true 582 583 err = m.fs.pacer.Call(func() (bool, error) { 584 resp, err = m.fs.srv.CallJSON(ctx, &opts, nil, nil) 585 return shouldRetry(ctx, resp, err) 586 }) 587 return resp, err 588 } 589 590 // getPermissions gets the current permissions for an object or dir, from the API. 591 func (f *Fs) getPermissions(ctx context.Context, normalizedID string) (p []*api.PermissionsType, resp *http.Response, err error) { 592 opts := f.newOptsCall(normalizedID, "GET", "/permissions") 593 594 permResp := &api.PermissionsResponse{} 595 err = f.pacer.Call(func() (bool, error) { 596 resp, err = f.srv.CallJSON(ctx, &opts, nil, &permResp) 597 return shouldRetry(ctx, resp, err) 598 }) 599 600 return permResp.Value, resp, err 601 } 602 603 func (f *Fs) newMetadata(remote string) *Metadata { 604 return &Metadata{fs: f, remote: remote} 605 } 606 607 // returns true if metadata includes a "permissions" key and f.opt.MetadataPermissions includes "write". 608 func (f *Fs) needsUpdatePermissions(metadata fs.Metadata) bool { 609 _, ok := metadata["permissions"] 610 return ok && f.opt.MetadataPermissions.IsSet(rwWrite) 611 } 612 613 // returns a non-zero btime if we have one 614 // otherwise falls back to mtime 615 func (o *Object) tryGetBtime(modTime time.Time) time.Time { 616 if o.meta != nil && !o.meta.btime.IsZero() { 617 return o.meta.btime 618 } 619 return modTime 620 } 621 622 // adds metadata (except permissions) if --metadata is in use 623 func (o *Object) fetchMetadataForCreate(ctx context.Context, src fs.ObjectInfo, options []fs.OpenOption, modTime time.Time) (createRequest api.CreateUploadRequest, metadata fs.Metadata, err error) { 624 createRequest = api.CreateUploadRequest{ // we set mtime no matter what 625 Item: api.Metadata{ 626 FileSystemInfo: &api.FileSystemInfoFacet{ 627 CreatedDateTime: api.Timestamp(o.tryGetBtime(modTime)), 628 LastModifiedDateTime: api.Timestamp(modTime), 629 }, 630 }, 631 } 632 633 meta, err := fs.GetMetadataOptions(ctx, o.fs, src, options) 634 if err != nil { 635 return createRequest, nil, fmt.Errorf("failed to read metadata from source object: %w", err) 636 } 637 if meta == nil { 638 return createRequest, nil, nil // no metadata or --metadata not in use, so just return mtime 639 } 640 if o.meta == nil { 641 o.meta = o.fs.newMetadata(o.Remote()) 642 } 643 o.meta.mtime = modTime 644 numSet, err := o.meta.Set(ctx, meta) 645 if err != nil { 646 return createRequest, meta, err 647 } 648 if numSet == 0 { 649 return createRequest, meta, nil 650 } 651 createRequest.Item = o.meta.toAPIMetadata() 652 return createRequest, meta, nil 653 } 654 655 // Fetch metadata and update updateInfo if --metadata is in use 656 // modtime will still be set when there is no metadata to set 657 func (f *Fs) fetchAndUpdateMetadata(ctx context.Context, src fs.ObjectInfo, options []fs.OpenOption, updateInfo *Object) (info *api.Item, err error) { 658 meta, err := fs.GetMetadataOptions(ctx, f, src, options) 659 if err != nil { 660 return nil, fmt.Errorf("failed to read metadata from source object: %w", err) 661 } 662 if meta == nil { 663 return updateInfo.setModTime(ctx, src.ModTime(ctx)) // no metadata or --metadata not in use, so just set modtime 664 } 665 if updateInfo.meta == nil { 666 updateInfo.meta = f.newMetadata(updateInfo.Remote()) 667 } 668 newInfo, err := updateInfo.updateMetadata(ctx, meta) 669 if newInfo == nil { 670 return info, err 671 } 672 return newInfo, err 673 } 674 675 // updateMetadata calls Get, Set, and Write 676 func (o *Object) updateMetadata(ctx context.Context, meta fs.Metadata) (info *api.Item, err error) { 677 _, err = o.meta.Get(ctx) // refresh permissions 678 if err != nil { 679 return nil, err 680 } 681 682 numSet, err := o.meta.Set(ctx, meta) 683 if err != nil { 684 return nil, err 685 } 686 if numSet == 0 { 687 return nil, nil 688 } 689 info, err = o.meta.Write(ctx, o.fs.needsUpdatePermissions(meta)) 690 if err != nil { 691 return info, err 692 } 693 err = o.setMetaData(info) 694 if err != nil { 695 return info, err 696 } 697 698 // Remove versions if required 699 if o.fs.opt.NoVersions { 700 err := o.deleteVersions(ctx) 701 if err != nil { 702 return info, fmt.Errorf("%v: Failed to remove versions: %v", o, err) 703 } 704 } 705 return info, nil 706 } 707 708 // MkdirMetadata makes the directory passed in as dir. 709 // 710 // It shouldn't return an error if it already exists. 711 // 712 // If the metadata is not nil it is set. 713 // 714 // It returns the directory that was created. 715 func (f *Fs) MkdirMetadata(ctx context.Context, dir string, metadata fs.Metadata) (fs.Directory, error) { 716 var info *api.Item 717 var meta *Metadata 718 dirID, err := f.dirCache.FindDir(ctx, dir, false) 719 if err == fs.ErrorDirNotFound { 720 // Directory does not exist so create it 721 var leaf, parentID string 722 leaf, parentID, err = f.dirCache.FindPath(ctx, dir, true) 723 if err != nil { 724 return nil, err 725 } 726 info, meta, err = f.createDir(ctx, parentID, dir, leaf, metadata) 727 if err != nil { 728 return nil, err 729 } 730 if f.driveType != driveTypePersonal { 731 // for some reason, OneDrive Business needs this extra step to set modtime, while Personal does not. Seems like a bug... 732 fs.Debugf(dir, "setting time %v", meta.mtime) 733 info, err = meta.Write(ctx, false) 734 } 735 } else if err == nil { 736 // Directory exists and needs updating 737 info, meta, err = f.updateDir(ctx, dirID, dir, metadata) 738 } 739 if err != nil { 740 return nil, err 741 } 742 743 // Convert the info into a directory entry 744 parent, _ := dircache.SplitPath(dir) 745 entry, err := f.itemToDirEntry(ctx, parent, info) 746 if err != nil { 747 return nil, err 748 } 749 directory, ok := entry.(*Directory) 750 if !ok { 751 return nil, fmt.Errorf("internal error: expecting %T to be a *Directory", entry) 752 } 753 directory.meta = meta 754 f.setSystemMetadata(info, directory.meta, entry.Remote(), dirMimeType) 755 756 dirEntry, ok := entry.(fs.Directory) 757 if !ok { 758 return nil, fmt.Errorf("internal error: expecting %T to be an fs.Directory", entry) 759 } 760 761 return dirEntry, nil 762 } 763 764 // createDir makes a directory with pathID as parent and name leaf with optional metadata 765 func (f *Fs) createDir(ctx context.Context, pathID, dirWithLeaf, leaf string, metadata fs.Metadata) (info *api.Item, meta *Metadata, err error) { 766 // fs.Debugf(f, "CreateDir(%q, %q)\n", dirID, leaf) 767 var resp *http.Response 768 opts := f.newOptsCall(pathID, "POST", "/children") 769 770 mkdir := api.CreateItemWithMetadataRequest{ 771 CreateItemRequest: api.CreateItemRequest{ 772 Name: f.opt.Enc.FromStandardName(leaf), 773 ConflictBehavior: "fail", 774 }, 775 } 776 m := f.newMetadata(dirWithLeaf) 777 m.mimeType = dirMimeType 778 numSet := 0 779 if len(metadata) > 0 { 780 781 numSet, err = m.Set(ctx, metadata) 782 if err != nil { 783 return nil, m, err 784 } 785 if numSet > 0 { 786 mkdir.Metadata = m.toAPIMetadata() 787 } 788 } 789 790 err = f.pacer.Call(func() (bool, error) { 791 resp, err = f.srv.CallJSON(ctx, &opts, &mkdir, &info) 792 return shouldRetry(ctx, resp, err) 793 }) 794 if err != nil { 795 return nil, m, err 796 } 797 798 if f.needsUpdatePermissions(metadata) && numSet > 0 { // permissions must be done as a separate step 799 m.normalizedID = info.GetID() 800 err = m.RefreshPermissions(ctx) 801 if err != nil { 802 return info, m, err 803 } 804 805 err = m.WritePermissions(ctx) 806 if err != nil { 807 fs.Errorf(m.remote, "error writing permissions: %v", err) 808 return info, m, err 809 } 810 } 811 return info, m, nil 812 } 813 814 // updateDir updates an existing a directory with the metadata passed in 815 func (f *Fs) updateDir(ctx context.Context, dirID, remote string, metadata fs.Metadata) (info *api.Item, meta *Metadata, err error) { 816 d := f.newDir(dirID, remote) 817 _, err = d.meta.Set(ctx, metadata) 818 if err != nil { 819 return nil, nil, err 820 } 821 info, err = d.meta.Write(ctx, f.needsUpdatePermissions(metadata)) 822 return info, d.meta, err 823 } 824 825 func (f *Fs) newDir(dirID, remote string) (d *Directory) { 826 d = &Directory{ 827 fs: f, 828 remote: remote, 829 size: -1, 830 items: -1, 831 id: dirID, 832 meta: f.newMetadata(remote), 833 } 834 d.meta.normalizedID = dirID 835 return d 836 } 837 838 // Metadata returns metadata for a DirEntry 839 // 840 // It should return nil if there is no Metadata 841 func (o *Object) Metadata(ctx context.Context) (metadata fs.Metadata, err error) { 842 err = o.readMetaData(ctx) 843 if err != nil { 844 fs.Logf(o, "Failed to read metadata: %v", err) 845 return nil, err 846 } 847 return o.meta.Get(ctx) 848 } 849 850 // DirSetModTime sets the directory modtime for dir 851 func (f *Fs) DirSetModTime(ctx context.Context, dir string, modTime time.Time) error { 852 dirID, err := f.dirCache.FindDir(ctx, dir, false) 853 if err != nil { 854 return err 855 } 856 d := f.newDir(dirID, dir) 857 return d.SetModTime(ctx, modTime) 858 } 859 860 // SetModTime sets the metadata on the DirEntry to set the modification date 861 // 862 // If there is any other metadata it does not overwrite it. 863 func (d *Directory) SetModTime(ctx context.Context, t time.Time) error { 864 btime := t 865 if d.meta != nil && !d.meta.btime.IsZero() { 866 btime = d.meta.btime // if we already have a non-zero btime, preserve it 867 } 868 d.meta = d.fs.newMetadata(d.remote) // set only the mtime and btime 869 d.meta.mtime = t 870 d.meta.btime = btime 871 _, err := d.meta.Write(ctx, false) 872 return err 873 } 874 875 // Metadata returns metadata for a DirEntry 876 // 877 // It should return nil if there is no Metadata 878 func (d *Directory) Metadata(ctx context.Context) (metadata fs.Metadata, err error) { 879 return d.meta.Get(ctx) 880 } 881 882 // SetMetadata sets metadata for a Directory 883 // 884 // It should return fs.ErrorNotImplemented if it can't set metadata 885 func (d *Directory) SetMetadata(ctx context.Context, metadata fs.Metadata) error { 886 _, meta, err := d.fs.updateDir(ctx, d.id, d.remote, metadata) 887 d.meta = meta 888 return err 889 } 890 891 // Fs returns read only access to the Fs that this object is part of 892 func (d *Directory) Fs() fs.Info { 893 return d.fs 894 } 895 896 // String returns the name 897 func (d *Directory) String() string { 898 return d.remote 899 } 900 901 // Remote returns the remote path 902 func (d *Directory) Remote() string { 903 return d.remote 904 } 905 906 // ModTime returns the modification date of the file 907 // 908 // If one isn't available it returns the configured --default-dir-time 909 func (d *Directory) ModTime(ctx context.Context) time.Time { 910 if !d.meta.mtime.IsZero() { 911 return d.meta.mtime 912 } 913 ci := fs.GetConfig(ctx) 914 return time.Time(ci.DefaultTime) 915 } 916 917 // Size returns the size of the file 918 func (d *Directory) Size() int64 { 919 return d.size 920 } 921 922 // Items returns the count of items in this directory or this 923 // directory and subdirectories if known, -1 for unknown 924 func (d *Directory) Items() int64 { 925 return d.items 926 } 927 928 // ID gets the optional ID 929 func (d *Directory) ID() string { 930 return d.id 931 } 932 933 // MimeType returns the content type of the Object if 934 // known, or "" if not 935 func (d *Directory) MimeType(ctx context.Context) string { 936 return dirMimeType 937 }