Download R/data.R from fmegahed/decisions: direct link, hf CLI and curl.
- Browser
- Download file 13 kB
-
https://huggingface.co/spaces/fmegahed/decisions/resolve/main/R/data.R
- Command line
-
hf download hf://spaces/fmegahed/decisions/R/data.R
-
curl -L -o data.R https://huggingface.co/spaces/fmegahed/decisions/resolve/main/R/data.R
13 kB
| # Data of the visitor: images (loose files or zip files), references, pasted | |
| # text and CSV files. Each reader returns a list with `inputs` (the inputs | |
| # tibble of spec.R) and `problems` (short sentences for the visitor). The | |
| # readers never stop on bad data: they skip it and say why. | |
| IMAGE_EXTENSIONS <- c("jpg", "jpeg", "png", "webp") | |
| # Bytes in a limit given in MB | |
| mb_bytes <- function(mb) mb * 1024^2 | |
| # TRUE when a file name is an image that Jev can read | |
| is_image_name <- function(x) tolower(tools::file_ext(x)) %in% IMAGE_EXTENSIONS | |
| # TRUE when a zip entry stays inside the folder of the zip: no absolute path, | |
| # no drive letter, no backslash and no `..` | |
| safe_entry <- function(x) { | |
| !startsWith(x, "/") && !grepl("^[A-Za-z]:", x) && !grepl("\\", x, fixed = TRUE) && | |
| !any(strsplit(x, "/", fixed = TRUE)[[1]] == "..") | |
| } | |
| # TRUE for the files that macOS and Windows add to a zip file: a folder | |
| # __MACOSX, a dot file or folder (._a.jpg, .DS_Store), Thumbs.db, desktop.ini | |
| is_junk <- function(x) { | |
| parts <- strsplit(x, "/", fixed = TRUE)[[1]] | |
| any(parts == "__MACOSX" | startsWith(parts, ".")) || | |
| tolower(utils::tail(parts, 1)) %in% c("thumbs.db", "desktop.ini") | |
| } | |
| # The inputs tibble with no rows | |
| empty_inputs <- function() { | |
| tibble::tibble( | |
| id = character(), path = character(), text = character(), | |
| file = character(), label = character() | |
| ) | |
| } | |
| # Unique ids: a running number joined with the file name, so that the same | |
| # name in two folders gives two ids. Spaces and odd characters become "_". | |
| make_ids <- function(file) { | |
| clean <- stringr::str_replace_all(file, "[^\\p{L}\\p{N}._-]+", "_") | |
| sprintf("%03d_%s", seq_along(file), clean) | |
| } | |
| # Unpack the safe image entries of a zip file into the folder `dest`. The | |
| # entries are counted from the listing first: when they are more than | |
| # `budget`, nothing is extracted. An entry over the file limit is never | |
| # extracted. One call to zip::unzip() extracts the rest, below `dest` only. | |
| # Returns the files (`rel`, `path`, `size`), the count of other files, the | |
| # count of images and the problems. | |
| unpack_zip <- function(zipfile, name, dest, budget = MAX_IMAGES) { | |
| entries <- tryCatch(zip::zip_list(zipfile), error = function(e) NULL) | |
| if (is.null(entries)) { | |
| return(list( | |
| files = NULL, other = 0L, count = 0L, | |
| problems = sprintf("%s could not be opened. Check that it is a zip file.", name) | |
| )) | |
| } | |
| kept <- entries |> | |
| dplyr::filter(!endsWith(filename, "/")) |> | |
| dplyr::filter(purrr::map_lgl(filename, safe_entry)) |> | |
| dplyr::filter(!purrr::map_lgl(filename, is_junk)) | |
| # A single top folder around the label folders is not a label | |
| parts <- strsplit(kept$filename, "/", fixed = TRUE) | |
| tops <- unique(purrr::map_chr(parts, 1)) | |
| rel <- kept$filename | |
| if (length(tops) == 1 && any(lengths(parts) >= 3)) { | |
| rel <- stringr::str_remove(rel, stringr::fixed(paste0(tops, "/"))) | |
| } | |
| kept <- kept |> dplyr::mutate(rel = rel) | |
| is_img <- is_image_name(kept$filename) | |
| too_big <- is_img & kept$uncompressed_size > mb_bytes(MAX_FILE_MB) | |
| use <- kept |> dplyr::filter(is_img, !too_big) | |
| problems <- sprintf("%s was skipped. A file can be at most %s MB.", basename(kept$rel[too_big]), MAX_FILE_MB) | |
| out <- list(files = NULL, other = sum(!is_img), count = nrow(use), problems = problems) | |
| if (nrow(use) == 0 || nrow(use) > budget) return(out) | |
| dir.create(dest, recursive = TRUE, showWarnings = FALSE) | |
| zip::unzip(zipfile, files = use$filename, exdir = dest) | |
| out$files <- use |> | |
| dplyr::mutate(path = file.path(dest, filename), size = uncompressed_size) |> | |
| dplyr::select(rel, path, size) | |
| out | |
| } | |
| # Sentence for the files that are not images | |
| not_image_problem <- function(n) { | |
| if (n == 0) return(character()) | |
| if (n == 1) return("1 file is not an image and was skipped. Use JPEG, PNG or WebP.") | |
| sprintf("%d files are not images and were skipped. Use JPEG, PNG or WebP.", n) | |
| } | |
| # Copy a loose upload into `dir` (one subfolder for each upload); returns the new path | |
| keep_upload <- function(path, name, dir, i) { | |
| dest <- file.path(dir, sprintf("file%03d", i)) | |
| dir.create(dest, recursive = TRUE, showWarnings = FALSE) | |
| file.copy(path, file.path(dest, basename(name))) | |
| file.path(dest, basename(name)) | |
| } | |
| # Images from the files of shiny::fileInput() (columns `name`, `datapath`). | |
| # A folder in a zip file gives the label; loose files have no label. Every | |
| # file is written below `dir`, so the caller can delete `dir` at the end of | |
| # the session. The result carries `dir`. | |
| read_images <- function(files, dir = tempfile("upload")) { | |
| if (is.null(files) || nrow(files) == 0) { | |
| return(list(inputs = empty_inputs(), problems = character(), dir = dir)) | |
| } | |
| problems <- character() | |
| other <- 0L | |
| loaded <- 0L | |
| counted <- 0L | |
| found <- purrr::pmap(list(files$name, files$datapath, seq_len(nrow(files))), \(name, path, i) { | |
| if (tolower(tools::file_ext(name)) == "zip") { | |
| if (file.size(path) > mb_bytes(MAX_ZIP_MB)) { | |
| problems <<- c(problems, sprintf("%s was skipped. A zip file can be at most %s MB.", name, MAX_ZIP_MB)) | |
| return(NULL) | |
| } | |
| zipped <- unpack_zip(path, name, file.path(dir, sprintf("zip%03d", i)), MAX_IMAGES - loaded) | |
| problems <<- c(problems, zipped$problems) | |
| other <<- other + zipped$other | |
| counted <<- counted + zipped$count | |
| loaded <<- loaded + NROW(zipped$files) | |
| return(zipped$files) | |
| } | |
| if (is_junk(name)) return(NULL) | |
| if (!is_image_name(name)) { | |
| other <<- other + 1L | |
| return(NULL) | |
| } | |
| if (file.size(path) > mb_bytes(MAX_FILE_MB)) { | |
| problems <<- c(problems, sprintf("%s was skipped. A file can be at most %s MB.", name, MAX_FILE_MB)) | |
| return(NULL) | |
| } | |
| counted <<- counted + 1L | |
| if (loaded >= MAX_IMAGES) return(NULL) | |
| loaded <<- loaded + 1L | |
| tibble::tibble(rel = name, path = keep_upload(path, name, dir, i), size = file.size(path)) | |
| }) | |
| images <- dplyr::bind_rows(found) | |
| problems <- c(problems, not_image_problem(other)) | |
| if (counted > MAX_IMAGES) { | |
| problems <- c(problems, sprintf( | |
| "A run can have at most %d images. This upload has %d. Upload fewer images.", | |
| MAX_IMAGES, counted | |
| )) | |
| images <- images[0, ] | |
| } | |
| if (nrow(images) == 0) return(list(inputs = empty_inputs(), problems = problems, dir = dir)) | |
| parts <- strsplit(images$rel, "/", fixed = TRUE) | |
| file <- purrr::map_chr(parts, utils::tail, 1) | |
| label <- purrr::map_chr(parts, \(p) if (length(p) >= 2) p[1] else NA_character_) | |
| inputs <- tibble::tibble( | |
| id = make_ids(file), path = images$path, text = NA_character_, | |
| file = file, label = label | |
| ) | |
| list(inputs = inputs, problems = problems, dir = dir) | |
| } | |
| # Reference images: loose image files, at most MAX_REFERENCES, copied below `dir` | |
| read_references <- function(files, dir = tempfile("upload")) { | |
| if (is.null(files) || nrow(files) == 0) { | |
| return(list(paths = character(), problems = character(), dir = dir)) | |
| } | |
| is_img <- is_image_name(files$name) | |
| too_big <- is_img & file.size(files$datapath) > mb_bytes(MAX_FILE_MB) | |
| problems <- c( | |
| sprintf("%s was skipped. A file can be at most %s MB.", files$name[too_big], MAX_FILE_MB), | |
| not_image_problem(sum(!is_img)) | |
| ) | |
| use <- which(is_img & !too_big) | |
| if (length(use) > MAX_REFERENCES) { | |
| problems <- c(problems, sprintf( | |
| "You can add at most %d references for each answer. The app used the first %d.", | |
| MAX_REFERENCES, MAX_REFERENCES | |
| )) | |
| use <- use[seq_len(MAX_REFERENCES)] | |
| } | |
| paths <- purrr::map_chr(use, \(i) keep_upload(files$datapath[i], files$name[i], dir, i)) | |
| list(paths = paths, problems = problems, dir = dir) | |
| } | |
| # Inputs from texts: skip the texts that are too long, and stop when there | |
| # are too many. `file` is the display name of each text. | |
| texts_to_inputs <- function(text, file, label, problems = character()) { | |
| long <- stringr::str_length(text) > MAX_TEXT_CHARS | |
| if (any(long)) { | |
| problems <- c(problems, sprintf( | |
| "%s was skipped. A text can have at most %s characters.", | |
| file[long], format(MAX_TEXT_CHARS, big.mark = ",") | |
| )) | |
| } | |
| keep <- !long | |
| if (sum(keep) > MAX_TEXTS) { | |
| problems <- c(problems, sprintf( | |
| "A run can have at most %d texts. This upload has %d. Use fewer texts.", | |
| MAX_TEXTS, sum(keep) | |
| )) | |
| keep <- rep(FALSE, length(text)) | |
| } | |
| if (!any(keep)) return(list(inputs = empty_inputs(), problems = problems)) | |
| inputs <- tibble::tibble( | |
| id = make_ids(file[keep]), path = NA_character_, text = text[keep], | |
| file = file[keep], label = label[keep] | |
| ) | |
| list(inputs = inputs, problems = problems) | |
| } | |
| # One input for each paragraph; one or more blank lines separate paragraphs | |
| read_text_paste <- function(text) { | |
| if (is.null(text) || length(text) == 0 || is.na(text[1])) text <- "" | |
| paragraphs <- text[1] |> | |
| stringr::str_replace_all("\r\n?", "\n") |> | |
| stringr::str_split("\\n\\s*\\n") |> | |
| unlist() |> | |
| stringr::str_trim() | |
| paragraphs <- paragraphs[paragraphs != ""] | |
| texts_to_inputs( | |
| paragraphs, sprintf("Text %d", seq_along(paragraphs)), | |
| rep(NA_character_, length(paragraphs)) | |
| ) | |
| } | |
| # The encoding of a CSV file: UTF-8 when the bytes are valid UTF-8 (a BOM is | |
| # fine), else the best guess of readr, else Latin-1 | |
| csv_encoding <- function(path) { | |
| bytes <- readBin(path, "raw", file.size(path)) | |
| if (!0x00 %in% bytes && validUTF8(rawToChar(bytes))) return("UTF-8") | |
| guess <- readr::guess_encoding(path)$encoding | |
| if (length(guess) == 0 || is.na(guess[1])) "latin1" else guess[1] | |
| } | |
| # Read a CSV file with every column as text; NULL when it cannot be read | |
| read_csv_text <- function(path, n_max = Inf) { | |
| tryCatch( | |
| readr::read_csv( | |
| path, locale = readr::locale(encoding = csv_encoding(path)), | |
| col_types = readr::cols(.default = "c"), na = character(), | |
| n_max = n_max, progress = FALSE | |
| ), | |
| error = function(e) NULL | |
| ) | |
| } | |
| # The column names of a CSV file | |
| csv_columns <- function(path) { | |
| if (file.size(path) > mb_bytes(MAX_FILE_MB)) return(character()) | |
| names(read_csv_text(path, n_max = 0)) | |
| } | |
| # Inputs from a CSV file: the text column, and optionally the label column. | |
| # Empty texts are dropped with a count; blank labels stay unlabelled. | |
| read_text_csv <- function(path, text_col, label_col = NULL, name = basename(path)) { | |
| if (file.size(path) > mb_bytes(MAX_FILE_MB)) { | |
| return(list( | |
| inputs = empty_inputs(), | |
| problems = sprintf("%s was skipped. A file can be at most %s MB.", name, MAX_FILE_MB) | |
| )) | |
| } | |
| data <- read_csv_text(path) | |
| if (is.null(data)) { | |
| return(list(inputs = empty_inputs(), problems = "The CSV file could not be read.")) | |
| } | |
| if (!text_col %in% names(data)) { | |
| return(list( | |
| inputs = empty_inputs(), | |
| problems = sprintf("The column %s is not in the file.", text_col) | |
| )) | |
| } | |
| has_label <- !is.null(label_col) && label_col %in% names(data) | |
| data <- tibble::tibble( | |
| text = stringr::str_trim(data[[text_col]]), | |
| label = if (has_label) stringr::str_trim(data[[label_col]]) else NA_character_, | |
| file = sprintf("Row %d", seq_len(nrow(data))) | |
| ) |> | |
| dplyr::mutate(label = dplyr::na_if(label, "")) | |
| empty <- data$text == "" | |
| problems <- if (sum(empty) == 1) { | |
| "1 empty text was dropped" | |
| } else if (sum(empty) > 1) { | |
| sprintf("%d empty texts were dropped", sum(empty)) | |
| } else { | |
| character() | |
| } | |
| data <- data[!empty, ] | |
| texts_to_inputs(data$text, data$file, data$label, problems) | |
| } | |
| # Match the labels to the answer names after str_squish() and str_to_lower(). | |
| # A matched label gets the exact answer name; any other label becomes NA (the | |
| # input stays, unlabelled) and is listed once. `answers` is the vector of | |
| # answers (named) or of answer names. | |
| match_labels <- function(inputs, answers) { | |
| clean <- \(x) stringr::str_to_lower(stringr::str_squish(x)) | |
| answer_names <- if (is.null(names(answers))) answers else names(answers) | |
| hit <- match(clean(inputs$label), clean(answer_names)) | |
| unmatched <- stringr::str_squish(inputs$label[is.na(hit) & !is.na(inputs$label)]) | |
| unmatched <- unmatched[!duplicated(clean(unmatched))] | |
| inputs$label <- answer_names[hit] | |
| list(inputs = inputs, problems = sprintf("%s is not an answer", unmatched)) | |
| } | |
| # A small JPEG copy of an image (at most 320 pixels on the long side) for the | |
| # page; FALSE when the image cannot be read | |
| make_thumb <- function(path, dest) { | |
| tryCatch( | |
| { | |
| magick::image_read(path)[1] |> | |
| magick::image_background("white") |> | |
| magick::image_scale("320x320>") |> | |
| magick::image_write(dest, format = "jpeg") | |
| TRUE | |
| }, | |
| error = \(e) FALSE | |
| ) | |
| } | |
| # Delete the files of Shiny uploads, and their folders when they are empty | |
| remove_uploads <- function(paths) { | |
| unlink(paths) | |
| folders <- setdiff( | |
| unique(normalizePath(dirname(paths), winslash = "/", mustWork = FALSE)), | |
| normalizePath(tempdir(), winslash = "/") | |
| ) | |
| files <- lengths(lapply(folders, list.files, all.files = TRUE, no.. = TRUE)) | |
| empty <- folders[dir.exists(folders) & files == 0] | |
| unlink(empty, recursive = TRUE) | |
| } | |