Skip to content

Commit 08352e9

Browse files
committed
[annotation] FSA refactor pass. Server-side storage API that mimics the FSA client-side API.
1 parent 7fe154b commit 08352e9

5 files changed

Lines changed: 226 additions & 64 deletions

File tree

R/mod_fsa.R

Lines changed: 179 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1-
fsa_init <- function(input_id, session = shiny::getDefaultReactiveDomain()) {
1+
# TODO: Rename file. No longer FSA-specific. Not really a davinci or shiny "module".
2+
3+
# Web browser File System Access abstraction.
4+
# Client-side coarse-grained file system API. The server sends file system commands as custom messages to the browser.
5+
# The browser uses the (Chrome-specific, experimental) File System Access API
6+
# (Intro: https://developer.chrome.com/docs/capabilities/web-apis/file-system-access,
7+
# Spec: https://wicg.github.io/file-system-access/)
8+
# to perform the requested actions. It reports the results back through pre-allocated shiny inputs.
9+
fsa_init <- function(input, input_id, callbacks, session = shiny::getDefaultReactiveDomain()) {
210

311
checkmate::assert_string(input_id, min.chars = 1)
412

13+
checkmate::assert_list(callbacks, types = "function") # in shiny apps, members will likely be reactiveVals
14+
checkmate::assert_set_equal(
15+
names(callbacks), c("attach", "list", "read", "write", "append", "read_folder", "execute_IO_plan")
16+
)
17+
518
ns <- session[["ns"]]
619

720
attach_id <- paste0(input_id, "_attach")
@@ -15,54 +28,209 @@ fsa_init <- function(input_id, session = shiny::getDefaultReactiveDomain()) {
1528
.attach <- function() {
1629
session$sendCustomMessage("dv_fsa_attach", list(status_input_id = ns(attach_id)))
1730
}
31+
shiny::observe(callbacks[["attach"]](input[[attach_id]]))
1832

1933
.list <- function() {
2034
session$sendCustomMessage("dv_fsa_list", list(status_input_id = ns(list_id)))
2135
}
36+
shiny::observe(callbacks[["list"]](input[[list_id]]))
2237

2338
.read <- function(file_name, contents) {
2439
session$sendCustomMessage(
2540
"dv_fsa_read",
2641
list(status_input_id = ns(read_id), file_name = file_name))
2742
}
43+
shiny::observe(callbacks[["read"]](input[[read_id]]))
2844

2945
.write <- function(file_name, contents) {
3046
session$sendCustomMessage(
3147
"dv_fsa_write",
3248
list(status_input_id = ns(write_id), file_name = file_name, contents = contents))
3349
}
50+
shiny::observe(callbacks[["write"]](input[[write_id]]))
3451

3552
.append <- function(file_name, contents) {
3653
session$sendCustomMessage(
3754
"dv_fsa_append",
3855
list(status_input_id = ns(read_id), file_name = file_name, contents = contents))
3956
}
57+
shiny::observe(callbacks[["append"]](input[[append_id]]))
4058

4159
.read_folder <- function(subfolder_candidates) {
4260
session$sendCustomMessage(
4361
"dv_fsa_read_folder",
4462
list(status_input_id = ns(read_folder_id), subfolder_candidates = base::I(subfolder_candidates)))
4563
}
64+
shiny::observe({
65+
folder_structure_base64_decode <- function(encoded_struct) {
66+
decoded_struct <- encoded_struct
67+
for (dataset_nm in names(encoded_struct)) {
68+
for (file_nm in names(encoded_struct[[dataset_nm]])) {
69+
encoded_contents <- encoded_struct[[dataset_nm]][[file_nm]][["contents"]]
70+
if (!is.null(encoded_contents)) {
71+
decoded_contents <- base64enc::base64decode(encoded_contents)
72+
} else {
73+
decoded_contents <- NULL
74+
}
75+
decoded_struct[[dataset_nm]][[file_nm]][["contents"]] <- decoded_contents
76+
}
77+
}
78+
return(decoded_struct)
79+
}
80+
81+
encoded_folder_contents <- input[[read_folder_id]]
82+
shiny::req(is.list(encoded_folder_contents))
83+
decoded_folder_contents <- folder_structure_base64_decode(encoded_folder_contents)
84+
callbacks[["read_folder"]](decoded_folder_contents)
85+
})
4686

4787
.execute_IO_plan <- function(IO_plan, is_init = FALSE) {
88+
IO_plan_base64_encode <- function(plan) {
89+
encoded_plan <- plan
90+
for (idx in seq_along(plan)) {
91+
if (encoded_plan[[idx]][["type"]] == "write_file") {
92+
encoded_plan[[idx]][["contents"]] <- base64enc::base64encode(encoded_plan[[idx]][["contents"]])
93+
}
94+
}
95+
return(encoded_plan)
96+
}
97+
98+
IO_plan_base64 <- IO_plan_base64_encode(IO_plan)
99+
48100
session$sendCustomMessage(
49101
"dv_fsa_execute_io_plan",
50-
list(status_input_id = ns(execute_IO_plan_id), plan = IO_plan, is_init = is_init)
102+
list(status_input_id = ns(execute_IO_plan_id), plan = IO_plan_base64, is_init = is_init)
51103
)
52104
}
105+
shiny::observe(callbacks[["execute_IO_plan"]](input[[execute_IO_plan_id]]))
53106

54-
.show_overlay <- function(message) {session$sendCustomMessage("dv_fsa_show_overlay", list(message = message))}
107+
.show_overlay <- function(message) { # nolint
108+
session$sendCustomMessage("dv_fsa_show_overlay", list(message = message))
109+
}
55110

56-
.hide_overlay <- function() {session$sendCustomMessage("dv_fsa_hide_overlay", list())}
111+
.hide_overlay <- function() { # nolint
112+
session$sendCustomMessage("dv_fsa_hide_overlay", list())
113+
}
57114

58115
res <- list(
59-
attach = list(f = .attach, id = attach_id),
60-
list = list(f = .list, id = list_id),
61-
write = list(f = .write, id = write_id),
62-
read = list(f = .read, id = read_id),
63-
append = list(f = .append, id = append_id),
64-
read_folder = list(f = .read_folder, id = read_folder_id),
65-
execute_IO_plan = list(f = .execute_IO_plan, id = execute_IO_plan_id)
116+
attach = .attach, list = .list, read = .read, write = .write, append = .append, read_folder = .read_folder,
117+
execute_IO_plan = .execute_IO_plan
118+
)
119+
return(res)
120+
}
121+
122+
# Server-side File System Access abstraction.
123+
# Pure R implementation of the `fsa_init` file system API. It allows to:
124+
# - Test the module end to end without user intervention.
125+
# - Provide a server-only implementation for shiny hosting services that offer app-specific storage.
126+
fs_init <- function(callbacks, path) {
127+
checkmate::assert_list(callbacks, types = "function") # in shiny apps, these will likely be reactiveVals
128+
checkmate::assert_set_equal(
129+
names(callbacks), c("attach", "list", "read", "write", "append", "read_folder", "execute_IO_plan")
130+
)
131+
132+
path <- normalizePath(path) # remove trailing slash, etc.
133+
134+
res <- list(
135+
attach = function() {
136+
v <- list(connected = TRUE, name = basename(path), error = NULL)
137+
callbacks[["attach"]](v)
138+
},
139+
list = function() {
140+
callbacks[["list"]](error = "Not implemented")
141+
},
142+
read = function(file_name, contents) {
143+
callbacks[["read"]](error = "Not implemented")
144+
},
145+
write = function(file_name, contents) {
146+
callbacks[["write"]](error = "Not implemented")
147+
},
148+
append = function(file_name, contents) {
149+
callbacks[["append"]](error = "Not implemented")
150+
},
151+
read_folder = function(subfolder_candidates) {
152+
# NOTE: Adapted from:
153+
# https://github.com/dull-systems/yours_truelib/blob/441740eb02fc9a9029c63c6e3c1d56c5ad638d97/YT.R#L153-L166
154+
read_file_set <- function(paths) {
155+
# Provides a consistent view of a set of files by checking that they don't change while we read them.
156+
# Gets their mtimes and sizes; reads their contents; asserts that mtimes and sizes have not changed;
157+
# returns contents, mtimes and sizes.
158+
res <- list()
159+
file_info <- file.info(paths)
160+
no_size <- paths[!is.finite(file_info$size)]
161+
if (length(no_size) > 0)
162+
return(simpleCondition(sprintf("Could not get file size for: `%s`.", paste(no_size, collapse = ", "))))
163+
164+
for (path in paths){
165+
res[[basename(path)]] <- list(
166+
size = file_info[path, "size"],
167+
time = as.numeric(file_info[path, "mtime"]),
168+
contents = readBin(con = path, what = raw(), n = file_info[path, "size"]),
169+
error = NULL
170+
)
171+
}
172+
file_info_after <- file.info(paths)
173+
altered <- paths[rowSums(file_info[c("size", "mtime")] != file_info_after[c("size", "mtime")]) != 0]
174+
if (length(altered) > 0)
175+
return(simpleCondition(sprintf("Files changed while reading them: %s", paste(altered, collapse = ", "))))
176+
return(res)
177+
}
178+
179+
v <- list()
180+
subfolders <- file.path(path, subfolder_candidates)
181+
for (subfolder in subfolders) {
182+
contents <- read_file_set(list.files(subfolder, full.names = TRUE, recursive = FALSE))
183+
if (inherits(contents, "condition")) {
184+
# NOTE: early out
185+
callbacks[["read_folder"]](list(error = contents[["message"]]))
186+
return()
187+
}
188+
v[[basename(subfolder)]] <- contents
189+
}
190+
callbacks[["read_folder"]](v)
191+
},
192+
execute_IO_plan = function(IO_plan, is_init = FALSE) {
193+
first_error_message <- NULL
194+
for (i_command in seq_along(IO_plan)) {
195+
command <- IO_plan[[i_command]]
196+
IO_plan[[i_command]][["error"]] <- NULL
197+
if (command[["type"]] == "write_file") {
198+
if (command[["mode"]] != "bin") {
199+
first_error_message <- "The only supported write mode is `bin`"
200+
break
201+
}
202+
fname <- file.path(path, command[["path"]], command[["fname"]])
203+
dname <- dirname(fname)
204+
dir.create(dname, showWarnings = FALSE, recursive = TRUE)
205+
writeBin(command[["contents"]], fname) # TODO: Checks
206+
} else if (command[["type"]] == "append_file") {
207+
if (command[["mode"]] != "bin") {
208+
first_error_message <- "The only supported append mode is `bin`"
209+
break
210+
}
211+
fname <- file.path(path, command[["path"]], command[["fname"]])
212+
con <- file(fname, open = "ab")
213+
on.exit(close(con))
214+
writeBin(command[["contents"]], con) # TODO: Checks
215+
} else {
216+
first_error_message <- sprintf("Command type '%s' not supported yet", command[["type"]])
217+
break
218+
}
219+
}
220+
221+
if (is.character(first_error_message)) {
222+
IO_plan[[i_command]][["error"]] <- first_error_message
223+
# we ignore commands following the one that failed, in case they depend on its correct execution
224+
i_command <- i_command + 1
225+
while (i_command <= length(IO_plan)) {
226+
IO_plan[[i_command]][["error"]] <- sprintf("Ignored command due to previous error")
227+
i_command <- i_command + 1
228+
}
229+
}
230+
231+
callbacks[["execute_IO_plan"]](list(status = IO_plan, is_init = is_init))
232+
}
66233
)
234+
67235
return(res)
68236
}

R/mod_listings.R

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,22 @@ listings_server <- function(module_id,
413413
REV_state <- new.env(parent = emptyenv())
414414
if (enable_review) {
415415

416-
fsa_client <- fsa_init(TBL$FSA_CLIENT)
416+
fs_callbacks <- list(
417+
attach = shiny::reactiveVal(NULL),
418+
list = shiny::reactiveVal(NULL),
419+
read = shiny::reactiveVal(NULL),
420+
write = shiny::reactiveVal(NULL),
421+
append = shiny::reactiveVal(NULL),
422+
read_folder = shiny::reactiveVal(NULL),
423+
execute_IO_plan = shiny::reactiveVal(NULL)
424+
)
425+
426+
fs_client <- NULL
427+
if (is.null(review[["store_path"]])) {
428+
fs_client <- fsa_init(input, TBL$FSA_CLIENT, fs_callbacks)
429+
} else {
430+
fs_client <- fs_init(fs_callbacks, review[["store_path"]])
431+
}
417432

418433
output[[TBL$REVIEW_UI_ID]] <- shiny::renderUI(
419434
shinyWidgets::dropdownButton(
@@ -437,7 +452,7 @@ listings_server <- function(module_id,
437452

438453
# TODO: Extract the REV_logic_1 logic, it just creates a set of observers that maybe better
439454
# located out here. Otherwise this observer declarations may be ignored.
440-
REV_logic_1(REV_state, input, review, review[["data"]], fsa_client)
455+
REV_logic_1(REV_state, input, review, review[["data"]], fs_client, fs_callbacks)
441456
show_review_columns <- REV_state[["contents_ready"]]
442457
}
443458

@@ -529,7 +544,7 @@ listings_server <- function(module_id,
529544
selected_dataset_name = shiny::reactive(input[[TBL$DATASET_ID]]),
530545
data = shiny::reactive(output_table_data()[["data"]]),
531546
dt_proxy = dt_proxy,
532-
fsa_client = fsa_client
547+
fs_execute_IO_plan = fs_client[["execute_IO_plan"]]
533548
)
534549
}
535550

0 commit comments

Comments
 (0)