diff --git a/backend/onedrive/api/types.go b/backend/onedrive/api/types.go index 6f631556a..8c8d18126 100644 --- a/backend/onedrive/api/types.go +++ b/backend/onedrive/api/types.go @@ -9,6 +9,9 @@ import ( const ( timeFormat = `"` + time.RFC3339 + `"` + + // PackageTypeOneNote is the package type value for OneNote files + PackageTypeOneNote = "oneNote" ) // Error is returned from one drive when things go wrong @@ -107,6 +110,7 @@ type RemoteItemFacet struct { LastModifiedDateTime Timestamp `json:"lastModifiedDateTime"` // Date and time the item was last modified. Read-only. Folder *FolderFacet `json:"folder"` // Folder metadata, if the item is a folder. Read-only. File *FileFacet `json:"file"` // File metadata, if the item is a file. Read-only. + Package *PackageFacet `json:"package"` // 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. Read-only. FileSystemInfo *FileSystemInfoFacet `json:"fileSystemInfo"` // File system information on client. Read-write. ParentReference *ItemReference `json:"parentReference"` // Parent information, if the item has a parent. Read-write. Size int64 `json:"size"` // Size of the item in bytes. Read-only. @@ -147,6 +151,13 @@ type FileSystemInfoFacet struct { type DeletedFacet struct { } +// PackageFacet indicates that a DriveItem is the top level item +// in a "package" or a collection of items that should be treated as a collection instead of individual items. +// `oneNote` is the only currently defined value. +type PackageFacet struct { + Type string `json:"type"` +} + // Item represents metadata for an item in OneDrive type Item struct { ID string `json:"id"` // The unique identifier of the item within the Drive. Read-only. @@ -170,6 +181,7 @@ type Item struct { // Audio *AudioFacet `json:"audio"` // Audio metadata, if the item is an audio file. Read-only. // Video *VideoFacet `json:"video"` // Video metadata, if the item is a video. Read-only. // Location *LocationFacet `json:"location"` // Location metadata, if the item has location data. Read-only. + Package *PackageFacet `json:"package"` // 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. Read-only. Deleted *DeletedFacet `json:"deleted"` // Information about the deleted state of the item. Read-only. } @@ -281,6 +293,24 @@ func (i *Item) GetFolder() *FolderFacet { return i.Folder } +// GetPackage returns a normalized Package of the item +func (i *Item) GetPackage() *PackageFacet { + if i.IsRemote() && i.RemoteItem.Package != nil { + return i.RemoteItem.Package + } + return i.Package +} + +// GetPackageType returns the package type of the item if available, +// otherwise "" +func (i *Item) GetPackageType() string { + pack := i.GetPackage() + if pack == nil { + return "" + } + return pack.Type +} + // GetFile returns a normalized File of the item func (i *Item) GetFile() *FileFacet { if i.IsRemote() && i.RemoteItem.File != nil { diff --git a/backend/onedrive/onedrive.go b/backend/onedrive/onedrive.go index fbfef8aa5..3ff37d559 100644 --- a/backend/onedrive/onedrive.go +++ b/backend/onedrive/onedrive.go @@ -226,15 +226,21 @@ func init() { Help: "The type of the drive ( personal | business | documentLibrary )", Default: "", Advanced: true, + }, { + Name: "expose_onenote_files", + Help: "If true, OneNote files will show up in directory listing (see docs)", + Default: false, + Advanced: true, }}, }) } // Options defines the configuration for this backend type Options struct { - ChunkSize fs.SizeSuffix `config:"chunk_size"` - DriveID string `config:"drive_id"` - DriveType string `config:"drive_type"` + ChunkSize fs.SizeSuffix `config:"chunk_size"` + DriveID string `config:"drive_id"` + DriveType string `config:"drive_type"` + ExposeOneNoteFiles bool `config:"expose_onenote_files"` } // Fs represents a remote one drive @@ -255,15 +261,16 @@ type Fs struct { // // Will definitely have info but maybe not meta type Object struct { - fs *Fs // what this object is part of - remote string // The remote path - hasMetaData bool // whether info below has been set - size int64 // size of the object - modTime time.Time // modification time of the object - id string // ID of the object - sha1 string // SHA-1 of the object content - quickxorhash string // QuickXorHash of the object content - mimeType string // Content-Type of object from server (may not be as uploaded) + fs *Fs // what this object is part of + remote string // The remote path + hasMetaData bool // whether info below has been set + isOneNoteFile bool // Whether the object is a OneNote file + size int64 // size of the object + modTime time.Time // modification time of the object + id string // ID of the object + sha1 string // SHA-1 of the object content + quickxorhash string // QuickXorHash of the object content + mimeType string // Content-Type of object from server (may not be as uploaded) } // ------------------------------------------------------------ @@ -488,6 +495,9 @@ func (f *Fs) FindLeaf(pathID, leaf string) (pathIDOut string, found bool, err er } return "", false, err } + if info.GetPackageType() == api.PackageTypeOneNote { + return "", false, errors.New("found OneNote file when looking for folder") + } if info.GetFolder() == nil { return "", false, errors.New("found file when looking for folder") } @@ -596,6 +606,11 @@ func (f *Fs) List(dir string) (entries fs.DirEntries, err error) { } var iErr error _, err = f.listAll(directoryID, false, false, func(info *api.Item) bool { + if !f.opt.ExposeOneNoteFiles && info.GetPackageType() == api.PackageTypeOneNote { + fs.Debugf(info.Name, "OneNote file not shown in directory listing") + return false + } + remote := path.Join(dir, info.GetName()) folder := info.GetFolder() if folder != nil { @@ -1121,6 +1136,8 @@ func (o *Object) setMetaData(info *api.Item) (err error) { o.hasMetaData = true o.size = info.GetSize() + o.isOneNoteFile = info.GetPackageType() == api.PackageTypeOneNote + // Docs: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/resources/hashes // // We use SHA1 for onedrive personal and QuickXorHash for onedrive for business @@ -1232,6 +1249,10 @@ func (o *Object) Open(options ...fs.OpenOption) (in io.ReadCloser, err error) { if o.id == "" { return nil, errors.New("can't download - no id") } + if o.isOneNoteFile { + return nil, errors.New("can't open a OneNote file") + } + fs.FixRangeOption(options, o.size) var resp *http.Response opts := newOptsCall(o.id, "GET", "/content") @@ -1275,6 +1296,12 @@ func (o *Object) createUploadSession(modTime time.Time) (response *api.CreateUpl var resp *http.Response err = o.fs.pacer.Call(func() (bool, error) { resp, err = o.fs.srv.CallJSON(&opts, &createRequest, &response) + if apiErr, ok := err.(*api.Error); ok { + if apiErr.ErrorInfo.Code == "nameAlreadyExists" { + // Make the error more user-friendly + err = errors.New(err.Error() + " (is it a OneNote file?)") + } + } return shouldRetry(resp, err) }) return response, err @@ -1407,6 +1434,12 @@ func (o *Object) uploadSinglepart(in io.Reader, size int64, modTime time.Time) ( err = o.fs.pacer.Call(func() (bool, error) { resp, err = o.fs.srv.CallJSON(&opts, nil, &info) + if apiErr, ok := err.(*api.Error); ok { + if apiErr.ErrorInfo.Code == "nameAlreadyExists" { + // Make the error more user-friendly + err = errors.New(err.Error() + " (is it a OneNote file?)") + } + } return shouldRetry(resp, err) }) if err != nil { @@ -1425,6 +1458,10 @@ func (o *Object) uploadSinglepart(in io.Reader, size int64, modTime time.Time) ( // // The new object may have been created if an error is returned func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { + if o.hasMetaData && o.isOneNoteFile { + return errors.New("can't upload content to a OneNote file") + } + o.fs.tokenRenewer.Start() defer o.fs.tokenRenewer.Stop() diff --git a/docs/content/onedrive.md b/docs/content/onedrive.md index df3631673..3ea94d90f 100644 --- a/docs/content/onedrive.md +++ b/docs/content/onedrive.md @@ -165,6 +165,13 @@ system. Above this size files will be chunked - must be multiple of 320k. The default is 10MB. Note that the chunks will be buffered into memory. +#### --onedrive-expose-onenote-files #### + +By default rclone will hide OneNote files in directory listing because operations like `Open` +and `Update` won't work on them. But this behaviour may also prevent you from deleting them. +If you want to delete OneNote files or otherwise want them to show up in directory listing, +set this flag. + ### Limitations ### Note that OneDrive is case insensitive so you can't have a