-
Notifications
You must be signed in to change notification settings - Fork 46
/
Copy pathshared_drives.R
280 lines (248 loc) · 10.4 KB
/
shared_drives.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#' Access shared drives
#'
#' @description
#' A shared drive supports files owned by an organization rather than an
#' individual user. Shared drives follow different sharing and ownership models
#' from a specific user's "My Drive". Shared drives are the successors to the
#' earlier concept of Team Drives.
#' @description How to capture a shared drive or files/folders that live on a
#' shared drive for downstream use:
#' * [shared_drive_find()] and [shared_drive_get()] return a [`dribble`] with
#' metadata on shared drives themselves. You will need this in order to use a
#' shared drive in certain file operations. For example, you can specify a
#' shared drive as the parent folder via the `path` argument for upload, move,
#' copy, etc. In that context, the id of a shared drive functions like the id of
#' its top-level or root folder.
#' * [drive_find()] and [drive_get()] return a [`dribble`] with metadata on
#' files, including folders. Both can be directed to search for files on shared
#' drives using the optional arguments `shared_drive` or `corpus` (documented
#' below).
#'
#' @description Regard the functions mentioned above as the official "port of
#' entry" for working with shared drives. Use these functions to capture your
#' target(s) in a [`dribble`] to pass along to other googledrive functions.
#' The flexibility to refer to files by name or path does not apply as broadly
#' to shared drives. While it's always a good idea to get things into a
#' [`dribble`] early, for shared drives it's often required.
#'
#' @section Specific shared drive:
#' To search one specific shared drive, pass its name, marked id, or
#' [`dribble`] to `shared_drive` somewhere in the call, like so:
#' ```
#' drive_find(..., shared_drive = "i_am_a_shared_drive_name")
#' drive_find(..., shared_drive = as_id("i_am_a_shared_drive_id"))
#' drive_find(..., shared_drive = i_am_a_shared_drive_dribble)
#' ```
#' The value provided to `shared_drive` is pre-processed with
#' [as_shared_drive()].
#' @section Other collections:
#' To search other collections, pass the `corpus` parameter somewhere in the
#' call, like so:
#' ```
#' drive_find(..., corpus = "user")
#' drive_find(..., corpus = "allDrives")
#' drive_find(..., corpus = "domain")
#' ```
#' Possible values of `corpus` and what they mean:
#' * `"user"`: Queries files that the user has accessed, including both shared
#' drive and My Drive files.
#' * `"drive"`: Queries all items in the shared drive specified via
#' `shared_drive`. googledrive automatically fills this in whenever
#' `shared_drive` is not `NULL`.
#' * `"allDrives"`: Queries files that the user has accessed and all shared
#' drives in which they are a member. Note that the response may include
#' `incompleteSearch : true`, indicating that some corpora were not searched
#' for this request (currently, googledrive does not surface this). Prefer
#' `"user"` or `"drive"` to `"allDrives"` for efficiency.
#' * `"domain"`: Queries files that are shared to the domain, including both
#' shared drive and My Drive files.
#' @section Google blogs and docs:
#' Here is some of the best official Google reading about shared drives:
#' * [Team Drives is being renamed to shared drives](https://workspaceupdates.googleblog.com/2019/04/shared-drives.html) from Google Workspace blog
#' * [Upcoming changes to the Google Drive API and Google Picker API](https://cloud.google.com/blog/products/application-development/upcoming-changes-to-the-google-drive-api-and-google-picker-api) from the Google Cloud blog
#' * <https://developers.google.com/drive/api/v3/about-shareddrives>
#' * <https://developers.google.com/drive/api/v3/shared-drives-diffs>
#' * Get started with shared drives: `https://support.google.com/a/users/answer/9310351` from Google Workspace Learning Center
#' * Best practices for shared drives: `https://support.google.com/a/users/answer/9310156` from Google Workspace Learning Center
#' @section API docs:
#' googledrive implements shared drive support as outlined here:
#' * <https://developers.google.com/drive/api/v3/enable-shareddrives>
#'
#' Users shouldn't need to know any of this, but here are details for the
#' curious. The extra information needed to search shared drives consists of the
#' following query parameters:
#' * `corpora`: Where to search? Formed from googledrive's `corpus` argument.
#' * `driveId`: The id of a specific shared drive. Only allowed -- and also
#' absolutely required -- when `corpora = "drive"`. When user specifies a
#' `shared_drive`, googledrive sends its id and also infers that `corpora`
#' should be set to `"drive"`.
#' * `includeItemsFromAllDrives`: Do you want to see shared drive items?
#' Obviously, this should be `TRUE` and googledrive sends this whenever shared
#' drive parameters are detected.
#' * `supportsAllDrives`: Does the sending application (googledrive, in this
#' case) know about shared drive? Obviously, this should be `TRUE` and
#' googledrive sends it for all applicable endpoints, all the time.
#'
#' @name shared_drives
NULL
# Shared drive and "file groupings" support
#
# Internal documentation!
#
# It is intentional that googledrive's two parameters `shared_drive` and
# `corpus` have NO OVERLAP with actual Drive query parameters: this way the
# googledrive high-level wrapping is option A but user could also pass raw
# params through `...` as option B.
#
# @param driveId Character, a shared drive id.
# @template corpus
# @param includeItemsFromAllDrives Logical, googledrive always sets this to
# `TRUE`.
#
# @examples
# \dontrun{
# shared_drive_params(driveId = "123456789")
# shared_drive_params(corpora = "user")
# shared_drive_params(corpora = "allDrives")
# shared_drive_params(corpora = "domain")
#
# # this will error because `corpora = "drive"` also requires the id
# shared_drive_params(corpora = "drive")
#
# # this will error because `corpora = "domain"` forbids inclusion of id
# shared_drive_params(corpora = "domain", driveId = "123456789")
# }
shared_drive_params <- function(driveId = NULL,
corpora = NULL,
includeItemsFromAllDrives = NULL) {
rationalize_corpus(
new_corpus(
driveId, corpora, includeItemsFromAllDrives
)
)
}
new_corpus <- function(driveId = NULL,
corpora = NULL,
includeItemsFromAllDrives = NULL) {
if (!is.null(driveId)) {
# can't use is_string() because object of class drive_id IS acceptable
stopifnot(is.character(driveId), length(driveId) == 1)
}
if (!is.null(corpora)) {
stopifnot(is_string(corpora))
}
if (!is.null(includeItemsFromAllDrives)) {
stopifnot(
is.logical(includeItemsFromAllDrives),
length(includeItemsFromAllDrives) == 1
)
}
structure(
list(
corpora = corpora,
driveId = driveId,
includeItemsFromAllDrives = includeItemsFromAllDrives
),
class = "corpus"
)
}
# this isn't a classic validator, because it fills in some gaps
# there is, however, no magic
# there is user input that is unambiguous but still won't satisfy the API
# 1. if `driveId` is provided and `corpora` is not, we fill in
# `corpora = "drive"`
# 2. we always send `includeItemsFromAllDrives = TRUE`
rationalize_corpus <- function(corpus) {
stopifnot(inherits(corpus, "corpus"))
if (!is.null(corpus[["driveId"]])) {
corpus[["corpora"]] <- corpus[["corpora"]] %||% "drive"
}
validate_corpora(corpus[["corpora"]])
if (corpus[["corpora"]] == "drive" && is.null(corpus[["driveId"]])) {
drive_abort('
When {.code corpus = "drive"}, you must also specify \\
the {.arg shared_drive}.')
}
if (corpus[["corpora"]] != "drive" && !is.null(corpus[["driveId"]])) {
drive_abort('
When {.code corpus != "drive"}, you must not specify \\
a {.arg shared_drive}.')
}
corpus[["includeItemsFromAllDrives"]] <- TRUE
corpus
}
valid_corpora <- c("user", "drive", "allDrives", "domain")
validate_corpora <- function(corpora) {
if (!corpora %in% valid_corpora) {
# yes, I intentionally use `corpus` in this message, even
# though the actual API parameter is `corpora`
# googledrive's user-facing functions have `corpus` in their signature and
# the rationale is explained elsewhere in this file
drive_abort(c(
"Invalid value for {.arg corpus}:",
bulletize(gargle_map_cli(corpora), bullet = "x"),
"These are the only acceptable values:",
bulletize(gargle_map_cli(valid_corpora))
))
}
invisible(corpora)
}
#' Coerce to shared drive
#'
#' @description Converts various representations of a shared drive into a
#' [`dribble`], the object used by googledrive to hold Drive file metadata.
#' Shared drives can be specified via
#' * Name
#' * Shared drive id, marked with [as_id()] to distinguish from name
#' * Data frame or [`dribble`] consisting solely of shared drives
#' * List representing [Drives resource](https://developers.google.com/drive/api/v3/reference/drives#resource-representations)
#' objects (mostly for internal use)
#'
#' @template shared-drive-description
#'
#' @description This is a generic function.
#'
#' @param x A vector of shared drive names, a vector of shared drive ids marked
#' with [as_id()], a list of Drives resource objects, or a suitable data
#' frame.
#' @param ... Other arguments passed down to methods. (Not used.)
#' @export
#' @examples
#' \dontrun{
#' # specify the name
#' as_shared_drive("abc")
#'
#' # specify the id (substitute one of your own!)
#' as_shared_drive(as_id("0AOPK1X2jaNckUk9PVA"))
#' }
as_shared_drive <- function(x, ...) UseMethod("as_shared_drive")
#' @export
as_shared_drive.default <- function(x, ...) {
drive_abort("
Don't know how to coerce an object of class {.cls {class(x)}} into \\
a shared drive {.cls dribble}.")
}
#' @export
as_shared_drive.NULL <- function(x, ...) dribble()
#' @export
as_shared_drive.character <- function(x, ...) shared_drive_get(name = x)
#' @export
as_shared_drive.drive_id <- function(x, ...) shared_drive_get(id = x)
#' @export
as_shared_drive.dribble <- function(x, ...) validate_shared_drive_dribble(x)
#' @export
as_shared_drive.data.frame <- function(x, ...) {
validate_shared_drive_dribble(as_dribble(x))
}
#' @export
as_shared_drive.list <- function(x, ...) {
validate_shared_drive_dribble(as_dribble(x))
}
validate_shared_drive_dribble <- function(x) {
stopifnot(inherits(x, "dribble"))
if (!all(is_shared_drive(x))) {
drive_abort("
All rows of shared drive {.cls dribble} must contain a shared drive.")
}
x
}