diff --git a/DESCRIPTION b/DESCRIPTION index f556ca8..7c79f2d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,24 +1,26 @@ Package: rdrop2 Title: Programmatic Interface to the 'Dropbox' API -Version: 0.8.2.2 +Version: 0.10.0 Authors@R: c(person("Karthik", "Ram", email = "karthik.ram@gmail.com", role = c("aut", "cre")), person("Clayton", "Yochum", role = "aut"), person("Caleb", "Scheidel", role = "ctb"), - person("Akhil", "Bhel", role = "cph") + person("Akhil", "Bhel", role = "cph"), + person("Tadhg", "Moore", role = "ctb") ) -Description: Provides full programmatic access to the 'Dropbox' file hosting platform , including support for all standard file operations. -Depends: R (>= 3.1.1) +Description: Provides full programmatic access to the 'Dropbox' file hosting platform , including support for all standard file operations. Uses the 'httr2' package for HTTP requests and the Dropbox API v2. +Depends: R (>= 3.6.0) License: MIT + file LICENSE BugReports: https://github.com/karthik/rdrop2/issues LazyData: true -Imports: digest, +Imports: cli, + digest, dplyr, - httr, + httr2, jsonlite, magrittr, purrr, assertthat Suggests: testthat, uuid -RoxygenNote: 7.2.3 Encoding: UTF-8 +Config/roxygen2/version: 8.0.0 diff --git a/NAMESPACE b/NAMESPACE index 4fcced0..f70bff5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,22 +3,37 @@ export("%>%") export(drop_acc) export(drop_auth) +export(drop_auth_env) export(drop_content_hash) export(drop_copy) +export(drop_copy_batch) export(drop_create) export(drop_delete) +export(drop_delete_batch) export(drop_dir) export(drop_download) export(drop_exists) export(drop_get) export(drop_get_metadata) +export(drop_get_shared_link_file) +export(drop_get_thumbnail) export(drop_history) export(drop_list_shared_links) export(drop_media) export(drop_move) +export(drop_move_batch) export(drop_read_csv) +export(drop_restore) +export(drop_save_url) export(drop_search) +export(drop_search_continue) export(drop_share) +export(drop_space_usage) export(drop_upload) -import(httr) +export(get_dropbox_token) +import(httr2) +importFrom(cli,cli_abort) +importFrom(cli,cli_alert_danger) +importFrom(cli,cli_alert_info) +importFrom(cli,cli_alert_success) importFrom(magrittr,"%>%") diff --git a/NEWS.md b/NEWS.md index d86a54a..8f46320 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,8 @@ +# rdrop2 0.10.0 +* Switched from `httr` to `httr2` for all HTTP requests. +* Added `cli` package for improved user-facing messages, warnings, and errors. +* Added Tadhg Moore as a contributor. + # rdrop2 0.8.2.2 * replaced dependency package `assertive` by `assertthat`. diff --git a/R/drop_acc.R b/R/drop_acc.R index d0fd279..ecc37b1 100644 --- a/R/drop_acc.R +++ b/R/drop_acc.R @@ -1,12 +1,9 @@ #' Get information about current Dropbox account. #' -#' Fields returned will vary by account; +#' Fields returned will vary by account. #' #' @template token #' -#' @import httr -#' @export -#' #' @return #' Nested list with elements \code{account_id}, #' \code{name} (list), \code{email}, \code{email_verified}, \code{disabled}, @@ -18,6 +15,8 @@ #' #' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account}{API documentation} #' +#' @export +#' #' @examples #' \dontrun{ #' @@ -29,8 +28,32 @@ drop_acc <- function(dtoken = get_dropbox_token()) { url <- "https://api.dropbox.com/2/users/get_current_account" + drop_request(url, dtoken) +} + + +#' Get Dropbox storage space usage. +#' +#' Returns how much space the current account is using and how much is +#' allocated. +#' +#' @template token +#' +#' @return A list with elements \code{used} (bytes used) and \code{allocation} +#' (a list with \code{.tag} and \code{allocated} bytes, or team-level info). +#' +#' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#users-get_space_usage}{API documentation} +#' +#' @export +#' +#' @examples +#' \dontrun{ +#' usage <- drop_space_usage() +#' cat("Used:", usage$used, "bytes\n") +#' cat("Allocated:", usage$allocation$allocated, "bytes\n") +#' } +drop_space_usage <- function(dtoken = get_dropbox_token()) { - # make request and parse response - req <- httr::POST(url, httr::config(token = dtoken)) - httr::content(req) + url <- "https://api.dropbox.com/2/users/get_space_usage" + drop_request(url, dtoken) } diff --git a/R/drop_auth.R b/R/drop_auth.R index c5a86d7..7b5ea3d 100644 --- a/R/drop_auth.R +++ b/R/drop_auth.R @@ -1,55 +1,52 @@ # environment to store credentials .dstate <- new.env(parent = emptyenv()) +# default cache path for the token RDS file +.default_token_cache <- ".rdrop2-token.rds" + #' Authentication for Dropbox #' -#' This function authenticates you into Dropbox. The documentation for the -#' \href{https://www.dropbox.com/developers/documentation?_tk=pilot_lp&_ad=topbar1&_camp=docs}{core Dropbox API} -#' provides more details including alternate methods if you desire to -#' reimplement your own. +#' This function authenticates you into Dropbox using OAuth 2.0 via the +#' \pkg{httr2} package. The documentation for the +#' \href{https://www.dropbox.com/developers/documentation}{Dropbox API v2} +#' provides more details. #' #' @param new_user Set to \code{TRUE} if you need to switch to a new user -#' account or just flush existing token. Default is \code{FALSE}. -#' @param key Your application key. \code{rdrop2} already comes with a key/secret but -#' you are welcome to swap out with our own. Since these keys are shipped with -#' the package, there is a small chance they could be voided if someone abuses -#' the key. If you plan to use this in production, or for an internal tool, -#' the recommended practice is to create a new application on Dropbox and use -#' those keys for your purposes. -#' @param secret Your application secret. Like \code{key}, \code{rdrop2} comes -#' with a secret but you are welcome to swap out with our own. -#' @param cache By default your credentials are locally cached in a file called -#' \code{.httr-oauth}. Set to FALSE if you need to authenticate separately -#' each time. -#' @param rdstoken File path to stored RDS token. In server environments where -#' interactive OAuth is not possible, a token can be created on a desktop -#' client and used in production. See examples. -#' -#' @return A Token2.0 object, invisibly -#' -#' @import httr +#' account or flush the existing cached token. Default is \code{FALSE}. +#' @param key Your application key. \code{rdrop2} ships with a default key, but +#' for production use you should create your own Dropbox app and supply its +#' credentials. +#' @param secret Your application secret. +#' @param cache Either \code{TRUE} (save token to \code{.rdrop2-token.rds} in +#' the working directory), \code{FALSE} (do not cache), or a file path string +#' specifying where to save the token RDS file. +#' @param rdstoken File path to a previously saved RDS token. In non-interactive +#' (server) environments, create a token on a desktop machine with +#' \code{drop_auth()}, save it with \code{saveRDS()}, and supply the path +#' here. See examples. +#' +#' @return The \code{httr2_token} object, invisibly. +#' +#' @import httr2 #' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#authorization}{API documentation} #' @export #' #' @examples #' \dontrun{ #' -#' # To either read token from .httr-oauth in the working directory or open a -#' # web browser to authenticate (and cache a token) +#' # Open a browser to authenticate (and cache the token) #' drop_auth() #' -#' # If you want to overwrite an existing local token and switch to a new -#' # user, set new_user to TRUE. +#' # Switch to a new user account #' drop_auth(new_user = TRUE) #' -#' # To store a token for re-use (more flexible than .httr-oauth), save the -#' # output of drop_auth and save it to an RDS file +#' # Save the token for later re-use #' token <- drop_auth() -#' saveRDS(token, "/path/to/tokenfile.RDS") +#' saveRDS(token, "/path/to/tokenfile.rds") #' -#' # To use a stored token provide token location -#' drop_auth(rdstoken = "/path/to/tokenfile.RDS") +#' # Load a previously saved token +#' drop_auth(rdstoken = "/path/to/tokenfile.rds") #' } drop_auth <- function(new_user = FALSE, key = "mmhfsybffdom42w", @@ -57,60 +54,218 @@ drop_auth <- function(new_user = FALSE, cache = TRUE, rdstoken = NA) { - # check if token file exists & use it - if (new_user == FALSE & !is.na(rdstoken)) { - - # read token or error - if (file.exists(rdstoken)) { - .dstate$token <- readRDS(rdstoken) - } else { - stop("token file not found") - } - - # authenticate normally + # resolve cache path + cache_path <- if (isTRUE(cache)) { + .default_token_cache + } else if (is.character(cache)) { + cache } else { + NULL + } - # remove any cached token if new user - if (new_user && file.exists(".httr-oauth")) { - message("Removing old credentials...") - file.remove(".httr-oauth") - } + # load token from explicit RDS path + if (!isTRUE(new_user) && !is.na(rdstoken)) { + if (!file.exists(rdstoken)) cli::cli_abort("token file not found") + .dstate$token <- readRDS(rdstoken) + .dstate$cache_path <- rdstoken + return(invisible(.dstate$token)) + } - # set dropbox oauth2 endpoints - dropbox <- httr::oauth_endpoint( - authorize = "https://www.dropbox.com/oauth2/authorize", - access = "https://api.dropbox.com/oauth2/token" - ) + # load token from cache path (unless new_user) + if (!isTRUE(new_user) && !is.null(cache_path) && file.exists(cache_path)) { + .dstate$token <- readRDS(cache_path) + .dstate$cache_path <- cache_path + return(invisible(.dstate$token)) + } - # registered dropbox app's key & secret - dropbox_app <- httr::oauth_app("dropbox", key, secret) + # remove old cache if switching users + if (isTRUE(new_user) && !is.null(cache_path) && file.exists(cache_path)) { + cli::cli_alert_info("Removing old cached credentials...") + file.remove(cache_path) + } - # get the token - dropbox_token <- httr::oauth2.0_token(dropbox, dropbox_app, cache = cache) + # build OAuth2 client + dropbox_client <- httr2::oauth_client( + id = key, + secret = secret, + token_url = "https://api.dropbox.com/oauth2/token", + name = "dropbox" + ) - # make sure we got a token - if (!inherits(dropbox_token, "Token2.0")) { - stop("something went wrong, try again") - } + # run auth-code flow (opens browser); request offline access so that + # Dropbox returns a refresh_token alongside the short-lived access_token. + # redirect_uri must match the URI registered for the Dropbox app (port 1410). + dropbox_token <- httr2::oauth_flow_auth_code( + client = dropbox_client, + auth_url = "https://www.dropbox.com/oauth2/authorize", + auth_params = list(token_access_type = "offline"), + redirect_uri = "http://localhost:1410/", + pkce = FALSE + ) - # cache token in rdrop2 namespace - .dstate$token <- dropbox_token + if (is.null(dropbox_token$access_token)) { + cli::cli_abort("Authentication failed: no access token returned. Please try again.") } + + # persist to cache + if (!is.null(cache_path)) { + saveRDS(dropbox_token, cache_path) + } + + .dstate$token <- dropbox_token + .dstate$client <- dropbox_client + .dstate$cache_path <- cache_path + invisible(.dstate$token) } +#' Authenticate with Dropbox using environment variables +#' +#' A non-interactive authentication method that exchanges a long-lived refresh +#' token for a short-lived access token. Suitable for server or CI/CD +#' environments where browser-based authentication is not possible. +#' +#' Set the following environment variables before calling this function (or any +#' rdrop2 function that requires authentication): +#' \itemize{ +#' \item \code{DROPBOX_APP_KEY} – the app key from your Dropbox app console. +#' \item \code{DROPBOX_APP_SECRET} – the app secret. +#' \item \code{DROPBOX_REFRESH_TOKEN} – a long-lived refresh token. Obtain +#' one by running \code{drop_auth()} interactively and inspecting +#' \code{token$refresh_token}. +#' } +#' +#' @param app_key Dropbox application key. Defaults to +#' \code{Sys.getenv("DROPBOX_APP_KEY")}. +#' @param app_secret Dropbox application secret. Defaults to +#' \code{Sys.getenv("DROPBOX_APP_SECRET")}. +#' @param refresh_token Long-lived refresh token. Defaults to +#' \code{Sys.getenv("DROPBOX_REFRESH_TOKEN")}. +#' +#' @return The token list (invisibly). The access token is stored in the rdrop2 +#' session environment for use by all other rdrop2 functions. +#' +#' @import httr2 +#' @importFrom cli cli_alert_info cli_abort +#' @export +#' +#' @examples +#' \dontrun{ +#' Sys.setenv( +#' DROPBOX_APP_KEY = "your_app_key", +#' DROPBOX_APP_SECRET = "your_app_secret", +#' DROPBOX_REFRESH_TOKEN = "your_refresh_token" +#' ) +#' drop_auth_env() +#' +#' # Alternatively, pass values directly: +#' drop_auth_env( +#' app_key = "your_app_key", +#' app_secret = "your_app_secret", +#' refresh_token = "your_refresh_token" +#' ) +#' } +drop_auth_env <- function( + app_key = Sys.getenv("DROPBOX_APP_KEY"), + app_secret = Sys.getenv("DROPBOX_APP_SECRET"), + refresh_token = Sys.getenv("DROPBOX_REFRESH_TOKEN") +) { + cli::cli_alert_info("Authenticating with Dropbox using environment variables...") + + if (!nzchar(app_key)) + cli::cli_abort("app_key is empty. Set {.envvar DROPBOX_APP_KEY} or pass it directly.") + if (!nzchar(app_secret)) + cli::cli_abort("app_secret is empty. Set {.envvar DROPBOX_APP_SECRET} or pass it directly.") + if (!nzchar(refresh_token)) + cli::cli_abort("refresh_token is empty. Set {.envvar DROPBOX_REFRESH_TOKEN} or pass it directly.") + + token_resp <- httr2::request("https://api.dropbox.com/oauth2/token") |> + httr2::req_body_form( + refresh_token = refresh_token, + grant_type = "refresh_token", + client_id = app_key, + client_secret = app_secret + ) |> + httr2::req_error(body = function(resp) httr2::resp_body_json(resp)$error) |> + httr2::req_perform() + + token <- httr2::resp_body_json(token_resp) + + # Convert expires_in (seconds) to an absolute timestamp for expiry checks + if (!is.null(token$expires_in)) { + token$expires_at <- Sys.time() + token$expires_in + } + + # Dropbox does not re-issue the refresh token on refresh; preserve the original + token$refresh_token <- refresh_token + + # Store the OAuth client so that get_dropbox_token() can refresh automatically later + .dstate$client <- httr2::oauth_client( + id = app_key, + secret = app_secret, + token_url = "https://api.dropbox.com/oauth2/token", + name = "dropbox" + ) + .dstate$token <- token + + invisible(token) +} + -#' Retrieve oauth2 token from rdrop2-namespaced environment +#' Retrieve the Dropbox bearer token string #' -#' Retrieves a token if it is previously stored, otherwise prompts user to get one. +#' Returns the access token string stored in the rdrop2 environment. If no +#' token is cached, \code{\link{drop_auth}} is called interactively. If the +#' stored token has expired and a refresh token is available, the token is +#' silently refreshed and the cache file is updated. #' #' @keywords internal +#' @export +#' @return The Dropbox access token string. get_dropbox_token <- function() { + if (!exists(".dstate") || is.null(.dstate$token)) { + # Prefer non-interactive env-var authentication when the variables are set + env_key <- Sys.getenv("DROPBOX_APP_KEY") + env_secret <- Sys.getenv("DROPBOX_APP_SECRET") + env_refresh <- Sys.getenv("DROPBOX_REFRESH_TOKEN") + if (nzchar(env_key) && nzchar(env_secret) && nzchar(env_refresh)) { + drop_auth_env(env_key, env_secret, env_refresh) + } else { + drop_auth() + } + } - if (!exists('.dstate') || is.null(.dstate$token)) { - drop_auth() - } else { - .dstate$token + token <- .dstate$token + + # Refresh if the access token has expired and we have a refresh token + has_refresh <- !is.null(token$refresh_token) + has_expiry <- !is.null(token$expires_at) + is_expired <- has_expiry && token$expires_at < Sys.time() + + if (has_refresh && is_expired) { + client <- if (!is.null(.dstate$client)) { + .dstate$client + } else { + httr2::oauth_client( + id = "mmhfsybffdom42w", + secret = "l8zeqqqgm1ne5z0", + token_url = "https://api.dropbox.com/oauth2/token", + name = "dropbox" + ) + } + old_refresh_token <- token$refresh_token + token <- httr2::oauth_flow_refresh(client, old_refresh_token) + # Dropbox does not re-issue the refresh token on refresh; preserve the + # original so subsequent refreshes continue to work. + if (is.null(token$refresh_token)) { + token$refresh_token <- old_refresh_token + } + .dstate$token <- token + if (!is.null(.dstate$cache_path)) { + saveRDS(token, .dstate$cache_path) + } } + + token$access_token } diff --git a/R/drop_content_hash.R b/R/drop_content_hash.R index 39a4929..a35a922 100644 --- a/R/drop_content_hash.R +++ b/R/drop_content_hash.R @@ -35,7 +35,7 @@ #' } drop_content_hash <- function(file) { if (!is.character(file)) { - stop("Expected 'file' to be a character vector") + cli::cli_abort("Expected {.arg file} to be a character vector") } if (length(file) != 1L) { return(vapply(file, drop_content_hash, character(1), USE.NAMES = FALSE)) diff --git a/R/drop_dir.R b/R/drop_dir.R index 358c7e4..51505e1 100644 --- a/R/drop_dir.R +++ b/R/drop_dir.R @@ -45,9 +45,6 @@ drop_dir <- function( ) { # check args - #assertive::assert_is_a_string(path) - #if (!is.null(limit)) assertive::assert_is_numeric(limit) - #assertive::assert_is_any_of(cursor, c("logical", "character")) assertthat::assert_that(is.character(path), is.null(limit) || is.numeric(limit), class(cursor) %in% c("logical", "character")) @@ -104,7 +101,7 @@ drop_dir <- function( while (content$has_more) { # update content, append results - content <- drop_list_folder_continue(content$cursor) + content <- drop_list_folder_continue(content$cursor, dtoken) results <- append(results, content$entries) } } @@ -138,24 +135,15 @@ drop_list_folder <- function( url <- "https://api.dropboxapi.com/2/files/list_folder" - req <- httr::POST( - url = url, - httr::config(token = dtoken), - body = drop_compact(list( - path = path, - recursive = recursive, - include_media_info = include_media_info, - include_deleted = include_deleted, - include_has_explicit_shared_members = include_has_explicit_shared_members, - include_mounted_folders = include_mounted_folders, - limit = limit - )), - encode = "json" - ) - - httr::stop_for_status(req) - - httr::content(req) + drop_request(url, dtoken, body = drop_compact(list( + path = path, + recursive = recursive, + include_media_info = include_media_info, + include_deleted = include_deleted, + include_has_explicit_shared_members = include_has_explicit_shared_members, + include_mounted_folders = include_mounted_folders, + limit = limit + ))) } @@ -172,16 +160,7 @@ drop_list_folder_continue <- function(cursor, dtoken = get_dropbox_token()) { url <- "https://api.dropboxapi.com/2/files/list_folder/continue" - req <- httr::POST( - url = url, - httr::config(token = dtoken), - body = list(cursor = cursor), - encode = "json" - ) - - httr::stop_for_status(req) - - httr::content(req) + drop_request(url, dtoken, body = list(cursor = cursor)) } @@ -207,22 +186,13 @@ drop_list_folder_get_latest_cursor <- function( url <- "https://api.dropboxapi.com/2/files/list_folder/get_latest_cursor" - req <- httr::POST( - url = url, - httr::config(token = dtoken), - body = drop_compact(list( - path = path, - recursive = recursive, - include_media_info = include_media_info, - include_deleted = include_deleted, - include_has_explicit_shared_members = include_has_explicit_shared_members, - include_mounted_folders = include_mounted_folders, - limit = limit - )), - encode = "json" - ) - - httr::stop_for_status(req) - - httr::content(req) + drop_request(url, dtoken, body = drop_compact(list( + path = path, + recursive = recursive, + include_media_info = include_media_info, + include_deleted = include_deleted, + include_has_explicit_shared_members = include_has_explicit_shared_members, + include_mounted_folders = include_mounted_folders, + limit = limit + ))) } diff --git a/R/drop_download.R b/R/drop_download.R index b253210..ff9ba0e 100644 --- a/R/drop_download.R +++ b/R/drop_download.R @@ -8,22 +8,24 @@ #' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-download}{API documentation} #' @template token #' +#' @importFrom cli cli_abort cli_alert_success +#' #' @return TRUE if successful; error thrown otherwise. #' #' @examples \dontrun{ #' #' # download a file to the current working directory -#' drop_get("dataset.zip") +#' drop_download("dataset.zip") #' #' # download again, overwriting previous result -#' drop_get("dataset.zip", overwrite = TRUE) +#' drop_download("dataset.zip", overwrite = TRUE) #' #' # download to a different path, keeping file name constant #' # will download to "some/other/place/dataset.zip" -#' drop_get("dataset.zip", local_path = "some/other/place/") +#' drop_download("dataset.zip", local_path = "some/other/place/") #' -#' # download to to a different path, changing filename -#' drop_get("dataset.zip", local_path = "some/other/place/not_a_dataset.zip") +#' # download to a different path, changing filename +#' drop_download("dataset.zip", local_path = "some/other/place/not_a_dataset.zip") #' } #' #' @export @@ -42,43 +44,39 @@ drop_download <- function( # if no local path given, download it to working directory # if path given is folder, append filename to it if (is.null(local_path)) { - local_path = basename(path) + local_path <- basename(path) } else if (dir.exists(local_path)) { local_path <- file.path(local_path, basename(path)) } + if (file.exists(local_path) && !overwrite) { + cli::cli_abort( + "Local file {.file {local_path}} already exists. Set {.code overwrite = TRUE} to replace it." + ) + } + url <- "https://content.dropboxapi.com/2/files/download" - req <- httr::POST( - url = url, - httr::config(token = dtoken), - httr::add_headers("Dropbox-API-Arg" = jsonlite::toJSON( - list( - path = path - ), - auto_unbox = TRUE - )), - if (progress) httr::progress(), - httr::write_disk(local_path, overwrite) - ) + arg_json <- jsonlite::toJSON(list(path = path), auto_unbox = TRUE) + + req <- httr2::request(url) + req <- httr2::req_auth_bearer_token(req, resolve_token(dtoken)) + req <- httr2::req_headers(req, `Dropbox-API-Arg` = arg_json) + req <- httr2::req_body_raw(req, "", type = "application/octet-stream") + if (progress) req <- httr2::req_progress(req) - httr::stop_for_status(req) + httr2::req_perform(req, path = local_path) # print message in verbose mode if (verbose) { - size <- file.size(local_path) class(size) <- "object_size" - - message(sprintf( - "Downloaded %s to %s: %s on disk", - path, - local_path, - format(size, units = "auto") - )) + size_fmt <- format(size, units = "auto") + cli::cli_alert_success( + "Downloaded {.path {path}} to {.path {local_path}} ({.val {size_fmt}})" + ) } - # must have been successful TRUE } @@ -88,9 +86,10 @@ drop_download <- function( #' @template path #' @param local_file The name of the local copy. Leave this blank if you're fine with the original name. #' @param overwrite Default is \code{FALSE} but can be set to \code{TRUE}. -#' @param progress Progress bars are turned off by default. Set to \code{TRUE} ot turn this on. Progress is only reported when file sizes are known. Otherwise just bytes downloaded. +#' @param progress Progress bars are turned off by default. Set to \code{TRUE} to turn this on. #' @template token #' @template verbose +#' @importFrom cli cli_alert_danger cli_alert_success #' #' @examples \dontrun{ #' drop_get(path = 'dataset.zip', local_file = "~/Desktop") @@ -110,23 +109,24 @@ drop_get <- function( .Deprecated("drop_download") - #assertive::assert_is_not_null(path) assertthat::assert_that(!is.null(path)) if (drop_exists(path, dtoken = dtoken)) { filename <- ifelse(is.null(local_file), basename(path), local_file) - drop_download(path, filename, overwrite, progress, verbose, dtoken) if (!verbose) { - # prints file sizes in kb but this could also be pretty printed - message(sprintf("\n %s on disk %s KB", filename, file.size(filename)/1000)) + size <- file.size(filename) + class(size) <- "object_size" + cli::cli_alert_success( + "{.path {filename}} downloaded ({format(size, units = 'auto')})" + ) TRUE } else { drop_get_metadata(path) } } else { - message("File not found on Dropbox \n") + cli::cli_alert_danger("File not found on Dropbox: {.path {path}}") FALSE } } diff --git a/R/drop_file_ops.R b/R/drop_file_ops.R index ee53f48..d7f513d 100644 --- a/R/drop_file_ops.R +++ b/R/drop_file_ops.R @@ -1,5 +1,7 @@ +#' Copies a file or folder to a new location. +#' #' Copies a file or folder to a new location. #' #' @template from_to @@ -20,8 +22,7 @@ #' drop_create("drop_test2") #' drop_copy("mt.csv", "drop_test2/mt2.csv") #' } -drop_copy <- - function(from_path = NULL, +drop_copy <- function(from_path = NULL, to_path = NULL, allow_shared_folder = FALSE, autorename = FALSE, @@ -29,29 +30,21 @@ drop_copy <- verbose = FALSE, dtoken = get_dropbox_token()) { copy_url <- "https://api.dropboxapi.com/2/files/copy_v2" - from_path <- add_slashes(from_path) to_path <- add_slashes(to_path) - # Copying a file into a folder - file_to_folder <- - c(drop_type(from_path) == "file", - drop_type(to_path) == "folder") - to_path <- - ifelse(all(file_to_folder), paste0(to_path, from_path), to_path) - - # coping a folder to another folder - folder_to_folder <- - c(drop_type(from_path) == "folder", - drop_type(to_path) == "folder") - to_path <- - ifelse(all(folder_to_folder), paste0(to_path, from_path), to_path) - + file_to_folder <- c(drop_type(from_path) == "file", + drop_type(to_path) == "folder") + to_path <- ifelse(all(file_to_folder), paste0(to_path, from_path), to_path) + # Copying a folder to another folder + folder_to_folder <- c(drop_type(from_path) == "folder", + drop_type(to_path) == "folder") + to_path <- ifelse(all(folder_to_folder), paste0(to_path, from_path), to_path) + # Nothing to do, since both paths reflect origin and destination # Copying a file to a file # Nothing to do, since both paths reflect origin and destination # Copying a folder to an existing filename will result in a HTTP 409 (conflict error) - args <- drop_compact( list( from_path = from_path, @@ -63,22 +56,16 @@ drop_copy <- ) if (drop_exists(from_path)) { - # copy - x <- - httr::POST(copy_url, - httr::config(token = dtoken), - body = args, - encode = "json") - res <- httr::content(x) + res <- drop_request(copy_url, dtoken, body = args) if (!verbose) { - message(sprintf("%s copied to %s", from_path, res$metadata$path_lower)) + cli::cli_alert_success("{from_path} copied to {res$metadata$path_lower}") invisible(res) } else { pretty_lists(res) invisible(res) } } else { - stop("File or folder not found \n") + cli::cli_abort("File or folder not found") } } @@ -103,8 +90,7 @@ drop_copy <- #' drop_create("drop_test2") #' drop_move("mt.csv", "drop_test2/mt.csv") #' } -drop_move <- - function(from_path = NULL, +drop_move <- function(from_path = NULL, to_path = NULL, allow_shared_folder = FALSE, autorename = FALSE, @@ -117,18 +103,14 @@ drop_move <- to_path <- add_slashes(to_path) # Moving a file into a folder - file_to_folder <- - c(drop_type(from_path) == "file", - drop_type(to_path) == "folder") - to_path <- - ifelse(all(file_to_folder), paste0(to_path, from_path), to_path) + file_to_folder <- c(drop_type(from_path) == "file", + drop_type(to_path) == "folder") + to_path <- ifelse(all(file_to_folder), paste0(to_path, from_path), to_path) # Moving a folder to another folder - folder_to_folder <- - c(drop_type(from_path) == "folder", - drop_type(to_path) == "folder") - to_path <- - ifelse(all(folder_to_folder), paste0(to_path, from_path), to_path) + folder_to_folder <- c(drop_type(from_path) == "folder", + drop_type(to_path) == "folder") + to_path <- ifelse(all(folder_to_folder), paste0(to_path, from_path), to_path) # Moving a file to a file # Nothing to do, since both paths reflect origin and destination @@ -146,23 +128,17 @@ drop_move <- ) if (drop_exists(from_path)) { - # move - x <- - httr::POST(move_url, - httr::config(token = dtoken), - body = args, - encode = "json") - res <- httr::content(x) + res <- drop_request(move_url, dtoken, body = args) if (!verbose) { - message(sprintf("%s moved to %s", from_path, res$metadata$path_lower)) + cli::cli_alert_success("{from_path} moved to {res$metadata$path_lower}") invisible(res) } else { pretty_lists(res) invisible(res) } } else { - stop("File or folder not found \n") + cli::cli_abort("File or folder not found") } } @@ -174,21 +150,13 @@ drop_move <- #' @template token #' @export #' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-delete_v2}{API documentation} -drop_delete <- - function (path = NULL, +drop_delete <- function(path = NULL, verbose = FALSE, dtoken = get_dropbox_token()) { create_url <- "https://api.dropboxapi.com/2/files/delete_v2" if (drop_exists(path)) { path <- add_slashes(path) - x <- - httr::POST( - create_url, - httr::config(token = dtoken), - body = list(path = path), - encode = "json" - ) - res <- httr::content(x) + res <- drop_request(create_url, dtoken, body = list(path = path)) if (verbose) { res @@ -197,7 +165,7 @@ drop_delete <- } } else { # Since file/folder wasn't found, report a stop error - stop("File not found on current path") + cli::cli_abort("File not found on current path") } } @@ -215,8 +183,7 @@ drop_delete <- #' @examples \dontrun{ #' drop_create(path = "foobar") #'} -drop_create <- - function(path = NULL, +drop_create <- function(path = NULL, autorename = FALSE, verbose = FALSE, dtoken = get_dropbox_token()) { @@ -227,34 +194,26 @@ drop_create <- create_url <- "https://api.dropboxapi.com/2/files/create_folder_v2" path <- add_slashes(path) - x <- - httr::POST( - create_url, - config(token = dtoken), - body = list(path = path, autorename = autorename), - encode = "json" - ) - results <- httr::content(x) + results <- drop_request( + create_url, dtoken, + body = list(path = path, autorename = autorename) + ) if (verbose) { pretty_lists(results) invisible(results) } else { - message(sprintf("Folder %s created successfully \n", results$metadata$path_lower)) + cli::cli_alert_success("Folder {results$metadata$path_lower} created successfully") invisible(results) } invisible(results) } else { - stop("Folder already exists") + cli::cli_abort("Folder already exists") } } - - - - #' Checks to see if a file/folder exists on Dropbox #' #' Since many file operations such as move, copy, delete and history can only act @@ -289,8 +248,7 @@ drop_exists <- function(path = NULL, dtoken = get_dropbox_token()) { # # Other solution is to use purrr::safely to trap the error and return FALSE # (TODO): Explore uninteded consequence of this. - safe_dir_check <- - purrr::safely(drop_get_metadata, otherwise = FALSE, quiet = TRUE) + safe_dir_check <- purrr::safely(drop_get_metadata, otherwise = FALSE, quiet = TRUE) dir_listing <- safe_dir_check(path = path, dtoken = dtoken) # browser() if (length(dir_listing$result) == 1) { @@ -329,8 +287,7 @@ drop_is_folder <- function(x, dtoken = get_dropbox_token()) { #' Checks on a name and returns file, folder, or FALSE for dropbox status #' @noRd drop_type <- function(x, dtoken = get_dropbox_token()) { - safe_meta <- - purrr::safely(drop_get_metadata, otherwise = FALSE, quiet = TRUE) + safe_meta <- purrr::safely(drop_get_metadata, otherwise = FALSE, quiet = TRUE) x <- safe_meta(x) if (length(x$result) == 1 && !x$result) { FALSE @@ -338,3 +295,148 @@ drop_type <- function(x, dtoken = get_dropbox_token()) { x$result$.tag } } + + +#' Copy multiple files or folders in a single batch request. +#' +#' More efficient than calling \code{\link{drop_copy}} repeatedly for large +#' numbers of files. The function blocks until the batch job completes. +#' +#' @param entries A list of named lists, each with \code{from_path} and +#' \code{to_path} character elements. +#' @param autorename If \code{TRUE}, the Dropbox server will try to autorename +#' files to avoid conflicts. Default \code{FALSE}. +#' @template token +#' +#' @return A list of per-entry results returned by the Dropbox API. +#' +#' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-copy_batch_v2}{API documentation} +#' +#' @export +#' +#' @examples \dontrun{ +#' entries <- list( +#' list(from_path = "/file1.csv", to_path = "/backup/file1.csv"), +#' list(from_path = "/file2.csv", to_path = "/backup/file2.csv") +#' ) +#' drop_copy_batch(entries) +#' } +drop_copy_batch <- function(entries, autorename = FALSE, + dtoken = get_dropbox_token()) { + url_start <- "https://api.dropboxapi.com/2/files/copy_batch_v2" + url_check <- "https://api.dropboxapi.com/2/files/copy_batch/check_v2" + + res <- drop_request(url_start, dtoken, + body = list(entries = entries, autorename = autorename)) + + .poll_async_job(res, url_check, dtoken) +} + + +#' Move multiple files or folders in a single batch request. +#' +#' More efficient than calling \code{\link{drop_move}} repeatedly for large +#' numbers of files. The function blocks until the batch job completes. +#' +#' @param entries A list of named lists, each with \code{from_path} and +#' \code{to_path} character elements. +#' @param autorename If \code{TRUE}, the Dropbox server will try to autorename +#' files to avoid conflicts. Default \code{FALSE}. +#' @param allow_ownership_transfer If \code{TRUE}, allow moves that would +#' result in ownership transfer. Default \code{FALSE}. +#' @template token +#' +#' @return A list of per-entry results returned by the Dropbox API. +#' +#' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-move_batch_v2}{API documentation} +#' +#' @export +#' +#' @examples \dontrun{ +#' entries <- list( +#' list(from_path = "/file1.csv", to_path = "/archive/file1.csv") +#' ) +#' drop_move_batch(entries) +#' } +drop_move_batch <- function(entries, autorename = FALSE, + allow_ownership_transfer = FALSE, + dtoken = get_dropbox_token()) { + url_start <- "https://api.dropboxapi.com/2/files/move_batch_v2" + url_check <- "https://api.dropboxapi.com/2/files/move_batch/check_v2" + + res <- drop_request(url_start, dtoken, + body = list( + entries = entries, + autorename = autorename, + allow_ownership_transfer = allow_ownership_transfer + )) + + .poll_async_job(res, url_check, dtoken) +} + + +#' Delete multiple files or folders in a single batch request. +#' +#' More efficient than calling \code{\link{drop_delete}} repeatedly for large +#' numbers of files. The function blocks until the batch job completes. +#' +#' @param entries A list of named lists, each with a \code{path} character +#' element specifying the Dropbox path to delete. +#' @template token +#' +#' @return A list of per-entry results returned by the Dropbox API. +#' +#' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-delete_batch}{API documentation} +#' +#' @export +#' +#' @examples \dontrun{ +#' entries <- list( +#' list(path = "/old_file1.csv"), +#' list(path = "/old_file2.csv") +#' ) +#' drop_delete_batch(entries) +#' } +drop_delete_batch <- function(entries, dtoken = get_dropbox_token()) { + url_start <- "https://api.dropboxapi.com/2/files/delete_batch" + url_check <- "https://api.dropboxapi.com/2/files/delete_batch/check" + + res <- drop_request(url_start, dtoken, body = list(entries = entries)) + + .poll_async_job(res, url_check, dtoken) +} + + +#' Poll an async Dropbox job until it completes. +#' +#' @param res Initial response from a batch/async endpoint. +#' @param check_url URL of the corresponding check endpoint. +#' @param dtoken ****** string. +#' @param interval Polling interval in seconds. Default 2. +#' +#' @return The completed job result list. +#' +#' @noRd +.poll_async_job <- function(res, check_url, dtoken, interval = 2) { + # If already complete, return immediately + if (!is.null(res[[".tag"]]) && res[[".tag"]] == "complete") { + return(res$entries) + } + + async_job_id <- res$async_job_id + if (is.null(async_job_id)) return(res) + + repeat { + Sys.sleep(interval) + status <- drop_request(check_url, dtoken, + body = list(async_job_id = async_job_id)) + tag <- status[[".tag"]] + if (!is.null(tag) && tag == "complete") { + return(status$entries) + } + if (!is.null(tag) && tag == "failed") { + cli::cli_abort("Batch job failed: {status$failure}") + } + # tag == "in_progress": keep polling + } +} diff --git a/R/drop_get_metadata.R b/R/drop_get_metadata.R index 23f5a87..b30a27b 100644 --- a/R/drop_get_metadata.R +++ b/R/drop_get_metadata.R @@ -3,7 +3,7 @@ #' Details vary by input and args. #' #' @param path Path to a file or folder on Dropbox. Can also be an ID ("id:...") or revision ("rev:..."). -#' @param include_media_info If TRUE, additional metadata for photo or video is returns. Defaults to FALSE. +#' @param include_media_info If TRUE, additional metadata for photo or video is returned. Defaults to FALSE. #' @param include_deleted If TRUE, metadata will be returned for a deleted file, otherwise error. Defaults to FALSE. #' @param include_has_explicit_shared_members If TRUE, the results will include a flag for each file indicating whether or not that file has any explicit members. Defaults to FALSE. #' @template token @@ -25,19 +25,10 @@ drop_get_metadata <- function( if (!grepl("^(id|rev):", path)) path <- add_slashes(path) - req <- httr::POST( - url = url, - httr::config(token = dtoken), - body = list( - path = path, - include_media_info = include_media_info, - include_deleted = include_deleted, - include_has_explicit_shared_members = include_has_explicit_shared_members - ), - encode = "json" - ) - - httr::stop_for_status(req) - - httr::content(req) + drop_request(url, dtoken, body = list( + path = path, + include_media_info = include_media_info, + include_deleted = include_deleted, + include_has_explicit_shared_members = include_has_explicit_shared_members + )) } diff --git a/R/drop_history.R b/R/drop_history.R index 75e8bbd..c455e79 100644 --- a/R/drop_history.R +++ b/R/drop_history.R @@ -48,17 +48,41 @@ drop_list_revisions <- function(path, limit = 10, dtoken = get_dropbox_token()) url <- "https://api.dropboxapi.com/2/files/list_revisions" - req <- httr::POST( - url = url, - httr::config(token = dtoken), - body = list( - path = add_slashes(path), - limit = limit - ), - encode = "json" - ) + drop_request(url, dtoken, body = list( + path = add_slashes(path), + limit = limit + )) +} + + +#' Restore a file to a specific revision. +#' +#' Reverts a file on Dropbox to the content it had at a given revision. Use +#' \code{\link{drop_history}} to find available revision IDs. +#' +#' @param path Path to the file on Dropbox. +#' @param rev The revision identifier string (e.g. \code{"a1c10ce0dd78"}) to +#' restore to. Revision IDs are returned in the \code{rev} column of +#' \code{\link{drop_history}}. +#' @template token +#' +#' @return A list of file metadata reflecting the restored version. +#' +#' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-restore}{API documentation} +#' +#' @export +#' +#' @examples \dontrun{ +#' history <- drop_history("report.csv") +#' # restore to the second most recent version +#' drop_restore("report.csv", rev = history$rev[2]) +#' } +drop_restore <- function(path, rev, dtoken = get_dropbox_token()) { - httr::stop_for_status(req) + url <- "https://api.dropboxapi.com/2/files/restore" - httr::content(req) + drop_request(url, dtoken, body = list( + path = add_slashes(path), + rev = rev + )) } diff --git a/R/drop_media.R b/R/drop_media.R index f69353b..9b203b2 100644 --- a/R/drop_media.R +++ b/R/drop_media.R @@ -1,5 +1,4 @@ - #'Returns a link directly to a file. #' #'Similar to \code{drop_shared}. The difference is that this bypasses the @@ -16,16 +15,91 @@ #' drop_media('Public/gifs/duck_rabbit.gif') #'} drop_media <- function(path = NULL, dtoken = get_dropbox_token()) { - # assertive::assert_is_not_null(path) assertthat::assert_that(!is.null(path)) if(drop_exists(path)) { media_url <- "https://api.dropbox.com/2/files/get_temporary_link" path <- add_slashes(path) - res <- POST(media_url, body = list(path = path), httr::config(token = dtoken), encode = "json") - content(res) + drop_request(media_url, dtoken, body = list(path = path)) } else { - stop("File not found \n") - FALSE + cli::cli_abort("File not found") + } +} + + +#' Retrieve a thumbnail for an image file on Dropbox. +#' +#' Downloads a JPEG or PNG thumbnail for a photo or video stored in Dropbox. +#' Supported formats: jpg, png, tiff, tif, gif, webp, ppm, bmp. +#' +#' @param path Path to the image file on Dropbox. +#' @param local_path Local path to save the thumbnail. If \code{NULL} +#' (default), a temporary file is created and its path is returned. +#' @param format Thumbnail format: \code{"jpeg"} (default) or \code{"png"}. +#' @param size Thumbnail size preset. One of \code{"w32h32"}, +#' \code{"w64h64"}, \code{"w128h128"}, \code{"w256h256"} (default), +#' \code{"w480h320"}, \code{"w640h480"}, \code{"w960h640"}, +#' \code{"w1024h768"}, \code{"w2048h1536"}. +#' @param overwrite If \code{TRUE}, overwrite an existing local file. +#' Defaults to \code{FALSE}. +#' @template token +#' +#' @return Path to the saved thumbnail file, invisibly. +#' +#' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail_v2}{API documentation} +#' +#' @export +#' +#' @examples \dontrun{ +#' thumb_path <- drop_get_thumbnail("photos/vacation.jpg") +#' # display in R (requires the 'magick' package) +#' # magick::image_read(thumb_path) +#' } +drop_get_thumbnail <- function(path, + local_path = NULL, + format = "jpeg", + size = "w256h256", + overwrite = FALSE, + dtoken = get_dropbox_token()) { + + valid_formats <- c("jpeg", "png") + valid_sizes <- c("w32h32", "w64h64", "w128h128", "w256h256", + "w480h320", "w640h480", "w960h640", + "w1024h768", "w2048h1536") + assertthat::assert_that(format %in% valid_formats) + assertthat::assert_that(size %in% valid_sizes) + + if (!grepl("^(id|rev):", path)) path <- add_slashes(path) + + if (is.null(local_path)) { + local_path <- tempfile(fileext = paste0(".", format)) } + + if (file.exists(local_path) && !overwrite) { + cli::cli_abort( + "Local file {.file {local_path}} already exists. Set {.code overwrite = TRUE} to replace it." + ) + } + + url <- "https://content.dropboxapi.com/2/files/get_thumbnail_v2" + + arg_json <- jsonlite::toJSON( + list( + resource = list( + ".tag" = "path", + path = path + ), + format = list(".tag" = format), + size = list(".tag" = size) + ), + auto_unbox = TRUE + ) + + req <- httr2::request(url) + req <- httr2::req_auth_bearer_token(req, resolve_token(dtoken)) + req <- httr2::req_headers(req, `Dropbox-API-Arg` = arg_json) + req <- httr2::req_body_raw(req, "", type = "application/octet-stream") + httr2::req_perform(req, path = local_path) + + invisible(local_path) } diff --git a/R/drop_search.R b/R/drop_search.R index 9444d0b..f9b6acc 100644 --- a/R/drop_search.R +++ b/R/drop_search.R @@ -1,62 +1,96 @@ - -#'Returns metadata for all files and folders whose filename contains the given -#'search string as a substring. -#' -#'@param query The search string. This string is split (on spaces) into -#' individual words. Files and folders will be returned if they contain all -#' words in the search string. -#'@template path -#'@param start The starting index within the search results (used for paging). -#' The default for this field is 0 -#'@param max_results The maximum number of search results to return. The default -#' for this field is 100. -#'@param mode Mode can take the option of filename, filename_and_content, or search deleted files with deleted_filename -#'@template token -#' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-search}{API documentation} -#'@export +#' Search for files and folders on Dropbox. +#' +#' Returns metadata for all files and folders whose filename (or content, if +#' enabled) matches the given search string. Uses the Dropbox +#' \code{files/search_v2} API endpoint which supports richer filtering options +#' than the original search endpoint. +#' +#' @param query The search string. Split on spaces into individual words; +#' results are returned if they contain all words. +#' @param path Dropbox path to restrict the search to. Defaults to the entire +#' Dropbox (\code{""}). +#' @param max_results The maximum number of search results to return. Defaults +#' to 100. +#' @param file_status Filter by file status: \code{"active"} (default) returns +#' only existing files; \code{"deleted"} returns only deleted files. +#' @param filename_only If \code{TRUE}, restricts the search to filenames only +#' (faster). Defaults to \code{FALSE}. +#' @param file_extensions Optional character vector of file extensions to +#' restrict results to (e.g., \code{c("pdf", "docx")}). +#' @param file_categories Optional character vector of file category tags to +#' restrict results to. Valid values: \code{"image"}, \code{"document"}, +#' \code{"pdf"}, \code{"spreadsheet"}, \code{"presentation"}, \code{"audio"}, +#' \code{"video"}, \code{"folder"}, \code{"paper"}, \code{"others"}. +#' @template token +#' +#' @return A list as returned by the Dropbox API, with a \code{matches} element +#' (list of match objects) and an optional \code{cursor} for pagination. +#' +#' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-search_v2}{API documentation} +#' +#' @export +#' #' @examples \dontrun{ -#' # If you know me, you know why this query exists -#' drop_search('gif') %>% select(path, is_dir, mime_type) -#'} +#' # simple filename search +#' results <- drop_search("report") +#' results$matches[[1]]$metadata$metadata$name +#' +#' # search only PDF files +#' drop_search("budget", file_extensions = "pdf") +#' +#' # search images only +#' drop_search("vacation", file_categories = "image") +#' } drop_search <- function(query, path = "", - start = 0, max_results = 100, - mode = "filename", + file_status = "active", + filename_only = FALSE, + file_extensions = NULL, + file_categories = NULL, dtoken = get_dropbox_token()) { - available_modes <- - c("filename", "filename_and_content", "deleted_filename") - # assertive::assert_any_are_matching_fixed(available_modes, mode) - assertthat::assert_that(mode %in% available_modes) - - # A search cannot have a negative start index and a negative max_results - #assertive::assert_all_are_non_negative(start, max_results) - assertthat::assert_that(start >= 0, - max_results >= 0) - - args <- drop_compact( - list( - query = query, - path = path, - start = as.integer(start), - max_results = as.integer(max_results), - mode = mode - ) - ) - - search_url <- "https://api.dropboxapi.com/2/files/search" - res <- - httr::POST(search_url, - body = args, - httr::config(token = dtoken), - encode = "json") - httr::stop_for_status(res) - httr::content(res) - # TODO - # Need to do a verbose return but also print a nice data.frame - # One way to do that is with purrr::flatten - # e.g. purrr::flatten(results$matches) - # But, do we want purrr as another import??? + + valid_file_status <- c("active", "deleted") + assertthat::assert_that(file_status %in% valid_file_status) + assertthat::assert_that(max_results >= 0) + + # build options list, dropping NULLs + options <- drop_compact(list( + path = if (nchar(path) > 0) add_slashes(path) else NULL, + max_results = as.integer(max_results), + file_status = list(".tag" = file_status), + filename_only = filename_only, + file_extensions = if (!is.null(file_extensions)) as.list(file_extensions) else NULL, + file_categories = if (!is.null(file_categories)) + lapply(file_categories, function(x) list(".tag" = x)) + else NULL + )) + + search_url <- "https://api.dropboxapi.com/2/files/search_v2" + drop_request(search_url, dtoken, + body = list(query = query, options = options)) +} + + +#' Continue a paginated search begun with \code{drop_search}. +#' +#' @param cursor A cursor string returned in a previous \code{drop_search} call. +#' @template token +#' +#' @return Same structure as \code{\link{drop_search}}. +#' +#' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-search-continue_v2}{API documentation} +#' +#' @export +#' +#' @examples \dontrun{ +#' first_page <- drop_search("report", max_results = 20) +#' second_page <- drop_search_continue(first_page$cursor) +#' } +drop_search_continue <- function(cursor, dtoken = get_dropbox_token()) { + + url <- "https://api.dropboxapi.com/2/files/search/continue_v2" + drop_request(url, dtoken, body = list(cursor = cursor)) } diff --git a/R/drop_shared.R b/R/drop_shared.R index e4036a5..9da6e4b 100644 --- a/R/drop_shared.R +++ b/R/drop_shared.R @@ -24,23 +24,11 @@ drop_share <- function(path = NULL, link_password = NULL, expires = NULL, dtoken = get_dropbox_token()) { - # This is a list because in the POST call it becomes a nested JSON. - # A sample response looks like this: - - # curl -X POST https://api.dropboxapi.com/2/sharing/create_shared_link_with_settings \ - # --header "Authorization: Bearer " \ - # --header "Content-Type: application/json" \ - # --data "{\"path\": \"/Prime_Numbers.txt\",\"settings\": {\"requested_visibility\": \"public\"} # Check to see if only supported modes are specified visibilities <- c("public", "team_only", "password") - # assertive::assert_any_are_matching_fixed(visibilities, requested_visibility) assertthat::assert_that(requested_visibility %in% visibilities) - # TODO - # Once the new drop_exists is done, one must check to see if a file/folder - # exists on Dropbox before proceeding - path <- add_slashes(path) settings <- drop_compact( @@ -50,20 +38,12 @@ drop_share <- function(path = NULL, expires = expires ) ) - # TODO: Check to see if this is necessary when we have encode to json below + share_url <- "https://api.dropboxapi.com/2/sharing/create_shared_link_with_settings" - req <- httr::POST( - url = share_url, - httr::config(token = dtoken), - body = list(path = path, settings = settings), - encode = "json" - ) - # stopping for status otherwise content fails - httr::stop_for_status(req) - response <- httr::content(req) - response + drop_request(share_url, dtoken, + body = list(path = path, settings = settings)) } #' List all shared links @@ -82,16 +62,76 @@ drop_list_shared_links <- function(verbose = TRUE, dtoken = get_dropbox_token()) { shared_links_url <- "https://api.dropboxapi.com/2/sharing/list_shared_links" - res <- - httr::POST(shared_links_url, httr::config(token = dtoken), encode = "json") - httr::stop_for_status(res) - z <- httr::content(res) + z <- drop_request(shared_links_url, dtoken) if (verbose) { invisible(z) pretty_lists(z) } else { invisible(z) - # TODO - # Clean up the verbose and non-verbose options } } + + +#' Download a file from Dropbox via a shared link. +#' +#' Retrieves the content of a file identified by a shared link URL (rather than +#' a Dropbox file path). This is useful when you have received a shared link +#' from another user. +#' +#' @param url The shared link URL pointing to the file. +#' @param local_path Path to save the downloaded file to. If \code{NULL} +#' (default), the file is saved to the working directory using the filename +#' derived from the link. If a directory, the file is placed inside it. +#' @param overwrite If \code{TRUE}, overwrite an existing local file. +#' Defaults to \code{FALSE}. +#' @param path Optional Dropbox path within the shared folder to a specific +#' sub-file/folder. Leave as \code{NULL} for root of the shared link. +#' @template token +#' +#' @return \code{TRUE} invisibly on success. +#' +#' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#sharing-get_shared_link_file}{API documentation} +#' +#' @export +#' +#' @examples \dontrun{ +#' drop_get_shared_link_file( +#' url = "https://www.dropbox.com/s/xxxx/example.csv?dl=0", +#' local_path = "example.csv" +#' ) +#' } +drop_get_shared_link_file <- function(url, + local_path = NULL, + overwrite = FALSE, + path = NULL, + dtoken = get_dropbox_token()) { + + download_url <- "https://content.dropboxapi.com/2/sharing/get_shared_link_file" + + arg <- drop_compact(list(url = url, path = path)) + arg_json <- jsonlite::toJSON(arg, auto_unbox = TRUE) + + # derive local filename from the shared URL if not provided + if (is.null(local_path)) { + local_path <- basename(gsub("\\?.*$", "", url)) + if (nchar(local_path) == 0) local_path <- "dropbox_download" + } else if (dir.exists(local_path)) { + fname <- basename(gsub("\\?.*$", "", url)) + if (nchar(fname) == 0) fname <- "dropbox_download" + local_path <- file.path(local_path, fname) + } + + if (file.exists(local_path) && !overwrite) { + cli::cli_abort( + "Local file {.file {local_path}} already exists. Set {.code overwrite = TRUE} to replace it." + ) + } + + req <- httr2::request(download_url) + req <- httr2::req_auth_bearer_token(req, resolve_token(dtoken)) + req <- httr2::req_headers(req, `Dropbox-API-Arg` = arg_json) + req <- httr2::req_body_raw(req, "", type = "application/json") + httr2::req_perform(req, path = local_path) + + invisible(TRUE) +} diff --git a/R/drop_upload.R b/R/drop_upload.R index a295233..978b07e 100644 --- a/R/drop_upload.R +++ b/R/drop_upload.R @@ -1,29 +1,32 @@ +# Chunk size for session-based uploads (150 MB) +.UPLOAD_CHUNK_SIZE <- 150L * 1024L * 1024L +# Files larger than this threshold use the session-based chunked upload +.UPLOAD_THRESHOLD <- 150L * 1024L * 1024L - - - -#'Uploads a file to Dropbox. +#' Uploads a file to Dropbox. +#' +#' This function will allow you to write files of any size to Dropbox. Files +#' larger than 150 MB are automatically uploaded in chunks using the Dropbox +#' upload session API. #' -#'This function will allow you to write files of any size to Dropbox(even ones -#'that cannot be read into memory) by uploading them in chunks. +#' @param file Relative path to local file. +#' @param path The relative path on Dropbox where the file should get uploaded. +#' @param mode Upload mode: \code{"overwrite"} (default) will always overwrite +#' an existing file; \code{"add"} will not overwrite and instead create a +#' renamed copy on conflict; \code{"update"} requires a \code{rev} argument. +#' @param autorename If \code{TRUE} (default), the file being uploaded will be +#' automatically renamed to avoid conflicts when using \code{mode = "add"}. +#' @param mute Set to \code{TRUE} to suppress desktop/mobile notifications. +#' Defaults to \code{FALSE}. +#' @template verbose +#' @template token +#' +#' @return Dropbox file metadata list, invisibly. #' -#'@param file Relative path to local file. -#'@param path The relative path on Dropbox where the file should get uploaded. -#'@param mode - "add" - will not overwrite an existing file in case of a -#' conflict. With this mode, when a a duplicate file.txt is uploaded, it will -#' become file (2).txt. - "overwrite" will always overwrite a file - -#'@param autorename This logical determines what happens when there is a -#' conflict. If true, the file being uploaded will be automatically renamed to -#' avoid the conflict. (For example, test.txt might be automatically renamed to -#' test (1).txt.) The new name can be obtained from the returned metadata. If -#' false, the call will fail with a 409 (Conflict) response code. The default is `TRUE` -#'@param mute Set to FALSE to prevent a notification trigger on the desktop and -#' mobile apps #' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-upload}{API documentation} -#'@template verbose -#'@template token -#'@export +#' @export +#' #' @examples \dontrun{ #' write.csv(mtcars, file = "mtt.csv") #' drop_upload("mtt.csv") @@ -35,57 +38,197 @@ drop_upload <- function(file, mute = FALSE, verbose = FALSE, dtoken = get_dropbox_token()) { - put_url <- "https://content.dropboxapi.com/2/files/upload" - # Check that object exists locally before adding slashes - # assertive::assert_all_are_existing_files(file, severity = ("stop")) assertthat::assert_that(file.exists(file)) - # Check to see if only supported modes are specified standard_modes <- c("overwrite", "add", "update") - # assertive::assert_any_are_matching_fixed(standard_modes, mode) assertthat::assert_that(mode %in% standard_modes) # Dropbox API requires a / before an object name. if (is.null(path)) { - path <- add_slashes(basename(file)) + dest_path <- add_slashes(basename(file)) } else { - path <- paste0("/", strip_slashes(path), "/", basename(file)) + dest_path <- paste0("/", strip_slashes(path), "/", basename(file)) } - req <- httr::POST( - url = put_url, - httr::config(token = dtoken), - httr::add_headers("Dropbox-API-Arg" = jsonlite::toJSON( - list( - path = path, - mode = mode, - autorename = autorename, - mute = mute - ), - auto_unbox = TRUE - )), - body = httr::upload_file(file, type = "application/octet-stream") - # application/octet-stream is to save to a file to disk and not worry about - # what application/function might handle it. This lets another application - # figure out how to read it. So for this purpose we're totally ok. - ) - httr::stop_for_status(req) - response <- httr::content(req) + file_size <- file.size(file) + + if (file_size <= .UPLOAD_THRESHOLD) { + response <- .upload_small(file, dest_path, mode, autorename, mute, dtoken) + } else { + response <- .upload_chunked(file, dest_path, mode, autorename, mute, dtoken) + } if (verbose) { pretty_lists(response) invisible(response) } else { invisible(response) - message( - sprintf( - 'File %s uploaded as %s successfully at %s', - file, - response$path_display, - response$server_modified - ) + cli::cli_alert_success( + "File {.file {file}} uploaded as {response$path_display} successfully at {response$server_modified}" ) } +} + + +#' Single-request upload for files <= 150 MB. +#' @noRd +.upload_small <- function(file, dest_path, mode, autorename, mute, dtoken) { + + put_url <- "https://content.dropboxapi.com/2/files/upload" + + arg_json <- jsonlite::toJSON( + list( + path = dest_path, + mode = mode, + autorename = autorename, + mute = mute + ), + auto_unbox = TRUE + ) + + req <- httr2::request(put_url) + req <- httr2::req_auth_bearer_token(req, resolve_token(dtoken)) + req <- httr2::req_headers(req, `Dropbox-API-Arg` = arg_json) + req <- httr2::req_body_file(req, file, type = "application/octet-stream") + resp <- httr2::req_perform(req) + httr2::resp_body_json(resp) +} + + +#' Session-based chunked upload for files > 150 MB. +#' +#' Uses the three-step upload session API: +#' 1. \code{upload_session/start} – open a session +#' 2. \code{upload_session/append_v2} – upload all but the last chunk +#' 3. \code{upload_session/finish} – upload last chunk and commit +#' +#' @noRd +.upload_chunked <- function(file, dest_path, mode, autorename, mute, dtoken) { + + start_url <- "https://content.dropboxapi.com/2/files/upload_session/start" + append_url <- "https://content.dropboxapi.com/2/files/upload_session/append_v2" + finish_url <- "https://content.dropboxapi.com/2/files/upload_session/finish" + + file_size <- file.size(file) + con <- file(file, "rb") + on.exit(close(con), add = TRUE) + + offset <- 0L + # ---- step 1: start session with first chunk ---- + first_chunk <- readBin(con, raw(), .UPLOAD_CHUNK_SIZE) + offset <- length(first_chunk) + + start_arg <- jsonlite::toJSON(list(close = FALSE), auto_unbox = TRUE) + req <- httr2::request(start_url) + req <- httr2::req_auth_bearer_token(req, resolve_token(dtoken)) + req <- httr2::req_headers(req, `Dropbox-API-Arg` = start_arg) + req <- httr2::req_body_raw(req, first_chunk, type = "application/octet-stream") + resp <- httr2::req_perform(req) + session_id <- httr2::resp_body_json(resp)$session_id + + # ---- step 2: append middle chunks ---- + while (offset + .UPLOAD_CHUNK_SIZE < file_size) { + chunk <- readBin(con, raw(), .UPLOAD_CHUNK_SIZE) + if (length(chunk) == 0L) break + + append_arg <- jsonlite::toJSON( + list(cursor = list(session_id = session_id, + offset = offset), + close = FALSE), + auto_unbox = TRUE + ) + req <- httr2::request(append_url) + req <- httr2::req_auth_bearer_token(req, resolve_token(dtoken)) + req <- httr2::req_headers(req, `Dropbox-API-Arg` = append_arg) + req <- httr2::req_body_raw(req, chunk, type = "application/octet-stream") + httr2::req_perform(req) + offset <- offset + length(chunk) + } + + # ---- step 3: finish with the last chunk ---- + last_chunk <- readBin(con, raw(), .UPLOAD_CHUNK_SIZE) + + finish_arg <- jsonlite::toJSON( + list( + cursor = list(session_id = session_id, + offset = offset), + commit = list( + path = dest_path, + mode = mode, + autorename = autorename, + mute = mute + ) + ), + auto_unbox = TRUE + ) + req <- httr2::request(finish_url) + req <- httr2::req_auth_bearer_token(req, resolve_token(dtoken)) + req <- httr2::req_headers(req, `Dropbox-API-Arg` = finish_arg) + req <- httr2::req_body_raw(req, last_chunk, type = "application/octet-stream") + resp <- httr2::req_perform(req) + httr2::resp_body_json(resp) +} + + +#' Save a remote URL directly to Dropbox. +#' +#' Instructs Dropbox to download a file from the given URL and save it to your +#' Dropbox without you needing to download it locally first. Useful for +#' automated pipelines that source data from the web. +#' +#' @param path Destination path in Dropbox (including filename). +#' @param url The URL of the file to download into Dropbox. +#' @param poll If \code{TRUE} (default), block until the save operation +#' completes and return the saved file metadata. If \code{FALSE}, return +#' immediately with the async job ID. +#' @param interval Polling interval in seconds when \code{poll = TRUE}. +#' Default \code{2}. +#' @template token +#' +#' @return If \code{poll = TRUE}, a file metadata list for the saved file. +#' If \code{poll = FALSE}, a list with \code{async_job_id} (or the completed +#' metadata if the server finished synchronously). +#' +#' @references \href{https://www.dropbox.com/developers/documentation/http/documentation#files-save_url}{API documentation} +#' +#' @export +#' +#' @examples \dontrun{ +#' drop_save_url( +#' path = "/data/penguins.csv", +#' url = "https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv" +#' ) +#' } +drop_save_url <- function(path, url, poll = TRUE, interval = 2, + dtoken = get_dropbox_token()) { + + save_url <- "https://api.dropboxapi.com/2/files/save_url" + check_url <- "https://api.dropboxapi.com/2/files/save_url/check_job_status" + + path <- add_slashes(path) + + res <- drop_request(save_url, dtoken, + body = list(path = path, url = url)) + + # Dropbox may return immediately with 'complete' or give an async job id + if (!is.null(res[[".tag"]]) && res[[".tag"]] == "complete") { + return(res) + } + if (!poll) return(res) + + # Poll until done + async_job_id <- res$async_job_id + if (is.null(async_job_id)) return(res) + + repeat { + Sys.sleep(interval) + status <- drop_request(check_url, dtoken, + body = list(async_job_id = async_job_id)) + tag <- status[[".tag"]] + if (!is.null(tag) && tag == "complete") return(status) + if (!is.null(tag) && tag == "failed") cli::cli_abort("save_url job failed: {status$failed}") + # in_progress: keep polling + } } diff --git a/R/drop_utils.R b/R/drop_utils.R index cad85a1..5dcdf3c 100644 --- a/R/drop_utils.R +++ b/R/drop_utils.R @@ -17,15 +17,15 @@ drop_compact <- function(l) Filter(Negate(is.null), l) #' A small function to strip trailing slashes from a path #' @noRd strip_slashes <- function(path) { - if (length(path) && grepl("/$", path)) { - path <- substr(path, 1, nchar(path) - 1) - } - # Also remove leading slashes + if (length(path) && grepl("/$", path)) { + path <- substr(path, 1, nchar(path) - 1) + } + # Also remove leading slashes if (length(path) && grepl("^/", path)) { path <- substr(path, 2, nchar(path)) } - path + path } #' A small function to add a prefix slash @@ -37,156 +37,87 @@ add_slashes <- function(path) { path } +#' Internal helper: build, perform, and parse a standard JSON Dropbox API call. +#' +#' Constructs an httr2 request with bearer-token authentication, an optional +#' JSON body, and performs it. HTTP errors are raised automatically by httr2. +#' +#' @param url Full Dropbox API endpoint URL. +#' @param token ****** token string (from \code{get_dropbox_token()}). +#' @param body Named list to serialise as the JSON request body, or +#' \code{NULL} for a body-less POST (some Dropbox endpoints +#' require an empty body POST). +#' +#' @return Parsed JSON response as a named R list. +#' +#' Extract the bearer token string from a token argument. +#' +#' Accepts either a plain character string (as returned by +#' \code{get_dropbox_token}) or an \code{httr2_token} / list object (as +#' returned by \code{drop_auth}) and always returns the access-token string. +#' +#' @noRd +resolve_token <- function(token) { + if (is.character(token)) return(token) + if (is.list(token) && !is.null(token$access_token)) return(token$access_token) + cli::cli_abort("Invalid token: supply the output of {.fn drop_auth} or {.fn get_dropbox_token}.") +} + +drop_request <- function(url, token, body = NULL) { + req <- httr2::request(url) + req <- httr2::req_auth_bearer_token(req, resolve_token(token)) + if (!is.null(body)) { + req <- httr2::req_body_json(req, body) + } else { + # Dropbox expects POST; send empty body + req <- httr2::req_body_raw(req, "", type = "application/json") + } + resp <- httr2::req_perform(req) + httr2::resp_body_json(resp) +} + #' @noRd # This is an internal function to linearize lists # Source: https://gist.github.com/mrdwab/4205477 # Author page (currently unreachable): https://sites.google.com/site/akhilsbehl/geekspace/articles/r/linearize_nested_lists_in # Original Author: Akhil S Bhel # Notes: Current author could not be reached and original site () appears defunct. Copyright remains with original author -LinearizeNestedList <- function(NList, LinearizeDataFrames=FALSE, - NameSep="/", ForceNames=FALSE) { - # LinearizeNestedList: - # - # https://sites.google.com/site/akhilsbehl/geekspace/ - # articles/r/linearize_nested_lists_in_r - # - # Akhil S Bhel - # - # Implements a recursive algorithm to linearize nested lists upto any - # arbitrary level of nesting (limited by R's allowance for recursion-depth). - # By linearization, it is meant to bring all list branches emanating from - # any nth-nested trunk upto the top-level trunk s.t. the return value is a - # simple non-nested list having all branches emanating from this top-level - # branch. - # - # Since dataframes are essentially lists a boolean option is provided to - # switch on/off the linearization of dataframes. This has been found - # desirable in the author's experience. - # - # Also, one'd typically want to preserve names in the lists in a way as to - # clearly denote the association of any list element to it's nth-level - # history. As such we provide a clean and simple method of preserving names - # information of list elements. The names at any level of nesting are - # appended to the names of all preceding trunks using the `NameSep` option - # string as the seperator. The default `/` has been chosen to mimic the unix - # tradition of filesystem hierarchies. The default behavior works with - # existing names at any n-th level trunk, if found; otherwise, coerces simple - # numeric names corresponding to the position of a list element on the - # nth-trunk. Note, however, that this naming pattern does not ensure unique - # names for all elements in the resulting list. If the nested lists had - # non-unique names in a trunk the same would be reflected in the final list. - # Also, note that the function does not at all handle cases where `some` - # names are missing and some are not. - # - # Clearly, preserving the n-level hierarchy of branches in the element names - # may lead to names that are too long. Often, only the depth of a list - # element may only be important. To deal with this possibility a boolean - # option called `ForceNames` has been provided. ForceNames shall drop all - # original names in the lists and coerce simple numeric names which simply - # indicate the position of an element at the nth-level trunk as well as all - # preceding trunk numbers. - # - # Returns: - # LinearList: Named list. - # - # Sanity checks: - # - stopifnot(is.character(NameSep), length(NameSep) == 1) - stopifnot(is.logical(LinearizeDataFrames), length(LinearizeDataFrames) == 1) - stopifnot(is.logical(ForceNames), length(ForceNames) == 1) - if (! is.list(NList)) return(NList) - # - # If no names on the top-level list coerce names. Recursion shall handle - # naming at all levels. - # - if (is.null(names(NList)) | ForceNames == TRUE) - names(NList) <- as.character(1:length(NList)) - # - # If simply a dataframe deal promptly. - # - if (is.data.frame(NList) & LinearizeDataFrames == FALSE) - return(NList) - if (is.data.frame(NList) & LinearizeDataFrames == TRUE) - return(as.list(NList)) - # - # Book-keeping code to employ a while loop. - # - A <- 1 - B <- length(NList) - # - # We use a while loop to deal with the fact that the length of the nested - # list grows dynamically in the process of linearization. - # - while (A <= B) { - Element <- NList[[A]] - EName <- names(NList)[A] - if (is.list(Element)) { - # - # Before and After to keep track of the status of the top-level trunk - # below and above the current element. - # - if (A == 1) { - Before <- NULL - } else { - Before <- NList[1:(A - 1)] - } - if (A == B) { - After <- NULL - } else { - After <- NList[(A + 1):B] - } - # - # Treat dataframes specially. - # - if (is.data.frame(Element)) { - if (LinearizeDataFrames == TRUE) { - # - # `Jump` takes care of how much the list shall grow in this step. - # - Jump <- length(Element) - NList[[A]] <- NULL - # - # Generate or coerce names as need be. - # - if (is.null(names(Element)) | ForceNames == TRUE) - names(Element) <- as.character(1:length(Element)) - # - # Just throw back as list since dataframes have no nesting. - # - Element <- as.list(Element) - # - # Update names - # - names(Element) <- paste(EName, names(Element), sep=NameSep) - # - # Plug the branch back into the top-level trunk. - # - NList <- c(Before, Element, After) - } - Jump <- 1 - } else { - NList[[A]] <- NULL - # - # Go recursive! :) - # - if (is.null(names(Element)) | ForceNames == TRUE) - names(Element) <- as.character(1:length(Element)) - Element <- LinearizeNestedList(Element, LinearizeDataFrames, - NameSep, ForceNames) - names(Element) <- paste(EName, names(Element), sep=NameSep) - Jump <- length(Element) - NList <- c(Before, Element, After) - } - } else { - Jump <- 1 - } - # - # Update book-keeping variables. - # - A <- A + Jump - B <- length(NList) +LinearizeNestedList <- function(NList, LinearizeDataFrames = FALSE, + NameSep = "/", ForceNames = FALSE) { + + if (!is.list(NList) || length(NList) == 0) return(NList) + + stack <- vector("list", length(NList)) + for (i in seq_along(NList)) { + nm <- if (!is.null(names(NList)) && !ForceNames) names(NList)[i] else as.character(i) + stack[[i]] <- list(value = NList[[i]], prefix = nm) + } + + result <- list() + + while (length(stack) > 0) { + item <- stack[[1]] + stack <- stack[-1] + val <- item$value + prefix <- item$prefix + + is_df <- is.data.frame(val) + is_lst <- is.list(val) && (!is_df || LinearizeDataFrames) + + if (is_lst && length(val) > 0) { + children <- vector("list", length(val)) + for (i in seq_along(val)) { + nm <- if (!is.null(names(val)) && !ForceNames) names(val)[i] else as.character(i) + children[[i]] <- list(value = val[[i]], prefix = paste(prefix, nm, sep = NameSep)) + } + stack <- c(children, stack) + } else { + # Convert empty lists to NA before storing + result[[prefix]] <- if (is.list(val) && length(val) == 0) NA else val } - return(NList) + } + + result } #' A pretty list printer. Reduces extraneous space. @@ -196,16 +127,61 @@ pretty_lists <- function(x) # assertive::assert_is_list(x) assertthat::assert_that(is.list(x)) - for(key in names(x)){ - value <- format(x[[key]]) - if(value == "") next - cat(key, "=", value, "\n") - } - invisible(x) + for(key in names(x)){ + value <- format(x[[key]]) + if(value == "") next + cat(key, "=", value, "\n") + } + invisible(x) +} + + +#' @noRd +release_questions <- function() { + c("For the love of God did I add skip_on_cran?") +} + + +#' A small function to strip trailing slashes from a path +#' @noRd +strip_slashes <- function(path) { + if (length(path) && grepl("/$", path)) { + path <- substr(path, 1, nchar(path) - 1) + } + # Also remove leading slashes + if (length(path) && grepl("^/", path)) { + path <- substr(path, 2, nchar(path)) + } + + path +} + +#' A small function to add a prefix slash +#' @noRd +add_slashes <- function(path) { + if (length(path) && !grepl("^/", path)) { + path <- paste0("/", path) + } + path +} + +#' A pretty list printer. Reduces extraneous space. +#' @noRd +pretty_lists <- function(x) +{ + # assertive::assert_is_list(x) + assertthat::assert_that(is.list(x)) + + for(key in names(x)){ + value <- format(x[[key]]) + if(value == "") next + cat(key, "=", value, "\n") + } + invisible(x) } #' @noRd release_questions <- function() { - c("For the love of God did I add skip_on_cran?") + c("For the love of God did I add skip_on_cran?") } diff --git a/man/drop_acc.Rd b/man/drop_acc.Rd index 56e1a90..a62875e 100644 --- a/man/drop_acc.Rd +++ b/man/drop_acc.Rd @@ -23,7 +23,7 @@ Nested list with elements \code{account_id}, \code{country}, \code{team} (list), \code{team_member_id}. } \description{ -Fields returned will vary by account; +Fields returned will vary by account. } \examples{ \dontrun{ diff --git a/man/drop_auth.Rd b/man/drop_auth.Rd index bb8450c..ff911a8 100644 --- a/man/drop_auth.Rd +++ b/man/drop_auth.Rd @@ -14,53 +14,47 @@ drop_auth( } \arguments{ \item{new_user}{Set to \code{TRUE} if you need to switch to a new user -account or just flush existing token. Default is \code{FALSE}.} +account or flush the existing cached token. Default is \code{FALSE}.} -\item{key}{Your application key. \code{rdrop2} already comes with a key/secret but -you are welcome to swap out with our own. Since these keys are shipped with -the package, there is a small chance they could be voided if someone abuses -the key. If you plan to use this in production, or for an internal tool, -the recommended practice is to create a new application on Dropbox and use -those keys for your purposes.} +\item{key}{Your application key. \code{rdrop2} ships with a default key, but +for production use you should create your own Dropbox app and supply its +credentials.} -\item{secret}{Your application secret. Like \code{key}, \code{rdrop2} comes -with a secret but you are welcome to swap out with our own.} +\item{secret}{Your application secret.} -\item{cache}{By default your credentials are locally cached in a file called -\code{.httr-oauth}. Set to FALSE if you need to authenticate separately -each time.} +\item{cache}{Either \code{TRUE} (save token to \code{.rdrop2-token.rds} in +the working directory), \code{FALSE} (do not cache), or a file path string +specifying where to save the token RDS file.} -\item{rdstoken}{File path to stored RDS token. In server environments where -interactive OAuth is not possible, a token can be created on a desktop -client and used in production. See examples.} +\item{rdstoken}{File path to a previously saved RDS token. In non-interactive +(server) environments, create a token on a desktop machine with +\code{drop_auth()}, save it with \code{saveRDS()}, and supply the path +here. See examples.} } \value{ -A Token2.0 object, invisibly +The \code{httr2_token} object, invisibly. } \description{ -This function authenticates you into Dropbox. The documentation for the -\href{https://www.dropbox.com/developers/documentation?_tk=pilot_lp&_ad=topbar1&_camp=docs}{core Dropbox API} -provides more details including alternate methods if you desire to -reimplement your own. +This function authenticates you into Dropbox using OAuth 2.0 via the +\pkg{httr2} package. The documentation for the +\href{https://www.dropbox.com/developers/documentation}{Dropbox API v2} +provides more details. } \examples{ \dontrun{ - # To either read token from .httr-oauth in the working directory or open a - # web browser to authenticate (and cache a token) + # Open a browser to authenticate (and cache the token) drop_auth() - # If you want to overwrite an existing local token and switch to a new - # user, set new_user to TRUE. + # Switch to a new user account drop_auth(new_user = TRUE) - # To store a token for re-use (more flexible than .httr-oauth), save the - # output of drop_auth and save it to an RDS file + # Save the token for later re-use token <- drop_auth() - saveRDS(token, "/path/to/tokenfile.RDS") + saveRDS(token, "/path/to/tokenfile.rds") - # To use a stored token provide token location - drop_auth(rdstoken = "/path/to/tokenfile.RDS") + # Load a previously saved token + drop_auth(rdstoken = "/path/to/tokenfile.rds") } } \references{ diff --git a/man/drop_auth_env.Rd b/man/drop_auth_env.Rd new file mode 100644 index 0000000..2c08d46 --- /dev/null +++ b/man/drop_auth_env.Rd @@ -0,0 +1,59 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/drop_auth.R +\name{drop_auth_env} +\alias{drop_auth_env} +\title{Authenticate with Dropbox using environment variables} +\usage{ +drop_auth_env( + app_key = Sys.getenv("DROPBOX_APP_KEY"), + app_secret = Sys.getenv("DROPBOX_APP_SECRET"), + refresh_token = Sys.getenv("DROPBOX_REFRESH_TOKEN") +) +} +\arguments{ +\item{app_key}{Dropbox application key. Defaults to +\code{Sys.getenv("DROPBOX_APP_KEY")}.} + +\item{app_secret}{Dropbox application secret. Defaults to +\code{Sys.getenv("DROPBOX_APP_SECRET")}.} + +\item{refresh_token}{Long-lived refresh token. Defaults to +\code{Sys.getenv("DROPBOX_REFRESH_TOKEN")}.} +} +\value{ +The token list (invisibly). The access token is stored in the rdrop2 + session environment for use by all other rdrop2 functions. +} +\description{ +A non-interactive authentication method that exchanges a long-lived refresh +token for a short-lived access token. Suitable for server or CI/CD +environments where browser-based authentication is not possible. +} +\details{ +Set the following environment variables before calling this function (or any +rdrop2 function that requires authentication): +\itemize{ + \item \code{DROPBOX_APP_KEY} – the app key from your Dropbox app console. + \item \code{DROPBOX_APP_SECRET} – the app secret. + \item \code{DROPBOX_REFRESH_TOKEN} – a long-lived refresh token. Obtain + one by running \code{drop_auth()} interactively and inspecting + \code{token$refresh_token}. +} +} +\examples{ +\dontrun{ + Sys.setenv( + DROPBOX_APP_KEY = "your_app_key", + DROPBOX_APP_SECRET = "your_app_secret", + DROPBOX_REFRESH_TOKEN = "your_refresh_token" + ) + drop_auth_env() + + # Alternatively, pass values directly: + drop_auth_env( + app_key = "your_app_key", + app_secret = "your_app_secret", + refresh_token = "your_refresh_token" + ) +} +} diff --git a/man/drop_copy_batch.Rd b/man/drop_copy_batch.Rd new file mode 100644 index 0000000..032c5c6 --- /dev/null +++ b/man/drop_copy_batch.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/drop_file_ops.R +\name{drop_copy_batch} +\alias{drop_copy_batch} +\title{Copy multiple files or folders in a single batch request.} +\usage{ +drop_copy_batch(entries, autorename = FALSE, dtoken = get_dropbox_token()) +} +\arguments{ +\item{entries}{A list of named lists, each with \code{from_path} and +\code{to_path} character elements.} + +\item{autorename}{If \code{TRUE}, the Dropbox server will try to autorename +files to avoid conflicts. Default \code{FALSE}.} + +\item{dtoken}{The Dropbox token generated by \code{\link{drop_auth}}. rdrop2 +will try to automatically locate your local credential cache and use them. +However, if the credentials are not found, the function will initiate a new +authentication request. You can override this in \code{\link{drop_auth}} by +pointing to a different location where your credentials are stored.} +} +\value{ +A list of per-entry results returned by the Dropbox API. +} +\description{ +More efficient than calling \code{\link{drop_copy}} repeatedly for large +numbers of files. The function blocks until the batch job completes. +} +\examples{ +\dontrun{ + entries <- list( + list(from_path = "/file1.csv", to_path = "/backup/file1.csv"), + list(from_path = "/file2.csv", to_path = "/backup/file2.csv") + ) + drop_copy_batch(entries) +} +} +\references{ +\href{https://www.dropbox.com/developers/documentation/http/documentation#files-copy_batch_v2}{API documentation} +} diff --git a/man/drop_delete_batch.Rd b/man/drop_delete_batch.Rd new file mode 100644 index 0000000..283b073 --- /dev/null +++ b/man/drop_delete_batch.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/drop_file_ops.R +\name{drop_delete_batch} +\alias{drop_delete_batch} +\title{Delete multiple files or folders in a single batch request.} +\usage{ +drop_delete_batch(entries, dtoken = get_dropbox_token()) +} +\arguments{ +\item{entries}{A list of named lists, each with a \code{path} character +element specifying the Dropbox path to delete.} + +\item{dtoken}{The Dropbox token generated by \code{\link{drop_auth}}. rdrop2 +will try to automatically locate your local credential cache and use them. +However, if the credentials are not found, the function will initiate a new +authentication request. You can override this in \code{\link{drop_auth}} by +pointing to a different location where your credentials are stored.} +} +\value{ +A list of per-entry results returned by the Dropbox API. +} +\description{ +More efficient than calling \code{\link{drop_delete}} repeatedly for large +numbers of files. The function blocks until the batch job completes. +} +\examples{ +\dontrun{ + entries <- list( + list(path = "/old_file1.csv"), + list(path = "/old_file2.csv") + ) + drop_delete_batch(entries) +} +} +\references{ +\href{https://www.dropbox.com/developers/documentation/http/documentation#files-delete_batch}{API documentation} +} diff --git a/man/drop_download.Rd b/man/drop_download.Rd index 358a0b4..494910b 100644 --- a/man/drop_download.Rd +++ b/man/drop_download.Rd @@ -40,17 +40,17 @@ Download a file from Dropbox to disk. \dontrun{ # download a file to the current working directory - drop_get("dataset.zip") + drop_download("dataset.zip") # download again, overwriting previous result - drop_get("dataset.zip", overwrite = TRUE) + drop_download("dataset.zip", overwrite = TRUE) # download to a different path, keeping file name constant # will download to "some/other/place/dataset.zip" - drop_get("dataset.zip", local_path = "some/other/place/") + drop_download("dataset.zip", local_path = "some/other/place/") - # download to to a different path, changing filename - drop_get("dataset.zip", local_path = "some/other/place/not_a_dataset.zip") + # download to a different path, changing filename + drop_download("dataset.zip", local_path = "some/other/place/not_a_dataset.zip") } } diff --git a/man/drop_get.Rd b/man/drop_get.Rd index d45b1cc..30c9773 100644 --- a/man/drop_get.Rd +++ b/man/drop_get.Rd @@ -23,7 +23,7 @@ drop_get( \item{verbose}{By default verbose output is \code{FALSE}. Set to \code{TRUE} if you need to troubleshoot any output or grab additional parameters.} -\item{progress}{Progress bars are turned off by default. Set to \code{TRUE} ot turn this on. Progress is only reported when file sizes are known. Otherwise just bytes downloaded.} +\item{progress}{Progress bars are turned off by default. Set to \code{TRUE} to turn this on.} \item{dtoken}{The Dropbox token generated by \code{\link{drop_auth}}. rdrop2 will try to automatically locate your local credential cache and use them. diff --git a/man/drop_get_metadata.Rd b/man/drop_get_metadata.Rd index ae3d285..b839862 100644 --- a/man/drop_get_metadata.Rd +++ b/man/drop_get_metadata.Rd @@ -15,7 +15,7 @@ drop_get_metadata( \arguments{ \item{path}{Path to a file or folder on Dropbox. Can also be an ID ("id:...") or revision ("rev:...").} -\item{include_media_info}{If TRUE, additional metadata for photo or video is returns. Defaults to FALSE.} +\item{include_media_info}{If TRUE, additional metadata for photo or video is returned. Defaults to FALSE.} \item{include_deleted}{If TRUE, metadata will be returned for a deleted file, otherwise error. Defaults to FALSE.} diff --git a/man/drop_get_shared_link_file.Rd b/man/drop_get_shared_link_file.Rd new file mode 100644 index 0000000..5d589aa --- /dev/null +++ b/man/drop_get_shared_link_file.Rd @@ -0,0 +1,52 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/drop_shared.R +\name{drop_get_shared_link_file} +\alias{drop_get_shared_link_file} +\title{Download a file from Dropbox via a shared link.} +\usage{ +drop_get_shared_link_file( + url, + local_path = NULL, + overwrite = FALSE, + path = NULL, + dtoken = get_dropbox_token() +) +} +\arguments{ +\item{url}{The shared link URL pointing to the file.} + +\item{local_path}{Path to save the downloaded file to. If \code{NULL} +(default), the file is saved to the working directory using the filename +derived from the link. If a directory, the file is placed inside it.} + +\item{overwrite}{If \code{TRUE}, overwrite an existing local file. +Defaults to \code{FALSE}.} + +\item{path}{Optional Dropbox path within the shared folder to a specific +sub-file/folder. Leave as \code{NULL} for root of the shared link.} + +\item{dtoken}{The Dropbox token generated by \code{\link{drop_auth}}. rdrop2 +will try to automatically locate your local credential cache and use them. +However, if the credentials are not found, the function will initiate a new +authentication request. You can override this in \code{\link{drop_auth}} by +pointing to a different location where your credentials are stored.} +} +\value{ +\code{TRUE} invisibly on success. +} +\description{ +Retrieves the content of a file identified by a shared link URL (rather than +a Dropbox file path). This is useful when you have received a shared link +from another user. +} +\examples{ +\dontrun{ + drop_get_shared_link_file( + url = "https://www.dropbox.com/s/xxxx/example.csv?dl=0", + local_path = "example.csv" + ) +} +} +\references{ +\href{https://www.dropbox.com/developers/documentation/http/documentation#sharing-get_shared_link_file}{API documentation} +} diff --git a/man/drop_get_thumbnail.Rd b/man/drop_get_thumbnail.Rd new file mode 100644 index 0000000..e87f783 --- /dev/null +++ b/man/drop_get_thumbnail.Rd @@ -0,0 +1,54 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/drop_media.R +\name{drop_get_thumbnail} +\alias{drop_get_thumbnail} +\title{Retrieve a thumbnail for an image file on Dropbox.} +\usage{ +drop_get_thumbnail( + path, + local_path = NULL, + format = "jpeg", + size = "w256h256", + overwrite = FALSE, + dtoken = get_dropbox_token() +) +} +\arguments{ +\item{path}{Path to the image file on Dropbox.} + +\item{local_path}{Local path to save the thumbnail. If \code{NULL} +(default), a temporary file is created and its path is returned.} + +\item{format}{Thumbnail format: \code{"jpeg"} (default) or \code{"png"}.} + +\item{size}{Thumbnail size preset. One of \code{"w32h32"}, +\code{"w64h64"}, \code{"w128h128"}, \code{"w256h256"} (default), +\code{"w480h320"}, \code{"w640h480"}, \code{"w960h640"}, +\code{"w1024h768"}, \code{"w2048h1536"}.} + +\item{overwrite}{If \code{TRUE}, overwrite an existing local file. +Defaults to \code{FALSE}.} + +\item{dtoken}{The Dropbox token generated by \code{\link{drop_auth}}. rdrop2 +will try to automatically locate your local credential cache and use them. +However, if the credentials are not found, the function will initiate a new +authentication request. You can override this in \code{\link{drop_auth}} by +pointing to a different location where your credentials are stored.} +} +\value{ +Path to the saved thumbnail file, invisibly. +} +\description{ +Downloads a JPEG or PNG thumbnail for a photo or video stored in Dropbox. +Supported formats: jpg, png, tiff, tif, gif, webp, ppm, bmp. +} +\examples{ +\dontrun{ + thumb_path <- drop_get_thumbnail("photos/vacation.jpg") + # display in R (requires the 'magick' package) + # magick::image_read(thumb_path) +} +} +\references{ +\href{https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail_v2}{API documentation} +} diff --git a/man/drop_move_batch.Rd b/man/drop_move_batch.Rd new file mode 100644 index 0000000..940e475 --- /dev/null +++ b/man/drop_move_batch.Rd @@ -0,0 +1,47 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/drop_file_ops.R +\name{drop_move_batch} +\alias{drop_move_batch} +\title{Move multiple files or folders in a single batch request.} +\usage{ +drop_move_batch( + entries, + autorename = FALSE, + allow_ownership_transfer = FALSE, + dtoken = get_dropbox_token() +) +} +\arguments{ +\item{entries}{A list of named lists, each with \code{from_path} and +\code{to_path} character elements.} + +\item{autorename}{If \code{TRUE}, the Dropbox server will try to autorename +files to avoid conflicts. Default \code{FALSE}.} + +\item{allow_ownership_transfer}{If \code{TRUE}, allow moves that would +result in ownership transfer. Default \code{FALSE}.} + +\item{dtoken}{The Dropbox token generated by \code{\link{drop_auth}}. rdrop2 +will try to automatically locate your local credential cache and use them. +However, if the credentials are not found, the function will initiate a new +authentication request. You can override this in \code{\link{drop_auth}} by +pointing to a different location where your credentials are stored.} +} +\value{ +A list of per-entry results returned by the Dropbox API. +} +\description{ +More efficient than calling \code{\link{drop_move}} repeatedly for large +numbers of files. The function blocks until the batch job completes. +} +\examples{ +\dontrun{ + entries <- list( + list(from_path = "/file1.csv", to_path = "/archive/file1.csv") + ) + drop_move_batch(entries) +} +} +\references{ +\href{https://www.dropbox.com/developers/documentation/http/documentation#files-move_batch_v2}{API documentation} +} diff --git a/man/drop_restore.Rd b/man/drop_restore.Rd new file mode 100644 index 0000000..891e73b --- /dev/null +++ b/man/drop_restore.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/drop_history.R +\name{drop_restore} +\alias{drop_restore} +\title{Restore a file to a specific revision.} +\usage{ +drop_restore(path, rev, dtoken = get_dropbox_token()) +} +\arguments{ +\item{path}{Path to the file on Dropbox.} + +\item{rev}{The revision identifier string (e.g. \code{"a1c10ce0dd78"}) to +restore to. Revision IDs are returned in the \code{rev} column of +\code{\link{drop_history}}.} + +\item{dtoken}{The Dropbox token generated by \code{\link{drop_auth}}. rdrop2 +will try to automatically locate your local credential cache and use them. +However, if the credentials are not found, the function will initiate a new +authentication request. You can override this in \code{\link{drop_auth}} by +pointing to a different location where your credentials are stored.} +} +\value{ +A list of file metadata reflecting the restored version. +} +\description{ +Reverts a file on Dropbox to the content it had at a given revision. Use +\code{\link{drop_history}} to find available revision IDs. +} +\examples{ +\dontrun{ + history <- drop_history("report.csv") + # restore to the second most recent version + drop_restore("report.csv", rev = history$rev[2]) +} +} +\references{ +\href{https://www.dropbox.com/developers/documentation/http/documentation#files-restore}{API documentation} +} diff --git a/man/drop_save_url.Rd b/man/drop_save_url.Rd new file mode 100644 index 0000000..00a1307 --- /dev/null +++ b/man/drop_save_url.Rd @@ -0,0 +1,53 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/drop_upload.R +\name{drop_save_url} +\alias{drop_save_url} +\title{Save a remote URL directly to Dropbox.} +\usage{ +drop_save_url( + path, + url, + poll = TRUE, + interval = 2, + dtoken = get_dropbox_token() +) +} +\arguments{ +\item{path}{Destination path in Dropbox (including filename).} + +\item{url}{The URL of the file to download into Dropbox.} + +\item{poll}{If \code{TRUE} (default), block until the save operation +completes and return the saved file metadata. If \code{FALSE}, return +immediately with the async job ID.} + +\item{interval}{Polling interval in seconds when \code{poll = TRUE}. +Default \code{2}.} + +\item{dtoken}{The Dropbox token generated by \code{\link{drop_auth}}. rdrop2 +will try to automatically locate your local credential cache and use them. +However, if the credentials are not found, the function will initiate a new +authentication request. You can override this in \code{\link{drop_auth}} by +pointing to a different location where your credentials are stored.} +} +\value{ +If \code{poll = TRUE}, a file metadata list for the saved file. + If \code{poll = FALSE}, a list with \code{async_job_id} (or the completed + metadata if the server finished synchronously). +} +\description{ +Instructs Dropbox to download a file from the given URL and save it to your +Dropbox without you needing to download it locally first. Useful for +automated pipelines that source data from the web. +} +\examples{ +\dontrun{ + drop_save_url( + path = "/data/penguins.csv", + url = "https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv" + ) +} +} +\references{ +\href{https://www.dropbox.com/developers/documentation/http/documentation#files-save_url}{API documentation} +} diff --git a/man/drop_search.Rd b/man/drop_search.Rd index 3f71bf5..ec5470c 100644 --- a/man/drop_search.Rd +++ b/man/drop_search.Rd @@ -2,32 +2,42 @@ % Please edit documentation in R/drop_search.R \name{drop_search} \alias{drop_search} -\title{Returns metadata for all files and folders whose filename contains the given -search string as a substring.} +\title{Search for files and folders on Dropbox.} \usage{ drop_search( query, path = "", - start = 0, max_results = 100, - mode = "filename", + file_status = "active", + filename_only = FALSE, + file_extensions = NULL, + file_categories = NULL, dtoken = get_dropbox_token() ) } \arguments{ -\item{query}{The search string. This string is split (on spaces) into -individual words. Files and folders will be returned if they contain all -words in the search string.} +\item{query}{The search string. Split on spaces into individual words; +results are returned if they contain all words.} -\item{path}{Path in the user's Dropbox, relative to root} +\item{path}{Dropbox path to restrict the search to. Defaults to the entire +Dropbox (\code{""}).} -\item{start}{The starting index within the search results (used for paging). -The default for this field is 0} +\item{max_results}{The maximum number of search results to return. Defaults +to 100.} -\item{max_results}{The maximum number of search results to return. The default -for this field is 100.} +\item{file_status}{Filter by file status: \code{"active"} (default) returns +only existing files; \code{"deleted"} returns only deleted files.} -\item{mode}{Mode can take the option of filename, filename_and_content, or search deleted files with deleted_filename} +\item{filename_only}{If \code{TRUE}, restricts the search to filenames only +(faster). Defaults to \code{FALSE}.} + +\item{file_extensions}{Optional character vector of file extensions to +restrict results to (e.g., \code{c("pdf", "docx")}).} + +\item{file_categories}{Optional character vector of file category tags to +restrict results to. Valid values: \code{"image"}, \code{"document"}, +\code{"pdf"}, \code{"spreadsheet"}, \code{"presentation"}, \code{"audio"}, +\code{"video"}, \code{"folder"}, \code{"paper"}, \code{"others"}.} \item{dtoken}{The Dropbox token generated by \code{\link{drop_auth}}. rdrop2 will try to automatically locate your local credential cache and use them. @@ -35,16 +45,29 @@ However, if the credentials are not found, the function will initiate a new authentication request. You can override this in \code{\link{drop_auth}} by pointing to a different location where your credentials are stored.} } +\value{ +A list as returned by the Dropbox API, with a \code{matches} element + (list of match objects) and an optional \code{cursor} for pagination. +} \description{ -Returns metadata for all files and folders whose filename contains the given -search string as a substring. +Returns metadata for all files and folders whose filename (or content, if +enabled) matches the given search string. Uses the Dropbox +\code{files/search_v2} API endpoint which supports richer filtering options +than the original search endpoint. } \examples{ \dontrun{ -# If you know me, you know why this query exists -drop_search('gif') \%>\% select(path, is_dir, mime_type) + # simple filename search + results <- drop_search("report") + results$matches[[1]]$metadata$metadata$name + + # search only PDF files + drop_search("budget", file_extensions = "pdf") + + # search images only + drop_search("vacation", file_categories = "image") } } \references{ -\href{https://www.dropbox.com/developers/documentation/http/documentation#files-search}{API documentation} +\href{https://www.dropbox.com/developers/documentation/http/documentation#files-search_v2}{API documentation} } diff --git a/man/drop_search_continue.Rd b/man/drop_search_continue.Rd new file mode 100644 index 0000000..8f7a75b --- /dev/null +++ b/man/drop_search_continue.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/drop_search.R +\name{drop_search_continue} +\alias{drop_search_continue} +\title{Continue a paginated search begun with \code{drop_search}.} +\usage{ +drop_search_continue(cursor, dtoken = get_dropbox_token()) +} +\arguments{ +\item{cursor}{A cursor string returned in a previous \code{drop_search} call.} + +\item{dtoken}{The Dropbox token generated by \code{\link{drop_auth}}. rdrop2 +will try to automatically locate your local credential cache and use them. +However, if the credentials are not found, the function will initiate a new +authentication request. You can override this in \code{\link{drop_auth}} by +pointing to a different location where your credentials are stored.} +} +\value{ +Same structure as \code{\link{drop_search}}. +} +\description{ +Continue a paginated search begun with \code{drop_search}. +} +\examples{ +\dontrun{ + first_page <- drop_search("report", max_results = 20) + second_page <- drop_search_continue(first_page$cursor) +} +} +\references{ +\href{https://www.dropbox.com/developers/documentation/http/documentation#files-search-continue_v2}{API documentation} +} diff --git a/man/drop_space_usage.Rd b/man/drop_space_usage.Rd new file mode 100644 index 0000000..a466d7c --- /dev/null +++ b/man/drop_space_usage.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/drop_acc.R +\name{drop_space_usage} +\alias{drop_space_usage} +\title{Get Dropbox storage space usage.} +\usage{ +drop_space_usage(dtoken = get_dropbox_token()) +} +\arguments{ +\item{dtoken}{The Dropbox token generated by \code{\link{drop_auth}}. rdrop2 +will try to automatically locate your local credential cache and use them. +However, if the credentials are not found, the function will initiate a new +authentication request. You can override this in \code{\link{drop_auth}} by +pointing to a different location where your credentials are stored.} +} +\value{ +A list with elements \code{used} (bytes used) and \code{allocation} + (a list with \code{.tag} and \code{allocated} bytes, or team-level info). +} +\description{ +Returns how much space the current account is using and how much is +allocated. +} +\examples{ +\dontrun{ + usage <- drop_space_usage() + cat("Used:", usage$used, "bytes\n") + cat("Allocated:", usage$allocation$allocated, "bytes\n") +} +} +\references{ +\href{https://www.dropbox.com/developers/documentation/http/documentation#users-get_space_usage}{API documentation} +} diff --git a/man/drop_upload.Rd b/man/drop_upload.Rd index 0ad092b..e6b1d12 100644 --- a/man/drop_upload.Rd +++ b/man/drop_upload.Rd @@ -19,18 +19,15 @@ drop_upload( \item{path}{The relative path on Dropbox where the file should get uploaded.} -\item{mode}{- "add" - will not overwrite an existing file in case of a -conflict. With this mode, when a a duplicate file.txt is uploaded, it will -become file (2).txt. - "overwrite" will always overwrite a file -} +\item{mode}{Upload mode: \code{"overwrite"} (default) will always overwrite +an existing file; \code{"add"} will not overwrite and instead create a +renamed copy on conflict; \code{"update"} requires a \code{rev} argument.} -\item{autorename}{This logical determines what happens when there is a -conflict. If true, the file being uploaded will be automatically renamed to -avoid the conflict. (For example, test.txt might be automatically renamed to -test (1).txt.) The new name can be obtained from the returned metadata. If -false, the call will fail with a 409 (Conflict) response code. The default is `TRUE`} +\item{autorename}{If \code{TRUE} (default), the file being uploaded will be +automatically renamed to avoid conflicts when using \code{mode = "add"}.} -\item{mute}{Set to FALSE to prevent a notification trigger on the desktop and -mobile apps} +\item{mute}{Set to \code{TRUE} to suppress desktop/mobile notifications. +Defaults to \code{FALSE}.} \item{verbose}{By default verbose output is \code{FALSE}. Set to \code{TRUE} if you need to troubleshoot any output or grab additional parameters.} @@ -41,9 +38,13 @@ However, if the credentials are not found, the function will initiate a new authentication request. You can override this in \code{\link{drop_auth}} by pointing to a different location where your credentials are stored.} } +\value{ +Dropbox file metadata list, invisibly. +} \description{ -This function will allow you to write files of any size to Dropbox(even ones -that cannot be read into memory) by uploading them in chunks. +This function will allow you to write files of any size to Dropbox. Files +larger than 150 MB are automatically uploaded in chunks using the Dropbox +upload session API. } \examples{ \dontrun{ diff --git a/man/get_dropbox_token.Rd b/man/get_dropbox_token.Rd index 74e53ea..e8d6683 100644 --- a/man/get_dropbox_token.Rd +++ b/man/get_dropbox_token.Rd @@ -2,11 +2,17 @@ % Please edit documentation in R/drop_auth.R \name{get_dropbox_token} \alias{get_dropbox_token} -\title{Retrieve oauth2 token from rdrop2-namespaced environment} +\title{Retrieve the Dropbox bearer token string} \usage{ get_dropbox_token() } +\value{ +The Dropbox access token string. +} \description{ -Retrieves a token if it is previously stored, otherwise prompts user to get one. +Returns the access token string stored in the rdrop2 environment. If no +token is cached, \code{\link{drop_auth}} is called interactively. If the +stored token has expired and a refresh token is available, the token is +silently refreshed and the cache file is updated. } \keyword{internal} diff --git a/tests/testthat/test-01_rdrop-auth.R b/tests/testthat/test-01_rdrop-auth.R index 6a9182f..eb934bf 100644 --- a/tests/testthat/test-01_rdrop-auth.R +++ b/tests/testthat/test-01_rdrop-auth.R @@ -2,8 +2,10 @@ context("authorization") test_that("Able to authenticate from saved RDS token", { skip_on_cran() - # read cached token and check its class - expect_is(drop_auth(rdstoken = "token.rds"), "Token2.0") + # read cached token and check it has an access_token field + token <- drop_auth(rdstoken = "token.rds") + expect_true(!is.null(token$access_token)) + expect_is(token, "httr2_token") }) @@ -21,4 +23,3 @@ test_that("Account information works correctly", { # name element should be its own list expect_is(acc_info$name, "list") }) - diff --git a/tests/testthat/test-99-rdrop2.R b/tests/testthat/test-99-rdrop2.R index f6aed12..ee1db82 100644 --- a/tests/testthat/test-99-rdrop2.R +++ b/tests/testthat/test-99-rdrop2.R @@ -38,7 +38,8 @@ test_that("drop_search works correctly", { x <- drop_search("mt") - expect_equal(x$matches[[1]]$metadata$name, "mtcars.csv") + # search_v2 nests metadata: matches[[i]]$metadata$metadata$name + expect_equal(x$matches[[1]]$metadata$metadata$name, "mtcars.csv") # A search with no query should fail expect_error(drop_search())