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}
0 commit comments