Skip to content

Commit e04811b

Browse files
authored
fix: Writing binary files & subdirectories (#2)
* Add new `write_file_content()` function to handle writing text & binary files. Also, handle writing files out to subdirectories if present in the path to the file. * Explicitly use `encoding = "UTF-8"` in `httr::content()` and use `useBytes = TRUE` in `writeBin()`. * Add NEWS file
1 parent 895df2e commit e04811b

File tree

6 files changed

+170
-13
lines changed

6 files changed

+170
-13
lines changed

NEWS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# peeky 0.1.0
2+
3+
- `peeky` is a package that allows users to peek at the source code of a
4+
Shinylive application.
5+
- It works with both standalone and embedded
6+
Shinylive applications written using either R or Python.
7+
- The package provides the following functions:
8+
- `peek_shinylive_app()` is the primary function for examining Shinylive
9+
applications.
10+
- `peek_standalone_shinylive_app()` is the primary function for examining
11+
standalone Shinylive applications.
12+
- `peek_quarto_shinylive_app()` is the primary function for examining
13+
Quarto documents with embedded Shinylive applications.

R/find.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ find_shinylive_app_json <- function(base_url) {
5656

5757
# If this is already JSON content, verify it's a valid app.json
5858
if (grepl("application/json", httr::headers(resp)[["content-type"]], fixed = TRUE)) {
59-
content <- httr::content(resp, "text")
59+
content <- httr::content(resp, "text", encoding = "UTF-8")
6060
# Try to parse as JSON and validate structure
6161
json_data <- jsonlite::fromJSON(content, simplifyDataFrame = FALSE)
6262
if (validate_app_json(json_data)) {

R/peek.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ peek_shinylive_app <- function(url, output_dir = "converted_shiny_app") {
8989

9090
# If HTML, determine if it's a Quarto document
9191
if (grepl("text/html", content_type, fixed = TRUE)) {
92-
html_content <- httr::content(resp, "text")
92+
html_content <- httr::content(resp, "text", encoding = "UTF-8")
9393
doc <- rvest::read_html(html_content)
9494

9595
# Check if it's a Quarto document (has main.content and quarto-specific elements)
@@ -215,7 +215,7 @@ peek_quarto_shinylive_app <- function(url,
215215
))
216216
}
217217

218-
html_content <- httr::content(resp, "text")
218+
html_content <- httr::content(resp, "text", encoding = "UTF-8")
219219

220220
# Find and parse all shinylive code blocks
221221
apps <- find_shinylive_code(html_content)

R/writers.R

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,74 @@
1+
#' Write File Content in a Shinylive App to Disk
2+
#'
3+
#' Writes file content extracted from Shinylive applications to disk, handling both
4+
#' text and binary content appropriately. Creates any necessary parent directories
5+
#' and ensures proper encoding of content. For binary files, automatically decodes
6+
#' the base64-encoded content before writing.
7+
#'
8+
#' @param content Character string containing the file content. For binary files,
9+
#' this should be base64-encoded content. For text files, this should be the raw
10+
#' text content.
11+
#' @param file_path Character string specifying the path where the file should be
12+
#' written. Parent directories will be created if they don't exist.
13+
#' @param type Character string specifying the file type, either "text" (default)
14+
#' or "binary". Binary files are assumed to be base64 encoded, as this is the
15+
#' standard format for binary content in Shinylive applications.
16+
#'
17+
#' @return Invisible NULL, called for its side effect of writing a file to disk.
18+
#'
19+
#' @details
20+
#' The function handles two types of content:
21+
#'
22+
#' * Text files (`type = "text"`):
23+
#' - Content is converted to UTF-8 encoding using `enc2utf8()`
24+
#' - Written using `writeLines()` with `useBytes = TRUE`
25+
#'
26+
#' * Binary files (`type = "binary"`):
27+
#' - Content is decoded from base64 using `jsonlite::base64_dec()`
28+
#' - Written as raw binary data using `writeBin()`
29+
#'
30+
#' Parent directories in the file path are automatically created if they don't
31+
#' exist using `fs::dir_create()` with `recurse = TRUE`.
32+
#'
33+
#' @examples
34+
#' \dontrun{
35+
#' # Writing a text file
36+
#' write_file_content(
37+
#' content = "library(shiny)\n\nui <- fluidPage()",
38+
#' file_path = "app/app.R",
39+
#' type = "text"
40+
#' )
41+
#'
42+
#' # Writing a binary file (base64-encoded content)
43+
#' write_file_content(
44+
#' content = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
45+
#' file_path = "app/www/image.png",
46+
#' type = "binary"
47+
#' )
48+
#' }
49+
#'
50+
#' @keywords internal
51+
write_file_content <- function(content, file_path, type = "text") {
52+
# Ensure parent directory exists
53+
parent_dir <- dirname(file_path)
54+
if (!dir.exists(parent_dir)) {
55+
fs::dir_create(parent_dir, recurse = TRUE)
56+
}
57+
58+
# Handle content based on type
59+
if (type == "binary") {
60+
# Decode base64 content and write as binary
61+
decoded <- jsonlite::base64_dec(content)
62+
writeBin(decoded, file_path, useBytes = TRUE)
63+
} else {
64+
# Write as text
65+
writeLines(enc2utf8(content), file_path, useBytes = TRUE)
66+
}
67+
68+
invisible(NULL)
69+
}
70+
71+
172
#' Write Shinylive Applications to a Quarto Document
273
#'
374
#' Converts a list of parsed Shinylive applications into a single Quarto document.
@@ -253,8 +324,7 @@ write_apps_to_quarto <- function(apps, qmd_path) {
253324
#' write_apps_to_dirs(apps, "extracted_apps")
254325
#' }
255326
write_apps_to_dirs <- function(apps, base_dir) {
256-
fs::dir_create(base_dir)
257-
327+
fs::dir_create(base_dir, recurse = TRUE)
258328

259329
# Calculate padding width based on number of apps
260330
number_padding <- padding_width(length(apps))
@@ -266,18 +336,19 @@ write_apps_to_dirs <- function(apps, base_dir) {
266336

267337
# Create numbered subdirectory using dynamic padding
268338
app_dir <- file.path(base_dir, sprintf(dir_format, i))
269-
fs::dir_create(app_dir)
339+
fs::dir_create(app_dir, recurse = TRUE)
270340

271341
# Write each file in the app
272342
for (file_name in names(app$files)) {
273343
file_data <- app$files[[file_name]]
274344
file_path <- file.path(app_dir, file_name)
275345

276-
# Ensure parent directory exists
277-
fs::dir_create(dirname(file_path))
278-
279-
# Write file content
280-
writeLines(file_data$content, file_path)
346+
# Write file as either a binary or text file
347+
write_file_content(
348+
content = file_data$content,
349+
file_path = file_path,
350+
type = file_data$type
351+
)
281352
}
282353

283354
# Write metadata
@@ -379,12 +450,18 @@ write_apps_to_dirs <- function(apps, base_dir) {
379450
#' @keywords internal
380451
write_standalone_shinylive_app <- function(json_data, source_url, output_dir = "converted_shiny_app") {
381452
# Create output directory
382-
fs::dir_create(output_dir)
453+
fs::dir_create(output_dir, recurse = TRUE)
383454

384455
# Extract files
385456
for (file in json_data) {
386457
file_path <- file.path(output_dir, file$name)
387-
writeLines(file$content, file_path)
458+
459+
# Write file as either a binary or text file
460+
write_file_content(
461+
content = file$content,
462+
file_path = file_path,
463+
type = file$type
464+
)
388465
}
389466

390467
# Return standalone command object

man/write_file_content.Rd

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

peeky.Rproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Version: 1.0
2+
ProjectId: 5f42eb5f-9f68-4233-a26b-6b6ad0403d55
23

34
RestoreWorkspace: Default
45
SaveWorkspace: Default

0 commit comments

Comments
 (0)