Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for basic digital ocean spaces API #138

Merged
merged 19 commits into from
Oct 17, 2017
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ Imports:
httr (>= 1.2.0),
jsonlite (>= 1.1),
magrittr,
yaml
yaml,
aws.s3 (>= 0.3.3)
Suggests:
roxygen2 (>= 6.0.1),
testthat,
Expand Down
11 changes: 11 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ S3method(as.snapshot,character)
S3method(as.snapshot,list)
S3method(as.snapshot,numeric)
S3method(as.snapshot,snapshot)
S3method(as.space,space)
S3method(as.sshkey,character)
S3method(as.sshkey,list)
S3method(as.sshkey,numeric)
Expand Down Expand Up @@ -46,11 +47,13 @@ S3method(print,domain_record)
S3method(print,droplet)
S3method(print,image)
S3method(print,snapshot)
S3method(print,space)
S3method(print,sshkey)
S3method(print,tag)
S3method(print,volume)
S3method(print,volume_snapshot)
S3method(summary,droplet)
S3method(summary,space)
export("%>%")
export(account)
export(action)
Expand All @@ -61,6 +64,7 @@ export(as.domain_record)
export(as.droplet)
export(as.image)
export(as.snapshot)
export(as.space)
export(as.sshkey)
export(as.tag)
export(as.volume)
Expand Down Expand Up @@ -159,6 +163,10 @@ export(sizes)
export(snapshot)
export(snapshot_delete)
export(snapshots)
export(space_create)
export(space_info)
export(spaces)
export(spaces_GET)
export(standardise_keys)
export(tag)
export(tag_create)
Expand All @@ -178,6 +186,9 @@ export(volume_resize)
export(volume_snapshot_create)
export(volume_snapshots)
export(volumes)
importFrom(aws.s3,bucketlist)
importFrom(aws.s3,get_bucket)
importFrom(aws.s3,put_bucket)
importFrom(httr,GET)
importFrom(httr,HEAD)
importFrom(httr,POST)
Expand Down
137 changes: 137 additions & 0 deletions R/spaces.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
spaces_base <- "nyc3.digitaloceanspaces.com"

check_space_access <- function(spaces_key) {
tmp <- if (is.null(spaces_key)) Sys.getenv("DO_SPACES_ACCESS_KEY") else spaces_key
if (tmp == "") stop("Need a digital ocean spaces access key defined in your session", call. = FALSE) else tmp
}

check_space_secret <- function(spaces_secret) {
tmp <- if (is.null(spaces_secret)) Sys.getenv("DO_SPACES_SECRET_KEY") else spaces_secret
if (tmp == "") stop("Need a digital ocean spaces access key defined in your session", call. = FALSE) else tmp
}

#' @param x Object to coerce to a space
#' @export
#' @rdname spaces
as.space <- function(x) UseMethod("as.space")
#' @export
as.space.space <- function(x) x

#' @export
print.space <- function(x, ...) {
cat("<space>", x$Name, "\n", sep = "")
cat(" Created at: ", x$CreationDate, "\n")
}

#' @export
summary.space <- function(object, ...) {
space_info <- space_info(name = object$Name, ...)

# obtain total size used by space
size <- space_size(space_info)

# obtain number of files in space
n_files <- space_files(space_info)

cat("<space_detail>", object$Name, "\n", sep = "")
cat(" Size (GB): ", size, "\n")
cat(" Files: ", n_files, "\n", sep = "")
cat(" Created at: ", object$CreationDate, "\n")
}

#' Spaces storage operations
#'
#' \describe{
#' \item{space}{List space contents}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't see a space function, though maybe it's something to be added

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, maybe space == space_info

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth with this, but now I'm going to omit a space() function since spaces() will grab everything and it is straight forward for the user to grab whichever space they want to work with. They all have a special space class so we'll be able to add many more methods and operations as time goes on I'll delete references to a space function in my next PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I've made space_info() an internal function

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good

#' \item{spaces}{List spaces in your digital ocean account}
#' \item{space_create}{Create a new space}
#' }
#'
#' @param name (character) Space name.
#' @param spaces_key (character) String containing a spaces access key. If
#' missing, defaults to value stored in an environment variable \code{DO_SPACES_ACCESS_KEY}.
#' @param spaces_secret (character) String containing the secret associated
#' with the spaces key. If missing, defaults to value stored in an environment
#' variable \code{DO_SPACES_SECRET_KEY}.
#' @param ... Additional arguments passed down to \code{\link[aws.s3]{bucketlist}},
#' \code{\link[aws.s3]{get_bucket}}, \code{\link[aws.s3]{put_bucket}} functions.

#' @importFrom aws.s3 bucketlist
#' @export
#' @rdname spaces
spaces_GET <- function(spaces_key = NULL, spaces_secret = NULL, ...) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you mean to export spaces_GET ? if so, what does it do? from the name of the fxn it seems like it's meant to be an internal fxn maybe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was also supposed to be an internal function, since the spaces() function is the one intended to be user facing. I'll make sure this is internal in my revisions.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, thx


spaces_key <- check_space_access(spaces_key)
spaces_secret <- check_space_secret(spaces_secret)

res <- aws.s3::s3HTTP(verb = "GET",
region = NULL,
key = spaces_key,
secret = spaces_secret,
base_url = spaces_base,
...)

return(res)
}

#' @export
#' @rdname spaces
spaces <- function(spaces_key = NULL, spaces_secret = NULL, ...) {

res <- spaces_GET(spaces_key = spaces_key, spaces_secret = spaces_secret, ...)
spaces <- lapply(res$Buckets, structure, class = "space")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try doing unname(res$Buckets) because if you have only 1 item returned, it fails, try it out you'll see what i mean

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was rather nefarious, but when only one space is present the structure of the result changed such that only the Name and CreationDate are populated in res$Buckets, instead of each space occupying a Bucket object (list) containing the Name and CreationDate. I've added a check of this via 35ff528.

setNames(spaces, vapply(res$Buckets, function(x) x$Name, character(1)))
}

#' @importFrom aws.s3 get_bucket
#' @export
#' @rdname spaces
space_info <- function(name, spaces_key = NULL, spaces_secret = NULL, ...) {
if (is.null(name)) stop("Please specify the space name")
spaces_key <- check_space_access(spaces_key)
spaces_secret <- check_space_secret(spaces_secret)

space_info <- get_bucket(name,
region = NULL,
check_region = FALSE,
key = spaces_key,
secret = spaces_secret,
base_url = spaces_base,
max = Inf,
...)

return(space_info)
}

space_size <- function(space_info) {
# grab the sizes from each file (unit is bytes)
sizes <- vapply(space_info, function(x) x$Size, numeric(1))

# compute total size (convert to gb)
sum(sizes) * 1e-09
}

space_files <- function(space_info) {
# remove entries with size 0 (those are nested directories)
length(lapply(space_info, function(x) x[x$Size > 0]))
}

#' Create a new space
#' @importFrom aws.s3 put_bucket
#' @export
#' @rdname spaces
space_create <- function(name, spaces_key = NULL, spaces_secret = NULL, ...) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of name with no default, you could do https://github.com/sckott/analogsea/blob/master/R/droplet-actions.R#L84

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't know if that makes sense for spaces, def. feel free to not change it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm inclined to not give name a default because like Amazon S3, a space name must be unique across all spaces defined in digital ocean, not just the spaces within a user's account. While the actual chance of users getting the same randomly generated name are quite small, I'm still weary of generating a random name as default.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

if (is.null(name)) stop("Please specify the space name")
spaces_key <- check_space_access(spaces_key)
spaces_secret <- check_space_secret(spaces_secret)

res <- put_bucket(name,
region = NULL,
acl = acl,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this acl thing is undefined, what is it supposed to be?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is one of the parameters used by put_bucket function from aws.s3 to determine if the bucket is made private or public upon creating. On default new buckets are made as private, and it appears to also be true when I use it to createi new spaces. I tried specifying this to be public in my testing but the spaces were still being created as private. Until I can get this resolved I will remove references to it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, thx

key = spaces_key,
secret = spaces_secret,
base_url = spaces_base,
...)

if (res) message(sprintf("New space %s created successfully"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't sprintf expect an object passed to it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I will fix that. It was supposed to be passing name. That's what I get for coding late 😞

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this error well if the space isn't created? curious what that looks like. i assume that error handling is in aws pkg

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far I haven't had any errors occur when creating a new space. But I am going to add a simple check to ensure the user does not have an existing space with the same name.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, something to explore later, though may be handled well already in aws.s3 pkg

}
44 changes: 44 additions & 0 deletions man/spaces.Rd

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

16 changes: 16 additions & 0 deletions tests/testthat/test-space.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# tests for spaces
context("space")

test_that("fails well with no input", {
expect_error(space_info(), "argument \"name\" is missing")
})

test_that("key checks fail when not defined in environment", {
skip_on_cran()

Sys.unsetenv("DO_SPACES_ACCESS_KEY")
expect_error(check_space_access(spaces_key = NULL), "Need a digital ocean spaces access key defined in your session")

Sys.unsetenv("DO_SPACES_SECRET_KEY")
expect_error(check_space_secret(spaces_secret = NULL), "Need a digital ocean spaces access key defined in your session")
})