3

I would like to have a box in my shiny app, which user can browse to a folder structure and select a file to download. I have tried the shinyFiles but the file selection is a pop-up window and I could just download a single file :

library(shiny)
library(shinyFiles)


ui <- fluidPage( 
  shinyFilesButton('files', label='File select', title='Please select a file', multiple=T) ,
  verbatimTextOutput('rawInputValue'),
  verbatimTextOutput('filepaths') ,
  downloadButton("downloadFiles", "Download Files")
)

server <- function(input, output) {
  
  roots =  c(wd = 'H:/')
  
  shinyFileChoose(input, 'files', 
                  roots =  roots, 
                  filetypes=c('', 'txt' , 'gz' , 'md5' , 'pdf' , 'fasta' , 'fastq' , 'aln'))
  
  output$rawInputValue <- renderPrint({str(input$files)})
  
  output$filepaths <- renderPrint({parseFilePaths(roots, input$files)})
  
  output$downloadFiles <- downloadHandler(
    filename = function() {
      as.character(parseFilePaths(roots, input$files)$name)
    },
    content = function(file) {
      fullName <- as.character(parseFilePaths(roots, input$files)$datapath)
      file.copy(fullName, file)
    }
  )
}

shinyApp(ui = ui , server = server)

What I would like is to have the file selection option like this

within the ui, not as new (pop-up) window !

Haribo
  • 2,071
  • 17
  • 37
  • 1
    Can you show demonstration in any language (not necessarily R) where the browser interface downloads multiple independent files with one click? In my experience, the *selection* of one or more files is one thing, but generally the download is typically a single `.zip` or `.tar.gz` or similar. Perhaps you should focus first on how to "select" multiple files, and then you can internally zip/tarball them into a single file for download. – r2evans May 23 '22 at 15:33
  • @r2evans,I meant exactly the same, user select files and will get them in a single zip file as download . I have added a shiny app which you could select multiple files and download them in a zip file ! – Haribo May 23 '22 at 15:38
  • 1
    I don't see any zip step in your code. If your folder is not too deep, you can try with the **jsTreeR** package. – Stéphane Laurent May 23 '22 at 20:03
  • @StéphaneLaurent, thanks for the reply, my main concern is how to get something like the `shinyFiles` in the code but not as pop-up ! as I shown in the picture attached – Haribo May 23 '22 at 20:16
  • What about your feedback when one tells you something?... You said to r2evans that your app allows to *select multiple files and download them in a zip file*. I replied I don't see any zip step and you don't react. And I told you about **jsTreeR**, no feedback as well. Do you really want some help? You don't seem to. – Stéphane Laurent May 24 '22 at 05:28
  • @StéphaneLaurent, In `jsTreeR` or `shinyTree` you explicitly define your folder structure, do you have an example how one could show a server or local machine folder structure with those packages ?! – Haribo May 24 '22 at 12:35
  • 1
    See the "folder gadget" in the jsTreeR package. The problem is that the app loads all the contents of the folder at the startup (subfolders, subsubfolders, etc), so this is problematic when the folder is big. I'm currently working on a Shiny app with jsTreeR which provides a folder navigator without this problem, i.e. it loads only the first level of a folder contents only when the user selects this folder. I think I will get a correct app within tomorrow. Screenshot of the current status: – Stéphane Laurent May 24 '22 at 14:00
  • @StéphaneLaurent, oh, that is great and exactly what I am looking for ! By any chance will you publish your app (source code) somewhere ? – Haribo May 24 '22 at 15:04

1 Answers1

4

Here is a first version of the app I talked about in my comment. Its advantage is that the contents of a folder are loaded only when the user selects this folder, and only the first descendants are loaded, no recursion.

enter image description here

App folder structure:

C:\PATH\TO\MYAPP
|   global.R
|   server.R
|   ui.R
|
\---www
        navigator.css
        navigator.js

File global.R:

library(shiny)
library(jsTreeR)
library(htmlwidgets)
library(magrittr)
library(shinyFiles)

roots <- c(wd = "C:/SL/MyPackages/", getVolumes()())

File server.R:

shinyServer(function(input, output, session){

  shinyDirChoose(
    input, "rootfolder", roots = roots,
    allowDirCreate = FALSE, defaultRoot = "wd"
  )

  RootFolder <- eventReactive(input[["rootfolder"]], {
    parseDirPath(roots, input[["rootfolder"]])
  })

  output[["choice"]] <- reactive({
    isTruthy(RootFolder())
  })
  outputOptions(output, "choice", suspendWhenHidden = FALSE)

  output[["navigator"]] <- renderJstree({
    req(isTruthy(RootFolder()))
    jstree(
      nodes = list(
        list(
          text = RootFolder(),
          type = "folder",
          children = FALSE,
          li_attr = list(
            class = "jstree-x"
          )
        )
      ),
      types = list(
        folder = list(
          icon = "fa fa-folder gold"
        ),
        file = list(
          icon = "far fa-file red"
        )
      ),
      checkCallback = TRUE,
      theme = "default",
      checkboxes = TRUE,
      search = TRUE,
      selectLeavesOnly = TRUE
    ) %>% onRender("function(el, x){tree = $(el).jstree(true);}")
  })

  observeEvent(input[["path"]], {
    lf <- list.files(input[["path"]], full.names = TRUE)
    fi <- file.info(lf, extra_cols = FALSE)
    x <- list(
      elem = as.list(basename(lf)),
      folder = as.list(fi[["isdir"]])
    )
    session$sendCustomMessage("getChildren", x)
  })

  Paths <- reactive({
    vapply(
      input[["navigator_selected_paths"]], `[[`,
      character(1L), "path"
    )
  })

  output[["selections"]] <- renderPrint({
    cat(Paths(), sep = "\n")
  })

  output[["dwnld"]] <- downloadHandler(
    filename = "myfiles.zip",
    content = function(file){
      zip(file, files = Paths())
    }
  )

})

File ui.R:

shinyUI(fluidPage(
  tags$head(
    tags$link(rel = "stylesheet", href = "navigator.css"),
    tags$script(src = "navigator.js")
  ),
  br(),
  conditionalPanel(
    condition = "!output.choice",
    fluidRow(
      column(
        width = 12,
        shinyDirButton(
          "rootfolder",
          label = "Browse to choose a root folder",
          title = "Choose a folder",
          buttonType = "primary",
          class = "btn-block"
        )
      )
    )
  ),
  conditionalPanel(
    condition = "output.choice",
    style = "display: none;",
    fluidRow(
      column(
        width = 6,
        jstreeOutput("navigator")
      ),
      column(
        width = 6,
        tags$fieldset(
          tags$legend(
            tags$h1("Selections:", style = "float: left;"),
            downloadButton(
              "dwnld",
              class = "btn-primary btn-lg",
              icon = icon("save"),
              style = "float: right;"
            )
          ),
          verbatimTextOutput("selections")
        )
      )
    )
  )
))

File navigator.css:

.jstree-default .jstree-x.jstree-closed > .jstree-icon.jstree-ocl,
.jstree-default .jstree-x.jstree-leaf > .jstree-icon.jstree-ocl {
  background-position: -100px -4px;
}

.red {
  color: red;
}
.gold {
  color: gold;
}
.jstree-proton {
  font-weight: bold;
}
.jstree-anchor {
  font-size: medium;
}

File navigator.js:

var tree;

$(document).ready(function () {
  var Children = null;

  Shiny.addCustomMessageHandler("getChildren", function (x) {
    Children = x;
  });

  $("#navigator").on("click", "li.jstree-x > i", function (e) {
    var $li = $(this).parent();
    if (!$li.hasClass("jstree-x")) {
      alert("that should not happen...");
      return;
    }
    var id = $li.attr("id");
    var node = tree.get_node(id);
    if (tree.is_leaf(node) && node.original.type === "folder") {
      var path = tree.get_path(node, "/");
      Shiny.setInputValue("path", path);
      var interval = setInterval(function () {
        if (Children !== null) {
          clearInterval(interval);
          for (var i = 0; i < Children.elem.length; i++) {
            var isdir = Children.folder[i];
            var newnode = tree.create_node(id, {
              text: Children.elem[i],
              type: isdir ? "folder" : "file",
              children: false,
              li_attr: isdir ? { class: "jstree-x" } : null
            });
          }
          Children = null;
          setTimeout(function () {
            tree.open_node(id);
          }, 10);
        }
      }, 100);
    }
  });
});

(I am the author of jsTreeR and I think I will do a Shiny module for this folder navigator and include it in the package.)


EDIT

I improved the app and it uses the proton theme now, which looks more pretty to me:

enter image description here

To use this app, you first need the updated version of the package:

remotes::install_github("stla/jsTreeR")

There are some changes in three files:

  • server.R:
shinyServer(function(input, output, session){

  shinyDirChoose(
    input, "rootfolder", roots = roots,
    allowDirCreate = FALSE, defaultRoot = "wd"
  )

  RootFolder <- eventReactive(input[["rootfolder"]], {
    parseDirPath(roots, input[["rootfolder"]])
  })

  output[["choice"]] <- reactive({
    isTruthy(RootFolder())
  })
  outputOptions(output, "choice", suspendWhenHidden = FALSE)

  output[["navigator"]] <- renderJstree({
    req(isTruthy(RootFolder()))
    jstree(
      nodes = list(
        list(
          text = RootFolder(),
          type = "folder",
          children = FALSE,
          li_attr = list(
            class = "jstree-x"
          )
        )
      ),
      types = list(
        folder = list(
          icon = "fa fa-folder gold"
        ),
        file = list(
          icon = "far fa-file red"
        )
      ),
      checkCallback = TRUE,
      theme = "proton",
      checkboxes = TRUE,
      search = TRUE,
      selectLeavesOnly = TRUE
    )
  })

  observeEvent(input[["path"]], {
    lf <- list.files(input[["path"]], full.names = TRUE)
    fi <- file.info(lf, extra_cols = FALSE)
    x <- list(
      elem = as.list(basename(lf)),
      folder = as.list(fi[["isdir"]])
    )
    session$sendCustomMessage("getChildren", x)
  })

  Paths <- reactive({
    vapply(
      input[["navigator_selected_paths"]], `[[`,
      character(1L), "path"
    )
  })

  output[["selections"]] <- renderPrint({
    cat(Paths(), sep = "\n")
  })

  output[["dwnld"]] <- downloadHandler(
    filename = "myfiles.zip",
    content = function(file){
      zip(file, files = Paths())
    }
  )

})
  • navigator.css:
.jstree-proton {
  font-weight: bold;
}

.jstree-anchor {
  font-size: medium;
}

.jstree-proton .jstree-x.jstree-closed > .jstree-icon.jstree-ocl,
.jstree-proton .jstree-x.jstree-leaf > .jstree-icon.jstree-ocl {
  background-position: -101px -5px;
}

.jstree-proton .jstree-checkbox.jstree-checkbox-disabled {
  background-position: -37px -69px;
}

.red {
  color: red;
}

.gold {
  color: gold;
}
  • navigator.js:
$(document).ready(function () {
  var tree;

  var Children = null;

  Shiny.addCustomMessageHandler("getChildren", function (x) {
    Children = x;
  });

  $navigator = $("#navigator");

  $navigator.one("ready.jstree", function (e, data) {
    tree = data.instance;
    tree.disable_checkbox("j1_1");
    tree.disable_node("j1_1");
  });

  $navigator.on("after_open.jstree", function (e, data) {
    tree.enable_checkbox(data.node);
    tree.enable_node(data.node);
  });

  $navigator.on("after_close.jstree", function (e, data) {
    tree.disable_checkbox(data.node);
    tree.disable_node(data.node);
  });

  $navigator.on("click", "li.jstree-x > i", function (e) {
    var $li = $(this).parent();
    if (!$li.hasClass("jstree-x")) {
      alert("that should not happen...");
      return;
    }
    var id = $li.attr("id");
    var node = tree.get_node(id);
    if (tree.is_leaf(node) && node.original.type === "folder") {
      var path = tree.get_path(node, "/");
      Shiny.setInputValue("path", path);
      var interval = setInterval(function () {
        if (Children !== null) {
          clearInterval(interval);
          for (var i = 0; i < Children.elem.length; i++) {
            var isdir = Children.folder[i];
            var newnode = tree.create_node(id, {
              text: Children.elem[i],
              type: isdir ? "folder" : "file",
              children: false,
              li_attr: isdir ? { class: "jstree-x" } : null
            });
            if (isdir) {
              tree.disable_checkbox(newnode);
              tree.disable_node(newnode);
            }
          }
          Children = null;
          setTimeout(function () {
            tree.open_node(id);
          }, 10);
        }
      }, 100);
    }
  });
});

EDIT 2

The new version of the package provides a Shiny module allowing to conveniently renders such a 'tree navigator' (or even several ones). This is the example given in the package:

library(shiny)
library(jsTreeR)

css <- HTML("
  .flexcol {
    display: flex;
    flex-direction: column;
    width: 100%;
    margin: 0;
  }
  .stretch {
    flex-grow: 1;
    height: 1px;
  }
  .bottomright {
    position: fixed;
    bottom: 0;
    right: 15px;
    min-width: calc(50% - 15px);
  }
")

ui <- fixedPage(
  tags$head(
    tags$style(css)
  ),
  class = "flexcol",

  br(),

  fixedRow(
    column(
      width = 6,
      treeNavigatorUI("explorer")
    ),
    column(
      width = 6,
      tags$div(class = "stretch"),
      tags$fieldset(
        class = "bottomright",
        tags$legend(
          tags$h1("Selections:", style = "float: left;"),
          downloadButton(
            "dwnld",
            class = "btn-primary btn-lg",
            style = "float: right;",
            icon  = icon("save")
          )
        ),
        verbatimTextOutput("selections")
      )
    )
  )
)

server <- function(input, output, session){

  Paths <- treeNavigatorServer(
    "explorer", rootFolder = getwd(),
    search = list( # (search in the visited folders only)
      show_only_matches  = TRUE,
      case_sensitive     = TRUE,
      search_leaves_only = TRUE
    )
  )

  output[["selections"]] <- renderPrint({
    cat(Paths(), sep = "\n")
  })

}

shinyApp(ui, server)
Stéphane Laurent
  • 75,186
  • 15
  • 119
  • 225
  • Could you please give me a hint how do I modify the provided answer ? I would like the app started from the point that we have a defined path and would like to see and select the tree structure !! – Haribo May 27 '22 at 18:58
  • 1
    @user9112767 Sorry for the delay. The fastest way is to do `RootFolder <- function() "path/to/your/folder"`. Of course the function is useless here, you can put the path directly in `RootFolder` but then removes the parentheses in `RootFolder()`. Remove the `req(......)` as well. – Stéphane Laurent May 27 '22 at 19:48
  • @user9112767 I've just edited my answer to add the second version. Please take a look. By the way do you also have some warnings from **shinyFiles**? – Stéphane Laurent May 28 '22 at 00:15
  • Yes I do have some warnings as well. but also I can not run the new version ! I re-installed the package to get the updated version , but : `Warning: Error in treeNavigatorUI: could not find function "treeNavigatorUI"` – Haribo May 30 '22 at 09:11
  • @user9112767 You have to install the new version of the package. – Stéphane Laurent May 30 '22 at 09:23
  • I just did ! `packageVersion("jsTreeR") [1] ‘1.6.0’` – Haribo May 30 '22 at 09:30
  • @user9112767 You have to install the Github version. – Stéphane Laurent May 30 '22 at 09:32