Skip to content

Commit

Permalink
Copy and move (#162)
Browse files Browse the repository at this point in the history
closes #137
  • Loading branch information
hongooi73 committed May 27, 2023
1 parent 1947d5c commit 6b5125e
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 0 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- The `list_shared_items()`/`list_shared_files()` method for drives now always returns a list of drive item objects, rather than a data frame. If the `info` argument is supplied with a value other than "items", a warning is issued.
- Add folder upload and download functionality for `ms_drive_item$upload()` and `download()`. Subfolders can also be transferred recursively, and optionally in parallel. There are also corresponding `ms_drive$upload_folder()` and `download_folder()` methods.
- Add convenience methods for saving and loading datasets and R objects: `save_dataframe()`, `save_rds()`, `save_rdata()`, `load_dataframe()`, `load_rds()`, and `load_rdata()`. See `?ms_drive_item` and `?ms_drive` for more details.
- Add `copy` and `move` methods for drive items, and corresponding `copy_item` and `move_item` methods for drives.
- Add ability to upload and download via connections/raw vectors instead of files. You can specify the source to `ms_drive_item$upload()` to be a raw or text connection. Similarly, if the destination for `ms_drive_item$download()` is NULL, the downloaded data is returned as a raw vector.
- Add the ability to use object IDs instead of file/folder paths in `ms_drive` methods, including getting, uploading and downloading. This can be useful since the object ID is immutable, whereas file paths can change, eg if the file is moved or renamed. See `?ms_drive` for more details.

Expand Down
16 changes: 16 additions & 0 deletions R/ms_drive.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
#' - `get_item(path, itemid)`: Get an item representing a file or folder.
#' - `get_item_properties(path, itemid)`: Get the properties (metadata) for a file or folder.
#' - `set_item_properties(path, itemid, ...)`: Set the properties for a file or folder.
#' - `copy_item(path, itemid, dest, dest_item_id)`: Copy a file or folder.
#' - `move_item(path, itemid, dest, dest_item_id)`: Move a file or folder.
#' - `list_shared_items(...), list_shared_files(...)`: List the drive items shared with you. See 'Shared items' below.
#' - `load_dataframe(path, itemid, ...)`: Download a delimited file and return its contents as a data frame. See 'Saving and loading data' below.
#' - `load_rds(path, itemid)`: Download a .rds file and return the saved object.
Expand All @@ -52,6 +54,10 @@
#'
#' `set_item_properties` sets the properties of a file or folder. The new properties should be specified as individual named arguments to the method. Any existing properties that aren't listed as arguments will retain their previous values or be recalculated based on changes to other properties, as appropriate. You can also call the `update` method on the corresponding `ms_drive_item` object.
#'
#' - `copy_item` and `move_item` can take the destination location as either a full pathname (in the `dest` argument), or a name plus a drive item object (in the `dest_folder_item` argument). If the latter is supplied, any path in `dest` is ignored with a warning. Note that copying is an _asynchronous_ operation, meaning the method returns before the copy is complete.
#'
#' For copying and moving, the destination folder must exist beforehand. When copying/moving a large number of files, it's much more efficient to supply the destination folder in the `dest_folder_item` argument rather than as a path.
#'
#' `list_items(path, info, full_names, pagesize)` lists the items under the specified path.
#'
#' `list_files` is a synonym for `list_items`.
Expand Down Expand Up @@ -236,6 +242,16 @@ public=list(
self$get_item(path, itemid)$update(...)
},

copy_item=function(path=NULL, itemid=NULL, dest, dest_folder_item=NULL)
{
self$get_item(path, itemid)$copy(dest, dest_folder_item)
},

move_item=function(path=NULL, itemid=NULL, dest, dest_folder_item=NULL)
{
self$get_item(path, itemid)$move(dest, dest_folder_item)
},

list_shared_items=function(allow_external=TRUE, filter=NULL, n=Inf, pagesize=1000, info=NULL)
{
if(!is.null(info) && info != "items")
Expand Down
74 changes: 74 additions & 0 deletions R/ms_drive_item.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#' - `do_operation(...)`: Carry out an arbitrary operation on the item.
#' - `sync_fields()`: Synchronise the R object with the item metadata in Microsoft Graph.
#' - `open()`: Open the item in your browser.
#' - `copy(dest, dest_folder_item=NULL)`: Copy the item to the given location.
#' - `move(dest, dest_folder_item=NULL)`: Move the item to the given location.
#' - `list_items(...), list_files(...)`: List the files and folders under the specified path.
#' - `download(dest, overwrite, recursive, parallel)`: Download the file or folder. See below.
#' - `create_share_link(type, expiry, password, scope)`: Create a shareable link to the file or folder.
Expand Down Expand Up @@ -62,6 +64,10 @@
#'
#' `get_item` retrieves the file or folder with the given path, as another object of class `ms_drive_item`.
#'
#' - `copy` and `move` can take the destination location as either a full pathname (in the `dest` argument), or a name plus a drive item object (in the `dest_folder_item` argument). If the latter is supplied, any path in `dest` is ignored with a warning. Note that copying is an _asynchronous_ operation, meaning the method returns before the copy is complete.
#'
#' For copying and moving, the destination folder must exist beforehand. When copying/moving a large number of files, it's much more efficient to supply the destination folder in the `dest_folder_item` argument rather than as a path.
#'
#' `create_folder` creates a folder with the specified path. Trying to create an already existing folder is an error. This returns an `ms_drive_item` object, invisibly.
#'
#' `create_share_link(path, type, expiry, password, scope)` returns a shareable link to the item. Its arguments are
Expand Down Expand Up @@ -109,6 +115,16 @@
#' # rename a file
#' myfile$update(name="newname.docx")
#'
#' # copy a file (destination folder must exist)
#' myfile$copy("/Documents/folder2/myfile_copied.docx")
#'
#' # alternate way of copying: supply the destination folder
#' destfolder <- docs$get_item("folder2")
#' myfile$copy("myfile_copied.docx", dest_folder_item=destfolder)
#'
#' # move a file (destination folder must exist)
#' myfile$move("Documents/folder2/myfile_moved.docx")
#'
#' # open the file in the browser
#' myfile$open()
#'
Expand Down Expand Up @@ -466,6 +482,58 @@ public=list(
self$upload(tmpsave, file)
},

copy=function(dest, dest_folder_item=NULL)
{
path <- dirname(dest)
body <- list(name=basename(dest))

if(!is.null(dest_folder_item))
{
if(path != ".")
warning("Destination folder object supplied; path will be ignored")
body$parentReference <- list(
driveId=dest_folder_item$properties$parentReference$driveId,
id=dest_folder_item$properties$id
)
}
else if(path != ".")
{
dest_folder_item <- private$get_drive()$get_item(path)
body$parentReference <- list(
driveId=dest_folder_item$properties$parentReference$driveId,
id=dest_folder_item$properties$id
)
}

self$do_operation("copy", body=body, http_verb="POST")
invisible(NULL)
},

move=function(dest, dest_folder_item=NULL)
{
path <- dirname(dest)
body <- list(name=basename(dest))

if(!is.null(dest_folder_item))
{
if(path != ".")
warning("Destination folder object supplied; path will be ignored")
body$parentReference <- list(
id=dest_folder_item$properties$id
)
}
else if(path != ".")
{
dest_folder_item <- private$get_drive()$get_item(path)
body$parentReference <- list(
id=dest_folder_item$properties$id
)
}

self$properties <- self$do_operation(body=body, encode="json", http_verb="PATCH")
invisible(self)
},

get_path=function()
{
private$assert_is_not_remote()
Expand Down Expand Up @@ -618,6 +686,12 @@ private=list(
utils::URLencode(enc2utf8(op))
},

get_drive=function()
{
dummy_props <- list(id=self$properties$parentReference$driveId)
ms_drive$new(self$token, self$tenant, dummy_props)
},

assert_is_folder=function()
{
if(!self$is_folder())
Expand Down
7 changes: 7 additions & 0 deletions man/ms_drive.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions man/ms_drive_item.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions tests/testthat/test01b_onedrive_copymove.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
tenant <- "consumers"
app <- Sys.getenv("AZ_TEST_NATIVE_APP_ID")

if(app == "")
skip("OneDrive tests skipped: Microsoft Graph credentials not set")

if(!interactive())
skip("OneDrive tests skipped: must be in interactive session")

tok <- get_test_token(tenant, app, c("Files.ReadWrite.All", "User.Read"))
if(is.null(tok))
skip("OneDrive tests skipped: unable to login to consumers tenant")

drv <- try(call_graph_endpoint(tok, "me/drive"), silent=TRUE)
if(inherits(drv, "try-error"))
skip("OneDrive tests skipped: service not available")

opt_use_itemid <- options(microsoft365r_use_itemid_in_path=TRUE)
od <- ms_drive$new(tok, tenant, drv)
folder <- od$create_folder(make_name())

test_that("OneDrive item copy/move methods work",
{
expect_is(folder, "ms_drive_item")

newfoldername <- make_name()
expect_is(newfolder <- od$create_folder(newfoldername), "ms_drive_item")

src <- "../resources/file.json"
expect_silent(folder$upload(src))

odsrc <- file.path(folder$properties$name, basename(src))
# copy via path
destpath1 <- file.path(newfoldername, "copy1.json")
expect_silent(od$copy_item(odsrc, dest=destpath1))

# wait for async copy
Sys.sleep(2)
expect_is(od$get_item(destpath1), "ms_drive_item")

# copy via name
expect_silent(od$copy_item(odsrc, dest="copy2.json", dest_folder_item=newfolder))

# wait for async copy
destpath2 <- file.path(newfoldername, "copy2.json")
Sys.sleep(2)
expect_is(od$get_item(destpath2), "ms_drive_item")

# move via path
it1 <- od$move_item(odsrc, dest=file.path(newfoldername, "move1.json"))
expect_identical(it1$properties$name, "move1.json")
expect_error(folder$get_item(basename(src)))

# move via name
newsrc <- file.path(newfoldername, "move1.json")
it2 <- od$move_item(newsrc, dest="move2.json", dest_folder_item=newfolder)
expect_identical(it2$properties$name, "move2.json")
expect_error(newfolder$get_item("move1.json"))
})


teardown({
options(opt_use_itemid)
})

0 comments on commit 6b5125e

Please sign in to comment.