Unable to download .xpt file using xportr package
12:19 13 Dec 2025

I am able to write a valid xpt file (that SAS can read) when I use the following code

library(xportr)
library(tibble)

# Create an example data frame
df_xp <- tibble(
  ID = c(1, 2, 3),
  Age = c(21, 32, 45),
  Gender = c("Male", "Female", "Male")
)
names(df_xp) <- toupper(names(df_xp))

# Define a metadata data frame with desired attributes
metadata <- tibble(
  variable = toupper(c("ID", "AGE", "GENDER")),
  label = c("Subject Identifier", "Age at Visit", "Gender of Subject"),
  length = c(8, 8, 20)
)

# Apply attributes using xportr functions
dfxp <- df_xp %>%
  xportr_label(metadata = metadata) %>% # Assign labels
  xportr_length(metadata = metadata)    # Set specified lengths

# Write to an XPT file
xportr_write(dfxp, "egxportr.xpt")

However, the same does not work in a shiny app.

library(shiny)
library(haven)   # For reading .xpt and writing .sas7bdat
library(readr)   # For reading .csv
library(readxl)  # For reading .xlsx
library(bslib)
library(jsonlite) # ADDED: For writing JSON files
library(xportr)
library(tools)
library(tibble)
library(DT)

# --- Utility Function to Read Data ---
# This function determines the file type and reads the data accordingly.
read_uploaded_file <- function(file_path, file_ext) {
  data <- NULL
  
  if (file_ext == "csv") {
    # Read CSV files
    data <- read_csv(file_path, show_col_types = FALSE)
  } else if (file_ext == "sas7bdat") {
    # Read SAS Transport (XPT) files
    data <- haven::read_sas(file_path)
  } else if (file_ext == "xpt") {
    # Read SAS Transport (XPT) files
    data <- read_xpt(file_path)
  } else if (file_ext == "xlsx" || file_ext == "xls") {
    # Read Excel files (assuming the first sheet for simplicity)
    data <- read_excel(file_path, sheet = 1)
  } else {
    # Return an error message for unsupported types
    stop("Unsupported file type selected.")
  }
  
  return(data)
}

ui <- page_fluid(
  theme = bs_theme(version = 5, preset = "darkly"), # Use a clean, professional theme
  title = "File Format Converter",
  
  tags$h2("Data Format Converter (.sas7bdat, .csv, .xlsx to .xpt)"),
  
  layout_columns(
    col_widths = c(4, 8),
    
    # 1. File Upload Card (Left Column)
    card(
      card_header("Upload File"),
      fileInput(
        "upload", 
        "Choose a file (.xpt, .csv, or .xlsx)",
        multiple = FALSE,
        accept = c(".xpt", ".csv", ".xlsx", ".xls", ".sas7bdat") # Accept all three types
      ),
      
      # Button to trigger the download of the converted file
      uiOutput("download_button_ui")
    ),
    
    # 2. Preview and Status Card (Right Column)
    card(
      card_header("Data Preview & Status"),
      # Display any error/status messages
      textOutput("status_message"),
      # Display the first few rows of the uploaded data
      DT::dataTableOutput("data_preview")
    )
  )
)

# --- Shiny Server ---
server <- function(input, output, session) {
  
  # Reactive value to store the uploaded data frame
  uploaded_data <- reactiveVal(NULL)
  
  # Reactive expression to process the uploaded file
  observeEvent(input$upload, {
    file_info <- input$upload
    
    # Guard clause: check if a file was uploaded
    req(file_info)
    
    # Extract file extension
    file_ext <- tolower(tools::file_ext(file_info$name))
    
    # Reset data and status
    uploaded_data(NULL)
    output$status_message <- renderText("")
    
    tryCatch({
      # Call the utility function to read the data
      data <- read_uploaded_file(file_info$datapath, file_ext)
      
      # Store the data in the reactive value
      uploaded_data(data)
      output$status_message <- renderText(
        paste("Success! File '", file_info$name, 
              "' loaded. Ready for conversion to .sas7bdat and .json.")
      )
      
    }, error = function(e) {
      # Display error message if reading fails
      uploaded_data(NULL)
      output$status_message <- renderText(paste("Error reading file:", e$message))
    })
  })
  
  # Render UI for the download button (only if data is available)
  output$download_button_ui <- renderUI({
    if (!is.null(uploaded_data())) {
      tagList(
        downloadButton("download_xpt", "Download as .xpt", class = "btn-success", style = "margin-left: 10px;")
      )
    }
  })
  
  # Render the data preview table
  output$data_preview <- DT::renderDataTable({
    req(uploaded_data())
    # Display only the first 10 rows for preview
    DT::datatable(head(uploaded_data(), 10), options = list(dom = 't'))
  })
  
  dfxpt <- reactive({
    req(uploaded_data())
    dfx <- uploaded_data()
    labels <- c()
    lengths <- c()
    for (var in names(dfx)) {
      mylabel <- ifelse(is.null(attr(dfx[[var]], "label")), var, attr(dfx[[var]], "label"))
      labels <- c(labels, mylabel)
      if (is.character(dfx[[var]])) {
        lengths <- c(lengths, max(nchar(dfx[[var]]), na.rm = TRUE))
      } else lengths <- c(lengths,8)
    }
    
    # Create the new filename based on the uploaded file name
    original_name <- input$upload$name
    df_name <- tools::file_path_sans_ext(original_name)
    
    # Define a metadata data frame with desired attributes
    metadata <- tibble(
      dataset = df_name,
      variable = toupper(names(dfx)),
      label = labels,
      length = lengths
    )
    
    # Apply attributes using xportr functions
    dfxp <- dfx %>%
      xportr_label(metadata = metadata) %>% # Assign labels
      xportr_length(metadata = metadata) # Set specified lengths
    
    dfxp
  })
  
  output$download_xpt <- downloadHandler(
    filename = function() {
      # Create the new filename based on the uploaded file name
      original_name <- input$upload$name
      base_name <- tools::file_path_sans_ext(original_name)
      paste0(base_name, ".xpt")
    },
    content = function(file) {
      req(dfxpt())
      df <- dfxpt()
      
      original_name <- input$upload$name
      df_name <- tools::file_path_sans_ext(original_name)
      temp_dir <- tempdir()
      xpt_filename <- paste0(df_name, ".xpt")               #########  temp
      xpt_filepath <- file.path(temp_dir, xpt_filename)     #########  temp
      
      # haven::write_xpt(df, file)              #  SAS cannot read this file - states not a SAS dataset
      
      # xportr::xportr_write(df, path = file)   #  Warning: Error in : Assertion on 'path' failed: .df file name must be 8 characters or less..
      
      xportr::xportr_write(df, path = xpt_filepath)  
    } #, contentType = "application/x-sas-xport" 
  )
}

shinyApp(ui, server)

Any idea how to fix it?

r shiny xpt