Skip to content

Commit

Permalink
Added remote capabilities for all read-only functions.
Browse files Browse the repository at this point in the history
This enables access to registry contents from remote applications, with
appropriate caching on the filesystem of the caller. It does so by
hitting the new /fetch and /list endpoints of Gobbler 0.3.1.
  • Loading branch information
LTLA committed Apr 11, 2024
1 parent 6186021 commit 3a0025c
Show file tree
Hide file tree
Showing 28 changed files with 582 additions and 106 deletions.
5 changes: 3 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: gobbler
Version: 0.3.1
Date: 2024-04-10
Version: 0.3.2
Date: 2024-04-11
Title: Interface to the gobbler service
Description:
Friendly interface to the gobbler service.
Expand All @@ -10,6 +10,7 @@ License: MIT + file LICENSE
Imports:
utils,
methods,
tools,
jsonlite,
httr2
Suggests:
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export(approveProbation)
export(checkHealth)
export(cloneVersion)
export(createProject)
export(fetchDirectory)
export(fetchLatest)
export(fetchManifest)
export(fetchPermissions)
Expand All @@ -29,4 +30,5 @@ import(httr2)
import(methods)
importFrom(jsonlite,fromJSON)
importFrom(jsonlite,toJSON)
importFrom(utils,URLencode)
importFrom(utils,download.file)
106 changes: 106 additions & 0 deletions R/fetchDirectory.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#' Fetch a directory from the registry
#'
#' Obtain a path to a subdirectory in the registry,
#' possibly creating a local copy of the subdirectory's contents if the caller is not on the same filesystem as the registry.
#'
#' @param path String containing the relative path to a subdirectory within the registry.
#' This usually takes the form of \code{PROJECT/ASSET/VERSION/*}.
#' @param registry String containing a path to the registry.
#' @param url String containing the URL to the Gobbler REST API.
#' @param cache String containing a path to a cache directory.
#' If \code{NULL}, an appropriate cache is automatically chosen.
#' @param forceRemote Logical scalar indicating whether to force remote access.
#' This will download all files in the \code{path} via the REST API and cache them locally.
#' @param overwrite Logical scalar indicating whether to overwrite the existing cache.
#'
#' @return Path to the subdirectory on the caller's filesystem.
#' This is either a path to the registry if it is accessible,
#' or a path to a local cache of the registry's contents otherwise.
#'
#' @author Aaron Lun
#'
#' @examples
#' info <- startGobbler()
#' removeProject("test", info$staging, url=info$url) # start with a clean slate.
#' createProject("test", info$staging, url=info$url)
#'
#' # Mocking up an upload.
#' src <- allocateUploadDirectory(info$staging)
#' write(file=file.path(src, "foo"), "BAR")
#' dir.create(file.path(src, "whee"))
#' write(file=file.path(src, "whee", "blah"), "stuff")
#' write(file=file.path(src, "whee2"), "more-stuff")
#' res <- uploadDirectory("test", "simple", "v1", src, staging=info$staging, url=info$url)
#'
#' # Now fetching the directory.
#' dir <- fetchDirectory("test/simple/v1", registry=info$registry, url=info$url)
#' dir
#' list.files(dir, recursive=TRUE)
#'
#' # Or, forcing remote access:
#' cache <- tempfile()
#' dir1 <- fetchDirectory("test/simple/v1",
#' registry=info$registry,
#' url=info$url,
#' cache=cache,
#' forceRemote=TRUE
#' )
#' dir1
#' list.files(dir1, recursive=TRUE)
#'
#' @export
#' @import httr2
fetchDirectory <- function(path, registry, url, cache=NULL, forceRemote=FALSE, overwrite=FALSE) {
if (!forceRemote && file.exists(registry)) {
return(file.path(registry, path))
}

cache <- local_registry(cache, url)
final <- file.path(cache, "REGISTRY", path)
ok <- file.path(cache, "SUCCESS", path, "....OK")
if (!overwrite && file.exists(ok) && file.exists(final)) {
return(final)
}

req <- request(paste0(url, "/list?path=", URLencode(path), "&recursive=true"))
req <- req_error(req, body = function(res) resp_body_json(res)$reason)
res <- req_perform(req)
listing <- resp_body_json(res)
for (l in listing) {
acquire_file(cache, path, l, url=url, overwrite=overwrite)
}

# We use a directory-level OK file to avoid having to scan through all
# the directory contents to indicate that it's complete.
dir.create(dirname(ok), showWarnings=FALSE, recursive=TRUE)
write(file=ok, character(0))
final
}

#' @importFrom utils download.file
acquire_file <- function(cache, path, name, url, overwrite) {
target <- file.path(cache, "REGISTRY", path, name)

if (overwrite || !file.exists(target)) {
tempdir <- file.path(cache, "TEMP")
dir.create(tempdir, recursive=TRUE, showWarnings=FALSE)
tempf <- tempfile(tmpdir=tempdir)
on.exit(unlink(tempf), add=TRUE, after=FALSE)

if (download.file(paste0(url, "/fetch/", path, "/", name), tempf)) {
stop("failed to download '", name, "' from the registry")
}
dir.create(dirname(target), recursive=TRUE, showWarnings=FALSE)
file.rename(tempf, target) # this should be more or less atomic, so no need for locks.
}

target
}

#' @importFrom utils URLencode
local_registry <- function(cache, url) {
if (is.null(cache)) {
cache <- tools::R_user_dir("gobbler", "data")
}
file.path(cache, URLencode(url, reserved=TRUE))
}
36 changes: 27 additions & 9 deletions R/fetchLatest.R
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#' Fetch the latest version
#'
#' Fetch the latest version of a project's asset.
#' This will call the REST API if the caller is not on the same filesystem as the registry.
#'
#' @param project String containing the project name.
#' @param asset String containing the asset name.
#' @param registry String containing a path to the registry.
#' @inheritParams listProjects
#'
#' @return String containing the latest version of the asset.
#' This may also be \code{NULL} if the asset has no (non-probational) versions.
Expand All @@ -22,19 +23,36 @@
#' res <- uploadDirectory("test", "simple", "v2", src, staging=info$staging, url=info$url)
#'
#' # Obtaining the latest version of this asset.
#' fetchLatest("test", "simple", registry=info$registry)
#' fetchLatest("test", "simple", registry=info$registry, url=info$url)
#'
#' # Forcing remote access.
#' fetchLatest("test", "simple", registry=info$registry, url=info$url, forceRemote=TRUE)
#'
#' @seealso
#' \code{\link{refreshLatest}}, to refresh the latest version.
#'
#' @export
#' @importFrom jsonlite fromJSON
fetchLatest <- function(project, asset, registry) {
proposed <- file.path(registry, project, asset, "..latest")
if (!file.exists(proposed)) {
NULL
} else {
vers <- fromJSON(proposed, simplifyVector=FALSE)
vers$version
#' @import httr2
fetchLatest <- function(project, asset, registry, url, forceRemote=FALSE) {
if (file.exists(registry) && !forceRemote) {
proposed <- file.path(registry, project, asset, "..latest")
if (!file.exists(proposed)) {
return(NULL)
} else {
vers <- fromJSON(proposed, simplifyVector=FALSE)
return(vers$version)
}
}

tryCatch(
{
req <- request(paste0(url, "/fetch/", paste(project, asset, "..latest", sep="/")))
resp <- req_perform(req)
body <- resp_body_string(resp)
vers <- fromJSON(body, simplifyVector=FALSE)
vers$version
},
httr2_http_404 = function(e) NULL
)
}
24 changes: 20 additions & 4 deletions R/fetchManifest.R
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#' Fetch version manifest
#'
#' Fetch the manifest for a version of an asset of a project.
#' This will call the REST API if the caller is not on the same filesystem as the registry.
#'
#' @param project String containing the project name.
#' @param asset String containing the asset name.
#' @param version String containing the version name.
#' @param registry String containing the path to the registry.
#' @inheritParams fetchDirectory
#'
#' @author Aaron Lun
#'
Expand All @@ -32,10 +33,25 @@
#' res <- uploadDirectory("test", "simple", "v1", src, staging=info$staging, url=info$url)
#'
#' # Obtaining the manifest for this version.
#' fetchManifest("test", "simple", "v1", registry=info$registry)
#' fetchManifest("test", "simple", "v1", registry=info$registry, url=info$url)
#'
#' # Force remote access.
#' fetchManifest(
#' "test",
#' "simple",
#' "v1",
#' registry=info$registry,
#' url=info$url,
#' forceRemote=TRUE
#' )
#' @export
#' @importFrom jsonlite fromJSON
fetchManifest <- function(project, asset, version, registry) {
fromJSON(file.path(registry, project, asset, version, "..manifest"), simplifyVector=FALSE)
fetchManifest <- function(project, asset, version, registry, url, cache=NULL, forceRemote=FALSE, overwrite=FALSE) {
if (!forceRemote && file.exists(registry)) {
path <- file.path(registry, project, asset, version, "..manifest")
} else {
cache <- local_registry(cache, url)
path <- acquire_file(cache, paste(project, asset, version, sep="/"), "..manifest", url=url, overwrite=overwrite)
}
fromJSON(path, simplifyVector=FALSE)
}
21 changes: 17 additions & 4 deletions R/fetchPermissions.R
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#' Fetch project permissions
#'
#' Fetch the permissions for a project.
#' This will call the REST API if the caller is not on the same filesystem as the registry.
#'
#' @param project String containing the project name.
#' @param registry String containing the path to the registry.
#' @inheritParams listProjects
#'
#' @return List containing the permissions for this project.
#' This has the following elements:
Expand Down Expand Up @@ -38,12 +39,24 @@
#' uploaders=list(list(id="urmom", until=Sys.time() + 1000)))
#'
#' # Fetching the permissions.
#' fetchPermissions("test", registry=info$registry)
#' fetchPermissions("test", registry=info$registry, url=info$url)
#'
#' # Forcing remote access.
#' fetchPermissions("test", registry=info$registry, url=info$url, forceRemote=TRUE)
#'
#' @export
#' @importFrom jsonlite fromJSON
fetchPermissions <- function(project, registry) {
perms <- fromJSON(file.path(registry, project, "..permissions"), simplifyVector=FALSE)
#' @import httr2
fetchPermissions <- function(project, registry, url, forceRemote=FALSE) {
if (file.exists(registry) && !forceRemote) {
content <- file.path(registry, project, "..permissions")
} else {
req <- request(paste0(url, "/fetch/", paste(project, "..permissions", sep="/")))
resp <- req_perform(req)
content <- resp_body_string(resp)
}

perms <- fromJSON(content, simplifyVector=FALSE)

# Converting everything to POSIX dates.
for (i in seq_along(perms$uploaders)) {
Expand Down
26 changes: 22 additions & 4 deletions R/fetchSummary.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#' Fetch version summary
#'
#' Fetch the summary for a version of an asset of a project.
#' This will call the REST API if the caller is not on the same filesystem as the registry.
#'
#' @inheritParams fetchManifest
#'
Expand All @@ -26,11 +27,28 @@
#' res <- uploadDirectory("test", "simple", "v1", src, staging=info$staging, url=info$url)
#'
#' # Obtain a summary for this version.
#' fetchSummary("test", "simple", "v1", registry=info$registry)
#'
#' fetchSummary("test", "simple", "v1", registry=info$registry, url=info$url)
#'
#' # Force remote access.
#' fetchSummary(
#' "test",
#' "simple",
#' "v1",
#' registry=info$registry,
#' url=info$url,
#' forceRemote=TRUE
#' )
#' @export
fetchSummary <- function(project, asset, version, registry) {
out <- fromJSON(file.path(registry, project, asset, version, "..summary"), simplifyVector=FALSE)
#' @importFrom jsonlite fromJSON
fetchSummary <- function(project, asset, version, registry, url, cache=NULL, forceRemote=FALSE, overwrite=FALSE) {
if (!forceRemote && file.exists(registry)) {
path <- file.path(registry, project, asset, version, "..summary")
} else {
cache <- local_registry(cache, url)
path <- acquire_file(cache, paste(project, asset, version, sep="/"), "..summary", url=url, overwrite=overwrite)
}

out <- fromJSON(path, simplifyVector=FALSE)
out$upload_start <- cast_datetime(out$upload_start)
out$upload_finish <- cast_datetime(out$upload_finish)
out
Expand Down
15 changes: 12 additions & 3 deletions R/fetchUsage.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#' Fetch the quota usage for a project.
#'
#' @param project String containing the project name.
#' @param registry String containing the path to the registry.
#' @inheritParams listProjects
#'
#' @return Numeric scalar specifying the quota usage for the project, in bytes.
#'
Expand All @@ -28,7 +28,16 @@
#'
#' @export
#' @importFrom jsonlite fromJSON
fetchUsage <- function(project, registry) {
out <- fromJSON(file.path(registry, project, "..usage"), simplifyVector=FALSE)
#' @import httr2
fetchUsage <- function(project, registry, url, forceRemote=FALSE) {
if (file.exists(registry) && !forceRemote) {
content <- file.path(registry, project, "..usage")
} else {
req <- request(paste0(url, "/fetch/", paste(project, "..usage", sep="/")))
resp <- req_perform(req)
content <- resp_body_string(resp)
}

out <- fromJSON(content, simplifyVector=FALSE)
out$total
}
12 changes: 8 additions & 4 deletions R/listAssets.R
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#' List assets
#'
#' List all assets in a project.
#' This will call the REST API if the caller is not on the same filesystem as the registry.
#'
#' @param project String containing the project name.
#' @param registry String containing a path to the registry.
#' @inheritParams listProjects
#'
#' @author Aaron Lun
#'
Expand All @@ -21,9 +22,12 @@
#' }
#'
#' # Listing available assets:
#' listAssets("test", registry=info$registry)
#' listAssets("test", registry=info$registry, url=info$url)
#'
#' # Force remote listing:
#' listAssets("test", registry=info$registry, url=info$url, forceRemote=TRUE)
#'
#' @export
listAssets <- function(project, registry) {
list.files(file.path(registry, project))
listAssets <- function(project, registry, url, forceRemote=FALSE) {
list_registry_directories(project, registry, url, forceRemote)
}
Loading

0 comments on commit 3a0025c

Please sign in to comment.