Showing preview only (1,263K chars total). Download the full file or copy to clipboard to get everything.
Repository: rstudio/rsconnect
Branch: main
Commit: 2395d024baa5
Files: 283
Total size: 1.2 MB
Directory structure:
gitextract__wtwsl8q/
├── .Rbuildignore
├── .github/
│ ├── .gitignore
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── config.yml
│ └── workflows/
│ ├── R-CMD-check.yaml
│ ├── check-no-suggests.yaml
│ ├── connect-integration.yaml
│ ├── format-suggest.yaml
│ ├── lint.yaml
│ ├── pkgdown.yaml
│ ├── rhub.yaml
│ └── shinyapps-integration.yaml
├── .gitignore
├── .lintr
├── DESCRIPTION
├── NAMESPACE
├── NEWS.md
├── R/
│ ├── .editorconfig
│ ├── account-find.R
│ ├── accounts.R
│ ├── appDependencies.R
│ ├── appMetadata-quarto.R
│ ├── appMetadata.R
│ ├── applications.R
│ ├── auth.R
│ ├── bundle.R
│ ├── bundleFiles.R
│ ├── bundlePackage.R
│ ├── bundlePackagePackrat.R
│ ├── bundlePackageRenv.R
│ ├── bundlePython.R
│ ├── certificates.R
│ ├── client-cloudAuth.R
│ ├── client-connect.R
│ ├── client-connectCloud.R
│ ├── client-connectCloudLogs.R
│ ├── client-identityFederation.R
│ ├── client-shinyapps.R
│ ├── client.R
│ ├── config.R
│ ├── configMigrate.R
│ ├── configureApp.R
│ ├── cookies.R
│ ├── deployAPI.R
│ ├── deployApp.R
│ ├── deployDoc.R
│ ├── deploySite.R
│ ├── deployTFModel.R
│ ├── deploymentTarget.R
│ ├── deployments-find.R
│ ├── deployments.R
│ ├── envvars.R
│ ├── http-httr2.R
│ ├── http-libcurl.R
│ ├── http.R
│ ├── ide.R
│ ├── import-standalone-obj-type.R
│ ├── import-standalone-types-check.R
│ ├── imports.R
│ ├── lint-framework.R
│ ├── lint.R
│ ├── linters.R
│ ├── locale.R
│ ├── purgeApp.R
│ ├── restartApp.R
│ ├── rpubs.R
│ ├── rsconnect-package.R
│ ├── secret.R
│ ├── servers.R
│ ├── tasks.R
│ ├── terminateApp.R
│ ├── title.R
│ ├── usage.R
│ ├── utils-cli.R
│ ├── utils.R
│ ├── utm.R
│ ├── writeManifest.R
│ └── zzz.R
├── README.Rmd
├── README.md
├── RELEASE.md
├── _pkgdown.yml
├── cran-comments.md
├── examples/
│ └── example-linter.R
├── inst/
│ ├── cert/
│ │ ├── api.connect.posit.cloud.pem
│ │ ├── cacert.pem
│ │ └── shinyapps.io.pem
│ ├── examples/
│ │ ├── diamonds/
│ │ │ ├── server.R
│ │ │ └── ui.R
│ │ └── sessioninfo/
│ │ ├── server.R
│ │ └── ui.R
│ └── resources/
│ ├── environment.py
│ └── pyproject.py
├── man/
│ ├── accountUsage.Rd
│ ├── accounts.Rd
│ ├── addAuthorizedUser.Rd
│ ├── addLinter.Rd
│ ├── addServer.Rd
│ ├── appDependencies.Rd
│ ├── applicationConfigDir.Rd
│ ├── applications.Rd
│ ├── configureApp.Rd
│ ├── connectApiUser.Rd
│ ├── connectCloudUser.Rd
│ ├── connectSPCSUser.Rd
│ ├── deployAPI.Rd
│ ├── deployApp.Rd
│ ├── deployDoc.Rd
│ ├── deploySite.Rd
│ ├── deployTFModel.Rd
│ ├── deployments.Rd
│ ├── forgetDeployment.Rd
│ ├── generateAppName.Rd
│ ├── lint.Rd
│ ├── linter.Rd
│ ├── listAccountEnvVars.Rd
│ ├── listBundleFiles.Rd
│ ├── listDeploymentFiles.Rd
│ ├── makeLinterMessage.Rd
│ ├── oldApplicationConfigDir.Rd
│ ├── options.Rd
│ ├── purgeApp.Rd
│ ├── removeAuthorizedUser.Rd
│ ├── resendInvitation.Rd
│ ├── restartApp.Rd
│ ├── rpubsUpload.Rd
│ ├── rsconnect-package.Rd
│ ├── rsconnectConfigDir.Rd
│ ├── rsconnectPackages.Rd
│ ├── rsconnectProxies.Rd
│ ├── servers.Rd
│ ├── setAccountInfo.Rd
│ ├── setProperty.Rd
│ ├── showInvited.Rd
│ ├── showLogs.Rd
│ ├── showMetrics.Rd
│ ├── showProperties.Rd
│ ├── showUsage.Rd
│ ├── showUsers.Rd
│ ├── syncAppMetadata.Rd
│ ├── taskLog.Rd
│ ├── tasks.Rd
│ ├── terminateApp.Rd
│ ├── unsetProperty.Rd
│ └── writeManifest.Rd
├── revdep/
│ ├── .gitignore
│ ├── README.md
│ ├── cran.md
│ ├── failures.md
│ └── problems.md
├── rsconnect.Rproj
├── tests/
│ ├── integration/
│ │ ├── example-shiny/
│ │ │ └── app.R
│ │ ├── setup.R
│ │ └── test-deploy.R
│ ├── manual/
│ │ ├── appMode.Rmd
│ │ ├── dependencies.Rmd
│ │ ├── deploySite.Rmd
│ │ └── publishing-dialog.Rmd
│ ├── shinyapps-integration/
│ │ ├── example-shiny/
│ │ │ ├── app.R
│ │ │ └── manifest.json
│ │ ├── setup.R
│ │ └── test-shinyapps-deploy.R
│ ├── testthat/
│ │ ├── _snaps/
│ │ │ ├── account-find.md
│ │ │ ├── accounts.md
│ │ │ ├── appDependencies.md
│ │ │ ├── appMetadata-quarto.md
│ │ │ ├── appMetadata.md
│ │ │ ├── applications.md
│ │ │ ├── bundle.md
│ │ │ ├── bundleFiles.md
│ │ │ ├── bundlePackage.md
│ │ │ ├── bundlePackagePackrat.md
│ │ │ ├── bundlePackageRenv.md
│ │ │ ├── bundlePython.md
│ │ │ ├── client-connect.md
│ │ │ ├── cookies.md
│ │ │ ├── deployApp.md
│ │ │ ├── deployDoc.md
│ │ │ ├── deploymentTarget.md
│ │ │ ├── deployments-find.md
│ │ │ ├── deployments.md
│ │ │ ├── http-libcurl.md
│ │ │ ├── http.md
│ │ │ ├── ide.md
│ │ │ ├── lint.md
│ │ │ ├── linters.md
│ │ │ ├── secret.md
│ │ │ ├── servers.md
│ │ │ └── writeManifest.md
│ │ ├── certs/
│ │ │ ├── example.com.pem
│ │ │ ├── invalid.crt
│ │ │ ├── localhost.pem
│ │ │ ├── sample.crt
│ │ │ └── two-cas.crt
│ │ ├── helper-content.R
│ │ ├── helper-http.R
│ │ ├── helper-paths.R
│ │ ├── helper.R
│ │ ├── multibyte-characters/
│ │ │ └── app.R
│ │ ├── packages/
│ │ │ ├── latin1package/
│ │ │ │ ├── DESCRIPTION
│ │ │ │ ├── NAMESPACE
│ │ │ │ ├── R/
│ │ │ │ │ └── hello.R
│ │ │ │ └── man/
│ │ │ │ └── hello.Rd
│ │ │ ├── utf8package/
│ │ │ │ ├── DESCRIPTION
│ │ │ │ ├── NAMESPACE
│ │ │ │ ├── R/
│ │ │ │ │ └── hello.R
│ │ │ │ └── man/
│ │ │ │ └── hello.Rd
│ │ │ └── windows1251package/
│ │ │ ├── DESCRIPTION
│ │ │ ├── NAMESPACE
│ │ │ ├── R/
│ │ │ │ └── hello.R
│ │ │ └── man/
│ │ │ └── hello.Rd
│ │ ├── quarto-doc-long-chunk/
│ │ │ └── index.qmd
│ │ ├── quarto-doc-none/
│ │ │ └── quarto-doc-none.qmd
│ │ ├── renv-recommended/
│ │ │ └── dependences.R
│ │ ├── shiny-app-in-subdir/
│ │ │ └── my-app/
│ │ │ ├── server.R
│ │ │ └── ui.r
│ │ ├── shiny-rmds/
│ │ │ ├── non-shiny-rmd.Rmd
│ │ │ ├── shiny-rmd-dashes.Rmd
│ │ │ └── shiny-rmd-dots.Rmd
│ │ ├── shinyapp-appR/
│ │ │ ├── app.R
│ │ │ └── rsconnect/
│ │ │ └── colorado.posit.co/
│ │ │ └── hadley/
│ │ │ └── shinyapp-appR.dcf
│ │ ├── shinyapp-simple/
│ │ │ ├── server.R
│ │ │ ├── shinyapp-simple.Rproj
│ │ │ └── ui.R
│ │ ├── shinyapp-singleR/
│ │ │ └── single.R
│ │ ├── shinyapp-with-absolute-paths/
│ │ │ ├── ShinyDocument.Rmd
│ │ │ ├── ShinyPresentation.Rmd
│ │ │ ├── data/
│ │ │ │ └── College.txt
│ │ │ ├── server.R
│ │ │ └── ui.R
│ │ ├── shinyapp-with-browser/
│ │ │ ├── server.R
│ │ │ └── ui.R
│ │ ├── static-with-quarto-yaml/
│ │ │ ├── _quarto.yml
│ │ │ └── slideshow.html
│ │ ├── test-account-find.R
│ │ ├── test-accounts.R
│ │ ├── test-appDependencies.R
│ │ ├── test-appMetadata-quarto.R
│ │ ├── test-appMetadata.R
│ │ ├── test-applications.R
│ │ ├── test-bundle.R
│ │ ├── test-bundleFiles.R
│ │ ├── test-bundleNodejs.R
│ │ ├── test-bundlePackage.R
│ │ ├── test-bundlePackagePackrat.R
│ │ ├── test-bundlePackageRenv.R
│ │ ├── test-bundlePython.R
│ │ ├── test-cert.R
│ │ ├── test-client-connect.R
│ │ ├── test-client-connectCloud.R
│ │ ├── test-client.R
│ │ ├── test-config.R
│ │ ├── test-cookies.R
│ │ ├── test-deployApp.R
│ │ ├── test-deployDoc.R
│ │ ├── test-deploySite.R
│ │ ├── test-deploymentTarget.R
│ │ ├── test-deployments-find.R
│ │ ├── test-deployments.R
│ │ ├── test-http-httr2.R
│ │ ├── test-http-libcurl.R
│ │ ├── test-http.R
│ │ ├── test-ide.R
│ │ ├── test-identityFederation.R
│ │ ├── test-lint.R
│ │ ├── test-linters.R
│ │ ├── test-locale.R
│ │ ├── test-plumber/
│ │ │ └── plumber.R
│ │ ├── test-reticulate-rmds/
│ │ │ ├── implicit.Rmd
│ │ │ └── index.Rmd
│ │ ├── test-rmd-bad-case/
│ │ │ └── index.Rmd
│ │ ├── test-rmds/
│ │ │ ├── index.Rmd
│ │ │ ├── parameterized.Rmd
│ │ │ └── simple.Rmd
│ │ ├── test-secret.R
│ │ ├── test-servers.R
│ │ ├── test-spcs.R
│ │ ├── test-title.R
│ │ ├── test-utils.R
│ │ └── test-writeManifest.R
│ └── testthat.R
└── vignettes/
├── .gitignore
└── custom-http.Rmd
================================================
FILE CONTENTS
================================================
================================================
FILE: .Rbuildignore
================================================
^.*\.Rproj$
^\.Rproj\.user$
^examples$
^R/\.editorconfig$
^README\.Rmd$
^README\.html$
^RELEASE\.md$
^_pkgdown\.yml$
^docs$
^pkgdown$
tags
^\.github$
^\.lintr$
^cran-comments\.md$
^revdep$
^CRAN-SUBMISSION$
^\.positai$
^\.claude$
================================================
FILE: .github/.gitignore
================================================
*.html
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug report
description: Report an error or unexpected behavior
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to share feedback on rsconnect. Please read the following to help your question find the right answer:
- If you're reporting an issue with *shinyapps.io*, please visit https://forum.posit.co/c/posit-professional-hosted/shinyappsio/24
- If you're reporting an issue with *Connect Cloud*, please visit https://forum.posit.co/c/posit-professional-hosted/posit-connect-cloud/67
- If you're reporting an issue with *Posit Connect*, please visit https://support.posit.co/hc/en-us/requests/new or contact your Posit representative
To report a bug with rsconnect, please provide details about the issue and, if possible, a reproducible example.
- type: textarea
attributes:
label: Description
description: |
Description of the bug, and how to reproduce it
- type: textarea
attributes:
label: Your environment
description: |
Please document the version of rsconnect you are using
placeholder: |
- rsconnect: 1.6.2
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
- name: Help with shinyapps.io
url: https://forum.posit.co/c/posit-professional-hosted/shinyappsio/24
about: For questions about shinyapps.io, visit Posit Community
- name: Help with Connect Cloud
url: https://forum.posit.co/c/posit-professional-hosted/posit-connect-cloud/67
about: For questions about Connect Cloud, visit Posit Community
- name: Help with Posit Connect
url: https://support.posit.co/hc/en-us
about: For questions about Posit Connect, visit the support forum or contact your Posit representative
================================================
FILE: .github/workflows/R-CMD-check.yaml
================================================
# Workflow derived from https://github.com/r-lib/actions/tree/master/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
name: R-CMD-check
jobs:
R-CMD-check:
runs-on: ${{ matrix.config.os }}
name: ${{ matrix.config.os }} (${{ matrix.config.r }})
strategy:
fail-fast: false
matrix:
config:
- {os: macos-latest, r: 'release'}
- {os: windows-latest, r: 'release'}
- {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
- {os: ubuntu-latest, r: 'release'}
- {os: ubuntu-latest, r: 'oldrel-1'}
- {os: ubuntu-latest, r: 'oldrel-2'}
- {os: ubuntu-latest, r: 'oldrel-3'}
- {os: ubuntu-latest, r: 'oldrel-4'}
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes
steps:
- uses: actions/checkout@v4
- uses: r-lib/actions/setup-pandoc@v2
- uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ matrix.config.r }}
http-user-agent: ${{ matrix.config.http-user-agent }}
use-public-rspm: true
- uses: quarto-dev/quarto-actions/setup@v2
- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rcmdcheck, Biobase=?ignore, BiocManager=?ignore
needs: check
- uses: r-lib/actions/check-r-package@v2
with:
upload-snapshots: true
================================================
FILE: .github/workflows/check-no-suggests.yaml
================================================
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
#
# NOTE: This workflow only directly installs "hard" dependencies, i.e. Depends,
# Imports, and LinkingTo dependencies. Notably, Suggests dependencies are never
# installed, with the exception of testthat, knitr, and rmarkdown. The cache is
# never used to avoid accidentally restoring a cache containing a suggested
# dependency.
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
name: check-no-suggests.yaml
permissions: read-all
jobs:
check-no-suggests:
runs-on: ${{ matrix.config.os }}
name: ${{ matrix.config.os }} (${{ matrix.config.r }})
strategy:
fail-fast: false
matrix:
config:
- {os: ubuntu-latest, r: 'release'}
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes
steps:
- uses: actions/checkout@v4
- uses: r-lib/actions/setup-pandoc@v2
- uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ matrix.config.r }}
http-user-agent: ${{ matrix.config.http-user-agent }}
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with:
dependencies: '"hard"'
cache: false
extra-packages: |
any::rcmdcheck
any::testthat
any::knitr
any::rmarkdown
needs: check
- uses: r-lib/actions/check-r-package@v2
with:
upload-snapshots: true
build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'
================================================
FILE: .github/workflows/connect-integration.yaml
================================================
name: Connect Integration Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
connect-integration:
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
version:
- "preview" # Nightly builds of Connect
- "release" # special value that always points to the latest Connect release
- "2025.09.0" # jammy
- "2025.03.0" # jammy (every 6 months, just bc every release would be overkill)
- "2024.09.0" # jammy
# These versions use R 4.2 and seem to take forever to install R dependencies
# - "2024.03.0" # jammy
# - "2023.09.0" # jammy
name: Connect ${{ matrix.version }}
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes
steps:
- uses: actions/checkout@v6
- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with:
# Only install what we need for this test, which is not all of Suggests
dependencies: '"hard"'
extra-packages: |
local::.
any::testthat
any::shiny
- name: Run integration tests
uses: posit-dev/with-connect@main
with:
version: ${{ matrix.version }}
license: ${{ secrets.CONNECT_LICENSE_FILE }}
command: |
Rscript -e 'testthat::test_dir("tests/integration", package="rsconnect", load_package="installed")'
================================================
FILE: .github/workflows/format-suggest.yaml
================================================
# Workflow derived from https://github.com/posit-dev/setup-air/tree/main/examples
on:
pull_request:
name: format-suggest.yaml
permissions: read-all
jobs:
format-suggest:
name: format-suggest
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Install
uses: posit-dev/setup-air@v1
- name: Format
run: air format .
- name: Suggest
uses: reviewdog/action-suggester@v1
with:
level: error
fail_level: error
tool_name: air
================================================
FILE: .github/workflows/lint.yaml
================================================
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
name: lint
jobs:
lint:
runs-on: ubuntu-latest
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v3
- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::lintr, local::.
needs: lint
- name: Lint
run: lintr::lint_package()
shell: Rscript {0}
env:
LINTR_ERROR_ON_LINT: true
================================================
FILE: .github/workflows/pkgdown.yaml
================================================
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
release:
types: [published]
workflow_dispatch:
name: pkgdown
jobs:
pkgdown:
runs-on: ubuntu-latest
# Only restrict concurrency for non-PR jobs
concurrency:
group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }}
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v3
- uses: r-lib/actions/setup-pandoc@v2
- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::pkgdown, local::.
needs: website
- name: Build site
run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)
shell: Rscript {0}
- name: Deploy to GitHub pages 🚀
if: github.event_name != 'pull_request'
uses: JamesIves/github-pages-deploy-action@v4.4.1
with:
clean: false
branch: gh-pages
folder: docs
================================================
FILE: .github/workflows/rhub.yaml
================================================
# R-hub's generic GitHub Actions workflow file. It's canonical location is at
# https://github.com/r-hub/actions/blob/v1/workflows/rhub.yaml
# You can update this file to a newer version using the rhub2 package:
#
# rhub::rhub_setup()
#
# It is unlikely that you need to modify this file manually.
name: R-hub
run-name: "${{ github.event.inputs.id }}: ${{ github.event.inputs.name || format('Manually run by {0}', github.triggering_actor) }}"
on:
workflow_dispatch:
inputs:
config:
description: 'A comma separated list of R-hub platforms to use.'
type: string
default: 'linux,windows,macos'
name:
description: 'Run name. You can leave this empty now.'
type: string
id:
description: 'Unique ID. You can leave this empty now.'
type: string
jobs:
setup:
runs-on: ubuntu-latest
outputs:
containers: ${{ steps.rhub-setup.outputs.containers }}
platforms: ${{ steps.rhub-setup.outputs.platforms }}
steps:
# NO NEED TO CHECKOUT HERE
- uses: r-hub/actions/setup@v1
with:
config: ${{ github.event.inputs.config }}
id: rhub-setup
linux-containers:
needs: setup
if: ${{ needs.setup.outputs.containers != '[]' }}
runs-on: ubuntu-latest
name: ${{ matrix.config.label }}
strategy:
fail-fast: false
matrix:
config: ${{ fromJson(needs.setup.outputs.containers) }}
container:
image: ${{ matrix.config.container }}
steps:
- uses: r-hub/actions/checkout@v1
- uses: r-hub/actions/platform-info@v1
with:
token: ${{ secrets.RHUB_TOKEN }}
job-config: ${{ matrix.config.job-config }}
- uses: r-hub/actions/setup-deps@v1
with:
token: ${{ secrets.RHUB_TOKEN }}
job-config: ${{ matrix.config.job-config }}
- uses: r-hub/actions/run-check@v1
with:
token: ${{ secrets.RHUB_TOKEN }}
job-config: ${{ matrix.config.job-config }}
other-platforms:
needs: setup
if: ${{ needs.setup.outputs.platforms != '[]' }}
runs-on: ${{ matrix.config.os }}
name: ${{ matrix.config.label }}
strategy:
fail-fast: false
matrix:
config: ${{ fromJson(needs.setup.outputs.platforms) }}
steps:
- uses: r-hub/actions/checkout@v1
- uses: r-hub/actions/setup-r@v1
with:
job-config: ${{ matrix.config.job-config }}
token: ${{ secrets.RHUB_TOKEN }}
- uses: r-hub/actions/platform-info@v1
with:
token: ${{ secrets.RHUB_TOKEN }}
job-config: ${{ matrix.config.job-config }}
- uses: r-hub/actions/setup-deps@v1
with:
job-config: ${{ matrix.config.job-config }}
token: ${{ secrets.RHUB_TOKEN }}
- uses: r-hub/actions/run-check@v1
with:
job-config: ${{ matrix.config.job-config }}
token: ${{ secrets.RHUB_TOKEN }}
================================================
FILE: .github/workflows/shinyapps-integration.yaml
================================================
name: shinyapps.io Integration Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
shinyapps-integration:
runs-on: ubuntu-latest
timeout-minutes: 20
# Only run if shinyapps.io credentials are available (not on forks)
if: github.event_name == 'push' || !github.event.pull_request.head.repo.fork
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
SHINYAPPS_NAME: ${{ secrets.SHINYAPPS_NAME }}
SHINYAPPS_TOKEN: ${{ secrets.SHINYAPPS_TOKEN }}
SHINYAPPS_SECRET: ${{ secrets.SHINYAPPS_SECRET }}
steps:
- uses: actions/checkout@v6
- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with:
dependencies: '"hard"'
extra-packages: |
local::.
any::testthat
any::shiny
- name: Run shinyapps.io integration tests
if: env.SHINYAPPS_SECRET != ''
run: |
Rscript -e 'testthat::test_dir("tests/shinyapps-integration", package="rsconnect", load_package="installed")'
================================================
FILE: .gitignore
================================================
.Rproj.user
.Rhistory
.RData
.DS_Store
README.html
*.Rcheck/
*.tar.gz
docs/
.rscignore
tags
inst/doc
inst/resources/__pycache__/
tests/**/*.quarto_ipynb
# license files should not be commited to this repository
*.lic
.positai
================================================
FILE: .lintr
================================================
linters: linters_with_defaults(
line_length_linter(160),
indentation_linter = NULL,
object_length_linter(60),
object_name_linter = NULL,
object_usage_linter = NULL,
brace_linter = NULL,
commented_code_linter = NULL,
seq_linter = NULL,
return_linter = NULL
)
================================================
FILE: DESCRIPTION
================================================
Type: Package
Package: rsconnect
Title: Deploy Docs, Apps, and APIs to 'Posit Connect', 'shinyapps.io', and 'RPubs'
Version: 1.8.0.9000
Authors@R: c(
person("Aron", "Atkins", , "aron@posit.co", role = c("aut", "cre")),
person("Toph", "Allen", role = "aut"),
person("Hadley", "Wickham", role = "aut"),
person("Jonathan", "McPherson", role = "aut"),
person("JJ", "Allaire", role = "aut"),
person("Posit Software, PBC", role = c("cph", "fnd"))
)
Description: Programmatic deployment interface for 'RPubs',
'shinyapps.io', and 'Posit Connect'. Supported content types include R
Markdown documents, Shiny applications, Plumber APIs, plots, and
static web content.
License: GPL-2
URL: https://rstudio.github.io/rsconnect/, https://github.com/rstudio/rsconnect
BugReports: https://github.com/rstudio/rsconnect/issues
Depends:
R (>= 3.5.0)
Imports:
cli,
curl,
digest,
httr2,
jsonlite,
lifecycle,
openssl (>= 2.0.0),
PKI,
packrat (>= 0.6),
renv (>= 1.0.0),
rlang (>= 1.0.0),
rstudioapi (>= 0.18.0),
snowflakeauth,
tools,
yaml (>= 2.1.5),
utils
Suggests:
Biobase,
BiocManager,
foreign,
knitr,
MASS,
plumber (>= 0.3.2),
quarto,
reticulate,
rmarkdown (>= 1.1),
shiny,
testthat (>= 3.1.9),
webfakes,
withr
VignetteBuilder:
knitr, rmarkdown
Config/Needs/website: tidyverse/tidytemplate
Config/testthat/edition: 3
Config/testthat/parallel: true
Config/Python/Note: Python <= 3.10 requires tomllib package for pyproject.toml parsing
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.3
================================================
FILE: NAMESPACE
================================================
# Generated by roxygen2: do not edit by hand
S3method(as.data.frame,rsconnect_secret)
S3method(format,rsconnect_secret)
S3method(print,linterResults)
S3method(print,rsconnect_secret)
S3method(str,rsconnect_secret)
export(accountInfo)
export(accountUsage)
export(accounts)
export(addAuthorizedUser)
export(addLinter)
export(addServer)
export(addServerCertificate)
export(appDependencies)
export(applications)
export(configureApp)
export(connectApiUser)
export(connectCloudUser)
export(connectSPCSUser)
export(connectUser)
export(deployAPI)
export(deployApp)
export(deployDoc)
export(deploySite)
export(deployTFModel)
export(deployments)
export(forgetDeployment)
export(generateAppName)
export(getLogs)
export(lint)
export(linter)
export(listAccountEnvVars)
export(listBundleFiles)
export(listDeploymentFiles)
export(purgeApp)
export(removeAccount)
export(removeAuthorizedUser)
export(removeServer)
export(resendInvitation)
export(restartApp)
export(rpubsUpload)
export(serverInfo)
export(servers)
export(setAccountInfo)
export(setProperty)
export(showInvited)
export(showLogs)
export(showMetrics)
export(showProperties)
export(showUsage)
export(showUsers)
export(syncAppMetadata)
export(taskLog)
export(tasks)
export(terminateApp)
export(unsetProperty)
export(updateAccountEnvVars)
export(writeManifest)
import(rlang)
importFrom(lifecycle,deprecated)
importFrom(stats,na.omit)
importFrom(stats,setNames)
importFrom(utils,available.packages)
importFrom(utils,contrib.url)
importFrom(utils,getFromNamespace)
importFrom(utils,glob2rx)
importFrom(utils,head)
importFrom(utils,installed.packages)
importFrom(utils,packageDescription)
importFrom(utils,packageVersion)
importFrom(utils,read.csv)
================================================
FILE: NEWS.md
================================================
# rsconnect (development version)
* Added support for deploying Node.js applications to Posit Connect.
`deployApp()` and `writeManifest()` now automatically detect Node.js content
from `package.json` and generate the appropriate manifest. Added
`envManagementNodejs` parameter for controlling Node.js environment
management. [Node.js support](https://docs.posit.co/connect/user/nodejs/) is
in Early Access as of Connect version 2026.04.0. (#1322)
* Fixed an issue where Bioconductor packages could be incorrectly associated
with a CRAN repository URL when the same package appeared in CRAN's Transit
directory. (#1314)
* The global deployment history file used by the Workbench dashboard no longer
uses a fixed temporary file name during updates, eliminating a race
condition that could cause the file to rapidly grow during concurrent
deployments. The history is also capped at 100 records and resets if the
file grows excessively large. (#1320)
* `deployApp()` with `logLevel = "verbose"` no longer errors using the `httr2` backend. (#1312)
* `deployApp()`, `writeManifest()`, and `appDependencies()` gain a
`dependencyResolution` parameter. Set `dependencyResolution = "library"` to ignore
the `renv.lock` file and resolve package dependencies by scanning the code.
The version that is recorded is what is installed in the libraries active in the
R session (i.e. what is displayed with `.libPaths()`). This is useful when
deploying from environments where there is a mismatch between the
renv lock file and the user's environment. (#1046, #1315, #1317)
* Improved error messages when `renv::snapshot()` fails during dependency
discovery. (#1078)
# rsconnect 1.8.0
* `rsconnect` now uses [`httr2`](https://httr2.r-lib.org/) as its HTTP client.
There should be no user-visible changes as a result, but if something does
not work as expected, please file an issue, and you can set
`options(rsconnect.httr2 = FALSE)` as a temporary workaround. (#1284)
* Added support for renv profiles and renv lockfiles that are located outside of
the project root. (#1122)
* Resolved a bug where `renv.lock` files that had multiple repositories were not
being translated faithfully when creating the manifest file. (#1268)
* Packages installed locally via pak are resolved against configured
repositories. (#1305)
* Added support for overriding R package repository resolution behavior. (#1272)
* Push-button publishing from desktop RStudio is now compatible with Connect
servers hosted on Snowflake. This includes support for browser-based
authentication during deployment. (#1289)
* The `snowflakeConnectionName` parameter now respects the default Snowflake
connection name in the `connections.toml` file (when it exists), making it
optional in many cases. This is only applicable to Connect servers hosted on
Snowflake. (#1283)
* Added support for using identity federation to authenticate against Connect
when running in Posit Workbench, when available. This allows deploying to
Connect servers without the need to store long-lived credentials. (#1287)
* Upgraded to use `v1` APIs for deploying to Connect servers, which enables
new features for specifying settings in the manifest file. (#1280)
* Removed support for log streaming from shinyapps.io due to loss of support
for this feature on the shinyapps.io platform (`showLogs(streaming = TRUE)`).
If this feature is important to your workflow, please file an issue and we
will consider reintroduction of log streaming via rsconnect in Connect Cloud.
(#1292)
* Removed several functions, including `addConnectServer()` and
`discoverServer()`, as well as HTTP backends other than libcurl,
which were deprecated in rsconnect 1.0.0. (#1282)
# rsconnect 1.7.0
* Added support for deploying from `manifest.json` files created by
`writeManifest()`: use the `manifestPath` argument of `deployApp()` and related
functions to specify the path to an existing manifest file. (#1259)
* `urlEncode()` now uses `curl::curl_escape()` instead of `RCurl::curlEscape()`,
as RCurl is a Suggests dependency. (#1265)
* The `User-Agent` header in requests made from rsconnect will now be of the
format `RSConnect/x.y.z` instead of `rsconnect/x.y.z` in order to satisfy web
application firewalls that enforce Pascal case.
# rsconnect 1.6.2
* Fix an opaque error when creating a manifest using Python <= 3.10 with a
version requirement in a `pyproject.toml` file. A warning is shown rather
than an error when the tomllib package is not present. (#1226)
* Address CRAN test failures caused by some openssl configurations. (#1255)
# rsconnect 1.6.1
* Fix account registration from RStudio. (#1250)
* SPCS/Snowflake authentication supports Connect API keys for user
identification. The `connectSPCSUser()` function now requires an `apiKey`
parameter, and the API key is included in the `X-RSC-Authorization` header
alongside Snowflake token authentication. This aligns with updated Connect
server requirements where Snowflake tokens provide proxied authentication
while API keys identify users to the Connect server itself.
# rsconnect 1.6.0
* Support deploying to Posit Connect Cloud. Use `connectCloudUser()` to add
Connect Cloud credentials.
* `rsconnect` now sets the `rsconnect.max.bundle.size` and
`rsconnect.max.bundle.files` options to their default values on startup
if they have not yet been set. (#1204)
* The default `rsconnect.max.bundle.size` limit has increased to 5 GiB. (#1200)
* `getLogs()` returns log lines for a shinyapps.io hosted application. (#1209)
* Python environment inspection errors include the path to the target Python
binary. (#1207)
* Improve cookie expiration date handling. (#1212)
* Improve documentation and advice for `deployApp(envVars...)`.
* Removed support for publishing to Posit Cloud. (#1215)
Existing Posit Cloud account records may be removed by using
`removeAccount("yourname", "posit.cloud")`.
Existing Posit Cloud deployment records may be removed by using
`forgetDeployment(name="deployment", account="yourname", server="posit.cloud")`.
* Removed the Posit Cloud-exclusive `space` argument from `deployApp()`. (#1215)
# rsconnect 1.5.1
* Address user registration for Posit Connect deployments hosted in Snowpark
Container Services when there is more than one configured Snowflake
connection. (#1189)
* Process cookie expiration dates in addition to the cookie max-age. Some
servers return already-expired cookies. (1187)
* Removed unused internal methods from Connect client. (#1182)
# rsconnect 1.5.0
* Functions for interacting with Posit Connect deployments in
Snowpark Container Services are now provided by the snowflakeauth package.
# rsconnect 1.4.2
* Address duplicate certificate errors on macOS with newer curl. (#1175)
# rsconnect 1.4.1
* Fixed processing error during server validation, which prevented
registration of new Connect accounts. (#1166)
* When waiting for initial Connect account authorization, allow HTTP 401
responses. (#1167)
# rsconnect 1.4.0
* Content directories with a period in their name are no longer treated as a
document path when computing the location for deployment records. (#1138)
* Quarto documents which specify a server must include executable code or an
engine declaration. (#1145)
* Fixed errors when analyzing Quarto documents containing long chunks. (#1114)
* A `_server.yml` file indicates that the content is an API. (#1144)
* Expand tilde when resolving the `rsconnect.ca.bundle` option. (#1152)
* Added support for interaction with Posit Connect deployments
hosted in Snowpark Container Services.
* Introduced detection of required R interpreter version based on
`DESCRIPTION` file and `renv.lock` file. This setting is inserted
into the manifest as `environment.r.requires`.
* Introduced detection of required Python interpreter version based on
project files `.python-version`, `pyproject.toml` and `setup.cfg`.
This setting is inserted into the manifest as `environment.python.requires`.
# rsconnect 1.3.4
* Use base64 encoded test data. Addresses CRAN test failures when run with
newer libssl. (#1130)
# rsconnect 1.3.3
* Avoid "legacy" time zone names in tests, as they are not available by
default in all environments. Addresses CRAN test failures. (#1115)
# rsconnect 1.3.2
* Primary Quarto document detection only considers `.R`, `.Rmd`, and `.qmd` as
end-of-file extensions. Previously, a file with `.R` elsewhere in its name,
such as `.Rprofile`, was incorrectly considered. (#1106)
* Use the repository name identified by renv when `available.packages()` does
not enumerate the package, which occurs for archived packages. (#1110)
* Remove remaining directory layout validation check. (#1102)
* Use the public Connect server API endpoint `/v1/tasks/{id}` to poll task
progress. (#1088)
# rsconnect 1.3.1
* Skip tests when packages "foreign" and "MASS" are not available. (#1081)
# rsconnect 1.3.0
* `deployApp(logLevel = "quiet")` suppresses Posit Connect deployment task
output. (#1051)
* `deployApp(logLevel = "quiet")` and `writeManifest(quiet=TRUE)` suppress
output when using renv to analyze dependencies. (#1051)
* `deployApp()` and `writeManifest()` receive the default value for the
`image` argument from the `RSCONNECT_IMAGE` environment variable. (#1063)
* `deployTF()` can deploy a TensorFlow model to Posit Connect. Requires Posit
Connect 2024.05.0 or higher.
* Skip tests when suggested packages are not available. Skip Quarto tests when
run by CRAN. (#1074)
# rsconnect 1.2.2
* Use internally computed SHA1 sums and PKI signing when SHA1 is disabled
in FIPS mode. (#768, #1054)
* Allow Quarto content with a rendered script as its primary target. (#1055)
# rsconnect 1.2.1
* Restore the `LC_TIME` locale after computing an RFC-2616 date. (#1035)
* Address a problem inspecting Quarto content when the file names and paths
needed to be quoted. The resulting manifest lacked information about the
Quarto runtime, which caused difficult-to-understand deployment errors.
(#1037)
* Produce an error when Quarto content cannot be inspected. (#1032)
# rsconnect 1.2.0
* Addressed a number of republishing and collaboration issues where the
content was incorrectly published to a new location rather than reusing an
existing deployment. (#981, #1007, #1013, #1019)
* `showLogs()`, `configureApp()`, `setProperty()`, and `unsetProperty()`
search for the application by name when there are no matching deployment
records. (#985, #989)
* `rpubsUpload()` correctly records the initial RPubs destination, allowing
republishing. (#976)
* `deployApp()` and friends record multi-value `metadata` entries as
comma-separated values. (#1017)
* `accountInfo()` includes `name` and `username` fields. Older versions of
rsconnect store account records with a `username` field. Recent rsconnect
versions record `name`. Both `name` and `username` should contain the same
value. (#1024)
# rsconnect 1.1.1
* Added `space` parameter to deploy directly to a space in Posit Cloud.
* Improve reporting of errors returned by shinyapps.io. (#997)
* Remove most directory layout validation checks. (#998)
* Do not use `getOption("available_packages_filters")` option when calling
`available.packages()`. (#1002)
* Packages installed from source within an renv project are not associated
with repositories. (#1004)
# rsconnect 1.1.0
* Fixed analysis of directories that were smaller than the
`rsconnect.max.bundle.files=10000` limit but larger than the
`renv.config.dependencies.limit=1000` limit. (#968)
* Ignore `.env`, `.venv`, and `venv` files only when they reference Python
virtual environments. (#972)
* `deployApp()` and `writeManifest()` accept optional `envManagement`,
`envManagementR`, and `envManagementPy` arguments. These args specify whether
Posit Connect should install packages in the package cache.
If `envManagement` is `FALSE` then Connect will not perform any package
installation and it is the administrator's responsibility to ensure the
required R/Python packages are available in the runtime environment. This is
especially useful if off-host execution is enabled, when the execution
environment (specified by the `image` argument) already contains the required
packages. These values are ignored when
`Applications.ManifestEnvironmentManagementSelection = false`.
Requires Posit Connect `>=2023.07.0`. (#977)
* Fix account discovery by `showProperties()`. (#980)
# rsconnect 1.0.2
* Fixed redeployments to shinyapps.io where `appName` is provided, but no local
record of the deployment exists. (#932)
* `deployApp()` and `writeManifest()` now error if your library and `renv.lock`
are out-of-sync. Previously it always used what was defined in the `renv.lock`
but that was (a) slow and (b) could lead to different results than what you
see when running locally (#930).
* Deploying from an renv project includes the `renv.lock` in the bundle. A
manifest created for an renv project references the `renv.lock` in the
`manifest.json`. (#926)
* Use the environment variable `RSCONNECT_PACKRAT` to analyze dependencies
using packrat, as was done prior to rsconnect-1.0.0. Use of the
`rsconnect.packrat` option is discouraged, as it is not effective when using
push-button deployment in the RStudio IDE. (#935)
* The `renv.lock` is ignored when the `RSCONNECT_PACKRAT` environment variable
or the `rsconnect.packrat` option is set. (#936)
* The content type is inferred by analyzing the set of top-level files. (#942)
* `deployApp()` and `writeManifest()` accept an optional `appMode` argument.
Provide this argument if your project includes auxiliary files which mislead
the existing `appMode` inference. For example, if an HTML project includes
a downloadable Shiny `app.R`, that content will be assumed to be a Shiny
application even if that application is not meant to be run. (#948)
* `appDependencies()` accepts an `appFileManifest` argument as an alternate
way of providing the target set of files.
# rsconnect 1.0.1
* `deployDoc()` includes `.Rprofile`, `requirements.txt` and `renv.lock` when
deploying `.Rmd` or `.qmd`. These additional files are not included with
rendered HTML documents. (#919)
* Explicit renv dependencies are preserved. (#916)
# rsconnect 1.0.0
## New features
* `deployApp()` and `deployDoc()` now support deploying static content to Posit
Cloud. Static RMarkdown and Quarto content can be rendered server-side.
* rsconnect requires renv 1.0.0.
* `deployApp()` and `writeManifest()` now respect renv lock files, if present.
If you don't want to use these lockfiles, and instead return the previous
behaviour of snapshotting on every deploy, add your `renv.lock` to
`.rscignore` (#671). Learn more `?appDependencies()`.
Additionally, `deployApp()` and `writeManifest()` now use renv to capture app
dependencies, rather than packrat. If this causes a previously working deploy
to fail, please file an issue then set `options(rsconnect.packrat = TRUE)` to
revert to the previous behaviour.
* `deployApp()`'s `quarto` argument now takes values `TRUE`, `FALSE` or
`NA`. The previous value (a path to a quarto binary) is now ignored,
and instead we automatically figure out the package from `QUARTO_PATH` and
`PATH` env vars (#658). `deploySite()` now supports quarto websites (#813).
* `deployApp()` gains a new `envVars` argument which takes a vector of the
names of environment variables that should be securely copied to the server.
The names (not values) of these environment variables are also saved in the
deployment record and will be updated each time you re-deploy the app (#667).
This currently only works with Connect, but we hope to add support to
Posit cloud and shinyapps.io in the future.
* rsconnect gains two new functions for understanding and updating the
environment variables that your apps currently use. `listServerEnvVars()`
will return a data frame of applications, with a `envVars` list-column
giving the names of the environment variables used by each application.
`updateServerEnvVars()` will update all applications that use a specific
environment variable with the current value of that environment variable
(#667).
## Lifecycle changes
* Non-libcurl `rsconnect.http` options have been deprecated. This allows us to
focus our efforts on a single backend, rather than spreading development
efforts across five. The old backends will remain available for at least 2
years, but if you are using them because libcurl doesn't work for you, please
report the problem ASAP so we can fix it.
* `addConnectServer()` has been deprecated because it does the same
thing as `addServer()` now that `addServer()` also validates URLs.
* `deployTFModel()` is defunct. Posit Connect no longer supports hosting of
TensorFlow Model APIs. A TensorFlow model can be deployed as a [Plumber
API](https://tensorflow.rstudio.com/guides/deploy/plumber.html), [Shiny
application](https://tensorflow.rstudio.com/guides/deploy/shiny), or other
supported content type.
* `discoverServer()` has been deprecated; it never worked.
* `deployApp("foo.Rmd")` has been deprecated. It was never documented, and
it does the same job as `deployDoc()` (#698).
## Minor improvements and bug fixes
* New `rsconnect.http.headers` and `rsconnect.http.cookies` allow you to
set extra arbitrary additional headers/cookies on each request (#405).
Their use is documented in the new `vignette("custom-http")`.
* Uploading large files to RPubs works once more (#450).
* When recording details about deployments to Posit Cloud, appId now represents
the content id (as seen in URLs of the format
`https://posit.cloud/content/{id}`) instead of the application id.
* Deployment records no longer contain the time the app was deployed (`when`)
or when it's metadata was last synced (`lastSyncTime`) as these variables
are not very useful, and they lead to uninteresting diffs if you have
committed the deployment records to git (#770). A `version` field has been
added to deployment DCF files to facilitate future file format changes, if
needed. Its value for this release is `1`.,
* `accounts()` returns a zero-row data frame if no accounts are registered.
* `accountInfo()` and `removeAccount()` no longer require `account` be
supplied (#666).
* `accountInfo()` and `servers()` redact sensitive information (secrets,
private keys, and certificates) to make it hard to accidentally reveal
such information in logs (#675).
* `addServer()` includes the port in the default server name, if present.
* `appDependencies()` includes implicit dependencies, and returns an additional
column giving the Repository (#670). Its documentation contains more
information about how dependency discovery works, and how you can control
it, if needed.
* `applications()` now returns the application title, if available (#484),
and processes multiple pages of results from a Connect server (#860).
* `connectApiUser()` now clearly requires an `apiKey` (#741).
* `deployApp()` output has been thoroughly reviewed and tweaked. As well as
general polish it now gives you more information about what it has discovered
about the deployment, like the app name, account & server, and which files
are included in the bundle (#669).
* `deployApp()` is more aggressive about saving deployment data, which should
make it less likely that you need to repeat yourself after a failed
deployment. In particular, it now saves both before and after uploading the
contents (#677) and it saves when you're updating content originally created
by someone else (#270).
* `deployApp()` now gives an actionable error if you attempt to set
visibility of an app deployed to posit.cloud (#838).
* `deployApp()` now uses a stricter policy for determining whether or not
a locally installed package can be successfully installed on the deployment
server. This means that you're more likely to get a clean failure prior to
deployment (#659).
* `deployApp()` will now detect if you're attempting to publish to an app
that has been deleted and will prompt you to create a new app (#226).
* `deployApp()` includes some new conveniences for large uploads including
reporting the size of the bundle you're uploading and showing a progress bar
in interactive sessions (#754).
* `deployApp()` now follows redirects, which should make it more robust to your
server moving to a new url (#674).
* `deployApp()` uses simpler logic for determining whether it should create a
new app or update an existing app. Now `appName`, `account`, and `server` are
used to find existing deployments. If none are found, it will create a new
deployment; if one is found, it'll be updated; if more than one are found, it
will prompt you to disambiguate (#666).
* `deployApp()` improves account resolution from `account` and `server`
arguments by giving specific recommendations on the values that you might use
in the case of ambiguity or lack of matches (#666). Additionally, you'll now
receive a clear error if you accidentally provide something other than a
string or `NULL` to these arguments.
* `deployApp()` now generates an interactive prompt to select `account`/`server`
(if no previous deployments) or `appName`/`account`/`server` (if multiple
previous deployments) (#691).
* `deployApp()` now advertises which startup scripts are run at the normal
`logLevel`, and it evaluates each script in its own environment (#542).
* `deployApp()` now derives `appName` from `appDir` and `appPrimaryDoc`,
never using the title (#538). It now only simplifies the path if you are
publishing to shinyapps.io, since its restrictions on application names are
much tighter than those of Posit Connect.
* `deployApp()` will now warn if `appFiles` or `appManifestFiles` contain
files that don't exist, rather than silently ignoring them (#706).
* `deployApp()` excludes temporary backup files (names starting or ending
with `~`) when automatically determining files to bundle (#111) as well as
directories that are likely to be Python virtual environments (#632).
Additionally, ignore rules are always now applied to all directories;
previously some (like `.Rproj.user` and `"manifest.json"`) were only
applied to the root directory. It correctly handles `.rscignore` files
(i.e. as documented) (#568).
* `deployApp(appSourceDoc)` has been deprecated; it did the same job as
`recordDir`.
* `deployDoc()` includes a `.Rprofile` in the bundle, if one is found in the
same directory as the document.
* `lint()` should have fewer false positives for path problems:
the relative path linter has been removed (#244) and the case-sensitive
linter now only checks strings containing a `/` (#611).
* New `listDeploymentFiles()`, which supsersedes `listBundleFiles()`.
It now errors when if the bundle is either too large or contains too many
files, rather than silently truncating as before (#684).
* `serverInfo()` and `removeServer()` no longer require a `server` when
called interactively.
* `showMetrics()` once again returns a correctly named data frame (#528).
* Removed Rmd generation code (`writeRmdIndex()`) which had not worked, or
been necessary, for quite some time (#106, #109).
* Locale detection has been improved on windows (#233).
* The `rsconnect.pre.deploy` and `rsconnect.post.deploy` hooks are now always
called with the content directory, not sometimes the path to a specific file
(#696).
* Functions that should only interact with shinyapps.io enforce the server
type. Updated `addAuthorizedUser()`, `removeAuthorizedUser()`,
`showUsers()`, `showInvited()`, `resendInvitation()`, `configureApp()`,
`setProperty()`, `unsetProperty()`, `purgeApp()`, `restartApp()`,
`terminateApp()`, `showUsage()`, and `showMetrics()` (#863, #864).
* When needed packages are not installed, and you're in an interactive
environment, rsconnect will now prompt you to install them (#665).
* The confirmation prompt presented upon lint failures indicates "no" as its
default. (#652)
# rsconnect 0.8.29
* Introduced support for publishing to Posit Cloud. This feature is currently
in closed beta and requires access to an enabled account on Posit Cloud.
See [Posit Cloud's Announcement](https://posit.cloud/learn/whats-new#publishing)
for more information and to request access.
* Update company and product names for rebranding to Posit.
# rsconnect 0.8.28
* Shiny applications and Shiny documents no longer include an implicit
dependency on [`ragg`](https://ragg.r-lib.org) when that package is present
in the local environment. This reverts a change introduced in 0.8.27.
Shiny applications should add an explicit dependency on `ragg` (usually with
a `library("ragg")` statement) to see it used by `shiny::renderPlot` (via
`shiny::plotPNG`).
The documentation for `shiny::plotPNG` explains the use of `ragg`. (#598)
* Fix bug that prevented publishing or writing manifests for non-Quarto content
when a Quarto path was provided to the `quarto` argument of `writeManifest()`,
`deployApp()`, and related functions.
* Escape account names when performing a directory search to determine an
appropriate server. (#620)
# rsconnect 0.8.27
* Quarto content will no longer silently deploy as R Markdown content when
Quarto metadata is missing or cannot be gathered. Functions will error,
requesting the path to a Quarto binary in the `quarto` argument. (#594)
* Fix typo for `.rscignore`. (#599)
* Quarto deployments specifying only an `appDir` and `quarto` binary but not an
`appPrimaryDoc` work more consistently. A directory containing a `.qmd` file
will deploy as Quarto content instead of failing, and a directory containing
an `.Rmd` file will successfully deploy as Quarto content instead of falling
back to R Markdown. (#601)
* If the `ragg` package is installed locally, it is now added as an implicit
dependency to `shiny` apps since `shiny::renderPlot()` now uses it by default
(when available). This way, `shiny` apps won't have to add `library(ragg)` to
get consistent (higher-quality) PNG images when deployed. (#598)
# rsconnect 0.8.26
* Add ability to resend shinyapps.io application invitations (#543)
* Show expiration status in shinyapps.io for invitations (#543)
* Allow shinyapps.io deployments to be private at creation time (#403)
* Update the minimum `openssl` version to 2.0.0 to enable publishing for users
on FIPS-compliant systems without the need for API keys. (#452)
* Added Quarto support to `writeManifest`, which requires passing the absolute
path to a Quarto executable to its new `quarto` parameter
* Added `quarto` parameter to `deployApp` to enable deploying Quarto documents
and websites by supplying the path to a Quarto executable
* Added support for deploying Quarto content that uses only the `jupyter` runtime
* Added support for selecting a target `image` in the bundle manifest
* The `showLogs` function takes a `server` parameter. (#57)
* Added the `rsconnect.tar` option, which can be used to specify the path to a
`tar` implementation instead of R's internal implementation. The previous
method, using the `RSCONNECT_TAR` environment variable, still works, but the
new option will take precedence if both are set.
# rsconnect 0.8.25
* Use the `curl` option `-T` when uploading files to avoid out of memory
errors with large files. (#544)
* The `rsconnect.max.bundle.size` and `rsconnect.max.bundle.files` options are
enforced when processing an enumerated set of files. Previously, these
limits were enforced only when bundling an entire content directory. (#542)
* Preserve file time stamps when copying files into the bundle staging
directory, which then propagates into the created tar file. (#540)
* Configuration directories align with CRAN policy and use the location named
by `tools::R_user_dir`. Configuration created by earlier versions of this
package is automatically migrated to the new location. (#550)
# rsconnect 0.8.24
* Added support for publishing Quarto documents and websites
* Added support for `.rscignore` file to exclude files or directories from publishing (#368)
* Fixed issue causing missing value errors when publishing content containing filenames with extended characters (#514)
* Fixed issue preventing error tracebacks from displaying (#518)
# rsconnect 0.8.18
* Fixed issue causing configuration directory to be left behind after `R CMD CHECK`
* Fixed incorrect subdirectory nesting when storing configuration in `R_USER_CONFIG_DIR`
* Added linter for different-case Markdown links (#388)
* Use new Packrat release on CRAN, 0.6.0 (#501)
* Fix incorrect linter messages referring to `shiny.R` instead of `server.R` (#509)
* Warn, rather than err, when the repository URL for a package dependency
cannot be validated. This allows deployment when using archived CRAN
packages, or when using packages installed from source that are available on
the server. (#508)
* Err when the app-mode cannot be inferred; seen with empty directories/file-sets (#512)
* Add `verbose` option to `writeManifest` utility (#468)
# rsconnect 0.8.17
* Fixed issue where setting `options(rsconnect.http.trace.json = TRUE)` could cause deployment errors with some HTTP transports (#490)
* Improve how large bundles (file size and count) are detected (#464)
* The `RSCONNECT_TAR` environment variable can be used to select the tar implementation used to create bundles (#446)
* Warn when files are owned by users or groups with long names, as this can cause the internal R tar implementation to produce invalid archives (#446)
* Add support for syncing the deployment metadata with the server (#396)
* Insist on ShinyApps accounts in `showUsers()` (#398)
* Improve the regex used for the browser and browseURL lints to include a word boundary (#400)
* Fixed bug where `connectApiUser()` did not set a user id (#407)
* New arguments to `deployApp` to force the generation of a Python environment file or a `requirements.txt` file (#409)
* Fail when no repository URL is available for a dependent package (#410)
* Fix error when an old version of a package is installed and a current version isn't available (#431, #436)
* Fix error where packages couldn't be found with nonstandard contrib URLs. (#451, #457)
* Improve detection of Shiny R Markdown files when `server.R` is present (#461)
* Fix failure to write manifest when package requires a newer R version than the active version (#467)
* Increase default HTTP timeout on non-Windows platforms (#476)
* Require `packrat` 0.5 or later (#434)
* Fix error when handling empty application / content lists (#417, #395)
* Calls to `writeManifest()` no longer reference `packrat` files in the generated `manifest.json`. The `packrat` entries were transient and only existed while computing dependencies. (#472)
* Fix `applications` when ShinyApps does not return `size` details (#496)
* GitLab is seen as a valid SCM source (#491)
# rsconnect 0.8.16
* Prevent attempts to deploy Connect applications without uploading (#145)
* Flag usage of `browser()` debugging calls when deploying (#196)
* Prevent accidental deployment of Plumber APIs to shinyapps.io (#204)
* Allow `appId` and other global deployment parameters to `deploySite` (#231)
* Fix error when running `deployments()` without any registered accounts (#261)
* Omit `renv` files from deployment bundle (#367)
* Fix failure to deploy in Packrat projects (#370)
* Fix issue deploying when a package exists in multiple repos (#372)
* Honor `RETICULATE_PYTHON` when writing manifests (#374)
* Add `on.failure` user hook to run a function when `deployApp()` fails (#375)
* Fix error when showing non-streaming logs (#377)
* Use internally computed MD5 sums when MD5 is disabled in FIPS mode (#378, #382)
* Make it clearer which log entries are emitted by RStudio Connect (#385)
* Add support for `requirements.txt` for Python, if it exists (#386)
* Restore compatibility with R < 3.5 (#394)
* Add support for authenticating with Connect via an API key rather than a token (#393)
# rsconnect 0.8.15
* Switch from **RCurl** to **curl** as the default HTTP backend (#325)
* Add `purgeApp()` function to purge previously deployed shinyapps.io applications (#352)
================================================
FILE: R/.editorconfig
================================================
[*.R]
indent_style = space
indent_size = 2
end_of_line = lf
max_line_length = 100
insert_final_newline = true
================================================
FILE: R/account-find.R
================================================
# Return a list containing the name and server associated with a matching account.
#
# Use `accountInfo()` and `findAccountInfo()` to load credentials associated with this account.
findAccount <- function(
accountName = NULL,
server = NULL,
error_call = caller_env()
) {
check_string(
accountName,
allow_null = TRUE,
arg = "account",
call = error_call
)
check_string(server, allow_null = TRUE, call = error_call)
accounts <- accounts()
if (nrow(accounts) == 0) {
cli::cli_abort(
c(
"No accounts registered.",
i = "To register an account, call {.fun rsconnect::connectCloudUser} (Posit Connect Cloud),
{.fun rsconnect::connectUser} (Posit Connect),
or {.fun rsconnect::setAccountInfo} (shinyapps.io)."
),
call = error_call
)
}
if (!is.null(accountName) && !is.null(server)) {
selected <- accounts$server == server & accounts$name == accountName
theseAccounts <- accounts[selected, , drop = FALSE]
if (nrow(theseAccounts) == 0) {
cli::cli_abort(
c(
"Can't find account with {.arg name} = {.str {accountName}} and {.arg server} = {.str {server}}",
i = "Call {.fun accounts} to see available options."
),
call = error_call
)
}
} else if (is.null(accountName) && !is.null(server)) {
selected <- accounts$server == server
theseAccounts <- accounts[selected, , drop = FALSE]
if (nrow(theseAccounts) == 0) {
cli::cli_abort(
c(
"Can't find any accounts with {.arg server} = {.str {server}}.",
i = "Known servers are {.str {unique(accounts$server)}}."
),
call = error_call
)
} else if (nrow(theseAccounts) > 1) {
cli::cli_abort(
c(
"Found multiple accounts for {.arg server} = {.str {server}}.",
"Please disambiguate by setting {.arg account}.",
i = "Known account names are {.str {theseAccounts$name}}."
),
call = error_call
)
}
} else if (!is.null(accountName) && is.null(server)) {
selected <- accounts$name == accountName
theseAccounts <- accounts[selected, , drop = FALSE]
if (nrow(theseAccounts) == 0) {
cli::cli_abort(
c(
"Can't find any accounts with {.arg account} = {.str {accountName}}.",
i = "Available account names: {.str {unique(accounts$name)}}."
),
call = error_call
)
} else if (nrow(theseAccounts) > 1) {
cli::cli_abort(
c(
"Found multiple accounts for {.arg account} = {.str {accountName}}.",
"Please disambiguate by setting {.arg server}.",
i = "Available servers: {.str {theseAccounts$server}}."
),
call = error_call
)
}
} else {
theseAccounts <- accounts
if (nrow(theseAccounts) > 1) {
if (is_interactive()) {
labels <- accountLabel(accounts$name, accounts$server)
choice <- cli_menu(
"Found multiple accounts.",
"Which one do you want to use?",
labels
)
theseAccounts <- theseAccounts[choice, , drop = FALSE]
} else {
cli::cli_abort(
c(
"Found multiple accounts.",
"Please disambiguate by setting {.arg server} and/or {.arg account}.",
i = "Available servers: {.str {unique(theseAccounts$server)}}.",
i = "Available account names: {.str {unique(theseAccounts$name)}}."
),
call = error_call
)
}
}
}
as.list(theseAccounts)
}
================================================
FILE: R/accounts.R
================================================
#' Account Management Functions
#'
#' @description
#' Functions to enumerate and remove accounts on the local system. Prior to
#' deploying applications you need to register your account on the local system.
#'
#' Supported servers: All servers
#'
#' @details
#' You register an account using the [setAccountInfo()] function (for
#' ShinyApps) or [connectUser()] function (for other servers). You can
#' subsequently remove the account using the `removeAccount` function.
#'
#' The `accounts` and `accountInfo` functions are provided for viewing
#' previously registered accounts.
#'
#' @param name Name of account
#' @param server Name of the server on which the account is registered
#' (optional; see [servers()])
#'
#' @return `accounts` returns a data frame with the names of all accounts
#' registered on the system and the servers on which they reside.
#' `accountInfo` returns a list with account details.
#'
#' @rdname accounts
#' @export
accounts <- function(server = NULL) {
configPaths <- accountConfigFiles(server)
names <- file_path_sans_ext(basename(configPaths))
servers <- basename(dirname(configPaths))
servers[servers == "."] <- "shinyapps.io"
data.frame(name = names, server = servers, stringsAsFactors = FALSE)
}
#' Register account on Posit Connect
#
#' @description
#' `connectUser()` and `connectApiUser()` connect your Posit Connect account to
#' the rsconnect package so that it can deploy and manage applications on
#' your behalf.
#'
#' `connectUser()` is the easiest place to start because it allows you to
#' authenticate in-browser to your Posit Connect server. `connectApiUser()` is
#' appropriate for non-interactive settings; you'll need to copy-and-paste the
#' API key from your account settings.
#'
#' Supported servers: Posit Connect servers
#'
#' @param account A name for the account to connect.
#' @param server The server to connect to.
#' @param launch.browser If true, the system's default web browser will be
#' launched automatically after the app is started. Defaults to `TRUE` in
#' interactive sessions only. If a function is passed, it will be called
#' after the app is started, with the app URL as a parameter.
#' @param apiKey The API key used to authenticate the user
#' @param quiet Whether or not to show messages and prompts while connecting the
#' account.
#' @family Account functions
#' @export
connectApiUser <- function(
account = NULL,
server = NULL,
apiKey,
quiet = FALSE
) {
server <- findServer(server)
checkConnectServer(server)
user <- getAuthedUser(server, apiKey = apiKey)
registerAccount(
serverName = server,
accountName = account %||% user$username,
accountId = user$id,
apiKey = apiKey
)
if (!quiet) {
accountLabel <- accountLabel(user$username, server)
cli::cli_alert_success("Registered account for {accountLabel}")
}
invisible()
}
#' Register account on Posit Connect in Snowpark Container Services
#'
#' @description
#' `connectSPCSUser()` connects your Posit Connect account to the rsconnect
#' package so it can deploy and manage applications on your behalf.
#' Configure a
#' [`connections.toml` file](https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-cli#location-of-the-toml-configuration-fil)
#' in the appropriate location.
#'
#' SPCS deployments require both Snowflake authentication (via the connection
#' name) and a Posit Connect API key. The Snowflake token provides proxied
#' authentication to reach the Connect server, while the API key identifies
#' the user to Connect itself.
#'
#' If `snowflakeConnectionName` is not provided, \pkg{rsconnect} will attempt to
#' use the default Snowflake connection from the `connections.toml` file,
#' provided that the account matches the Connect server's URL.
#'
#' Supported servers: Posit Connect servers
#'
#' @inheritParams connectApiUser
#' @param snowflakeConnectionName Name of the Snowflake connection in
#' `connections.toml` to use for authentication or `NULL` to use the default
#' (when applicable).
#' @export
connectSPCSUser <- function(
account = NULL,
server = NULL,
apiKey,
snowflakeConnectionName = NULL,
quiet = FALSE
) {
server <- findServer(server)
checkConnectServer(server)
serverUrl <- serverInfo(server)$url
snowflakeConnectionName <- snowflakeConnectionName %||%
getDefaultSnowflakeConnectionName(serverUrl)
user <- getSPCSAuthedUser(server, apiKey, snowflakeConnectionName)
registerAccount(
serverName = server,
accountName = account %||% user$username,
accountId = user$id,
apiKey = apiKey,
snowflakeConnectionName = snowflakeConnectionName
)
if (!quiet) {
accountLabel <- accountLabel(user$username, server)
cli::cli_alert_success("Registered account for {accountLabel}")
}
invisible()
}
getSPCSAuthedUser <- function(server, apiKey, snowflakeConnectionName) {
serverAddress <- serverInfo(server)
account <- list(
server = server,
apiKey = apiKey,
snowflakeConnectionName = snowflakeConnectionName
)
client <- clientForAccount(account)
client$currentUser()
}
#' @rdname connectApiUser
#' @export
connectUser <- function(
account = NULL,
server = NULL,
quiet = FALSE,
launch.browser = getOption("rsconnect.launch.browser", interactive())
) {
server <- findServer(server)
checkConnectServer(server)
resp <- getAuthTokenAndUser(server, launch.browser)
registerAccount(
serverName = server,
accountName = account %||% resp$user$username,
accountId = resp$user$id,
token = resp$token$token,
private_key = resp$token$private_key
)
if (!quiet) {
accountLabel <- accountLabel(resp$user$username, server)
cli::cli_alert_success("Registered account for {accountLabel}")
}
invisible()
}
# Filter accounts to those where the user has permission to create (i.e. publish) content.
filterPublishableAccounts <- function(accounts) {
Filter(
function(account) {
any(vapply(
account$permissions,
function(permission) {
identical(permission, "content:create")
},
logical(1)
))
},
accounts
)
}
#' Register account on Posit Connect Cloud
#
#' @description
#' `connectCloudUser()` connects your Posit Connect Cloud account to
#' the rsconnect package so that it can deploy and manage applications on
#' your behalf. It will open a browser window to authenticate, then prompt
#' you to create an account or select an account to use if you have multiple.
#'
#' Supported servers: Posit Connect Cloud servers
#'
#' @param launch.browser If true, the system's default web browser will be
#' launched automatically after the app is started. Defaults to `TRUE` in
#' interactive sessions only. If a function is passed, it will be called
#' after the app is started, with the app URL as a parameter.
#'
#' @family Account functions
#' @export
connectCloudUser <- function(launch.browser = TRUE) {
authClient <- cloudAuthClient()
deviceAuth <- authClient$createDeviceAuth()
verificationUriComplete <- addUtmParameters(
deviceAuth$verification_uri_complete
)
# Alert user and open browser for verification
if (isTRUE(launch.browser)) {
cli::cli_alert_info(
"Opening login page - confirm the code entered matches the code: {deviceAuth$user_code}."
)
utils::browseURL(verificationUriComplete)
} else if (is.function(launch.browser)) {
cli::cli_alert_info(
"Opening login page - confirm the code entered matches the code: {deviceAuth$user_code}."
)
launch.browser(verificationUriComplete)
} else {
cli::cli_alert_info(
"Open {.url {verificationUriComplete}} to authenticate and confirm the code entered matches the code: {deviceAuth$user_code}."
)
}
tokenResponse <- waitForDeviceAuth(authClient, deviceAuth)
accessToken <- tokenResponse$access_token
refreshToken <- tokenResponse$refresh_token
client <- connectCloudClient(
parseHttpUrl(connectCloudUrls()$api),
list(accessToken = accessToken, refreshToken = refreshToken)
)
getAccounts <- function() {
accountsResponse <- tryCatch(
{
response <- client$getAccounts()
response$data
},
rsconnect_http_401 = function(err) {
if (err$errorType == "no_user_for_lucid_user") {
return(list())
}
stop(err)
}
)
}
accounts <- getAccounts()
accountsWhereUserCanPublish <- filterPublishableAccounts(accounts)
cloudUiUrl <- connectCloudUrls()$ui
if (length(accountsWhereUserCanPublish) == 0) {
if (length(accounts) == 0) {
accountCreationPage <- addUtmParameters(paste0(
cloudUiUrl,
"/account/done"
))
if (isTRUE(launch.browser)) {
cli::cli_alert_info(
"To deploy, you must first create an account. Opening account creation page..."
)
utils::browseURL(accountCreationPage)
} else if (is.function(launch.browser)) {
cli::cli_alert_info(
"To deploy, you must first create an account. Opening account creation page..."
)
launch.browser(accountCreationPage)
} else {
cli::cli_alert_info(
"To deploy, you must first create an account. Please go to {.url accountCreationPage} to create one."
)
}
# poll for account for up to 10 minutes
for (i in 1:300) {
Sys.sleep(2)
accounts <- getAccounts()
if (length(accounts) > 0) {
accountsWhereUserCanPublish <- filterPublishableAccounts(accounts)
if (length(accountsWhereUserCanPublish) > 0) {
break
}
}
}
if (length(accountsWhereUserCanPublish) == 0) {
cli::cli_abort(
"Timed out waiting for an account to be created. Try again after creating a new account."
)
}
} else {
cli::cli_abort(
"You do not have permission to publish content on any of your accounts. To publish, you may create a new account at {.url cloudUiUrl}."
)
}
}
# prompt the user to select an account if there's more than one they can publish to
if (length(accountsWhereUserCanPublish) > 1) {
cli::cli_alert_info("You have permission to publish to multiple accounts.")
accountNames <- vapply(
accountsWhereUserCanPublish,
function(account) account$name,
character(1)
)
# selected <- utils::menu(accountNames, title = "Select an account to use:")
selected <- cli_menu(
"Multiple accounts found.",
"Which account do you want to use?",
accountNames
)
account <- accountsWhereUserCanPublish[[selected]]
} else {
account <- accountsWhereUserCanPublish[[1]]
}
registerAccount(
serverName = "connect.posit.cloud",
accountName = account$name,
accountId = account$id,
accessToken = accessToken,
refreshToken = refreshToken
)
cli::cli_alert_success("Registered account.")
}
# Poll the server until the user has completed device authentication, returning
# the token response once finished.
waitForDeviceAuth <- function(authClient, deviceAuth) {
pollingInterval <- deviceAuth$interval
while (TRUE) {
Sys.sleep(pollingInterval)
tokenResponse <- tryCatch(
authClient$exchangeToken(list(
grant_type = "urn:ietf:params:oauth:grant-type:device_code",
device_code = deviceAuth$device_code
)),
rsconnect_http_400 = function(err) {
errorCode <- err$body
if (errorCode == "authorization_pending") {
return(NULL)
} else if (errorCode == "slow_down") {
pollingInterval <<- pollingInterval + 5
return(NULL)
} else if (errorCode == "expired_token") {
cli::cli_abort("Verification code has expired.")
} else if (errorCode == "access_denied") {
cli::cli_abort("Authorization request was denied.")
}
cli::cli_abort("Error during authentication: {error_code}")
}
)
if (!is.null(tokenResponse)) {
return(tokenResponse)
}
}
}
getAuthTokenAndUser <- function(server, launch.browser = TRUE) {
token <- getAuthToken(server)
if (isTRUE(launch.browser)) {
utils::browseURL(token$claim_url)
} else if (is.function(launch.browser)) {
launch.browser(token$claim_url)
}
if (isFALSE(launch.browser)) {
cli::cli_alert_warning("Open {.url {token$claim_url}} to authenticate")
} else {
cli::cli_alert_info(
"A browser window should open to complete authentication"
)
cli::cli_alert_warning(
"If it doesn't open, please go to {.url {token$claim_url}}"
)
}
user <- waitForAuthedUser(
server,
token = token$token,
private_key = token$private_key
)
list(
token = token,
user = user
)
}
# Used by the IDE
getAuthToken <- function(server, userId = 0) {
account <- list(server = server)
client <- clientForAccount(account)
# Check if we already have an API key for this server, in which case we can
# bypass the token auth flow.
serverUrl <- serverInfo(server)$url
cachedApiKey <- getCachedApiKey(serverUrl)
if (!is.null(cachedApiKey)) {
# Verify that the API key actually works.
user <- tryCatch(client$currentUser(), error = function(e) NULL)
if (!is.null(user)) {
# Return the API key as the "token" with a zero-length private key.
# waitForAuthedUser will use this to detect federated authentication later
# on.
#
# This in fairly awkward in-band signalling, but reflects the fact that we
# can't change what RStudio expects to happen here.
return(list(
token = cachedApiKey,
private_key = secret(""),
# Open a special "you're already authenticated" page as the "claim URL".
claim_url = sub("/__api__$", "/connect/#/auth-success", serverUrl),
# Signal to future RStudio versions that auth is already complete and
# there is no need to open a browser window.
authenticated = TRUE
))
}
}
token <- generateToken()
# Send public key to server, and generate URL where the token can be claimed
response <- client$addToken(list(
token = token$token,
public_key = token$public_key,
user_id = 0L
))
list(
token = token$token,
private_key = secret(token$private_key),
claim_url = response$token_claim_url
)
}
# generateToken generates a token for signing requests sent to the Posit
# Connect service. The token's ID and public key are sent to the server, and
# the private key is saved locally.
generateToken <- function() {
key <- openssl::rsa_keygen(2048L)
priv.der <- openssl::write_der(key)
pub.der <- openssl::write_der(key$pubkey)
tokenId <- paste(c("T", openssl::rand_bytes(16)), collapse = "")
list(
token = tokenId,
public_key = openssl::base64_encode(pub.der),
private_key = openssl::base64_encode(priv.der)
)
}
waitForAuthedUser <- function(
server,
token = NULL,
private_key = NULL,
apiKey = NULL
) {
# Detect when the "token" is actually an API key by looking for an empty
# secret.
if (!is.null(token) && !nzchar(private_key)) {
return(getAuthedUser(server, apiKey = token))
}
# keep trying to authenticate until we're successful; server returns
# 500 "Token is unclaimed error" (Connect before 2024.05.0)
# 401 "Unauthorized" occurs before the token has been claimed.
cli::cli_progress_bar(format = "{cli::pb_spin} Waiting for authentication...")
repeat {
for (i in 1:10) {
Sys.sleep(0.1)
cli::cli_progress_update()
}
user <- tryCatch(
getAuthedUser(
server,
token = token,
private_key = private_key,
apiKey = apiKey
),
rsconnect_http_401 = function(err) NULL,
rsconnect_http_500 = function(err) NULL
)
if (!is.null(user)) {
cli::cli_progress_done()
break
}
}
user
}
getAuthedUser <- function(
server,
token = NULL,
private_key = NULL,
apiKey = NULL
) {
if (!xor(is.null(token) && is.null(private_key), is.null(apiKey))) {
cli::cli_abort(
"Must supply either {.arg token} + {private_key} or {.arg apiKey}"
)
}
account <- list(
server = server,
apiKey = apiKey,
token = token,
private_key = private_key
)
client <- clientForAccount(account)
client$currentUser()
}
#' Register account on shinyapps.io
#'
#' @description
#' Configure a ShinyApps account for publishing from this system.
#'
#' Supported servers: ShinyApps servers
#'
#' @param name Name of account to save or remove
#' @param token User token for the account
#' @param secret User secret for the account
#' @param server Server to associate account with.
#'
#' @examples
#' \dontrun{
#'
#' # register an account
#' setAccountInfo("user", "token", "secret")
#'
#' # remove the same account
#' removeAccount("user")
#' }
#'
#' @family Account functions
#' @export
setAccountInfo <- function(name, token, secret, server = "shinyapps.io") {
check_string(name)
check_string(token)
check_string(secret)
check_string(server)
accountId <- findShinyAppsAccountId(name, token, secret, server)
registerAccount(
serverName = server,
accountName = name,
accountId = accountId,
token = token,
secret = secret
)
invisible()
}
# A user can have multiple accounts, so iterate over all accounts looking
# for one with the specified name
findShinyAppsAccountId <- function(
name,
token,
secret,
server,
error_call = caller_env()
) {
if (secret == "<SECRET>") {
cli::cli_abort(
c(
"You've copied and pasted the wrong thing.",
i = "Either click 'Show secret' or 'Copy to clipboard'."
),
call = error_call
)
}
account <- list(token = token, secret = secret, server = server)
client <- clientForAccount(account)
userId <- client$currentUser()$id
accountId <- NULL
accounts <- client$accountsForUser(userId)
for (account in accounts) {
if (identical(account$name, name)) {
return(account$id)
}
}
cli::cli_abort(
"Unable to determine {.arg accountId} for account {.str {name}}"
)
}
#' @rdname accounts
#' @family Account functions
#' @export
accountInfo <- function(name = NULL, server = NULL) {
findAccountInfo(name, server)
}
# Discovers then loads details about an account from disk.
# Internal equivalent to accountInfo that lets callers provide error context.
findAccountInfo <- function(
name = NULL,
server = NULL,
error_call = caller_env()
) {
fullAccount <- findAccount(name, server, error_call = error_call)
configFile <- accountConfigFile(fullAccount$name, fullAccount$server)
accountDcf <- read.dcf(configFile, all = TRUE)
info <- as.list(accountDcf)
# Account records previously had username, now have name. Internal callers expect "name", but
# external callers may expect "username". (#1024)
info$name <- info$name %||% info$username
info$username <- info$name
# remove all whitespace from private key
if (!is.null(info$private_key)) {
info$private_key <- gsub("[[:space:]]", "", info$private_key)
}
# For standard Connect servers where there are no persisted credentials, try
# identity federation (added in v2026.01.0).
if (isConnectServer(fullAccount$server) && hasNoCredentials(info)) {
tryCatch(
{
serverUrl <- serverInfo(fullAccount$server)$url
info$apiKey <- attemptIdentityFederation(serverUrl)
},
error = function(e) NULL
)
}
# Hide credentials
info$private_key <- secret(info$private_key)
info$secret <- secret(info$secret)
info$apiKey <- secret(info$apiKey)
info$snowflakeToken <- secret(info$snowflakeToken)
info
}
hasAccount <- function(name, server) {
file.exists(accountConfigFile(name, server))
}
#' @rdname accounts
#' @export
removeAccount <- function(name = NULL, server = NULL) {
fullAccount <- findAccount(name, server)
configFile <- accountConfigFile(fullAccount$name, fullAccount$server)
file.remove(configFile)
invisible(NULL)
}
registerAccount <- function(
serverName,
accountName,
accountId,
token = NULL,
secret = NULL,
private_key = NULL,
apiKey = NULL,
snowflakeConnectionName = NULL,
accessToken = NULL,
refreshToken = NULL
) {
check_string(serverName)
check_string(accountName)
if (!is.null(secret)) {
secret <- as.character(secret)
}
fields <- list(
name = accountName,
server = serverName,
accountId = accountId,
token = token,
secret = secret,
private_key = private_key,
apiKey = apiKey,
snowflakeConnectionName = snowflakeConnectionName,
accessToken = accessToken,
refreshToken = refreshToken
)
path <- accountConfigFile(accountName, serverName)
dir.create(dirname(path), recursive = TRUE, showWarnings = FALSE)
write.dcf(compact(fields), path, width = 100)
# set restrictive permissions on it if possible
if (identical(.Platform$OS.type, "unix")) {
Sys.chmod(path, mode = "0600")
}
path
}
accountLabel <- function(account, server) {
# Note: The incoming "account" may correspond to our local account name, which does not always
# match the remote username.
paste0("server: ", server, " / username: ", account)
}
================================================
FILE: R/appDependencies.R
================================================
#' Detect application dependencies
#'
#' @description
#' `appDependencies()` recursively detects all R package dependencies for an
#' application by parsing all `.R` and `.Rmd` files and looking for calls
#' to `library()`, `require()`, `requireNamespace()`, `::`, and so on.
#' It then adds implicit dependencies (i.e. an `.Rmd` requires Rmarkdown)
#' and adds all recursive dependencies to create a complete manifest of
#' package packages need to be installed to run the app.
#'
#' Supported servers: All servers
#'
#' # Dependency discovery
#'
#' rsconnect use one of three mechanisms to find which packages your application
#' uses:
#'
#' 1. If `renv.lock` is present, it will use the versions and sources defined in
#' that file. If you're using the lockfile for some other purpose and
#' don't want it to affect deployment, add `renv.lock` to `.rscignore`.
#'
#' 2. Otherwise, rsconnect will call `renv::snapshot()` to find all packages
#' used by your code. If you'd instead prefer to only use the packages
#' declared in a `DESCRIPTION` file, run
#' `renv::settings$snapshot.type("explicit")` to activate renv's "explicit"
#' mode.
#'
#' 3. Dependency resolution using renv is a new feature in rsconnect 1.0.0, and
#' while we have done our best to test it, it still might fail for your app.
#' If this happens, please [file an issue](https://github.com/rstudio/rsconnect/issues)
#' then set `options(rsconnect.packrat = TRUE)` to revert to the old
#' dependency discovery mechanism.
#'
#' # Remote installation
#'
#' When deployed, the app must first install all of these packages, and
#' rsconnect ensures the versions used on the server will match the versions
#' you used locally. It knows how to install packages from the following
#' sources:
#'
#' * CRAN and BioConductor (`Source: CRAN` or `Source: Bioconductor`). The
#' remote server will ignore the specific CRAN or Bioconductor mirror that
#' you use locally, always using the CRAN/BioC mirror that has been configured
#' on the server.
#'
#' * Other CRAN like and CRAN-like repositories. These packages will have
#' a `Source` determined by the value of `getOptions("repos")`. For example,
#' if you've set the following options:
#'
#' ```R
#' options(
#' repos = c(
#' CRAN = "https://cran.rstudio.com/",
#' CORPORATE = "https://corporate-packages.development.company.com"
#' )
#' )
#' ```
#'
#' Then packages installed from your corporate package repository will
#' have source `CORPORATE`. Posit Connect
#' [can be configured](https://docs.posit.co/connect/admin/appendix/configuration/#RPackageRepository)
#' to override their repository url so that (e.g.) you can use different
#' packages versions on staging and production servers.
#'
#' * Packages installed from GitHub, GitLab, or BitBucket, have `Source`
#' `github`, `gitlab`, and `bitbucket` respectively. When deployed, the
#' bundle contains the additional metadata needed to precisely recreated
#' the installed version.
#'
#' It's not possible to recreate the packages that you have built and installed
#' from a directory on your local computer. This will have `Source: NA` and
#' will cause the deployment to error. To resolve this issue, you'll need to
#' install from one of the known sources described above.
#'
#' # Suggested packages
#'
#' The `Suggests` field is not included when determining recursive dependencies,
#' so it's possible that not every package required to run your application will
#' be detected.
#'
#' For example, ggplot2's `geom_hex()` requires the hexbin package to be
#' installed, but it is only suggested by ggplot2. So if your app uses
#' `geom_hex()` it will fail, reporting that the hexbin package is not
#' installed.
#'
#' You can overcome this problem with (e.g.) `requireNamespace(hexbin)`.
#' This will tell rsconnect that your app needs the hexbin package, without
#' otherwise affecting your code.
#'
#' @inheritParams deployApp
#' @returns A data frame with one row for each dependency (direct, indirect,
#' and inferred), and 4 columns:
#'
#' * `Package`: package name.
#' * `Version`: local version.
#' * `Source`: a short string describing the source of the package install,
#' as described above.
#' * `Repository`: for CRAN and CRAN-like repositories, the URL to the
#' repository. This will be ignored by the server if it has been configured
#' with its own repository name -> repository URL mapping.
#' @examples
#' \dontrun{
#'
#' # dependencies for the app in the current working dir
#' appDependencies()
#'
#' # dependencies for an app in another directory
#' appDependencies("~/projects/shiny/app1")
#' }
#' @seealso [rsconnectPackages](Using Packages with rsconnect)
#' @export
appDependencies <- function(
appDir = getwd(),
appFiles = NULL,
appFileManifest = NULL,
appMode = NULL,
dependencyResolution = c("strict", "library")
) {
dependencyResolution <- match.arg(dependencyResolution)
appFiles <- listDeploymentFiles(appDir, appFiles, appFileManifest)
appMetadata <- appMetadata(appDir, appFiles = appFiles, appMode = appMode)
if (!needsR(appMetadata)) {
return(data.frame(
Package = character(),
Version = character(),
Source = character(),
Repository = character(),
stringsAsFactors = FALSE
))
}
bundleDir <- bundleAppDir(
appDir = appDir,
appFiles = appFiles,
appMode = appMetadata$appMode
)
defer(unlink(bundleDir, recursive = TRUE))
extraPackages <- inferRPackageDependencies(appMetadata)
deps <- computePackageDependencies(
bundleDir,
extraPackages,
quiet = TRUE,
dependencyResolution = dependencyResolution
)
deps[c("Package", "Version", "Source", "Repository")]
}
needsR <- function(appMetadata) {
if (
appMetadata$appMode %in% c("static", "tensorflow-saved-model", "nodejs")
) {
return(FALSE)
}
# All non-Quarto content currently uses R by default.
# Currently R is only supported by the "knitr" engine, not "jupyter" or
# "markdown"
is.null(appMetadata$quartoInfo) ||
"knitr" %in% appMetadata$quartoInfo[["engines"]]
}
inferRPackageDependencies <- function(appMetadata) {
deps <- switch(
appMetadata$appMode,
"rmd-static" = c("rmarkdown", if (appMetadata$hasParameters) "shiny"),
"quarto-static" = "rmarkdown",
"quarto-shiny" = c("rmarkdown", "shiny"),
"rmd-shiny" = c("rmarkdown", "shiny"),
"shiny" = "shiny",
"api" = appMetadata$plumberInfo
)
if (appMetadata$documentsHavePython) {
deps <- c(deps, "reticulate")
}
deps
}
================================================
FILE: R/appMetadata-quarto.R
================================================
# Called only when the content is known to be Quarto.
inferQuartoInfo <- function(metadata, appDir, appPrimaryDoc) {
if (hasQuartoMetadata(metadata)) {
return(list(
version = metadata[["quarto_version"]],
engines = metadata[["quarto_engines"]]
))
}
# If we don't yet have Quarto details, run quarto inspect ourselves
inspect <- quartoInspect(
appDir = appDir,
appPrimaryDoc = appPrimaryDoc
)
list(
version = inspect[["quarto"]][["version"]],
engines = I(inspect[["engines"]])
)
}
hasQuartoMetadata <- function(x) {
!is.null(x$quarto_version)
}
# Run "quarto inspect" on the target and returns its output as a parsed object.
quartoInspect <- function(appDir = NULL, appPrimaryDoc = NULL) {
# If "quarto inspect appDir" fails, we will try "quarto inspect
# appPrimaryDoc", so that we can support single files as well as projects.
quarto <- quarto_path()
if (is.null(quarto)) {
cli::cli_abort(c(
"`quarto` not found.",
i = "Check that it is installed and available on your {.envvar PATH}."
))
}
json <- suppressWarnings(
system2(
quarto,
c("inspect", shQuote(appDir)),
stdout = TRUE,
stderr = TRUE
)
)
status <- attr(json, "status")
if (!is.null(status) && !is.null(appPrimaryDoc)) {
json <- suppressWarnings(
system2(
quarto,
c("inspect", shQuote(file.path(appDir, appPrimaryDoc))),
stdout = TRUE,
stderr = TRUE
)
)
status <- attr(json, "status")
}
if (!is.null(status)) {
cli::cli_abort(
c(
"Failed to run `quarto inspect` against your content:",
json
)
)
}
jsonlite::fromJSON(sanitizeSystem2json(json))
}
# inlined from quarto::quarto_path()
quarto_path <- function() {
path_env <- Sys.getenv("QUARTO_PATH", unset = NA)
if (is.na(path_env)) {
path <- unname(Sys.which("quarto"))
if (nzchar(path)) {
path
} else {
NULL
}
} else {
path_env
}
}
================================================
FILE: R/appMetadata.R
================================================
appMetadata <- function(
appDir,
appFiles,
appPrimaryDoc = NULL,
quarto = NA,
appMode = NULL,
contentCategory = NULL,
isShinyappsServer = FALSE,
metadata = list()
) {
check_bool(quarto, allow_na = TRUE)
# If quarto package/IDE has supplied metadata, always use quarto
# https://github.com/quarto-dev/quarto-r/blob/08caf0f42504e7/R/publish.R#L117-L121
# https://github.com/rstudio/rstudio/blob/3d45a20307f650/src/cpp/session/modules/SessionRSConnect.cpp#L81-L123
if (hasQuartoMetadata(metadata)) {
quarto <- TRUE
}
inferredPrimaryFile <- NULL
if (is.null(appMode)) {
# Generally we want to infer appPrimaryDoc from appMode, but there's one
# special case: RStudio provides appPrimaryDoc when deploying Shiny
# applications. They may have name.R, not app.R or server.R.
#
# This file is later renamed to app.R when deployed by bundleAppDir().
if (
!is.null(appPrimaryDoc) &&
tolower(tools::file_ext(appPrimaryDoc)) == "r"
) {
appMode <- "shiny"
} else {
# Inference only uses top-level files
appModeResult <- inferAppMode(
appDir,
appFiles,
usesQuarto = quarto,
isShinyappsServer = isShinyappsServer
)
appMode <- appModeResult$appMode
inferredPrimaryFile <- appModeResult$primaryFile
}
}
appPrimaryDoc <- inferAppPrimaryDoc(
appPrimaryDoc = appPrimaryDoc,
appFiles = appFiles,
appMode = appMode
)
hasParameters <- appHasParameters(
appDir = appDir,
appPrimaryDoc = appPrimaryDoc,
appMode = appMode,
contentCategory = contentCategory
)
documentsHavePython <- detectPythonInDocuments(
appDir = appDir,
files = appFiles
)
if (appIsQuartoDocument(appMode)) {
quartoInfo <- inferQuartoInfo(
metadata = metadata,
appDir = appDir,
appPrimaryDoc = appPrimaryDoc
)
if (appMode == "quarto-shiny") {
if (!any(c("knitr", "jupyter") %in% quartoInfo[["engines"]])) {
cli::cli_abort(c(
"The Quarto document requires a server but does not use an executable engine.",
"Consider including some executable code, specifying an engine, or removing the server configuration."
))
}
}
} else {
quartoInfo <- NULL
}
plumberInfo <- NULL
if (appMode == "api") {
plumberInfo <- inferPlumberInfo(appDir)
}
nodejsInfo <- NULL
if (appMode == "nodejs") {
nodejsInfo <- inferNodejsInfo(appDir)
if (!nodejsInfo$hasLockfile) {
cli::cli_abort(c(
"Node.js deployments require a {.file package-lock.json}.",
"i" = "Run {.code npm install} to generate one."
))
}
if (!file.exists(file.path(appDir, nodejsInfo$entrypoint))) {
cli::cli_warn(c(
"Entrypoint {.file {nodejsInfo$entrypoint}} not found in app directory.",
"i" = "Set the {.field main} field in {.file package.json} to your app\'s entry file."
))
}
}
list(
appMode = appMode,
appPrimaryDoc = appPrimaryDoc,
inferredPrimaryFile = inferredPrimaryFile,
hasParameters = hasParameters,
contentCategory = contentCategory,
documentsHavePython = documentsHavePython,
quartoInfo = quartoInfo,
plumberInfo = plumberInfo,
nodejsInfo = nodejsInfo
)
}
# Infer the mode of the application from included files. Most content types
# only consider files at the directory root. TensorFlow saved models may be
# anywhere in the hierarchy.
inferAppMode <- function(
appDir,
appFiles,
usesQuarto = NA,
isShinyappsServer = FALSE
) {
rootFiles <- appFiles[dirname(appFiles) == "."]
absoluteRootFiles <- file.path(appDir, rootFiles)
absoluteAppFiles <- file.path(appDir, appFiles)
matchingNames <- function(paths, pattern) {
idx <- grepl(pattern, basename(paths), ignore.case = TRUE, perl = TRUE)
paths[idx]
}
# plumber API
plumberFiles <- matchingNames(absoluteRootFiles, "^(plumber|entrypoint).r$")
if (length(plumberFiles) > 0) {
return(list(appMode = "api", primaryFile = basename(plumberFiles[1])))
}
# general API
server_yml <- matchingNames(absoluteRootFiles, "^_server.ya?ml$")
if (length(server_yml) > 0) {
return(list(appMode = "api", primaryFile = basename(server_yml[1])))
}
# Shiny application using single-file app.R style.
appR <- matchingNames(absoluteRootFiles, "^app.r$")
if (length(appR) > 0) {
return(list(appMode = "shiny", primaryFile = basename(appR[1])))
}
rmdFiles <- matchingNames(absoluteRootFiles, "\\.rmd$")
hasRmd <- length(rmdFiles) > 0
qmdFiles <- matchingNames(absoluteRootFiles, "\\.qmd$")
hasQmd <- length(qmdFiles) > 0
rFiles <- matchingNames(absoluteRootFiles, "\\.r$")
hasR <- length(rFiles) > 0
quartoYml <- matchingNames(absoluteRootFiles, "^_quarto.y(a)?ml$")
hasQuartoYml <- length(quartoYml) > 0
if (is.na(usesQuarto)) {
# Determine if the incoming content implies the need for Quarto.
#
# *.qmd files are enough of an indication by themselves.
# *.rmd and *.r files need a _quarto.yml file to emphasize the need for Quarto.
#
# Do not rely on _quarto.yml alone, as RStudio includes that file even when
# publishing HTML. https://github.com/rstudio/rstudio/issues/11444
usesQuarto <- (hasQmd ||
(hasQuartoYml && hasRmd) ||
(hasQuartoYml && hasR))
}
# Documents with "server: shiny" in their YAML front matter need shiny too
shinyRmdFiles <- NULL
if (length(rmdFiles) > 0) {
shinyRmdFiles <- rmdFiles[sapply(rmdFiles, isShinyRmd)]
}
shinyQmdFiles <- NULL
if (length(qmdFiles) > 0) {
shinyQmdFiles <- qmdFiles[sapply(qmdFiles, isShinyRmd)]
}
hasShinyRmd <- length(shinyRmdFiles) > 0
hasShinyQmd <- length(shinyQmdFiles) > 0
if (hasShinyQmd) {
return(list(
appMode = "quarto-shiny",
primaryFile = basename(shinyQmdFiles[1])
))
} else if (hasShinyRmd) {
shinyRmdFile <- shinyRmdFiles[1]
if (usesQuarto) {
return(list(
appMode = "quarto-shiny",
primaryFile = basename(shinyRmdFile)
))
} else {
return(list(appMode = "rmd-shiny", primaryFile = basename(shinyRmdFile)))
}
}
# Shiny application using server.R; checked later than Rmd with shiny runtime
# because server.R may contain the server code paired with a ShinyRmd and
# needs to be run by rmarkdown::run (rmd-shiny).
serverR <- matchingNames(absoluteRootFiles, "^server.r$")
if (length(serverR) > 0) {
return(list(appMode = "shiny", primaryFile = basename(serverR[1])))
}
# Any non-Shiny R Markdown or Quarto documents are rendered content and get
# rmd-static or quarto-static.
if (length(rmdFiles) > 0 || length(qmdFiles) > 0) {
# Prefer qmd files over rmd files for primary file selection
primaryDocFile <- if (length(qmdFiles) > 0) qmdFiles[1] else rmdFiles[1]
if (usesQuarto) {
return(list(
appMode = "quarto-static",
primaryFile = basename(primaryDocFile)
))
} else {
# For shinyapps.io, treat "rmd-static" app mode as "rmd-shiny" so that
# it can be served from a shiny process in Connect
if (isShinyappsServer) {
return(list(
appMode = "rmd-shiny",
primaryFile = basename(primaryDocFile)
))
}
return(list(
appMode = "rmd-static",
primaryFile = basename(primaryDocFile)
))
}
}
if (hasR) {
# We have R scripts but it was not otherwise identified as Shiny or Plumber
# and also not accompanied by *.qmd or *.rmd files.
#
# Assume that this is a rendered script, as this is a better fall-back than
# "static".
return(list(appMode = "quarto-static", primaryFile = basename(rFiles[1])))
}
# Node.js application (detected after all R/Python/Quarto signals)
packageJsonFiles <- matchingNames(absoluteRootFiles, "^package\\.json$")
if (length(packageJsonFiles) > 0) {
return(list(appMode = "nodejs", primaryFile = NULL))
}
# TensorFlow model files are lower in the hierarchy, not at the root.
modelFiles <- matchingNames(
absoluteAppFiles,
"^(saved_model.pb|saved_model.pbtxt)$"
)
if (length(modelFiles) > 0) {
return(list(
appMode = "tensorflow-saved-model",
# TODO: primaryFile is only required for Connect Cloud, but Connect Cloud
# doesn't support TensorFlow yet. More work needed here to be able to
# support this.
NULL
))
}
# no renderable content
list(appMode = "static", primaryFile = NULL)
}
isShinyRmd <- function(filename) {
yaml <- yamlFromRmd(filename)
if (is.null(yaml)) {
return(FALSE)
}
is_shiny_prerendered(yaml$runtime, yaml$server)
}
yamlFromRmd <- function(filename) {
lines <- readLines(filename, warn = FALSE, encoding = "UTF-8")
delim <- grep("^(---|\\.\\.\\.)\\s*$", lines)
if (length(delim) >= 2) {
# If at least two --- or ... lines were found...
if (delim[[1]] == 1 || all(grepl("^\\s*$", lines[1:delim[[1]]]))) {
# and the first is a ---
if (grepl("^---\\s*$", lines[delim[[1]]])) {
# ...and the first --- line is not preceded by non-whitespace...
if (diff(delim[1:2]) > 1) {
# ...and there is actually something between the two --- lines...
yamlData <- paste(
lines[(delim[[1]] + 1):(delim[[2]] - 1)],
collapse = "\n"
)
return(yaml::yaml.load(yamlData))
}
}
}
}
return(NULL)
}
# Adapted from rmarkdown:::is_shiny_prerendered()
is_shiny_prerendered <- function(runtime, server = NULL) {
if (!is.null(runtime) && grepl("^shiny", runtime)) {
TRUE
} else if (identical(server, "shiny")) {
TRUE
} else if (is.list(server) && identical(server[["type"]], "shiny")) {
TRUE
} else {
FALSE
}
}
# If deploying an R Markdown, Quarto, or static content, infer a primary
# document if one is not already specified.
# Note: functionality in inferQuartoInfo() depends on primary doc inference
# working the same across app modes.
inferAppPrimaryDoc <- function(appPrimaryDoc, appFiles, appMode) {
if (!is.null(appPrimaryDoc)) {
return(appPrimaryDoc)
}
# Only documents and static apps don't have primary _doc_
if (!(appIsDocument(appMode) || appMode == "static")) {
return(appPrimaryDoc)
}
# determine expected primary document extension
ext <- switch(
appMode,
"static" = "\\.html?$",
"quarto-static" = "\\.(r|rmd|qmd)$",
"quarto-shiny" = "\\.(rmd|qmd)$",
"\\.rmd$"
)
# use index file if it exists
matching <- grepl(paste0("^index", ext), appFiles, ignore.case = TRUE)
if (!any(matching)) {
# no index file found, so pick the first one we find
matching <- grepl(ext, appFiles, ignore.case = TRUE)
if (!any(matching)) {
cli::cli_abort(c(
"Failed to determine {.arg appPrimaryDoc} for {.str {appMode}} content.",
x = "No files matching {.str {ext}}."
))
}
}
if (sum(matching) > 1) {
# if we have multiple matches, pick the first
appFiles[matching][[1]]
} else {
appFiles[matching]
}
}
appIsDocument <- function(appMode) {
appMode %in%
c(
"rmd-static",
"rmd-shiny",
"quarto-static",
"quarto-shiny"
)
}
appIsQuartoDocument <- function(appMode) {
appMode %in%
c(
"quarto-static",
"quarto-shiny"
)
}
appHasParameters <- function(
appDir,
appPrimaryDoc,
appMode,
contentCategory = NULL
) {
# Only Rmd deployments are marked as having parameters. Shiny applications
# may distribute an Rmd alongside app.R, but that does not cause the
# deployment to be considered parameterized.
#
# https://github.com/rstudio/rsconnect/issues/246
if (!(appIsDocument(appMode))) {
return(FALSE)
}
# Sites don't ever have parameters
if (identical(contentCategory, "site")) {
return(FALSE)
}
# Only Rmd files have parameters.
if (tolower(tools::file_ext(appPrimaryDoc)) == "rmd") {
filename <- file.path(appDir, appPrimaryDoc)
yaml <- yamlFromRmd(filename)
if (!is.null(yaml)) {
params <- yaml[["params"]]
# We don't care about deep parameter processing, only that they exist.
return(!is.null(params) && length(params) > 0)
}
}
FALSE
}
detectPythonInDocuments <- function(appDir, files = NULL) {
if (is.null(files)) {
# for testing
files <- bundleFiles(appDir)
}
rmdFiles <- grep(
"^[^/\\\\]+\\.[rq]md$",
files,
ignore.case = TRUE,
perl = TRUE,
value = TRUE
)
for (rmdFile in rmdFiles) {
if (documentHasPythonChunk(file.path(appDir, rmdFile))) {
return(TRUE)
}
}
return(FALSE)
}
documentHasPythonChunk <- function(filename) {
lines <- readLines(filename, warn = FALSE, encoding = "UTF-8")
matches <- grep("`{python", lines, fixed = TRUE)
return(length(matches) > 0)
}
#' Infer node.js information
#'
#' @param appDir directory containing content
#' @return list containing the entrypoint, engines, and whether a
#' package-lock.json exists.
#' @noRd
inferNodejsInfo <- function(appDir) {
packageJsonPath <- file.path(appDir, "package.json")
pkg <- jsonlite::fromJSON(packageJsonPath, simplifyVector = FALSE)
entrypoint <- pkg$main
if (is.null(entrypoint) || !nzchar(entrypoint)) {
entrypoint <- "index.js"
}
enginesNode <- NULL
if (!is.null(pkg$engines) && !is.null(pkg$engines$node)) {
enginesNode <- pkg$engines$node
}
lockfilePath <- file.path(appDir, "package-lock.json")
hasLockfile <- file.exists(lockfilePath)
list(
entrypoint = entrypoint,
enginesNode = enginesNode,
hasLockfile = hasLockfile
)
}
#' Infer plumber information
#'
#' @param appDir directory containing content
#' @return `"plumber"` for plumber APIs; the contents of the `engine` field in
#' `_server.yml`/`_server.yaml` (usually `"plumber2"`) for plumber2 APIs.
#' @noRd
inferPlumberInfo <- function(appDir) {
files <- list.files(appDir)
is_plumber2 <- any(grepl("^_server\\.ya?ml$", files))
if (!is_plumber2) {
return("plumber")
}
server_file <- list.files(
appDir,
pattern = "^_server.ya?ml$",
full.names = TRUE
)
if (length(server_file) > 1) {
stop(
"Found both _server.yaml and _server.yml, please remove one from your project",
call. = FALSE
)
}
server_yaml <- yaml::read_yaml(server_file)
return(server_yaml$engine)
}
================================================
FILE: R/applications.R
================================================
#' List Deployed Applications
#'
#' @description
#' List all applications currently deployed for a given account.
#'
#' Supported servers: All servers
#'
#' @inheritParams deployApp
#' @return
#' Returns a data frame with the following columns:
#' \tabular{ll}{
#' `id` \tab Application unique id \cr
#' `name` \tab Name of application \cr
#' `title` \tab Application title \cr
#' `url` \tab URL where application can be accessed \cr
#'
#' `status` \tab Current status of application. Valid values are `pending`,
#' `deploying`, `running`, `terminating`, and `terminated` \cr
#' `size` \tab Instance size (small, medium, large, etc.) (on
#' ShinyApps.io) \cr
#' `instances` \tab Number of instances (on ShinyApps.io) \cr
#' `config_url` \tab URL where application can be configured \cr
#' }
#' @note To register an account you call the [setAccountInfo()] function.
#' @examples
#' \dontrun{
#'
#' # list all applications for the default account
#' applications()
#'
#' # list all applications for a specific account
#' applications("myaccount")
#'
#' # view the list of applications in the data viewer
#' View(applications())
#' }
#' @seealso [deployApp()], [terminateApp()]
#' @family Deployment functions
#' @export
applications <- function(account = NULL, server = NULL) {
# resolve account and create connect client
accountDetails <- accountInfo(account, server)
serverDetails <- serverInfo(accountDetails$server)
client <- clientForAccount(accountDetails)
if (isPositConnectCloudServer(accountDetails$server)) {
cli::cli_abort(
"The applications() function is not supported for Posit Connect Cloud accounts."
)
}
isConnect <- isConnectServer(accountDetails$server)
# retrieve applications
apps <- client$listApplications(accountDetails$accountId)
# extract the subset of fields we're interested in
keep <- if (isConnect) {
c(
"id",
"name",
"title",
"url",
"build_status",
"created_time",
"last_deployed_time",
"guid"
)
} else {
c(
"id",
"name",
"url",
"status",
"created_time",
"updated_time",
"deployment"
)
}
res <- lapply(apps, `[`, keep)
res <- if (isConnect) {
lapply(res, function(x) {
# set size and instance to NA since Connect doesn't return this info
x$size <- NA
x$instances <- NA
x$title <- x$title %||% NA_character_
x
})
} else {
lapply(res, function(x) {
# promote the size and instance data to first-level fields
x$size <- x$deployment$properties$application.instances.template
if (is.null(x$size)) {
x$size <- NA
}
x$instances <- x$deployment$properties$application.instances.count
if (is.null(x$instances)) {
x$instances <- NA
}
x$deployment <- NULL
x$guid <- NA
x$title <- NA_character_
x
})
}
# The config URL may be provided by the server at some point, but for now
# infer it from the account type
res <- lapply(res, function(row) {
if (isConnect) {
prefix <- sub("/__api__", "", serverDetails$url)
row$config_url <- paste(prefix, "connect/#/apps", row$id, sep = "/")
} else {
row$config_url <- paste(
"https://www.shinyapps.io/admin/#/application",
row$id,
sep = "/"
)
}
row
})
# convert to data frame
res <- lapply(res, as.data.frame, stringsAsFactors = FALSE)
res <- do.call("rbind", res)
# Ensure the Connect and ShinyApps.io data frames have same column names
idx <- match("last_deployed_time", names(res))
if (!is.na(idx)) {
names(res)[idx] <- "updated_time"
}
idx <- match("build_status", names(res))
if (!is.na(idx)) {
names(res)[idx] <- "status"
}
return(res)
}
# Use the API to filter applications by name and error when it does not exist.
getAppByName <- function(client, accountInfo, name, error_call = caller_env()) {
# NOTE: returns a list with 0 or 1 elements
app <- client$listApplications(
accountInfo$accountId,
filters = list(name = name)
)
if (length(app)) {
return(app[[1]])
}
cli::cli_abort(
c(
"No application found",
i = "Specify the application directory, name, and/or associated account."
),
call = error_call,
class = "rsconnect_app_not_found"
)
}
# Use the API to list all applications then filter the results client-side.
resolveApplication <- function(accountDetails, appName) {
client <- clientForAccount(accountDetails)
apps <- client$listApplications(accountDetails$accountId)
for (app in apps) {
if (identical(app$name, appName)) {
return(app)
}
}
stopWithApplicationNotFound(appName)
}
getApplication <- function(account, server, appId) {
accountDetails <- accountInfo(account, server)
client <- clientForAccount(accountDetails)
withCallingHandlers(
client$getApplication(appId, "unknown"),
rsconnect_http_404 = function(err) {
cli::cli_abort("Can't find app with id {.str {appId}}", parent = err)
}
)
}
stopWithApplicationNotFound <- function(appName) {
stop(
paste(
"No application named '",
appName,
"' is currently deployed.",
sep = ""
),
call. = FALSE
)
}
applicationTask <- function(taskDef, appName, accountDetails, quiet) {
# resolve target account and application
application <- resolveApplication(accountDetails, appName)
# get status function and display initial status
displayStatus <- displayStatus(quiet)
displayStatus(paste(taskDef$beginStatus, "...\n", sep = ""))
# perform the action
client <- clientForAccount(accountDetails)
task <- taskDef$action(client, application)
client$waitForTask(task$task_id, quiet)
displayStatus(paste(taskDef$endStatus, "\n", sep = ""))
invisible(NULL)
}
#' Application Logs
#'
#' @description
#' These functions provide access to the logs for deployed ShinyApps applications:
#'
#' * `showLogs()` displays the logs.
#' * `getLogs()` returns the logged lines.
#'
#' Supported servers: ShinyApps servers
#'
#' @param appPath The path to the directory or file that was deployed.
#' @param appFile The path to the R source file that contains the application
#' (for single file applications).
#' @param appName The name of the application to show logs for. May be omitted
#' if only one application deployment was made from `appPath`.
#' @param account The account under which the application was deployed. May be
#' omitted if only one account is registered on the system.
#' @param server Server name. Required only if you use the same account name on
#' multiple servers.
#' @param entries The number of log entries to show. Defaults to 50 entries.
#' @param streaming Deprecated. Streaming logs is not currently supported
#' as the ShinyApps.io backend no longer supports this feature. If `TRUE`,
#' an error will be thrown. Defaults to `FALSE`.
#'
#' @note These functions only work
#' for applications deployed to ShinyApps.io.
#'
#' @return `getLogs()` returns a data frame containing the logged lines.
#'
#' @export
showLogs <- function(
appPath = getwd(),
appFile = NULL,
appName = NULL,
account = NULL,
server = NULL,
entries = 50,
streaming = FALSE
) {
# determine the log target and target account info
deployment <- findDeployment(
appPath = appPath,
appName = appName,
server = server,
account = account
)
checkShinyappsServer(deployment$server)
if (streaming) {
cli::cli_abort(
c(
"Streaming logs is not currently supported.",
i = "The ShinyApps.io backend no longer supports the streaming API.",
i = "Use {.arg streaming = FALSE} (the default) to retrieve recent log entries."
)
)
}
accountDetails <- accountInfo(deployment$account, deployment$server)
client <- clientForAccount(accountDetails)
application <- getAppByName(client, accountDetails, deployment$name)
# Poll for the entries directly
logs <- client$getLogs(application$id, entries)
cat(logs)
}
#' @rdname showLogs
#' @export
getLogs <- function(
appPath = getwd(),
appFile = NULL,
appName = NULL,
account = NULL,
server = NULL,
entries = 50
) {
# determine the log target and target account info
deployment <- findDeployment(
appPath = appPath,
appName = appName,
server = server,
account = account
)
accountDetails <- accountInfo(deployment$account, deployment$server)
client <- clientForAccount(accountDetails)
application <- getAppByName(client, accountDetails, deployment$name)
payload <- client$getLogs(application$id, entries, format = "json")
# Convert to a dataframe before combining because the JSON payload has inconsistent field order
# containing nested single-element lists.
converted <- lapply(payload$results, as.data.frame)
df <- do.call(rbind, converted)
# shinyapps.io returns ns timestamps.
df$timestamp <- as.POSIXct(df$timestamp / (1000 * 1000))
# Return a subset of the included fields.
result <- df[c(
"timestamp",
"account_id",
"application_id",
"message"
)]
result
}
#' Update deployment records
#'
#' @description
#' Update the deployment records for applications published to Posit Connect.
#' This updates application title and URL, and deletes records for deployments
#' where the application has been deleted on the server.
#'
#' Supported servers: Posit Connect servers
#'
#' @param appPath The path to the directory or file that was deployed.
#' @export
syncAppMetadata <- function(appPath = ".") {
check_directory(appPath)
deploys <- deployments(appPath)
for (i in seq_len(nrow(deploys))) {
curDeploy <- deploys[i, ]
# don't sync if published to RPubs or Connect Cloud
if (isRPubs(curDeploy$server)) {
next
} else if (isPositConnectCloudServer(curDeploy$server)) {
next
}
account <- accountInfo(curDeploy$account, curDeploy$server)
client <- clientForAccount(account)
application <- tryCatch(
client$getApplication(curDeploy$appId),
rsconnect_http_404 = function(c) {
# if the app has been deleted, delete the deployment record
file.remove(curDeploy$deploymentFile)
cli::cli_inform(
"Deleting deployment record for deleted app {curDeploy$appId}."
)
NULL
}
)
if (is.null(application)) {
next
}
# update the record and save out a new config file
path <- curDeploy$deploymentFile
curDeploy$deploymentFile <- NULL # added on read
# remove old fields
curDeploy$when <- NULL
curDeploy$lastSyncTime <- NULL
curDeploy$title <- application$title
curDeploy$url <- application$url
writeDeploymentRecord(curDeploy, path)
}
}
================================================
FILE: R/auth.R
================================================
cleanupPasswordFile <- function(appDir) {
check_directory(appDir)
appDir <- normalizePath(appDir)
# get data dir from appDir
dataDir <- file.path(appDir, "shinyapps")
# get password file
passwordFile <- file.path(dataDir, paste("passwords", ".txt", sep = ""))
# check if password file exists
if (file.exists(passwordFile)) {
message(
"WARNING: Password file found! This application is configured to use scrypt ",
"authentication, which is no longer supported.\nIf you choose to proceed, ",
"all existing users of this application will be removed, ",
"and will NOT be recoverable.\nFor for more information please visit: ",
"http://shiny.rstudio.com/articles/migration.html"
)
response <- readline("Do you want to proceed? [Y/n]: ")
if (tolower(substring(response, 1, 1)) != "y") {
stop("Cancelled", call. = FALSE)
} else {
# remove old password file
file.remove(passwordFile)
}
}
invisible(TRUE)
}
#' Add authorized user to application
#'
#' @description
#' Add authorized user to application
#'
#' Supported servers: ShinyApps servers
#'
#' @param email Email address of user to add.
#' @param appDir Directory containing application. Defaults to
#' current working directory.
#' @param appName Name of application.
#' @inheritParams deployApp
#' @param sendEmail Send an email letting the user know the application
#' has been shared with them.
#' @param emailMessage Optional character vector of length 1 containing a
#' custom message to send in email invitation. Defaults to NULL, which
#' will use default invitation message.
#' @seealso [removeAuthorizedUser()] and [showUsers()]
#' @note This function works only for ShinyApps servers.
#' @export
addAuthorizedUser <- function(
email,
appDir = getwd(),
appName = NULL,
account = NULL,
server = NULL,
sendEmail = NULL,
emailMessage = NULL
) {
accountDetails <- accountInfo(account, server)
checkShinyappsServer(accountDetails$server)
# resolve application
if (is.null(appName)) {
appName <- basename(appDir)
}
application <- resolveApplication(accountDetails, appName)
# check for and remove password file
cleanupPasswordFile(appDir)
# fetch authoriztion list
api <- clientForAccount(accountDetails)
api$inviteApplicationUser(
application$id,
validateEmail(email),
sendEmail,
emailMessage
)
message(paste("Added:", email, "to application", sep = " "))
invisible(TRUE)
}
#' Remove authorized user from an application
#'
#' @description
#' Remove authorized user from an application
#'
#' Supported servers: ShinyApps servers
#'
#' @param user The user to remove. Can be id or email address.
#' @param appDir Directory containing application. Defaults to
#' current working directory.
#' @param appName Name of application.
#' @inheritParams deployApp
#' @seealso [addAuthorizedUser()] and [showUsers()]
#' @note This function works only for ShinyApps servers.
#' @export
removeAuthorizedUser <- function(
user,
appDir = getwd(),
appName = NULL,
account = NULL,
server = NULL
) {
accountDetails <- accountInfo(account, server)
checkShinyappsServer(accountDetails$server)
# resolve application
if (is.null(appName)) {
appName <- basename(appDir)
}
application <- resolveApplication(accountDetails, appName)
# check and remove password file
cleanupPasswordFile(appDir)
# get users
users <- showUsers(appDir, appName, account, server)
if (is.numeric(user)) {
# lookup by id
if (user %in% users$id) {
user <- users[users$id == user, ]
} else {
stop("User ", user, " not found", call. = FALSE)
}
} else {
# lookup by email
if (user %in% users$email) {
user <- users[users$email == user, ]
} else {
stop("User \"", user, "\" not found", call. = FALSE)
}
}
# remove user
api <- clientForAccount(accountDetails)
api$removeApplicationUser(application$id, user$id)
message(paste("Removed:", user$email, "from application", sep = " "))
invisible(TRUE)
}
#' List authorized users for an application
#'
#' @description
#' List authorized users for an application
#'
#' Supported servers: ShinyApps servers
#'
#' @param appDir Directory containing application. Defaults to
#' current working directory.
#' @param appName Name of application.
#' @inheritParams deployApp
#' @seealso [addAuthorizedUser()] and [showInvited()]
#' @note This function works only for ShinyApps servers.
#' @export
showUsers <- function(
appDir = getwd(),
appName = NULL,
account = NULL,
server = NULL
) {
accountDetails <- accountInfo(account, server)
checkShinyappsServer(accountDetails$server)
# resolve application
if (is.null(appName)) {
appName <- basename(appDir)
}
application <- resolveApplication(accountDetails, appName)
# fetch authoriztion list
api <- clientForAccount(accountDetails)
res <- api$listApplicationAuthorization(application$id)
# get interesting fields
users <- lapply(res, function(x) {
a <- list()
a$id <- x$user$id
a$email <- x$user$email
if (!is.null(x$account)) {
a$account <- x$account
} else {
a$account <- NA
}
return(a)
})
# convert to data frame
users <- do.call(rbind, users)
df <- as.data.frame(users, stringsAsFactors = FALSE)
return(df)
}
#' List invited users for an application
#'
#' @description
#' List invited users for an application
#'
#' Supported servers: ShinyApps servers
#'
#' @param appDir Directory containing application. Defaults to
#' current working directory.
#' @param appName Name of application.
#' @inheritParams deployApp
#' @seealso [addAuthorizedUser()] and [showUsers()]
#' @note This function works only for ShinyApps servers.
#' @export
showInvited <- function(
appDir = getwd(),
appName = NULL,
account = NULL,
server = NULL
) {
accountDetails <- accountInfo(account, server)
checkShinyappsServer(accountDetails$server)
# resolve application
if (is.null(appName)) {
appName <- basename(appDir)
}
application <- resolveApplication(accountDetails, appName)
# fetch invitation list
api <- clientForAccount(accountDetails)
res <- api$listApplicationInvitations(application$id)
# get interesting fields
users <- lapply(res, function(x) {
a <- list()
a$id <- x$id
a$email <- x$email
a$link <- x$link
a$expired <- x$expired
return(a)
})
# convert to data frame
users <- do.call(rbind, users)
df <- as.data.frame(users, stringsAsFactors = FALSE)
return(df)
}
#' Resend invitation for invited users of an application
#'
#' @description
#' Resend invitation for invited users of an application
#'
#' Supported servers: ShinyApps servers
#'
#' @param invite The invitation to resend. Can be id or email address.
#' @param regenerate Regenerate the invite code. Can be helpful is the
#' invitation has expired.
#' @param appDir Directory containing application. Defaults to
#' current working directory.
#' @param appName Name of application.
#' @inheritParams deployApp
#' @seealso [showInvited()]
#' @note This function works only for ShinyApps servers.
#' @export
resendInvitation <- function(
invite,
regenerate = FALSE,
appDir = getwd(),
appName = NULL,
account = NULL,
server = NULL
) {
accountDetails <- accountInfo(account, server)
checkShinyappsServer(accountDetails$server)
# get invitations
invited <- showInvited(appDir, appName, account, server)
if (is.numeric(invite)) {
# lookup by id
if (invite %in% invited$id) {
invite <- invited[invited$id == invite, ]
} else {
stop("Invitation \"", invite, "\" not found", call. = FALSE)
}
} else {
# lookup by email
if (invite %in% invited$email) {
invite <- invited[invited$email == invite, ]
} else {
stop("Invitiation for \"", invite, "\" not found", call. = FALSE)
}
}
# resend invitation
api <- clientForAccount(accountDetails)
api$resendApplicationInvitation(invite$id, regenerate)
message(paste("Sent invitation to", invite$email, "", sep = " "))
invisible(TRUE)
}
# Previously exported, but deprecated since 2015
authorizedUsers <- function(appDir = getwd()) {
# read password file
path <- getPasswordFile(appDir)
if (file.exists(path)) {
passwords <- readPasswordFile(path)
} else {
passwords <- NULL
}
return(passwords)
}
validateEmail <- function(email) {
if (is.null(email) || !grepl(".+\\@.+\\..+", email)) {
stop("Invalid email address.", call. = FALSE)
}
invisible(email)
}
getPasswordFile <- function(appDir) {
check_directory(appDir)
file.path(normalizePath(appDir), "shinyapps", "passwords.txt")
}
readPasswordFile <- function(path) {
# open and read file
lines <- readLines(path)
# extract fields
fields <- do.call(rbind, strsplit(lines, ":"))
users <- fields[, 1]
hashes <- fields[, 2]
# convert to data frame
df <- data.frame(user = users, hash = hashes, stringsAsFactors = FALSE)
# return data frame
return(df)
}
writePasswordFile <- function(path, passwords) {
# open and file
f <- file(path, open = "w")
defer(close(f))
# write passwords
apply(passwords, 1, function(r) {
l <- paste(r[1], ":", r[2], "\n", sep = "")
cat(l, file = f, sep = "")
})
message(
"Password file updated. You must deploy your application for these changes to take effect."
)
}
================================================
FILE: R/bundle.R
================================================
# Given a path to an directory and a list of files in that directory, copies
# those files to a new temporary directory. Performs some small modifications
# in this process, including renaming single-file Shiny apps to "app.R" and
# stripping packrat and renv commands from .Rprofile. Returns the path to the
# temporary directory.
bundleAppDir <- function(
appDir,
appFiles,
appPrimaryDoc = NULL,
appMode = NULL,
verbose = FALSE
) {
logger <- verboseLogger(verbose)
logger("Creating bundle staging directory")
bundleDir <- dirCreate(tempfile())
defer(unlink(bundleDir))
logger("Copying files into bundle staging directory")
for (file in appFiles) {
logger("Copying", file)
from <- file.path(appDir, file)
to <- file.path(bundleDir, file)
if (!is.null(appMode) && appMode == "shiny") {
# When deploying a single-file Shiny application and we have been provided
# appPrimaryDoc (usually by RStudio), rename that file to `app.R` so it
# will be discovered and run by shiny::runApp(getwd()).
#
# Note: We do not expect to see writeManifest(appPrimaryDoc="notapp.R").
if (
is.character(appPrimaryDoc) &&
tolower(tools::file_ext(appPrimaryDoc)) == "r" &&
file == appPrimaryDoc
) {
to <- file.path(bundleDir, "app.R")
}
}
dirCreate(dirname(to))
file.copy(from, to, copy.date = TRUE)
# ensure .Rprofile doesn't call packrat/init.R or renv/activate.R
if (basename(to) == ".Rprofile") {
tweakRProfile(to)
}
}
# When an renv profile is active, the profile-specific lockfile lives under
# renv/profiles/<name>/renv.lock, which is excluded from the bundle (the
# entire renv/ directory is ignored). Resolve the correct lockfile from the
# original appDir and copy it into the bundle so that
# parseRenvDependencies() compares against the right lockfile.
profileLockfile <- tryCatch(
resolveRenvLockFile(appDir),
error = function(e) NULL
)
if (!is.null(profileLockfile) && file.exists(profileLockfile)) {
bundleLockfile <- file.path(bundleDir, "renv.lock")
file.copy(profileLockfile, bundleLockfile, overwrite = TRUE)
}
bundleDir
}
tweakRProfile <- function(path) {
lines <- readLines(path)
packratLines <- grep('source("packrat/init.R")', lines, fixed = TRUE)
if (length(packratLines) > 0) {
lines[packratLines] <- paste0(
"# Packrat initialization disabled in published application\n",
'# source("packrat/init.R")'
)
}
renvLines <- grep('source("renv/activate.R")', lines, fixed = TRUE)
if (length(renvLines) > 0) {
lines[renvLines] <- paste0(
"# renv initialization disabled in published application\n",
'# source("renv/activate.R")'
)
}
if (length(renvLines) > 0 || length(packratLines) > 0) {
msg <- sprintf(
"# Modified by rsconnect package %s on %s",
packageVersion("rsconnect"),
Sys.time()
)
lines <- c(msg, lines)
}
writeLines(lines, path)
}
# Writes a tar.gz file located at bundlePath containing all files in bundleDir.
writeBundle <- function(bundleDir, bundlePath, verbose = FALSE) {
logger <- verboseLogger(verbose)
prevDir <- setwd(bundleDir)
defer(setwd(prevDir))
tarImplementation <- getTarImplementation()
logger("Using tar: ", tarImplementation)
if (tarImplementation == "internal") {
detectLongNames(bundleDir)
}
utils::tar(
bundlePath,
files = NULL,
compression = "gzip",
tar = tarImplementation
)
}
getTarImplementation <- function() {
# Check the rsconnect.tar option first. If that is unset, check the
# RSCONNECT_TAR environment var. If neither are set, use "internal".
tarImplementation <- getOption("rsconnect.tar", default = NA)
if (is.na(tarImplementation) || !nzchar(tarImplementation)) {
tarImplementation <- Sys.getenv("RSCONNECT_TAR", unset = NA)
}
if (is.na(tarImplementation) || !nzchar(tarImplementation)) {
tarImplementation <- "internal"
}
return(tarImplementation)
}
isWindows <- function() {
Sys.info()[["sysname"]] == "Windows"
}
versionFromDescription <- function(appDir) {
descriptionFilepath <- file.path(appDir, "DESCRIPTION")
if (!file.exists(descriptionFilepath)) {
return(NULL)
}
desc <- read.dcf(descriptionFilepath)
depends <- as.list(desc[1, ])$Depends
if (is.null(depends)) {
return(NULL)
}
regexExtract("R \\((.*?)\\)", depends)
}
versionFromLockfile <- function(appDir) {
tryCatch(
{
lockfile <- suppressWarnings(renv::lockfile_read(project = appDir))
v <- lockfile$R$Version
# only major specified “3” → ~=3.0 → >=3.0,<4.0
# major and minor specified “3.8” or “3.8.11” → ~=3.8.0 → >=3.8.0,<3.9.0
parts <- strsplit(v, "\\.")[[1]]
new_parts <- c(head(parts, 2), "0")
paste0("~=", paste(new_parts, collapse = "."))
},
error = function(e) {
return(NULL)
}
)
}
rVersionRequires <- function(appDir) {
# Look for requirement at DESCRIPTION file
requires <- versionFromDescription(appDir)
# If DESCRIPTION file does not have R requirement
# Look it up on renv lockfile
if (is.null(requires)) {
requires <- versionFromLockfile(appDir)
}
requires
}
createAppManifest <- function(
appDir,
appMetadata,
users = NULL,
pythonConfig = NULL,
retainPackratDirectory = TRUE,
image = NULL,
envManagement = NULL,
envManagementR = NULL,
envManagementPy = NULL,
envManagementNodejs = NULL,
packageRepositoryResolutionR = NULL,
dependencyResolution = "strict",
verbose = FALSE,
quiet = FALSE
) {
if (is.null(image)) {
imageEnv <- Sys.getenv("RSCONNECT_IMAGE", unset = NA)
if (!is.na(imageEnv) && nchar(imageEnv) > 0) {
image <- imageEnv
}
}
# Validate packageRepositoryResolutionR
if (!is.null(packageRepositoryResolutionR)) {
match.arg(
packageRepositoryResolutionR,
c("lax", "strict", "legacy", "lockfile")
)
}
if (needsR(appMetadata)) {
extraPackages <- inferRPackageDependencies(appMetadata)
# provide package entries for all dependencies
packages <- bundlePackages(
bundleDir = appDir,
extraPackages = extraPackages,
verbose = verbose,
quiet = quiet,
dependencyResolution = dependencyResolution
)
rVersionReq <- rVersionRequires(appDir)
} else {
packages <- list()
rVersionReq <- NULL
}
needsPython <- appMetadata$documentsHavePython ||
"jupyter" %in% appMetadata$quartoInfo$engines ||
"reticulate" %in% names(packages)
if (needsPython && !is.null(pythonConfig)) {
python <- pythonConfig(appDir)
pyVersionReq <- python$requires
packageFile <- file.path(appDir, python$package_manager$package_file)
writeLines(python$package_manager$contents, packageFile)
python$package_manager$contents <- NULL
} else {
python <- NULL
pyVersionReq <- NULL
}
if (!retainPackratDirectory) {
# Optionally remove the packrat directory when it will not be included in
# deployments, such as manifest-only deployments.
unlink(file.path(appDir, "packrat"), recursive = TRUE)
}
# build the list of files to checksum
files <- list.files(
appDir,
recursive = TRUE,
all.files = TRUE,
full.names = FALSE
)
# provide checksums for all files
filelist <- list()
for (file in files) {
filepath <- file.path(appDir, file)
checksum <- list(checksum = fileMD5(filepath))
filelist[[file]] <- I(checksum)
}
# create userlist
userlist <- list()
if (!is.null(users) && length(users) > 0) {
for (i in 1:nrow(users)) {
user <- users[i, "user"]
hash <- users[i, "hash"]
userinfo <- list()
userinfo$hash <- hash
userlist[[user]] <- userinfo
}
}
nodejsVersionReq <- if (appMetadata$appMode == "nodejs") {
appMetadata$nodejsInfo$enginesNode
}
# create the manifest
manifest <- list()
manifest$version <- 1
manifest$locale <- getOption("rsconnect.locale", detectLocale())
manifest$platform <- paste(R.Version()$major, R.Version()$minor, sep = ".")
metadata <- list(appmode = appMetadata$appMode)
# emit appropriate primary document information
primaryDoc <- ifelse(
is.null(appMetadata$appPrimaryDoc) ||
tolower(tools::file_ext(appMetadata$appPrimaryDoc)) == "r",
NA,
appMetadata$appPrimaryDoc
)
metadata$primary_rmd <- ifelse(
appMetadata$appMode %in%
c("rmd-shiny", "rmd-static", "quarto-shiny", "quarto-static"),
primaryDoc,
NA
)
metadata$primary_html <- ifelse(
appMetadata$appMode == "static",
primaryDoc,
NA
)
# emit content category (plots, etc)
metadata$content_category <- ifelse(
!is.null(appMetadata$contentCategory),
appMetadata$contentCategory,
NA
)
metadata$has_parameters <- appMetadata$hasParameters
if (appMetadata$appMode == "nodejs") {
metadata$entrypoint <- appMetadata$nodejsInfo$entrypoint
}
# add metadata
manifest$metadata <- metadata
# handle shorthand arg to enable/disable all runtimes
if (!is.null(envManagement)) {
envManagementR <- envManagement
envManagementPy <- envManagement
envManagementNodejs <- envManagement
}
# if envManagement is explicitly enabled/disabled,
# create an environment_management obj
envManagementInfo <- list()
if (!is.null(envManagementR)) {
envManagementInfo$r <- envManagementR
}
if (!is.null(envManagementPy)) {
envManagementInfo$python <- envManagementPy
}
if (!is.null(envManagementNodejs)) {
envManagementInfo$nodejs <- envManagementNodejs
}
# emit the environment field
if (
!is.null(image) ||
length(envManagementInfo) > 0 ||
!is.null(rVersionReq) ||
!is.null(pyVersionReq) ||
!is.null(nodejsVersionReq) ||
!is.null(packageRepositoryResolutionR)
) {
manifest$environment <- list()
# if there is a target image, attach it to the environment
if (!is.null(image)) {
manifest$environment$image <- image
}
# if there is an R version constraint or package repository resolution
if (!is.null(rVersionReq) || !is.null(packageRepositoryResolutionR)) {
manifest$environment$r <- list()
if (!is.null(rVersionReq)) {
manifest$environment$r$requires <- rVersionReq
}
if (!is.null(packageRepositoryResolutionR)) {
manifest$environment$r$package_repository_resolution <- packageRepositoryResolutionR
}
}
# if there is a Python version constraint
if (!is.null(pyVersionReq)) {
manifest$environment$python <- list(requires = pyVersionReq)
}
# if there is a Node.js version constraint
if (!is.null(nodejsVersionReq)) {
manifest$environment$nodejs <- list(requires = nodejsVersionReq)
}
# if environment_management is provided for any runtime,
# write the environment_management field
if (length(envManagementInfo) > 0) {
manifest$environment$environment_management <- envManagementInfo
}
}
# indicate whether this is a quarto app/doc
manifest$quarto <- appMetadata$quartoInfo
# if there is python info for reticulate or Quarto, attach it
if (!is.null(python)) {
manifest$python <- python
}
# node.js content includes an empty nodejs object
if (appMetadata$appMode == "nodejs") {
manifest$nodejs <- structure(list(), names = character(0))
}
# if there are no packages set manifest$packages to NA (json null)
if (length(packages) > 0) {
manifest$packages <- I(packages)
} else {
manifest$packages <- NA
}
# if there are no files, set manifest$files to NA (json null)
if (length(files) > 0) {
manifest$files <- I(filelist)
} else {
manifest$files <- NA
}
# if there are no users set manifest$users to NA (json null)
if (length(users) > 0) {
manifest$users <- I(userlist)
} else {
manifest$users <- NA
}
manifest
}
================================================
FILE: R/bundleFiles.R
================================================
#' Gather files to be bundled with an app
#'
#' @description
#' Given an app directory, and optional `appFiles` and `appFileManifest`
#' arguments, returns vector of paths to bundle in the app. (Note that
#' documents follow a different strategy; see [deployDoc()] for details.)
#'
#' When neither `appFiles` nor `appFileManifest` is supplied,
#' `listDeploymentFiles()` will include all files under `appDir`, apart
#' from the following:
#'
#' * Certain files and folders that don't need to be bundled, such as
#' version control directories, internal config files, and RStudio state,
#' are automatically excluded.
#'
#' * You can exclude additional files by listing them in in a `.rscignore`
#' file. This file must have one file or directory per line (with path
#' relative to the current directory). It doesn't support wildcards, or
#' ignoring files in subdirectories.
#'
#' `listDeploymentFiles()` will throw an error if the total file size exceeds
#' the maximum bundle size (as controlled by option `rsconnect.max.bundle.size`),
#' or the number of files exceeds the maximum file limit (as controlled by
#' option `rsconnect.max.bundle.files`). This prevents you from accidentally
#' bundling a very large direcfory (i.e. you home directory).
#'
#' Supported servers: All servers
#'
#' @inheritParams deployApp
#' @param error_call The call or environment for error reporting; expert
#' use only.
#' @return Character of paths to bundle, relative to `appDir`.
#' @export
listDeploymentFiles <- function(
appDir,
appFiles = NULL,
appFileManifest = NULL,
error_call = caller_env()
) {
no_content <- function(message) {
cli::cli_abort(
c("No content to deploy.", x = message),
call = error_call
)
}
if (!is.null(appFiles) && !is.null(appFileManifest)) {
cli::cli_abort(
"Must specify at most one of {.arg appFiles} and {.arg appFileManifest}",
call = error_call
)
} else if (is.null(appFiles) && is.null(appFileManifest)) {
# no files supplied at all, just bundle the whole directory
appFiles <- bundleFiles(appDir)
if (length(appFiles) == 0) {
no_content("{.arg appDir} is empty.")
}
} else if (!is.null(appFiles)) {
check_character(appFiles, allow_null = TRUE, call = error_call)
appFiles <- explodeFiles(appDir, appFiles, "appFiles")
if (length(appFiles) == 0) {
no_content("{.arg appFiles} didn't match any files in {.arg appDir}.")
}
} else if (!is.null(appFileManifest)) {
check_file(appFileManifest, error_call = error_call)
appFiles <- readFileManifest(appFileManifest)
appFiles <- explodeFiles(appDir, appFiles, "appFileManifest")
if (length(appFiles) == 0) {
no_content("{.arg appFileManifest} contains no usable files.")
}
}
appFiles
}
readFileManifest <- function(appFileManifest, error_call = caller_env()) {
lines <- readLines(appFileManifest, warn = FALSE)
# remove empty/comment lines
lines <- lines[nzchar(lines)]
lines <- lines[!grepl("^#", lines)]
lines
}
#' List Files to be Bundled
#'
#' @description
#' `r lifecycle::badge("superseded")`
#'
#' `listBundleFiles()` has been superseded in favour of [listDeploymentFiles()].
#'
#' Given a directory containing an application, returns the names of the files
#' that by default will be bundled in the application. It works similarly to
#' a recursive directory listing from [list.files()] but enforces bundle sizes
#' as described in [listDeploymentFiles()]
#'
#' @param appDir Directory containing the application.
#' @return Returns a list containing the following elements:
#' * `totalFiles`: Total number of files.
#' * `totalSize`: Total size of the files (in bytes).
#' * `contents`: Paths to bundle, relative to `appDir`.
#' @export
#' @keywords internal
listBundleFiles <- function(appDir) {
recursiveBundleFiles(appDir)
}
bundleFiles <- function(appDir) {
listBundleFiles(appDir)$contents
}
explodeFiles <- function(dir, files, error_arg = "appFiles") {
missing <- !file.exists(file.path(dir, files))
if (any(missing)) {
cli::cli_warn(c(
"All files listed in {.arg {error_arg}} must exist.",
"Problems: {.file {files[missing]}}"
))
files <- files[!missing]
}
recursiveBundleFiles(dir, contents = files, ignoreFiles = FALSE)$contents
}
recursiveBundleFiles <- function(
dir,
contents = NULL,
rootDir = dir,
totalFiles = 0,
totalSize = 0,
ignoreFiles = TRUE
) {
# generate a list of files at this level
if (is.null(contents)) {
contents <- list.files(dir, all.files = TRUE, no.. = TRUE)
}
if (ignoreFiles) {
contents <- ignoreBundleFiles(dir, contents)
}
# Info for each file lets us know to recurse (directories) or aggregate (files).
is_dir <- dir.exists(file.path(dir, contents))
names(is_dir) <- contents
children <- character()
for (name in contents) {
if (isTRUE(is_dir[[name]])) {
out <- recursiveBundleFiles(
dir = file.path(dir, name),
rootDir = rootDir,
totalFiles = totalFiles,
totalSize = totalSize,
ignoreFiles = ignoreFiles
)
children <- append(children, file.path(name, out$contents))
totalFiles <- out$totalFiles
totalSize <- out$totalSize
} else {
children <- append(children, name)
totalFiles <- totalFiles + 1
totalSize <- totalSize + file_size(file.path(dir, name))
}
enforceBundleLimits(rootDir, totalFiles, totalSize)
}
list(
contents = children,
totalFiles = totalFiles,
totalSize = totalSize
)
}
ignoreBundleFiles <- function(dir, contents) {
# entries ignored regardless of type
ignored <- c(
# rsconnect packages
"rsconnect",
"rsconnect-python",
"manifest.json",
# packrat + renv,
"renv",
"packrat",
# version control
".git",
".gitignore",
".svn",
# R/RStudio
".Rhistory",
".Rproj.user",
# Node.js
"node_modules",
".npm",
# other
".DS_Store",
".quarto",
"app_cache",
"__pycache__/"
)
contents <- setdiff(contents, ignored)
contents <- contents[!isKnitrCacheDir(contents)]
contents <- contents[!isPythonEnv(dir, contents)]
contents <- contents[!grepl("^~|~$", contents)]
contents <- contents[!grepl(glob2rx("*.Rproj"), contents)]
# remove any files lines listed .rscignore
if (".rscignore" %in% contents) {
ignoreContents <- readLines(file.path(dir, ".rscignore"))
contents <- setdiff(contents, c(ignoreContents, ".rscignore"))
}
contents
}
isKnitrCacheDir <- function(files) {
is_cache <- grepl("^.+_cache$", files)
cache_rmd <- gsub("_cache$", ".Rmd", files)
has_rmd <- tolower(cache_rmd) %in% tolower(files)
ifelse(is_cache, has_rmd, FALSE)
}
# https://github.com/rstudio/rsconnect-python/blob/94dbd28797ee503d6/rsconnect/bundle.py#L541-L543
isPythonEnv <- function(dir, files) {
(file.exists(file.path(dir, files, "bin", "python")) |
file.exists(file.path(dir, files, "Scripts", "python.exe")) |
file.exists(file.path(dir, files, "Scripts", "pythond.exe")) |
file.exists(file.path(dir, files, "Scripts", "pythonw.exe")))
}
enforceBundleLimits <- function(appDir, totalFiles, totalSize) {
maxSize <- getOption("rsconnect.max.bundle.size", defaultMaxBundleSize)
maxFiles <- getOption("rsconnect.max.bundle.files", defaultMaxBundleFiles)
if (totalSize > maxSize) {
cli::cli_abort(c(
"{.arg appDir} ({.path {appDir}}) is too large to be deployed.",
x = "The maximum size is {maxSize} bytes.",
x = "This directory is at least {totalSize} bytes.",
i = "Remove some files or adjust the rsconnect.max.bundle.size option.",
" " = "e.g., options(rsconnect.max.bundle.size = 6 * 1024^3)",
" " = "See {.topic rsconnect::rsconnectOptions} for additional guidance."
))
}
if (totalFiles > maxFiles) {
cli::cli_abort(c(
"{.arg appDir} ({.path {appDir}}) is too large to be deployed.",
x = "The maximum number of files is {maxFiles}.",
x = "This directory contains at least {totalFiles} files.",
i = "Remove some files or adjust the rsconnect.max.bundle.files option.",
" " = "e.g., options(rsconnect.max.bundle.files = 15000)",
" " = "See {.topic rsconnect::rsconnectOptions} for additional guidance."
))
}
}
# Scan the bundle directory looking for long user/group names.
#
# Warn that the internal tar implementation may produce invalid archives.
# https://github.com/rstudio/rsconnect/issues/446
# https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17871
detectLongNames <- function(bundleDir, lengthLimit = 32) {
files <- list.files(
bundleDir,
recursive = TRUE,
all.files = TRUE,
include.dirs = TRUE,
no.. = TRUE
)
info <- file.info(file.path(bundleDir, files))
ok <- (is.na(info$uname) | nchar(info$uname) <= lengthLimit) &
(is.na(info$grname) | nchar(info$grname) <= lengthLimit)
if (all(ok)) {
return(invisible(FALSE))
}
bad_files <- files[!ok]
cli::cli_warn(
c(
"The bundle contains files with user/group names longer than {lengthLimit}.",
x = "Files: {.path {bad_files}}",
x = "Long user and group names cause the internal R tar implementation to produce invalid archives",
i = "Set the {.code rsconnect.tar} option or the {.code RSCONNECT_TAR} environment variable to the path to a tar executable."
)
)
return(invisible(FALSE))
}
================================================
FILE: R/bundlePackage.R
================================================
bundlePackages <- function(
bundleDir,
extraPackages = character(),
quiet = FALSE,
verbose = FALSE,
dependencyResolution = "strict",
error_call = caller_env()
) {
deps <- computePackageDependencies(
bundleDir,
extraPackages,
quiet = quiet,
verbose = verbose,
dependencyResolution = dependencyResolution
)
if (nrow(deps) == 0) {
return(list())
}
checkBundlePackages(deps, call = error_call)
# Manifest packages used to generate packrat file on Connect
# https://github.com/rstudio/connect/blob/v2023.03.0/src/connect/manifest/convert.go#L261-L320
packages_list <- lapply(seq_len(nrow(deps)), function(i) {
out <- as.list(deps[i, , drop = FALSE])
out$description <- out$description[[1]]
out$Package <- NULL
out$Version <- NULL
out
})
names(packages_list) <- deps$Package
packages_list
}
usePackrat <- function() {
# Use RSCONNECT_PACKRAT when it has any value; fall-back to rsconnect.packrat when the environment
# variable is unset.
value <- Sys.getenv("RSCONNECT_PACKRAT", unset = NA)
if (is.na(value)) {
value <- getOption("rsconnect.packrat", default = FALSE)
}
return(truthy(value))
}
computePackageDependencies <- function(
bundleDir,
extraPackages = character(),
quiet = FALSE,
verbose = FALSE,
dependencyResolution = "strict"
) {
if (usePackrat()) {
taskStart(quiet, "Capturing R dependencies with packrat")
# Remove renv.lock so the packrat call to renv::dependencies does not report an implicit renv
# dependency. Mirrors rsconnect before 1.0.0, which did not include renv.lock in bundles.
# https://github.com/rstudio/rsconnect/blob/v0.8.29/R/bundle.R#L96
removeRenv(bundleDir)
deps <- snapshotPackratDependencies(
bundleDir,
extraPackages,
verbose = verbose
)
} else if (
dependencyResolution != "library" &&
!is.null(resolveRenvLockFile(bundleDir))
) {
lockfile <- resolveRenvLockFile(bundleDir)
# This ignores extraPackages; if you're using a lockfile it's your
# responsibility to install any other packages you need
taskStart(quiet, "Capturing R dependencies from renv.lock")
deps <- parseRenvDependencies(
lockfile,
bundleDir
)
# Once we've captured the deps, we can remove the renv directory
# from the bundle (retaining the renv.lock).
removeRenv(bundleDir, lockfile = FALSE)
} else {
taskStart(quiet, "Capturing R dependencies")
# TODO: give user option to choose between implicit and explicit
deps <- snapshotRenvDependencies(
bundleDir,
extraPackages,
quiet = quiet,
verbose = verbose
)
}
taskComplete(quiet, "Found {nrow(deps)} dependenc{?y/ies}")
deps
}
checkBundlePackages <- function(deps, call = caller_env()) {
unknown_source <- is.na(deps$Source)
if (any(unknown_source)) {
pkgs <- deps$Package[unknown_source]
cli::cli_abort(
c(
"All packages must be installed from a reproducible location.",
x = "Can't re-install packages installed from source: {.pkg {pkgs}}.",
i = "See {.fun rsconnect::appDependencies} for more details."
),
call = call
)
}
}
manifestPackageColumns <- function(df) {
# Fields defined in https://bit.ly/42CbD4P
# Most fields are retrieved from the complete embedded description.
# shinyapps.io needs GitHub fields for backward compatibility
github_cols <- grep("^Github", names(df), perl = TRUE, value = TRUE)
intersect(
c("Package", "Version", "Source", "Repository", github_cols),
names(df)
)
}
availablePackages <- function(repos) {
# read available.packages filters (allow user to override if necessary;
# this is primarily to allow debugging)
#
# note that we explicitly exclude the "R_version" filter as we want to ensure
# that packages which require newer versions of R than the one currently
# in use can still be marked as available on CRAN -- for example, currently
# the package "foreign" requires "R (>= 4.0.0)" but older versions of R
# can still successfully install older versions from the CRAN archive
filters <- c(
getOption("rsconnect.available_packages_filters", default = c()),
"duplicates"
)
available.packages(
repos = repos,
type = "source",
filters = filters
)
}
package_record <- function(name, lib_dir = NULL) {
record <- packageDescription(name, lib.loc = lib_dir, encoding = "UTF-8")
unclass(record)
}
================================================
FILE: R/bundlePackagePackrat.R
================================================
snapshotPackratDependencies <- function(
bundleDir,
implicit_dependencies = character(),
verbose = FALSE
) {
addPackratSnapshot(bundleDir, implicit_dependencies, verbose = verbose)
lockFilePath <- packratLockFile(bundleDir)
df <- as.data.frame(read.dcf(lockFilePath), stringsAsFactors = FALSE)
unlink(dirname(lockFilePath), recursive = TRUE)
# get repos defined in the lockfile
repos <- gsub("[\r\n]", " ", df[1, "Repos"])
repos <- strsplit(
unlist(strsplit(repos, "\\s*,\\s*", perl = TRUE)),
"=",
fixed = TRUE
)
repos <- setNames(
sapply(repos, "[[", 2),
sapply(repos, "[[", 1)
)
# get packages records defined in the lockfile
records <- utils::tail(df, -1)
if (nrow(records) == 0) {
return(data.frame())
}
rownames(records) <- NULL
records <- records[manifestPackageColumns(records)]
records[c("Source", "Repository")] <- standardizeRecords(records, repos)
records$description <- lapply(records$Package, package_record)
records
}
standardizeRecords <- function(records, repos) {
availablePackages <- availablePackages(repos)
repos <- standardizeRepos(repos)
rows <- lapply(seq_len(nrow(records)), function(i) {
standardizePackratPackage(records[i, ], availablePackages, repos = repos)
})
rows <- lapply(rows, as.data.frame, stringsAsFactors = FALSE)
rbind_fill(rows, c("Source", "Repository"))
}
standardizeRepos <- function(repos) {
# Ensure that each repository has a unique name
names(repos) <- ifelse(
names2(repos) == "",
paste0("repo_", seq_along(repos)),
names2(repos)
)
# And strip trailing /
repos <- gsub("/$", "", repos)
repos
}
standardizePackratPackage <- function(
record,
availablePackages,
repos = character()
) {
pkg <- record$Package
source <- record$Source
# source types are defined by packrat:
# https://github.com/rstudio/packrat/blob/v0.9.0/R/pkg.R#L328
if (source %in% c("github", "gitlab", "bitbucket")) {
# SCM information is recorded elsewhere
repository <- NA_character_
} else if (source == "source") {
# can't install source packages elsewhere
repository <- NA_character_
source <- NA_character_
} else if (
source == "CustomCRANLikeRepository" &&
isDevVersion(record, availablePackages)
) {
# Package was installed from source, but packrat guessed it was installed
# from a known repo.
repository <- NA_character_
source <- NA_character_
} else if (source %in% c("CRAN", "Bioconductor")) {
# shinyapps will ignore, but connect will use (unless admin
# has set up an override)
repository <- findRepoUrl(pkg, availablePackages)
} else {
# Installed from custom repository. Find URL from available.packages()
# and then name from repos.
repository <- findRepoUrl(pkg, availablePackages)
source <- findRepoName(repository, repos)
}
list(Source = source, Repository = repository)
}
findRepoName <- function(repository, repos) {
idx <- match(repository, repos)
names(repos)[idx]
}
findRepoUrl <- function(pkg, availablePackages) {
idx <- match(pkg, availablePackages[, "Package"])
if (!is.na(idx)) {
repo <- availablePackages[[idx, "Repository"]]
# Strip `/src/contrib/*` from package repository: `contrib.url()`
# adds /src/contrib, and RSPM adds additional directories
gsub("/src/contrib.*$", "", repo)
} else {
NA_character_
}
}
isDevVersion <- function(record, availablePackages) {
idx <- match(record$Package, availablePackages[, "Package"])
if (is.na(idx)) {
return(FALSE)
}
local_version <- record$Version
repo_version <- availablePackages[idx, "Version"]
package_version(local_version) > package_version(repo_version)
}
addPackratSnapshot <- function(
bundleDir,
implicit_dependencies = character(),
verbose = FALSE
) {
# if we discovered any extra dependencies, write them to a file for packrat to
# discover when it creates the snapshot
recordExtraDependencies(bundleDir, implicit_dependencies)
withCallingHandlers(
performPackratSnapshot(bundleDir, verbose = verbose),
error = function(err) {
abort("Failed to snapshot dependencies", parent = err)
},
warning = function(cnd) {
invokeRestart("muffleWarning")
}
)
invisible()
}
recordExtraDependencies <- function(bundleDir, pkgs, env = caller_env()) {
if (length(pkgs) == 0) {
return()
}
depPath <- file.path(bundleDir, "__rsconnect_deps.R")
writeLines(paste0("library(", pkgs, ")\n"), depPath)
# Automatically delete when the _caller_ finishes
defer(unlink(depPath), env = env)
invisible()
}
performPackratSnapshot <- function(bundleDir, verbose = FALSE) {
# ensure we snapshot recommended packages
srp <- packrat::opts$snapshot.recommended.packages()
packrat::opts$snapshot.recommended.packages(TRUE, persist = FALSE)
defer(packrat::opts$snapshot.recommended.packages(srp, persist = FALSE))
# Force renv dependency scanning within packrat unless the option has been
# explicitly configured. This is a no-op for older versions of packrat.
renvDiscovery <- getOption("packrat.dependency.discovery.renv")
if (is.null(renvDiscovery)) {
old <- options("packrat.dependency.discovery.renv" = TRUE)
defer(options(old))
}
# attempt to eagerly load the BiocInstaller or BiocManaager package if
# installed, to work around an issue where attempts to load the package could
# fail within a 'suppressMessages()' context
packages <- c("BiocManager", "BiocInstaller")
for (package in packages) {
if (is_installed(package)) {
requireNamespace(package, quietly = TRUE)
break
}
}
suppressMessages(
packrat::.snapshotImpl(
project = bundleDir,
snapshot.sources = FALSE,
fallback.ok = TRUE,
verbose = verbose,
implicit.packrat.dependency = FALSE
)
)
invisible()
}
packratLockFile <- function(bundleDir) {
file.path(bundleDir, "packrat", "packrat.lock")
}
================================================
FILE: R/bundlePackageRenv.R
================================================
snapshotRenvDependencies <- function(
bundleDir,
extraPackages = character(),
quiet = FALSE,
verbose = FALSE
) {
recordExtraDependencies(bundleDir, extraPackages)
old <- options(
renv.verbose = FALSE,
renv.consent = TRUE
)
defer(options(old))
dependenciesLimit <- getOption("renv.config.dependencies.limit")
if (is.null(dependenciesLimit)) {
maxFiles <- getOption("rsconnect.max.bundle.files", 10000)
oldlim <- options(
renv.config.dependencies.limit = maxFiles
)
defer(options(oldlim))
}
# analyze code dependencies ourselves rather than relying on the scan during renv::snapshot, as
# that will add renv to renv.lock as a dependency.
deps <- renv::dependencies(
bundleDir,
root = bundleDir,
quiet = if (quiet) TRUE else NULL,
progress = FALSE
)
tryCatch(
renv::snapshot(bundleDir, packages = deps$Package, prompt = FALSE),
error = function(err) {
cli::cli_abort(
c(
"Failed to snapshot dependencies with renv.",
i = "For example, you have a locally-developed package that is installed from disk."
),
parent = err
)
}
)
# renv::snapshot() respects RENV_PATHS_LOCKFILE and renv profiles, so the
# lockfile may have been written to a non-standard location.
lockfile <- resolveRenvLockFile(bundleDir)
if (is.null(lockfile)) {
cli::cli_abort("renv::snapshot() did not produce a lockfile")
}
defer(removeRenv(bundleDir))
parseRenvDependencies(lockfile, bundleDir, snapshot = TRUE)
}
parseRenvDependencies <- function(lockfile, bundleDir, snapshot = FALSE) {
renvLock <- jsonlite::read_json(lockfile)
repos <- setNames(
vapply(renvLock$R$Repositories, "[[", "URL", FUN.VALUE = character(1)),
vapply(renvLock$R$Repositories, "[[", "Name", FUN.VALUE = character(1))
)
bioc <- biocRepos(bundleDir)
if (any(bioc %in% repos)) {
biocPkgs <- NULL
} else {
signal("evaluating", class = "rsconnect_biocRepos")
biocPkgs <- availablePackages(bioc)
}
deps <- standardizeRenvPackages(
renvLock$Packages,
repos,
biocPackages = biocPkgs
)
if (nrow(deps) == 0) {
return(data.frame())
}
deps$description <- lapply(deps$Package, package_record)
if (!snapshot) {
lib_versions <- unlist(lapply(deps$description, "[[", "Version"))
if (any(deps$Version != lib_versions)) {
cli::cli_abort(c(
"Library and lockfile are out of sync",
i = "Use renv::restore() or renv::snapshot() to synchronise",
i = "Or ignore the lockfile by adding to your .rscignore",
i = "Or set {.code dependencyResolution = \"library\"} to ignore the lockfile and use the local library instead"
))
}
}
deps
}
standardizeRenvPackages <- function(packages, repos, biocPackages = NULL) {
repos <- standardizeRepos(repos)
availablePackages <- availablePackages(repos)
names(packages) <- NULL
out <- lapply(
packages,
standardizeRenvPackage,
availablePackages = availablePackages,
biocPackages = biocPackages,
repos = repos
)
out <- compact(out)
out <- lapply(out, as.data.frame, stringsAsFactors = FALSE)
rbind_fill(out)
}
standardizeRenvPackage <- function(
pkg,
availablePackages,
biocPackages = NULL,
repos = character(),
bioc
) {
# Convert renv source to manifest source/repository
# https://github.com/rstudio/renv/blob/0.17.2/R/snapshot.R#L730-L773
if (
is.null(pkg$Repository) &&
!is.null(pkg$RemoteRepos) &&
grepl("bioconductor.org", pkg$RemoteRepos)
) {
# Work around bug where renv fails to detect BioC package installed by pak
# https://github.com/rstudio/renv/issues/1202
pkg$Source <- "Bioconductor"
}
if (pkg$Source == "Repository") {
if (identical(pkg$Repository, "CRAN")) {
if (isDevVersion(pkg, availablePackages)) {
pkg$Source <- NA_character_
pkg$Repository <- NA_character_
} else {
pkg$Source <- "CRAN"
pkg$Repository <- findRepoUrl(pkg$Package, availablePackages)
}
} else {
# $Repository comes from DESCRIPTION and is set by repo, so can be
# anything. First check if it matches a known repository name,
# otherwise look up from the package name in availablePackages
# Note: our terminology is confusing: the name of the repsoitory is
# we call "Source" even though renv uses "Repository" and the URL of
# the repository we call "Repository"
originalRepository <- pkg$Repository
if (
!is.null(originalRepository) && originalRepository %in% names(repos)
) {
# Use the repository specified in renv.lock
pkg$Repository <- repos[[originalRepository]]
pkg$Source <- originalRepository
} else {
# Fall back to looking up from availablePackages
pkg$Repository <- findRepoUrl(pkg$Package, availablePackages)
pkg$Source <- findRepoName(pkg$Repository, repos)
if (is.na(pkg$Source)) {
# Archived packages are not publicized by available.packages(). Use
# the renv.lock repository as source, expecting that the package is
# available through one of the repository URLs.
pkg$Source <- originalRepository
}
}
}
} else if (pkg$Source == "Bioconductor") {
pkg$Repository <- findRepoUrl(pkg$Package, biocPackages)
if (is.na(pkg$Repository)) {
pkg$Repository <- findRepoUrl(pkg$Package, availablePackages)
}
} else if (pkg$Source %in% c("Bitbucket", "GitHub", "GitLab")) {
pkg$Source <- tolower(pkg$Source)
} else if (pkg$Source == "Local") {
# A locally-installed package (e.g. via pak "local::") may still be
# available from a configured repository. Make a best effort to locate
# it, the same way we handle packages installed from source.
pkg$Repository <- findRepoUrl(pkg$Package, availablePackages)
pkg$Source <- findRepoName(pkg$Repository, repos)
if (is.na(pkg$Source)) {
pkg$Source <- NA_character_
pkg$Repository <- NA_character_
}
} else if (pkg$Source == "unknown") {
pkg$Source <- NA_character_
pkg$Repository <- NA_character_
}
# Remove Remote fields that pak adds for "standard" installs from CRAN
if (identical(pkg$RemoteType, "standard")) {
pkg <- pkg[!grepl("^Remote", names(pkg))]
}
pkg[manifestPackageColumns(pkg)]
}
biocRepos <- function(bundleDir) {
repos <- getFromNamespace("renv_bioconductor_repos", "renv")(bundleDir)
repos[setdiff(names(repos), "CRAN")]
}
# Find the renv lockfile, checking both the renv-resolved path and the
# standard location. Returns the path if found, NULL otherwise.
resolveRenvLockFile <- function(bundleDir) {
resolved <- renv::paths$lockfile(project = bundleDir)
if (file.exists(resolved)) {
return(resolved)
}
standard <- file.path(bundleDir, "renv.lock")
if (file.exists(standard)) {
return(standard)
}
NULL
}
removeRenv <- function(path, lockfile = TRUE) {
if (lockfile) {
unlink(resolveRenvLockFile(path))
}
unlink(file.path(path, "renv"), recursive = TRUE)
}
================================================
FILE: R/bundlePython.R
================================================
# Create anonymous function that we can later call to get all needed python
# metdata for the manifest
pythonConfigurator <- function(python, forceGenerate = FALSE) {
if (is.null(python)) {
return(NULL)
}
force(forceGenerate)
function(appDir) {
withCallingHandlers(
inferPythonEnv(
appDir,
python = python,
forceGenerate = forceGenerate
),
error = function(err) {
cli::cli_abort(
"Failed to detect python environment using {.val {python}}",
parent = err
)
}
)
}
}
# python is enabled on Connect, but not on Shinyapps
getPythonForTarget <- function(path, accountDetails) {
targetIsShinyapps <- isShinyappsServer(accountDetails$server)
pythonEnabled <- getOption("rsconnect.python.enabled", !targetIsShinyapps)
if (pythonEnabled) {
getPython(path)
} else {
NULL
}
}
getPython <- function(path = NULL) {
if (!is.null(path)) {
return(path.expand(path))
}
path <- Sys.getenv("RETICULATE_PYTHON")
if (path != "") {
return(path.expand(path))
}
path <- Sys.getenv("RETICULATE_PYTHON_FALLBACK")
if (path != "") {
return(path.expand(path))
}
NULL
}
inferPythonEnv <- function(
workdir,
python = getPython(),
forceGenerate = FALSE
) {
# run the python introspection script
env_py <- system.file("resources/environment.py", package = "rsconnect")
args <- c(
shQuote(env_py),
if (forceGenerate) "-f",
shQuote(workdir)
)
hasConda <- is_installed("reticulate") &&
reticulate::py_available(initialize = FALSE) &&
reticulate::py_config()$anaconda
if (hasConda) {
prefix <- getCondaEnvPrefix(python)
conda <- getCondaExeForPrefix(prefix)
args <- c("run", "-p", prefix, python, args)
# conda run -p <prefix> python inst/resources/environment.py <flags> <dir>
output <- system2(
command = conda,
args = args,
stdout = TRUE,
stderr = NULL,
wait = TRUE
)
} else {
output <- system2(
command = python,
args = args,
stdout = TRUE,
stderr = NULL,
wait = TRUE
)
}
environment <- jsonlite::fromJSON(sanitizeSystem2json(output))
if (!is.null(environment$warning)) {
warning(environment$warning)
}
if (is.null(environment$error)) {
list(
version = environment$python,
requires = environment$requires,
package_manager = list(
name = environment$package_manager,
version = environment[[environment$package_manager]],
package_file = environment$filename,
contents = environment$contents
)
)
} else {
cli::cli_abort(environment$error)
}
}
getCondaEnvPrefix <- function(python) {
prefix <- dirname(dirname(python))
if (!file.exists(file.path(prefix, "conda-meta"))) {
stop(paste(
"Python from",
python,
"does not look like a conda environment: cannot find `conda-meta`"
))
}
prefix
}
getCondaExeForPrefix <- function(prefix) {
miniconda <- dirname(dirname(prefix))
conda <- file.path(miniconda, "bin", "conda")
if (isWindows()) {
conda <- paste(conda, ".exe", sep = "")
}
if (!file.exists(conda)) {
stop(paste(
"Conda env prefix",
prefix,
"does not have the `conda` command line interface."
))
}
conda
}
================================================
FILE: R/certificates.R
================================================
# sanity check to make sure we're looking at an ASCII armored cert
validateCertificate <- function(certificate) {
return(any(grepl("-----BEGIN CERTIFICATE-----", certificate, fixed = TRUE)))
}
createCertificateFile <- function(certificate) {
certificateFile <- NULL
# check the R option first, then fall back on the environment variable
systemStore <- getOption("rsconnect.ca.bundle")
if (is.null(systemStore) || !nzchar(systemStore)) {
systemStore <- Sys.getenv("RSCONNECT_CA_BUNDLE")
}
# start by checking for a cert file specified in an environment variable
if (!is.null(systemStore) && nzchar(systemStore)) {
if (file.exists(systemStore)) {
certificateFile <- path.expand(systemStore)
} else {
warning(
"The certificate store '",
systemStore,
"' specified in the ",
if (identical(systemStore, getOption("rsconnect.ca.bundle"))) {
"rsconnect.ca.bundle option "
} else {
"RSCONNECT_CA_BUNDLE environment variable "
},
"does not exist. The system certificate store will be used instead."
)
}
}
# if no certificate contents specified, we're done
if (is.null(certificate)) {
return(certificateFile)
}
# if we don't have a certificate file yet, try to find the system store
if (is.null(certificateFile)) {
if (.Platform$OS.type == "unix") {
# search known locations on Unix-like
stores <- c(
"/etc/ssl/certs/ca-certificates.crt",
"/etc/pki/tls/certs/ca-bundle.crt",
"/usr/share/ssl/certs/ca-bundle.crt",
"/usr/local/share/certs/ca-root.crt",
"/etc/ssl/cert.pem",
"/var/lib/ca-certificates/ca-bundle.pem"
)
} else {
# mirror behavior of curl on Windows, which looks in system folders,
# the working directory, and %PATH%.
stores <- c(
file.path(getwd(), "curl-ca-bundle.crt"),
"C:/Windows/System32/curl-ca-bundle.crt",
"C:/Windows/curl-ca-bundle.crt",
file.path(
strsplit(Sys.getenv("PATH"), ";", fixed = TRUE),
"curl-ca-bundle.crt"
)
)
}
# use our own baked-in bundle as a last resort
stores <- c(
stores,
system.file(package = "rsconnect", "cert", "cacert.pem")
)
for (store in stores) {
if (file.exists(store)) {
# if the bundle exists, stop here
certificateFile <- store
break
}
}
# if we didn't find the system store, it's okay; the fact that we're here
# means that we have a server-specific certificate so it's probably going
# to be all right to use only that cert.
}
# Combine the incoming server certificate with the discovered system
# certificate, removing duplicates (#1175).
allcerts <- c()
if (!is.null(certificateFile)) {
allcerts <- c(allcerts, openssl::read_cert_bundle(certificateFile))
}
allcerts <- c(allcerts, openssl::read_cert_bundle(certificate))
allcerts <- allcerts[!duplicated(allcerts)]
txtlines <- sapply(allcerts, openssl::write_pem)
# create a temporary file to house the certificates
certificateStore <- tempfile(pattern = "cacerts", fileext = ".pem")
writeLines(txtlines, certificateStore)
return(certificateStore)
}
inferCertificateContents <- function(certificate) {
# certificate can be specified as either a character vector or a filename;
# infer which we're dealing with
# tolerate NULL, which is a valid case representing no certificate
if (is.null(certificate) || identical(certificate, "")) {
return(NULL)
}
# collapse to a single string if we got a vector of lines
if (length(certificate) > 1) {
certificate <- paste(certificate, collapse = "\n")
}
# looks like ASCII armored certificate data, return as-is
if (validateCertificate(substr(certificate, 1, 27))) {
return(certificate)
}
# looks like a file; return its contents
if (file.exists(certificate)) {
if (file.size(certificate) > 1048576) {
stop(
"The file '",
certificate,
"' is too large. Certificate files must ",
"be less than 1MB."
)
}
contents <- paste(
readLines(con = certificate, warn = FALSE),
collapse = "\n"
)
if (validateCertificate(contents)) {
return(contents)
} else {
stop(
"The file '",
certificate,
"' does not appear to be a certificate. ",
"Certificate files should be in ASCII armored PEM format, with a ",
"first line reading -----BEGIN CERTIFICATE-----."
)
}
}
# doesn't look like something we can deal with; guess error based on length
if (nchar(certificate) < 200) {
stop("The certificate file '", certificate, "' does not exist.")
} else {
stop(
"The certificate '",
substr(certificate, 1, 10),
"...' is not ",
"correctly formed. Specify the certificate as either an ASCII armored string, ",
"beginning with -----BEGIN CERTIFICATE----, or a valid path to a file ",
"containing the certificate."
)
}
}
================================================
FILE: R/client-cloudAuth.R
================================================
# Returns the OAuth client ID to use for the configured Connect Cloud environment.
getClientId <- function() {
switch(
connectCloudEnvironment(),
production = "rsconnect",
staging = "rsconnect-staging",
development = "rsconnect-development",
)
}
# Creates a client for interacting with the Cloud Auth API.
cloudAuthClient <- function() {
service <- parseHttpUrl(connectCloudUrls()$auth)
authInfo <- list()
list(
createDeviceAuth = function() {
# Create form-encoded body
client_id <- getClientId()
content <- paste0("client_id=", getClientId(), "&scope=vivid")
response <- POST(
service,
list(),
path = "/oauth/device/authorize",
contentType = "application/x-www-form-urlencoded",
content = content
)
response
},
exchangeToken = function(request) {
client_id <- getClientId()
content <- paste0(
"client_id=",
client_id,
"&grant_type=",
request$grant_type,
"&scope=vivid"
)
if (!is.null(request$device_code)) {
content <- paste0(
content,
"&device_code=",
urlEncode(request$device_code)
)
}
if (!is.null(request$refresh_token)) {
content <- paste0(
content,
"&refresh_token=",
request$refresh_token
)
}
POST(
service,
list(),
path = "/oauth/token",
contentType = "application/x-www-form-urlencoded",
content = content
)
}
)
}
================================================
FILE: R/client-connect.R
================================================
# Docs: https://docs.posit.co/connect/api/
stripConnectTimestamps <- function(messages) {
# Strip timestamps, if found
timestamp_re <- "^\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3,} "
gsub(timestamp_re, "", messages)
}
connectClient <- function(service, authInfo) {
list(
service = function() {
"connect"
},
## Server settings API
serverSettings = function() {
GET(service, authInfo, unversioned_url("server_settings"))
},
## User API
currentUser = function() {
# All callers only need $id and $username,
# passed to registerAccount() (where account means user)
# and that gets written to a .dcf file
# /v1/user/ does not include $id
# But it looks like none of the Connect code paths use the account/user id,
# username is used to identify the "account", so this should be safe
# to upgrade to v1.
GET(service, authInfo, unversioned_url("users", "current"))
},
## Tokens API
addToken = function(token) {
POST_JSON(service, authInfo, unversioned_url("tokens"), token)
},
## Applications API
listApplications = function(accountId, filters = NULL) {
if (is.null(filters)) {
filters <- vector()
}
path <- unversioned_url("applications")
query <- paste(
filterQuery(
c("account_id", names(filters)),
c(accountId, unname(filters))
),
collapse = "&"
)
listApplicationsRequest(service, authInfo, path, query, "applications")
},
createApplication = function(
name,
title,
template,
accountId,
appMode,
contentCategory = NULL
) {
# add name; inject title if specified
details <- list(name = name)
if (!is.null(title) && nzchar(title)) {
details$title <- title
}
# Connect doesn't use the template or account ID
# parameters; they exist for compatibility with lucid.
result <- POST_JSON(service, authInfo, v1_url("content"), details)
list(
id = result$id,
guid = result$guid,
url = result$content_url,
# Include dashboard_url so we can open it or logs path after deploy
dashboard_url = result$dashboard_url
)
},
uploadBundle = function(contentGuid, bundlePath) {
path <- v1_url("content", contentGuid, "bundles")
POST(
service,
authInfo,
path,
contentType = "application/x-gzip",
file = bundlePath
)
},
deployApplication = function(application, bundleId = NULL) {
path <- v1_url("content", application$guid, "deploy")
POST_JSON(
service,
authInfo,
path,
json = list(bundle_id = bundleId)
)
},
getApplication = function(applicationId, deploymentRecordVersion) {
GET(service, authInfo, unversioned_url("applications", applicationId))
},
waitForTask = function(taskId, quiet = FALSE) {
path <- v1_url("tasks", taskId)
query <- list(first = 0, wait = 1)
while (TRUE) {
# ick, manual url construction
queryString <- paste(names(query), query, sep = "=", collapse = "&")
url <- paste0(path, "?", queryString)
response <- GET(service, authInfo, url)
if (length(response$output) > 0) {
if (!quiet) {
messages <- unlist(response$output)
messages <- stripConnectTimestamps(messages)
# Made headers more prominent.
heading <- grepl("^# ", messages)
messages[heading] <- cli::style_bold(messages[heading])
cat(paste0(messages, "\n", collapse = ""))
}
query$first <- response$last
}
if (length(response$finished) > 0 && response$finished) {
return(response)
}
}
},
# - Environment variables -----------------------------------------------
# https://docs.posit.co/connect/api/#get-/v1/content/{guid}/environment
getEnvVars = function(guid) {
path <- v1_url("content", guid, "environment")
as.character(unlist(GET(service, authInfo, path, list())))
},
setEnvVars = function(guid, vars) {
path <- v1_url("content", guid, "environment")
body <- unname(Map(
function(name, value) {
list(
name = name,
value = if (is.na(value)) NULL else value
)
},
vars,
Sys.getenv(vars, unset = NA)
))
PATCH_JSON(service, authInfo, path, body)
}
)
}
getSnowflakeAuthToken <- function(url, snowflakeConnectionName) {
parsedURL <- parseHttpUrl(url)
ingressURL <- parsedURL$host
# Detect when we're running in the Deploy pane of RStudio and enable
# "interactive" temporarily so that external browser authentication is
# permitted.
if (rstudioapi::isBackgroundJob()) {
rlang::local_options(rlang_interactive = TRUE)
}
token <- snowflakeauth::snowflake_credentials(
snowflakeauth::snowflake_connection(snowflakeConnectionName),
spcs_endpoint = ingressURL
)
token
}
# Gets the default Snowflake connection name if (1) it exists; and (2) it seems
# to match the server URL.
getDefaultSnowflakeConnectionName <- function(url) {
connection <- tryCatch(
snowflakeauth::snowflake_connection(),
error = function(e) {
cli::cli_abort(
c(
"No default {.arg snowflakeConnectionName}.",
i = "Provide {.arg snowflakeConnectionName} explicitly."
),
parent = e
)
}
)
# Validate that the default connection seems to match the account hosting the
# Connect server.
parsedURL <- parseHttpUrl(url)
serverAccount <- extractSnowflakeAccount(parsedURL$host)
normalizedAccount <- gsub("_", "-", connection$account, fixed = TRUE)
if (!identical(normalizedAccount, serverAccount)) {
cli::cli_abort(c(
"The default Snowflake connection account {.str {connection$account}} does
not appear to match the Connect server.",
i = "Pass {.arg snowflakeConnectionName} to use a different connection."
))
}
connectionName <- connection$name
if (is.null(connectionName) || !nzchar(connectionName)) {
# This should never happen.
cli::cli_abort(c(
"The Snowflake connection has an empty or missing name field.",
i = "Provide {.arg snowflakeConnectionName} explicitly."
))
}
connectionName
}
# Extract account name from an SPCS hostname.
extractSnowflakeAccount <- function(hostname) {
# For SPCS (including privatelink) URLs, there is some alphanumeric prefix
# followed by the hyphenated form of the account, followed by the Snowflake or
# Snowflake Computing domain, e.g. "bf2oiajb-testorg-testaccount.snowflakecomputing.app".
gsub(
"([^-]+)-([^\\.]+)(|\\.privatelink)\\.(snowflakecomputing|snowflake)\\.app$",
"\\2\\3",
hostname
)
}
# Utilities for URL construction
# Also to make it easier to identify where we're calling public APIs and not
v1_url <- function(...) {
# Start with empty string so we get a leading slash
paste("", "v1", ..., sep = "/")
}
unversioned_url <- function(...) {
paste("", ..., sep = "/")
}
================================================
FILE: R/client-connectCloud.R
================================================
# Docs: https://posit-hosted.github.io/vivid-api
# Map rsconnect appMode to Connect Cloud contentType
cloudContentTypeFromAppMode <- function(appMode) {
switch(
appMode,
"jupyter-notebook" = "jupyter",
"python-bokeh" = "bokeh",
"python-dash" = "dash",
"python-shiny" = "shiny",
"shiny" = "shiny",
"python-streamlit" = "streamlit",
"quarto" = "quarto",
"quarto-static" = "quarto",
"quarto-shiny" = "quarto",
"rmd-static" = "rmarkdown",
"rmd-shiny" = "rmarkdown",
"static" = "static",
stop(
"appMode '",
appMode,
"' is not supported by Connect Cloud",
call. = FALSE
)
)
}
# Creates a client for interacting with the Connect Cloud API.
connectCloudClient <- function(service, authInfo) {
# Generic retry wrapper. If a request fails with 401 Unauthorized, it will
# exchange the refresh token for a new access token and retry the request
# once.
withTokenRefreshRetry <- function(request_fn, ...) {
tryCatch(
{
request_fn(service, authInfo, ...)
},
rsconnect_http_401 = function(e) {
# Exchange refresh token for new access token
authClient <- cloudAuthClient()
tokenResponse <- authClient$exchangeToken(list(
grant_type = "refresh_token",
refresh_token = authInfo$refreshToken
))
# Save updated tokens
registerAccount(
authInfo$server,
authInfo$name,
authInfo$accountId,
accessToken = tokenResponse$access_token,
refreshToken = tokenResponse$refresh_token
)
# Retry the original request with refreshed token
authInfo$accessToken <<- tokenResponse$access_token
authInfo$refreshToken <<- tokenResponse$refresh_token
request_fn(service, authInfo, ...)
}
)
}
getAuthorization <- function(logChannel) {
json <- list(
resource_type = "log_channel",
resource_id = logChannel,
permission = "revision.logs:read"
)
response <- withTokenRefreshRetry(
POST_JSON,
"/authorization",
json
)
# Return the token from the response
response$token
}
list(
service = function() {
"connect.posit.cloud"
},
currentUser = function() {
GET(service, authInfo, "/users/me")
},
withTokenRefreshRetry = withTokenRefreshRetry,
listApplications = function(accountId, filters = list()) {
# TODO: call the real API when available (api doesn't support filtering by name yet)
return(list())
},
createContent = function(
name,
title,
accountId,
appMode,
primaryFile,
envVars
) {
title <- if (nzchar(title)) title else name
contentType <- cloudContentTypeFromAppMode(appMode)
# Build revision object, conditionally including primary_file
revision <- list(
source_type = "bundle",
content_type = contentType,
app_mode = appMode,
primary_file = primaryFile
)
secrets <- unname(Map(
function(name, value) {
list(
name = name,
value = value
)
},
envVars,
Sys.getenv(envVars)
))
json <- list(
account_id = accountId,
title = title,
next_revision = revision,
secrets = secrets
)
content <- withTokenRefreshRetry(
POST_JSON,
"/contents",
json
)
content$application_id <- content$id
content
},
getContent = function(contentId) {
path <- paste0("/contents/", contentId)
content <- withTokenRefreshRetry(GET, path)
if (content$state == "deleted") {
cli::cli_abort(
"Content is pending deletion.",
class = c(
"rsconnect_http_404",
"rsconnect_http"
)
)
}
content
},
updateContent = function(
contentId,
envVars,
newBundle = FALSE,
primaryFile,
appMode
) {
path <- paste0("/contents/", contentId)
if (newBundle) {
path <- paste0(path, "?new_bundle=true")
}
secrets <- unname(Map(
function(name, value) {
list(
name = name,
value = value
)
},
envVars,
Sys.getenv(envVars)
))
json <- list(
secrets = secrets,
revision_overrides = list(
primary_file = primaryFile,
app_mode = appMode
)
)
content <- withTokenRefreshRetry(PATCH_JSON, path, json)
content$application_id <- content$id
content
},
uploadBundle = function(bundlePath, uploadUrl) {
uploadService <- parseHttpUrl(uploadUrl)
headers <- list()
headers$`Content-Type` <- "application/gzip"
response <- httpLibCurl(
uploadService$protocol,
uploadService$host,
uploadService$port,
"POST",
uploadService$path,
headers,
headers$`Content-Type`,
bundlePath
)
response$status <= 299
},
publish = function(contentId) {
path <- paste0("/contents/", contentId, "/publish")
withTokenRefreshRetry(POST_JSON, path, list())
},
# Polls the revision until the publish process completes, returning whether
# the publish request succeeded and the error message if it failed.
awaitCompletion = function(revisionId) {
stateMessages <- list(
publish_deferred = "Content is currently publishing; your request will start soon.",
publish_requested = "Publish requested; waiting to start...",
publish_started = "Publish started.",
fetching = "Retrieving code...",
building = "Installing dependencies...",
rendering = "Rendering...",
publishing = "Publishing content...",
published = "Done."
)
lastStatus <- NULL
repeat {
path <- paste0("/revisions/", revisionId)
revision <- withTokenRefreshRetry(GET, path)
newStatus <- revision$status
if (!isTRUE(newStatus == lastStatus)) {
# Note: since we poll every second, it's possible to skip states in
# the output here
cli::cli_alert_info(stateMessages[[newStatus]])
lastStatus <- newStatus
}
contentUrl <- paste0(
connectCloudUrls()$ui,
"/",
authInfo$username,
"/content/",
revision$content_id
)
if (!is.null(revision$publish_result)) {
if (revision$publish_result == "failure") {
# Try to retrieve logs if log channel is available
if (!is.null(revision$publish_log_channel)) {
tryCatch(
{
# Get authorization token for the log channel
authToken <- getAuthorization(
revision$publish_log_channel
)
# Create logs client and fetch logs
logsClient <- connectCloudLogsClient()
logs <- logsClient$getLogs(
revision$publish_log_channel,
authToken
)
# Print logs to stderr
if (!is.null(logs) && !is.null(logs$data)) {
cli::cat_rule(
"Begin Publishing Log",
line = "#",
file = stderr()
)
for (log_entry in logs$data) {
local_timestamp <- as.POSIXct(
# Convert to seconds
log_entry$timestamp / 1e6,
origin = "1970-01-01",
)
# Format with millisecond precision
formatted_timestamp <- format(
local_timestamp,
"%Y-%m-%d %H:%M:%OS3"
)
cat(
sprintf(
"[%s] %s: %s\n",
formatted_timestamp,
toupper(log_entry$level),
log_entry$message
),
file = stderr()
)
}
cli::cat_rule(
"End Publishing Log",
line = "#",
file = stderr()
)
}
},
error = function(e) {
# If log retrieval fails, continue without logs
# Don't fail the entire operation just because logs couldn't be retrieved
cli::cli_alert_warning(
"Failed to retrieve logs: {e$message}"
)
}
)
}
return(list(
success = FALSE,
url = contentUrl,
error = revision$publish_error_details
))
}
return(list(success = TRUE, url = contentUrl, error = NULL))
}
Sys.sleep(1)
}
},
getAuthorization = getAuthorization,
getAccounts = function(revisionId) {
GET(service, authInfo, "/accounts?has_user_role=true")
}
)
}
================================================
FILE: R/client-connectCloudLogs.R
================================================
# Creates a client for interacting with the Connect Cloud Logs API.
connectCloudLogsClient <- function() {
list(
getLogs = function(logChannel, authToken) {
# Parse the logs URL to get service components
logsUrl <- connectCloudUrls()$logs
service <- parseHttpUrl(paste0(logsUrl, "/v1"))
# Create auth info with the authorization token
authInfo <- list(
accessToken = authToken
)
# Make the GET request to fetch logs
path <- paste0(
"/logs/",
logChannel,
"?traversal_direction=backward&limit=1500"
)
response <- GET(service, authInfo, path)
# Return the logs data
response
}
)
}
================================================
FILE: R/client-identityFederation.R
================================================
# Attempt exchange an identity token sourced from Posit Workbench for an
# ephemeral Connect API key. Returns NULL if this exchange fails or an API key
# otherwise.
attemptIdentityFederation <- function(serverUrl) {
cached <- getCachedApiKey(serverUrl)
if (!is.null(cached)) {
return(cached)
}
# Only attempt this in Workbench.
if (
Sys.getenv("POSIT_PRODUCT") != "WORKBENCH" &&
!nzchar(Sys.getenv("RS_SERVER_ADDRESS"))
) {
return(NULL)
}
token <- tryCatch(rstudioapi::getIdentityToken(), error = function(e) NULL)
if (is.null(token)) {
return(NULL)
}
# Call Connect's exchange endpoint.
service <- parseHttpUrl(serverUrl)
body <- paste0(
"grant_type=",
urlEncode("urn:ietf:params:oauth:grant-type:token-exchange"),
"&subject_token_type=",
urlEncode("urn:ietf:params:oauth:token-type:id_token"),
"&subject_token=",
urlEncode(token$token),
"&requested_token_type=",
urlEncode("urn:posit:connect:api-key")
)
tryCatch(
{
response <- POST(
service,
authInfo = list(),
path = "/v1/oauth/integrations/credentials",
contentType = "application/x-www-form-urlencoded",
content = body
)
apiKey <- response$access_token
if (!is.null(apiKey)) {
cacheApiKey(serverUrl, apiKey, token$expiry)
}
apiKey
},
error = function(e) NULL
)
}
cacheApiKey <- function(serverUrl, apiKey, expiry = NULL) {
env_poke(apiKeyCache, serverUrl, list(apiKey = apiKey, expiry = expiry))
}
getCachedApiKey <- function(serverUrl) {
cached <- env_get(apiKeyCache, serverUrl, default = NULL)
if (is.null(cached)) {
return(NULL)
}
# Evict expired API keys.
if (!is.null(cached$expiry) && Sys.time() >= (cached$expiry - 60L)) {
env_unbind(apiKeyCache, serverUrl)
return(NULL)
}
cached$apiKey
}
# Session-level cache for ephemeral API keys.
apiKeyCache <- new.env(parent = emptyenv())
================================================
FILE: R/client-shinyapps.R
================================================
shinyAppsClient <- function(service, authInfo) {
list(
status = function() {
GET(service, authInfo, "/internal/status")
},
service = function() {
"shinyapps.io"
},
currentUser = function() {
GET(service, authInfo, "/users/current/")
},
accountsForUser = function(userId) {
path <- "/accounts/"
query <- ""
listRequest(service, authInfo, path, query, "accounts")
},
getAccountUsage = function(
accountId,
usageType = "hours",
applicationId = NULL,
from = NULL,
until = NULL,
interval = NULL
) {
path <- paste(
"/accounts/",
accountId,
"/usage/",
usageType,
"/",
sep = ""
)
query <- list()
if (!is.null(applicationId)) {
query$application <- applicationId
}
if (!is.null(from)) {
query$from <- from
}
if (!is.null(until)) {
query$until <- until
}
if (!is.null(interval)) {
query$interval <- interval
}
GET(service, authInfo, path, queryString(query))
},
getBundle = function(bundleId) {
path <- paste("/bundles/", bundleId, sep = "")
GET(service, authInfo, path)
},
updateBundleStatus = function(bundleId, status) {
path <- paste("/bundles/", bundleId, "/status", sep = "")
json <- list()
json$status <- status
POST_JSON(service, authInfo, path, json)
},
createBundle = function(
application,
content_type,
content_length,
checksum
) {
json <- list()
json$application <- application
json$content_type <- content_type
json$content_length <- content_length
json$checksum <- checksum
POST_JSON(service, authInfo, "/bundles", json)
},
listApplications = function(accountId, filters = list()) {
path <- "/applications/"
query <- paste(
filterQuery(
c("account_id", "type", names(filters)),
c(accountId, "shiny", unname(filters))
),
collapse = "&"
)
listRequest(service, authInfo, path, query, "applications")
},
getApplication = function(applicationId, deploymentRecordVersion) {
path <- paste("/applications/", applicationId, sep = "")
application <- GET(service, authInfo, path)
application$application_id <- application$id
application
},
getApplicationMetrics = function(
applicationId,
series,
metrics,
from = NULL,
until = NULL,
interval = NULL
) {
path <- paste(
"/applications/",
applicationId,
"/metrics/",
series,
"/",
sep = ""
)
query <- list()
m <- paste(
lapply(metrics, function(x) {
paste("metric", urlEncode(x), sep = "=")
}),
collapse = "&"
)
if (!is.null(from)) {
query$from <- from
}
if (!is.null(until)) {
query$until <- until
}
if (!is.null(interval)) {
query$interval <- interval
}
GET(service, authInfo, path, paste(m, queryString(query), sep = "&"))
},
getLogs = function(applicationId, entries = 50, format = NULL) {
path <- paste0("/applications/", applicationId, "/logs")
query <- paste0("count=", entries, "&tail=0")
if (!is.null(format)) {
# format=json returns a structured response.
query <- paste0(query, "&format=", format)
}
GET(service, authInfo, path, query)
},
createApplication = function(
name,
title,
template,
accountId,
appMode,
contentCategory = NULL
) {
json <- list()
json$name <- name
# the title field is only used on connect
json$template <- template
json$account <- as.numeric(accountId)
application <- POST_JSON(service, authInfo, "/applications/", json)
list(
id = application$id,
application_id = application$id,
url = application$url
)
},
listApplicationProperties = function(applicationId) {
path <- paste("/applications/", applicationId, "/properties/", sep = "")
GET(service, authInfo, path)
},
setApplicationProperty = function(
applicationId,
propertyName,
propertyValue,
force = FALSE
) {
path <- paste(
"/applications/",
applicationId,
"/properties/",
propertyName,
sep = ""
)
v <- list()
v$value <- propertyValue
query <- paste("force=", if (force) "1" else "0", sep = "")
PUT_JSON(service, authInfo, path, v, query)
},
unsetApplicationProperty = function(
applicationId,
propertyName,
force = FALSE
) {
path <- paste(
"/applications/",
applicationId,
"/properties/",
propertyName,
sep = ""
)
query <- paste("force=", if (force) "1" else "0", sep = "")
DELETE(service, authInfo, path, query)
},
uploadApplication = function(applicationId, bundlePath) {
path <- paste("/applications/", applicationId, "/upload", sep = "")
POST(
service,
authInfo,
path,
contentType = "application/x-gzip",
file = bundlePath
)
},
deployApplication = function(application, bundleId = NULL) {
path <- paste("/applications/", application$id, "/deploy", sep = "")
json <- list()
if (length(bundleId) > 0 && nzchar(bundleId)) {
json$bundle <- as.numeric(bundleId)
} else {
json$rebuild <- FALSE
}
POST_JSON(service, authInfo, path, json)
},
terminateApplication = function(applicationId) {
path <- paste("/applications/", applicationId, "/terminate", sep = "")
POST(service, authInfo, path)
},
purgeApplication = function(applicationId) {
path <- paste("/applications/", applicationId, "/purge", sep = "")
POST(service, authInfo, path)
},
inviteApplicationUser = function(
applicationId,
email,
invite_email = NULL,
invite_email_message = NULL
) {
path <- paste(
"/applications/",
applicationId,
"/authorization/users",
sep = ""
)
json <- list()
json$email <- email
if (!is.null(invite_email)) {
json$invite_email <- invite_email
}
if (!is.null(invite_email_message)) {
json$invite_email_message <- invite_email_message
}
POST_JSON(service, authInfo, path, json)
},
addApplicationUser = function(applicationId, userId) {
path <- paste(
"/applications/",
applicationId,
"/authorization/users/",
userId,
sep = ""
)
PUT(service, authInfo, path, NULL)
},
removeApplicationUser = function(applicationId, userId) {
path <- paste(
"/applications/",
applicationId,
"/authorization/users/",
userId,
sep = ""
)
DELETE(service, authInfo, path, NULL)
},
listApplicationAuthorization = function(applicationId) {
path <- paste("/applications/", applicationId, "/authorization", sep = "")
listRequest(service, authInfo, path, NULL, "authorization")
},
listApplicationUsers = function(applicationId) {
path <- paste(
"/applications/",
applicationId,
"/authorization/users",
sep = ""
)
listRequest(service, authInfo, path, NULL, "users")
},
listApplicationGroups = function(applicationId) {
path <- paste(
"/applications/",
applicationId,
"/authorization/groups",
sep = ""
)
listRequest(service, authInfo, path, NULL, "groups")
},
listApplicationInvitations = function(applicationId) {
path <- "/invitations/"
query <- paste(filterQuery("app_id", applicationId), collapse = "&")
listRequest(service, authInfo, path, query, "invitations")
},
resendApplicationInvitation = function(invitationId, regenerate = FALSE) {
path <- paste("/invitations/", invitationId, "/send", sep = "")
json <- list()
json$regenerate <- regenerate
POST_JSON(service, authInfo, path, json)
},
listTasks = function(accountId, filters = NULL) {
if (is.null(filters)) {
filters <- vector()
}
path <- "/tasks/"
filte
gitextract__wtwsl8q/
├── .Rbuildignore
├── .github/
│ ├── .gitignore
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── config.yml
│ └── workflows/
│ ├── R-CMD-check.yaml
│ ├── check-no-suggests.yaml
│ ├── connect-integration.yaml
│ ├── format-suggest.yaml
│ ├── lint.yaml
│ ├── pkgdown.yaml
│ ├── rhub.yaml
│ └── shinyapps-integration.yaml
├── .gitignore
├── .lintr
├── DESCRIPTION
├── NAMESPACE
├── NEWS.md
├── R/
│ ├── .editorconfig
│ ├── account-find.R
│ ├── accounts.R
│ ├── appDependencies.R
│ ├── appMetadata-quarto.R
│ ├── appMetadata.R
│ ├── applications.R
│ ├── auth.R
│ ├── bundle.R
│ ├── bundleFiles.R
│ ├── bundlePackage.R
│ ├── bundlePackagePackrat.R
│ ├── bundlePackageRenv.R
│ ├── bundlePython.R
│ ├── certificates.R
│ ├── client-cloudAuth.R
│ ├── client-connect.R
│ ├── client-connectCloud.R
│ ├── client-connectCloudLogs.R
│ ├── client-identityFederation.R
│ ├── client-shinyapps.R
│ ├── client.R
│ ├── config.R
│ ├── configMigrate.R
│ ├── configureApp.R
│ ├── cookies.R
│ ├── deployAPI.R
│ ├── deployApp.R
│ ├── deployDoc.R
│ ├── deploySite.R
│ ├── deployTFModel.R
│ ├── deploymentTarget.R
│ ├── deployments-find.R
│ ├── deployments.R
│ ├── envvars.R
│ ├── http-httr2.R
│ ├── http-libcurl.R
│ ├── http.R
│ ├── ide.R
│ ├── import-standalone-obj-type.R
│ ├── import-standalone-types-check.R
│ ├── imports.R
│ ├── lint-framework.R
│ ├── lint.R
│ ├── linters.R
│ ├── locale.R
│ ├── purgeApp.R
│ ├── restartApp.R
│ ├── rpubs.R
│ ├── rsconnect-package.R
│ ├── secret.R
│ ├── servers.R
│ ├── tasks.R
│ ├── terminateApp.R
│ ├── title.R
│ ├── usage.R
│ ├── utils-cli.R
│ ├── utils.R
│ ├── utm.R
│ ├── writeManifest.R
│ └── zzz.R
├── README.Rmd
├── README.md
├── RELEASE.md
├── _pkgdown.yml
├── cran-comments.md
├── examples/
│ └── example-linter.R
├── inst/
│ ├── cert/
│ │ ├── api.connect.posit.cloud.pem
│ │ ├── cacert.pem
│ │ └── shinyapps.io.pem
│ ├── examples/
│ │ ├── diamonds/
│ │ │ ├── server.R
│ │ │ └── ui.R
│ │ └── sessioninfo/
│ │ ├── server.R
│ │ └── ui.R
│ └── resources/
│ ├── environment.py
│ └── pyproject.py
├── man/
│ ├── accountUsage.Rd
│ ├── accounts.Rd
│ ├── addAuthorizedUser.Rd
│ ├── addLinter.Rd
│ ├── addServer.Rd
│ ├── appDependencies.Rd
│ ├── applicationConfigDir.Rd
│ ├── applications.Rd
│ ├── configureApp.Rd
│ ├── connectApiUser.Rd
│ ├── connectCloudUser.Rd
│ ├── connectSPCSUser.Rd
│ ├── deployAPI.Rd
│ ├── deployApp.Rd
│ ├── deployDoc.Rd
│ ├── deploySite.Rd
│ ├── deployTFModel.Rd
│ ├── deployments.Rd
│ ├── forgetDeployment.Rd
│ ├── generateAppName.Rd
│ ├── lint.Rd
│ ├── linter.Rd
│ ├── listAccountEnvVars.Rd
│ ├── listBundleFiles.Rd
│ ├── listDeploymentFiles.Rd
│ ├── makeLinterMessage.Rd
│ ├── oldApplicationConfigDir.Rd
│ ├── options.Rd
│ ├── purgeApp.Rd
│ ├── removeAuthorizedUser.Rd
│ ├── resendInvitation.Rd
│ ├── restartApp.Rd
│ ├── rpubsUpload.Rd
│ ├── rsconnect-package.Rd
│ ├── rsconnectConfigDir.Rd
│ ├── rsconnectPackages.Rd
│ ├── rsconnectProxies.Rd
│ ├── servers.Rd
│ ├── setAccountInfo.Rd
│ ├── setProperty.Rd
│ ├── showInvited.Rd
│ ├── showLogs.Rd
│ ├── showMetrics.Rd
│ ├── showProperties.Rd
│ ├── showUsage.Rd
│ ├── showUsers.Rd
│ ├── syncAppMetadata.Rd
│ ├── taskLog.Rd
│ ├── tasks.Rd
│ ├── terminateApp.Rd
│ ├── unsetProperty.Rd
│ └── writeManifest.Rd
├── revdep/
│ ├── .gitignore
│ ├── README.md
│ ├── cran.md
│ ├── failures.md
│ └── problems.md
├── rsconnect.Rproj
├── tests/
│ ├── integration/
│ │ ├── example-shiny/
│ │ │ └── app.R
│ │ ├── setup.R
│ │ └── test-deploy.R
│ ├── manual/
│ │ ├── appMode.Rmd
│ │ ├── dependencies.Rmd
│ │ ├── deploySite.Rmd
│ │ └── publishing-dialog.Rmd
│ ├── shinyapps-integration/
│ │ ├── example-shiny/
│ │ │ ├── app.R
│ │ │ └── manifest.json
│ │ ├── setup.R
│ │ └── test-shinyapps-deploy.R
│ ├── testthat/
│ │ ├── _snaps/
│ │ │ ├── account-find.md
│ │ │ ├── accounts.md
│ │ │ ├── appDependencies.md
│ │ │ ├── appMetadata-quarto.md
│ │ │ ├── appMetadata.md
│ │ │ ├── applications.md
│ │ │ ├── bundle.md
│ │ │ ├── bundleFiles.md
│ │ │ ├── bundlePackage.md
│ │ │ ├── bundlePackagePackrat.md
│ │ │ ├── bundlePackageRenv.md
│ │ │ ├── bundlePython.md
│ │ │ ├── client-connect.md
│ │ │ ├── cookies.md
│ │ │ ├── deployApp.md
│ │ │ ├── deployDoc.md
│ │ │ ├── deploymentTarget.md
│ │ │ ├── deployments-find.md
│ │ │ ├── deployments.md
│ │ │ ├── http-libcurl.md
│ │ │ ├── http.md
│ │ │ ├── ide.md
│ │ │ ├── lint.md
│ │ │ ├── linters.md
│ │ │ ├── secret.md
│ │ │ ├── servers.md
│ │ │ └── writeManifest.md
│ │ ├── certs/
│ │ │ ├── example.com.pem
│ │ │ ├── invalid.crt
│ │ │ ├── localhost.pem
│ │ │ ├── sample.crt
│ │ │ └── two-cas.crt
│ │ ├── helper-content.R
│ │ ├── helper-http.R
│ │ ├── helper-paths.R
│ │ ├── helper.R
│ │ ├── multibyte-characters/
│ │ │ └── app.R
│ │ ├── packages/
│ │ │ ├── latin1package/
│ │ │ │ ├── DESCRIPTION
│ │ │ │ ├── NAMESPACE
│ │ │ │ ├── R/
│ │ │ │ │ └── hello.R
│ │ │ │ └── man/
│ │ │ │ └── hello.Rd
│ │ │ ├── utf8package/
│ │ │ │ ├── DESCRIPTION
│ │ │ │ ├── NAMESPACE
│ │ │ │ ├── R/
│ │ │ │ │ └── hello.R
│ │ │ │ └── man/
│ │ │ │ └── hello.Rd
│ │ │ └── windows1251package/
│ │ │ ├── DESCRIPTION
│ │ │ ├── NAMESPACE
│ │ │ ├── R/
│ │ │ │ └── hello.R
│ │ │ └── man/
│ │ │ └── hello.Rd
│ │ ├── quarto-doc-long-chunk/
│ │ │ └── index.qmd
│ │ ├── quarto-doc-none/
│ │ │ └── quarto-doc-none.qmd
│ │ ├── renv-recommended/
│ │ │ └── dependences.R
│ │ ├── shiny-app-in-subdir/
│ │ │ └── my-app/
│ │ │ ├── server.R
│ │ │ └── ui.r
│ │ ├── shiny-rmds/
│ │ │ ├── non-shiny-rmd.Rmd
│ │ │ ├── shiny-rmd-dashes.Rmd
│ │ │ └── shiny-rmd-dots.Rmd
│ │ ├── shinyapp-appR/
│ │ │ ├── app.R
│ │ │ └── rsconnect/
│ │ │ └── colorado.posit.co/
│ │ │ └── hadley/
│ │ │ └── shinyapp-appR.dcf
│ │ ├── shinyapp-simple/
│ │ │ ├── server.R
│ │ │ ├── shinyapp-simple.Rproj
│ │ │ └── ui.R
│ │ ├── shinyapp-singleR/
│ │ │ └── single.R
│ │ ├── shinyapp-with-absolute-paths/
│ │ │ ├── ShinyDocument.Rmd
│ │ │ ├── ShinyPresentation.Rmd
│ │ │ ├── data/
│ │ │ │ └── College.txt
│ │ │ ├── server.R
│ │ │ └── ui.R
│ │ ├── shinyapp-with-browser/
│ │ │ ├── server.R
│ │ │ └── ui.R
│ │ ├── static-with-quarto-yaml/
│ │ │ ├── _quarto.yml
│ │ │ └── slideshow.html
│ │ ├── test-account-find.R
│ │ ├── test-accounts.R
│ │ ├── test-appDependencies.R
│ │ ├── test-appMetadata-quarto.R
│ │ ├── test-appMetadata.R
│ │ ├── test-applications.R
│ │ ├── test-bundle.R
│ │ ├── test-bundleFiles.R
│ │ ├── test-bundleNodejs.R
│ │ ├── test-bundlePackage.R
│ │ ├── test-bundlePackagePackrat.R
│ │ ├── test-bundlePackageRenv.R
│ │ ├── test-bundlePython.R
│ │ ├── test-cert.R
│ │ ├── test-client-connect.R
│ │ ├── test-client-connectCloud.R
│ │ ├── test-client.R
│ │ ├── test-config.R
│ │ ├── test-cookies.R
│ │ ├── test-deployApp.R
│ │ ├── test-deployDoc.R
│ │ ├── test-deploySite.R
│ │ ├── test-deploymentTarget.R
│ │ ├── test-deployments-find.R
│ │ ├── test-deployments.R
│ │ ├── test-http-httr2.R
│ │ ├── test-http-libcurl.R
│ │ ├── test-http.R
│ │ ├── test-ide.R
│ │ ├── test-identityFederation.R
│ │ ├── test-lint.R
│ │ ├── test-linters.R
│ │ ├── test-locale.R
│ │ ├── test-plumber/
│ │ │ └── plumber.R
│ │ ├── test-reticulate-rmds/
│ │ │ ├── implicit.Rmd
│ │ │ └── index.Rmd
│ │ ├── test-rmd-bad-case/
│ │ │ └── index.Rmd
│ │ ├── test-rmds/
│ │ │ ├── index.Rmd
│ │ │ ├── parameterized.Rmd
│ │ │ └── simple.Rmd
│ │ ├── test-secret.R
│ │ ├── test-servers.R
│ │ ├── test-spcs.R
│ │ ├── test-title.R
│ │ ├── test-utils.R
│ │ └── test-writeManifest.R
│ └── testthat.R
└── vignettes/
├── .gitignore
└── custom-http.Rmd
SYMBOL INDEX (20 symbols across 2 files) FILE: inst/resources/environment.py function MakeEnvironment (line 36) | def MakeEnvironment( class EnvironmentException (line 52) | class EnvironmentException(Exception): function detect_environment (line 56) | def detect_environment(dirname, force_generate=False, conda_mode=False, ... function get_conda (line 108) | def get_conda(conda=None): function get_python_version (line 120) | def get_python_version(environment): function get_conda_version (line 134) | def get_conda_version(conda): function get_default_locale (line 152) | def get_default_locale(locale_source=locale.getdefaultlocale): function get_version (line 157) | def get_version(module): function output_file (line 172) | def output_file(dirname, filename, package_manager): function pip_freeze (line 199) | def pip_freeze(): function conda_env_export (line 237) | def conda_env_export(conda): function main (line 263) | def main(): FILE: inst/resources/pyproject.py function detect_python_version_requirement (line 22) | def detect_python_version_requirement(directory): function lookup_metadata_file (line 45) | def lookup_metadata_file(directory): function get_python_version_requirement_parser (line 66) | def get_python_version_requirement_parser(metadata_file): function parse_pyproject_python_requires (line 82) | def parse_pyproject_python_requires(pyproject_file): function parse_setupcfg_python_requires (line 107) | def parse_setupcfg_python_requires(setupcfg_file): function parse_pyversion_python_requires (line 121) | def parse_pyversion_python_requires(pyversion_file): function adapt_python_requires (line 132) | def adapt_python_requires(python_requires): class InvalidVersionConstraintError (line 171) | class InvalidVersionConstraintError(ValueError):
Condensed preview — 283 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,286K chars).
[
{
"path": ".Rbuildignore",
"chars": 230,
"preview": "^.*\\.Rproj$\n^\\.Rproj\\.user$\n^examples$\n^R/\\.editorconfig$\n^README\\.Rmd$\n^README\\.html$\n^RELEASE\\.md$\n^_pkgdown\\.yml$\n^do"
},
{
"path": ".github/.gitignore",
"chars": 7,
"preview": "*.html\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 1183,
"preview": "name: Bug report\ndescription: Report an error or unexpected behavior\nlabels: [bug]\n\nbody:\n - type: markdown\n attribu"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 586,
"preview": "blank_issues_enabled: true\ncontact_links:\n - name: Help with shinyapps.io\n url: https://forum.posit.co/c/posit-profe"
},
{
"path": ".github/workflows/R-CMD-check.yaml",
"chars": 1565,
"preview": "# Workflow derived from https://github.com/r-lib/actions/tree/master/examples\n# Need help debugging build failures? Star"
},
{
"path": ".github/workflows/check-no-suggests.yaml",
"chars": 1698,
"preview": "# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples\n# Need help debugging build failures? Start at"
},
{
"path": ".github/workflows/connect-integration.yaml",
"chars": 1578,
"preview": "name: Connect Integration Tests\non:\n push:\n branches: [main, master]\n pull_request:\n branches: [main, master]\n\nj"
},
{
"path": ".github/workflows/format-suggest.yaml",
"chars": 583,
"preview": "# Workflow derived from https://github.com/posit-dev/setup-air/tree/main/examples\non:\n pull_request:\n\nname: format-sugg"
},
{
"path": ".github/workflows/lint.yaml",
"chars": 766,
"preview": "# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples\n# Need help debugging build failures? Start at"
},
{
"path": ".github/workflows/pkgdown.yaml",
"chars": 1262,
"preview": "# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples\n# Need help debugging build failures? Start at"
},
{
"path": ".github/workflows/rhub.yaml",
"chars": 2942,
"preview": "# R-hub's generic GitHub Actions workflow file. It's canonical location is at\n# https://github.com/r-hub/actions/blob/v1"
},
{
"path": ".github/workflows/shinyapps-integration.yaml",
"chars": 1131,
"preview": "name: shinyapps.io Integration Tests\non:\n push:\n branches: [main, master]\n pull_request:\n branches: [main, maste"
},
{
"path": ".gitignore",
"chars": 227,
"preview": ".Rproj.user\n.Rhistory\n.RData\n.DS_Store\nREADME.html\n*.Rcheck/\n*.tar.gz\ndocs/\n.rscignore\ntags\ninst/doc\ninst/resources/__py"
},
{
"path": ".lintr",
"chars": 296,
"preview": "linters: linters_with_defaults(\n line_length_linter(160),\n indentation_linter = NULL,\n object_length_linter(60)"
},
{
"path": "DESCRIPTION",
"chars": 1645,
"preview": "Type: Package\nPackage: rsconnect\nTitle: Deploy Docs, Apps, and APIs to 'Posit Connect', 'shinyapps.io', and 'RPubs'\nVers"
},
{
"path": "NAMESPACE",
"chars": 1689,
"preview": "# Generated by roxygen2: do not edit by hand\n\nS3method(as.data.frame,rsconnect_secret)\nS3method(format,rsconnect_secret)"
},
{
"path": "NEWS.md",
"chars": 32568,
"preview": "# rsconnect (development version)\n\n* Added support for deploying Node.js applications to Posit Connect.\n `deployApp()` "
},
{
"path": "R/.editorconfig",
"chars": 110,
"preview": "[*.R]\nindent_style = space\nindent_size = 2\nend_of_line = lf\nmax_line_length = 100\ninsert_final_newline = true\n"
},
{
"path": "R/account-find.R",
"chars": 3588,
"preview": "# Return a list containing the name and server associated with a matching account.\n#\n# Use `accountInfo()` and `findAcco"
},
{
"path": "R/accounts.R",
"chars": 21297,
"preview": "#' Account Management Functions\n#'\n#' @description\n#' Functions to enumerate and remove accounts on the local system. Pr"
},
{
"path": "R/appDependencies.R",
"chars": 6616,
"preview": "#' Detect application dependencies\n#'\n#' @description\n#' `appDependencies()` recursively detects all R package dependenc"
},
{
"path": "R/appMetadata-quarto.R",
"chars": 2011,
"preview": "# Called only when the content is known to be Quarto.\ninferQuartoInfo <- function(metadata, appDir, appPrimaryDoc) {\n i"
},
{
"path": "R/appMetadata.R",
"chars": 14416,
"preview": "appMetadata <- function(\n appDir,\n appFiles,\n appPrimaryDoc = NULL,\n quarto = NA,\n appMode = NULL,\n contentCategor"
},
{
"path": "R/applications.R",
"chars": 10857,
"preview": "#' List Deployed Applications\n#'\n#' @description\n#' List all applications currently deployed for a given account.\n#'\n#' "
},
{
"path": "R/auth.R",
"chars": 9452,
"preview": "cleanupPasswordFile <- function(appDir) {\n check_directory(appDir)\n appDir <- normalizePath(appDir)\n\n # get data dir "
},
{
"path": "R/bundle.R",
"chars": 11947,
"preview": "# Given a path to an directory and a list of files in that directory, copies\n# those files to a new temporary directory."
},
{
"path": "R/bundleFiles.R",
"chars": 9448,
"preview": "#' Gather files to be bundled with an app\n#'\n#' @description\n#' Given an app directory, and optional `appFiles` and `app"
},
{
"path": "R/bundlePackage.R",
"chars": 4480,
"preview": "bundlePackages <- function(\n bundleDir,\n extraPackages = character(),\n quiet = FALSE,\n verbose = FALSE,\n dependency"
},
{
"path": "R/bundlePackagePackrat.R",
"chars": 5987,
"preview": "snapshotPackratDependencies <- function(\n bundleDir,\n implicit_dependencies = character(),\n verbose = FALSE\n) {\n add"
},
{
"path": "R/bundlePackageRenv.R",
"chars": 7126,
"preview": "snapshotRenvDependencies <- function(\n bundleDir,\n extraPackages = character(),\n quiet = FALSE,\n verbose = FALSE\n) {"
},
{
"path": "R/bundlePython.R",
"chars": 3333,
"preview": "# Create anonymous function that we can later call to get all needed python\n# metdata for the manifest\npythonConfigurato"
},
{
"path": "R/certificates.R",
"chars": 5096,
"preview": "# sanity check to make sure we're looking at an ASCII armored cert\nvalidateCertificate <- function(certificate) {\n retu"
},
{
"path": "R/client-cloudAuth.R",
"chars": 1578,
"preview": "# Returns the OAuth client ID to use for the configured Connect Cloud environment.\ngetClientId <- function() {\n switch("
},
{
"path": "R/client-connect.R",
"chars": 7234,
"preview": "# Docs: https://docs.posit.co/connect/api/\n\nstripConnectTimestamps <- function(messages) {\n # Strip timestamps, if foun"
},
{
"path": "R/client-connectCloud.R",
"chars": 9420,
"preview": "# Docs: https://posit-hosted.github.io/vivid-api\n\n# Map rsconnect appMode to Connect Cloud contentType\ncloudContentTypeF"
},
{
"path": "R/client-connectCloudLogs.R",
"chars": 697,
"preview": "# Creates a client for interacting with the Connect Cloud Logs API.\nconnectCloudLogsClient <- function() {\n list(\n g"
},
{
"path": "R/client-identityFederation.R",
"chars": 1961,
"preview": "# Attempt exchange an identity token sourced from Posit Workbench for an\n# ephemeral Connect API key. Returns NULL if th"
},
{
"path": "R/client-shinyapps.R",
"chars": 10160,
"preview": "shinyAppsClient <- function(service, authInfo) {\n list(\n status = function() {\n GET(service, authInfo, \"/intern"
},
{
"path": "R/client.R",
"chars": 5078,
"preview": "clientForAccount <- function(account) {\n serverInfo <- serverInfo(account$server)\n account$certificate <- serverInfo$c"
},
{
"path": "R/config.R",
"chars": 4531,
"preview": "#' rsconnect Configuration Directory\n#'\n#' Forms the path to a location on disk where user-level configuration data for\n"
},
{
"path": "R/configMigrate.R",
"chars": 3703,
"preview": "# account/server ----------------------------------------------------------\n\nmigrateConfig <- function(configDir) {\n # "
},
{
"path": "R/configureApp.R",
"chars": 5940,
"preview": "#' Configure an Application\n#'\n#' @description\n#' Configure an application running on a remote server.\n#'\n#' Supported s"
},
{
"path": "R/cookies.R",
"chars": 7517,
"preview": "# Environment in which cookies will be stored. Cookies are expected to survive\n# the duration of the R session, but are "
},
{
"path": "R/deployAPI.R",
"chars": 1057,
"preview": "#' Deploy a Plumber API\n#'\n#' @description\n#' Deploys an application consisting of plumber API routes. The given directo"
},
{
"path": "R/deployApp.R",
"chars": 38726,
"preview": "#' Deploy an Application\n#'\n#' @description\n#' Deploy a [shiny][shiny::shiny-package] application, an\n#' [RMarkdown][rma"
},
{
"path": "R/deployDoc.R",
"chars": 2738,
"preview": "#' Deploy a single document\n#'\n#' @description\n#' Deploys a single R Markdown, Quarto document, or other file (e.g. `.ht"
},
{
"path": "R/deploySite.R",
"chars": 4570,
"preview": "#' Deploy a website\n#'\n#' @description\n#' Deploy an R Markdown or quarto website to a server.\n#'\n#' Supported servers: P"
},
{
"path": "R/deployTFModel.R",
"chars": 365,
"preview": "#' Deploy a TensorFlow saved model\n#'\n#' @description\n#' Deploys a directory containing a TensorFlow saved model.\n#'\n#' "
},
{
"path": "R/deploymentTarget.R",
"chars": 11609,
"preview": "# Discover the deployment target given the passed information.\n#\n# Returns a list containing a deployment record and the"
},
{
"path": "R/deployments-find.R",
"chars": 1700,
"preview": "findDeployment <- function(\n appPath = getwd(),\n appName = NULL,\n server = NULL,\n account = NULL,\n error_call = cal"
},
{
"path": "R/deployments.R",
"chars": 9616,
"preview": "#' List Application Deployments\n#'\n#' @description\n#' List deployment records for a given application.\n#'\n#' Supported s"
},
{
"path": "R/envvars.R",
"chars": 2547,
"preview": "#' Maintain environment variables across multiple applications\n#'\n#' @description\n#' * `listAccountEnvVars()` lists the "
},
{
"path": "R/http-httr2.R",
"chars": 2894,
"preview": "# httr2-based HTTP backend for rsconnect\n#\n# These functions are called when getOption(\"rsconnect.httr2\") is TRUE.\n# The"
},
{
"path": "R/http-libcurl.R",
"chars": 3944,
"preview": "httpLibCurl <- function(\n protocol,\n host,\n port,\n method,\n path,\n headers,\n contentType = NULL,\n contentFile = "
},
{
"path": "R/http.R",
"chars": 18882,
"preview": "#' @param authInfo Typically an object created by `accountInfo()` augmented\n#' with the `certificate` from the corresp"
},
{
"path": "R/ide.R",
"chars": 4289,
"preview": "# These functions are intended to be called primarily by the RStudio IDE.\n\n# This function is poorly named because as we"
},
{
"path": "R/import-standalone-obj-type.R",
"chars": 8375,
"preview": "# Standalone file: do not edit by hand\n# Source: <https://github.com/r-lib/rlang/blob/main/R/standalone-obj-type.R>\n# --"
},
{
"path": "R/import-standalone-types-check.R",
"chars": 10732,
"preview": "# Standalone file: do not edit by hand\n# Source: <https://github.com/r-lib/rlang/blob/main/R/standalone-types-check.R>\n#"
},
{
"path": "R/imports.R",
"chars": 187,
"preview": "#' @importFrom stats na.omit setNames\n#' @importFrom utils available.packages installed.packages contrib.url getFromName"
},
{
"path": "R/lint-framework.R",
"chars": 1725,
"preview": ".__LINTERS__. <- new.env(parent = emptyenv())\n\n##' Add a Linter\n##'\n##' @description\n##' Add a linter, to be used in sub"
},
{
"path": "R/lint.R",
"chars": 7631,
"preview": "##' Lint a Project\n##'\n##' @description\n##' Takes the set of active linters (see [addLinter()]), and applies\n##' them to"
},
{
"path": "R/linters.R",
"chars": 9770,
"preview": ".__LINTERS__. <- new.env(parent = emptyenv())\n\n##' Add a Linter\n##'\n##' Add a linter, to be used in subsequent calls to "
},
{
"path": "R/locale.R",
"chars": 723,
"preview": "detectLocale <- function() {\n if (!isWindows()) {\n locales <- strsplit(Sys.getlocale(\"LC_CTYPE\"), \".\", fixed = TRUE)"
},
{
"path": "R/purgeApp.R",
"chars": 1257,
"preview": "#' Purge an Application\n#'\n#' @description\n#' Purge a currently archived ShinyApps application.\n#'\n#' Supported servers:"
},
{
"path": "R/restartApp.R",
"chars": 1277,
"preview": "#' Restart an Application\n#'\n#' @description\n#' Restart an application currently running on a remote server.\n#'\n#' Suppo"
},
{
"path": "R/rpubs.R",
"chars": 5307,
"preview": "#' Upload a file to RPubs\n#'\n#' @description\n#' This function publishes a file to rpubs.com. If the upload succeeds a\n#'"
},
{
"path": "R/rsconnect-package.R",
"chars": 394,
"preview": "#' @keywords internal\n\"_PACKAGE\"\n\n#' Using Packages with rsconnect\n#'\n#' See ?[appDependencies()]\n#' @keywords internal\n"
},
{
"path": "R/secret.R",
"chars": 624,
"preview": "secret <- function(x) {\n if (is.null(x)) {\n return(NULL)\n }\n\n stopifnot(is.character(x) || all(is.na(x)))\n struct"
},
{
"path": "R/servers.R",
"chars": 10608,
"preview": "#' Server metadata\n#'\n#' @description\n#' `servers()` lists all known servers; `serverInfo()` gets metadata about\n#' a sp"
},
{
"path": "R/tasks.R",
"chars": 2240,
"preview": "#' List Tasks\n#'\n#' @description\n#' List Tasks\n#'\n#' Supported servers: ShinyApps servers\n#'\n#' @inheritParams deployApp"
},
{
"path": "R/terminateApp.R",
"chars": 1319,
"preview": "#' Terminate an Application\n#'\n#' @description\n#' Terminate and archive a currently deployed ShinyApps application.\n#'\n#"
},
{
"path": "R/title.R",
"chars": 3282,
"preview": "#' Generate Application Name\n#'\n#' @description\n#' Generate a short name (identifier) for an application given an applic"
},
{
"path": "R/usage.R",
"chars": 4777,
"preview": "#' Show Application Usage\n#'\n#' @description\n#' Show application usage of a currently deployed application\n#'\n#' Support"
},
{
"path": "R/utils-cli.R",
"chars": 1578,
"preview": "# https://github.com/r-lib/cli/issues/228 ---------------------------------\n\nsimulate_user_input <- function(x, env = ca"
},
{
"path": "R/utils.R",
"chars": 4299,
"preview": "# Returns a logging function when enabled, a noop function otherwise.\nverboseLogger <- function(verbose) {\n if (verbose"
},
{
"path": "R/utm.R",
"chars": 383,
"preview": "getUtmValue <- function() {\n if (Sys.getenv(\"RSTUDIO\") == \"1\") {\n return(\"rsconnect-rstudio\")\n } else {\n return("
},
{
"path": "R/writeManifest.R",
"chars": 3186,
"preview": "#' Create a `manifest.json`\n#'\n#' @description\n#' Use `writeManifest()` to generate a `manifest.json`. Among other thing"
},
{
"path": "R/zzz.R",
"chars": 598,
"preview": "defaultMaxBundleSize <- 5 * 1024^3\ndefaultMaxBundleFiles <- 10000\n\nsetOptionDefaults <- function(...) {\n # Resolve dots"
},
{
"path": "README.Rmd",
"chars": 2766,
"preview": "---\noutput: github_document\n---\n\n<!-- README.md is generated from README.Rmd. Please edit that file -->\n\n```{r, include "
},
{
"path": "README.md",
"chars": 2593,
"preview": "\n<!-- README.md is generated from README.Rmd. Please edit that file -->\n\n# rsconnect <a href='https://rstudio.github.io/"
},
{
"path": "RELEASE.md",
"chars": 166,
"preview": "## Release instructions\n\nCreate a release issue with:\n\n```r\nusethis::use_release_issue()\n```\n\nRun noSuggests test:\n\n```r"
},
{
"path": "_pkgdown.yml",
"chars": 953,
"preview": "url: https://rstudio.github.io/rsconnect\n\ndevelopment:\n mode: auto\n\ntemplate:\n package: tidytemplate\n bootstrap: 5\n\nr"
},
{
"path": "cran-comments.md",
"chars": 914,
"preview": "## Summary\n\nUse httr2 as HTTP client. Support renv profiles. Remove several\nlong-deprecated functions, such as `addConne"
},
{
"path": "examples/example-linter.R",
"chars": 578,
"preview": "addLinter(\"no.capitals\", linter(\n\n ## Identify lines containing capital letters -- either by name or by index\n apply ="
},
{
"path": "inst/cert/api.connect.posit.cloud.pem",
"chars": 2124,
"preview": "-----BEGIN CERTIFICATE-----\r\nMIIF2jCCBMKgAwIBAgIQAemXdyxv7CGHCzR9b3LgmTANBgkqhkiG9w0BAQsFADA8\r\nMQswCQYDVQQGEwJVUzEPMA0GA"
},
{
"path": "inst/cert/cacert.pem",
"chars": 236051,
"preview": "##\n## Bundle of CA Root Certificates\n##\n## Certificate data from Mozilla as of: Wed Sep 20 03:12:05 2017 GMT\n##\n## This "
},
{
"path": "inst/cert/shinyapps.io.pem",
"chars": 12133,
"preview": "Amazon Root CA 1\n================\n-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w"
},
{
"path": "inst/examples/diamonds/server.R",
"chars": 635,
"preview": "library(shiny)\nlibrary(ggplot2)\n\nshinyServer(function(input, output) {\n\n dataset <- reactive(function() {\n diamonds["
},
{
"path": "inst/examples/diamonds/ui.R",
"chars": 732,
"preview": "library(shiny)\nlibrary(ggplot2)\n\ndataset <- diamonds\n\nshinyUI(pageWithSidebar(\n\n headerPanel(\"Diamonds Explorer\"),\n\n s"
},
{
"path": "inst/examples/sessioninfo/server.R",
"chars": 670,
"preview": "library(shiny)\n\nshinyServer(function(input, output, session) {\n\n # output sessionInfo\n output$sessionInfo <- renderPri"
},
{
"path": "inst/examples/sessioninfo/ui.R",
"chars": 187,
"preview": "library(shiny)\n\nshinyUI(fluidPage(\n h3(\"URL\"),\n verbatimTextOutput(\"urlInfo\"),\n\n h3(\"Session\"),\n verbatimTextOutput("
},
{
"path": "inst/resources/environment.py",
"chars": 9661,
"preview": "#!/usr/bin/env python\n\"\"\"\nEnvironment data class abstraction that is usable as an executable module\n\n```bash\npython -m r"
},
{
"path": "inst/resources/pyproject.py",
"chars": 6191,
"preview": "\"\"\"\nSupport for detecting various information from python projects metadata.\n\nMetadata can only be loaded from static fi"
},
{
"path": "man/accountUsage.Rd",
"chars": 1162,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/usage.R\n\\name{accountUsage}\n\\alias{account"
},
{
"path": "man/accounts.Rd",
"chars": 1427,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/accounts.R\n\\name{accounts}\n\\alias{accounts"
},
{
"path": "man/addAuthorizedUser.Rd",
"chars": 1358,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/auth.R\n\\name{addAuthorizedUser}\n\\alias{add"
},
{
"path": "man/addLinter.Rd",
"chars": 1710,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/lint-framework.R, R/linters.R\n\\name{addLin"
},
{
"path": "man/addServer.Rd",
"chars": 1729,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/servers.R\n\\name{addServer}\n\\alias{addServe"
},
{
"path": "man/appDependencies.Rd",
"chars": 6703,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/appDependencies.R\n\\name{appDependencies}\n\\"
},
{
"path": "man/applicationConfigDir.Rd",
"chars": 484,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/config.R\n\\name{applicationConfigDir}\n\\alia"
},
{
"path": "man/applications.Rd",
"chars": 1953,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/applications.R\n\\name{applications}\n\\alias{"
},
{
"path": "man/configureApp.Rd",
"chars": 1590,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/configureApp.R\n\\name{configureApp}\n\\alias{"
},
{
"path": "man/connectApiUser.Rd",
"chars": 1604,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/accounts.R\n\\name{connectApiUser}\n\\alias{co"
},
{
"path": "man/connectCloudUser.Rd",
"chars": 1036,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/accounts.R\n\\name{connectCloudUser}\n\\alias{"
},
{
"path": "man/connectSPCSUser.Rd",
"chars": 1631,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/accounts.R\n\\name{connectSPCSUser}\n\\alias{c"
},
{
"path": "man/deployAPI.Rd",
"chars": 1251,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/deployAPI.R\n\\name{deployAPI}\n\\alias{deploy"
},
{
"path": "man/deployApp.Rd",
"chars": 15708,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/deployApp.R\n\\name{deployApp}\n\\alias{deploy"
},
{
"path": "man/deployDoc.Rd",
"chars": 1800,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/deployDoc.R\n\\name{deployDoc}\n\\alias{deploy"
},
{
"path": "man/deploySite.Rd",
"chars": 3624,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/deploySite.R\n\\name{deploySite}\n\\alias{depl"
},
{
"path": "man/deployTFModel.Rd",
"chars": 634,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/deployTFModel.R\n\\name{deployTFModel}\n\\alia"
},
{
"path": "man/deployments.Rd",
"chars": 1896,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/deployments.R\n\\name{deployments}\n\\alias{de"
},
{
"path": "man/forgetDeployment.Rd",
"chars": 1416,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/deployments.R\n\\name{forgetDeployment}\n\\ali"
},
{
"path": "man/generateAppName.Rd",
"chars": 1424,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/title.R\n\\name{generateAppName}\n\\alias{gene"
},
{
"path": "man/lint.Rd",
"chars": 688,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/lint.R\n\\name{lint}\n\\alias{lint}\n\\title{Lin"
},
{
"path": "man/linter.Rd",
"chars": 2120,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/lint-framework.R, R/linters.R\n\\name{linter"
},
{
"path": "man/listAccountEnvVars.Rd",
"chars": 1581,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/envvars.R\n\\name{listAccountEnvVars}\n\\alias"
},
{
"path": "man/listBundleFiles.Rd",
"chars": 1168,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/bundleFiles.R\n\\name{listBundleFiles}\n\\alia"
},
{
"path": "man/listDeploymentFiles.Rd",
"chars": 2289,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/bundleFiles.R\n\\name{listDeploymentFiles}\n\\"
},
{
"path": "man/makeLinterMessage.Rd",
"chars": 599,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/lint.R\n\\name{makeLinterMessage}\n\\alias{mak"
},
{
"path": "man/oldApplicationConfigDir.Rd",
"chars": 718,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/configMigrate.R\n\\name{oldApplicationConfig"
},
{
"path": "man/options.Rd",
"chars": 4746,
"preview": "\\name{rsconnectOptions}\n\\alias{rsconnectOptions}\n\n\\title{Package Options}\n\n\\description{\nThe \\pkg{rsconnect} package sup"
},
{
"path": "man/purgeApp.Rd",
"chars": 1003,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/purgeApp.R\n\\name{purgeApp}\n\\alias{purgeApp"
},
{
"path": "man/removeAuthorizedUser.Rd",
"chars": 1074,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/auth.R\n\\name{removeAuthorizedUser}\n\\alias{"
},
{
"path": "man/resendInvitation.Rd",
"chars": 1156,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/auth.R\n\\name{resendInvitation}\n\\alias{rese"
},
{
"path": "man/restartApp.Rd",
"chars": 1032,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/restartApp.R\n\\name{restartApp}\n\\alias{rest"
},
{
"path": "man/rpubsUpload.Rd",
"chars": 2058,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/rpubs.R\n\\name{rpubsUpload}\n\\alias{rpubsUpl"
},
{
"path": "man/rsconnect-package.Rd",
"chars": 1081,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/rsconnect-package.R\n\\docType{package}\n\\nam"
},
{
"path": "man/rsconnectConfigDir.Rd",
"chars": 514,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/config.R\n\\name{rsconnectConfigDir}\n\\alias{"
},
{
"path": "man/rsconnectPackages.Rd",
"chars": 277,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/rsconnect-package.R\n\\name{rsconnectPackage"
},
{
"path": "man/rsconnectProxies.Rd",
"chars": 258,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/rsconnect-package.R\n\\name{rsconnectProxies"
},
{
"path": "man/servers.Rd",
"chars": 907,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/servers.R\n\\name{servers}\n\\alias{servers}\n\\"
},
{
"path": "man/setAccountInfo.Rd",
"chars": 857,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/accounts.R\n\\name{setAccountInfo}\n\\alias{se"
},
{
"path": "man/setProperty.Rd",
"chars": 1245,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/configureApp.R\n\\name{setProperty}\n\\alias{s"
},
{
"path": "man/showInvited.Rd",
"chars": 957,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/auth.R\n\\name{showInvited}\n\\alias{showInvit"
},
{
"path": "man/showLogs.Rd",
"chars": 1673,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/applications.R\n\\name{showLogs}\n\\alias{show"
},
{
"path": "man/showMetrics.Rd",
"chars": 2055,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/usage.R\n\\name{showMetrics}\n\\alias{showMetr"
},
{
"path": "man/showProperties.Rd",
"chars": 887,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/configureApp.R\n\\name{showProperties}\n\\alia"
},
{
"path": "man/showUsage.Rd",
"chars": 1359,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/usage.R\n\\name{showUsage}\n\\alias{showUsage}"
},
{
"path": "man/showUsers.Rd",
"chars": 961,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/auth.R\n\\name{showUsers}\n\\alias{showUsers}\n"
},
{
"path": "man/syncAppMetadata.Rd",
"chars": 561,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/applications.R\n\\name{syncAppMetadata}\n\\ali"
},
{
"path": "man/taskLog.Rd",
"chars": 953,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/tasks.R\n\\name{taskLog}\n\\alias{taskLog}\n\\ti"
},
{
"path": "man/tasks.Rd",
"chars": 1004,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/tasks.R\n\\name{tasks}\n\\alias{tasks}\n\\title{"
},
{
"path": "man/terminateApp.Rd",
"chars": 1051,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/terminateApp.R\n\\name{terminateApp}\n\\alias{"
},
{
"path": "man/unsetProperty.Rd",
"chars": 1171,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/configureApp.R\n\\name{unsetProperty}\n\\alias"
},
{
"path": "man/writeManifest.Rd",
"chars": 6750,
"preview": "% Generated by roxygen2: do not edit by hand\n% Please edit documentation in R/writeManifest.R\n\\name{writeManifest}\n\\alia"
},
{
"path": "revdep/.gitignore",
"chars": 79,
"preview": "checks\nlibrary\nchecks.noindex\nlibrary.noindex\ncloud.noindex\ndata.sqlite\n*.html\n"
},
{
"path": "revdep/README.md",
"chars": 197,
"preview": "# Revdeps\n\n## New problems (1)\n\n|package |version |error |warning |note |\n|:-----------|:-------|:-----|:-------|:--"
},
{
"path": "revdep/cran.md",
"chars": 374,
"preview": "## revdepcheck results\n\nWe checked 24 reverse dependencies, comparing R CMD check results across CRAN and dev versions o"
},
{
"path": "revdep/failures.md",
"chars": 29,
"preview": "*Wow, no problems at all. :)*"
},
{
"path": "revdep/problems.md",
"chars": 623,
"preview": "# Rsconctdply (0.1.3)\n\n* Email: <mailto:schimata@yotabitesllc.com>\n* GitHub mirror: <https://github.com/cran/Rsconctdply"
},
{
"path": "rsconnect.Rproj",
"chars": 458,
"preview": "Version: 1.0\nProjectId: 3d8f30a6-9562-4255-8985-ba9413556e97\n\nRestoreWorkspace: No\nSaveWorkspace: No\nAlwaysSaveHistory: "
},
{
"path": "tests/integration/example-shiny/app.R",
"chars": 77,
"preview": "shinyApp(\n ui = fluidPage(\"Hello\"),\n server = function(input, output) {}\n)\n"
},
{
"path": "tests/integration/setup.R",
"chars": 1170,
"preview": "library(testthat)\nlibrary(rsconnect)\n\n# options(\"rsconnect.httr2\" = FALSE)\n\n# Configure the account() for testing, with "
},
{
"path": "tests/integration/test-deploy.R",
"chars": 472,
"preview": "test_that(\"deploy does not error\", {\n # Also test verbose logging\n expect_true(deployApp(\n \"example-shiny\",\n app"
},
{
"path": "tests/manual/appMode.Rmd",
"chars": 2431,
"preview": "# appMode overrides\n\nThese test deploying one project to multiple targets having different types,\nconfirming `deployApp("
},
{
"path": "tests/manual/dependencies.Rmd",
"chars": 1926,
"preview": "# renv snapshots\n\nThese test the full end-to-end publishing experience with renv.\n\n## Archived package\n\n1. Create a new"
},
{
"path": "tests/manual/deploySite.Rmd",
"chars": 3770,
"preview": "# Sites\n\n## Quarto\n\n### Quarto using R (no additional dependencies)\n\n1. Clone <https://github.com/rstudio/connect-conte"
},
{
"path": "tests/manual/publishing-dialog.Rmd",
"chars": 2026,
"preview": "# Connections\n\n## Remove an account\n\n1. Open Options | Publishing.\n1. Select `colorado.posit.co` then click disconnect.\n"
},
{
"path": "tests/shinyapps-integration/example-shiny/app.R",
"chars": 77,
"preview": "shinyApp(\n ui = fluidPage(\"Hello\"),\n server = function(input, output) {}\n)\n"
},
{
"path": "tests/shinyapps-integration/example-shiny/manifest.json",
"chars": 91728,
"preview": "{\n \"version\": 1,\n \"locale\": \"en_US\",\n \"platform\": \"4.5.2\",\n \"metadata\": {\n \"appmode\": \"shiny\",\n \"primary_rmd\":"
},
{
"path": "tests/shinyapps-integration/setup.R",
"chars": 2029,
"preview": "library(testthat)\nlibrary(rsconnect)\n\n# Skip if shinyapps.io credentials are not available\nshinyapps_name <- Sys.getenv("
},
{
"path": "tests/shinyapps-integration/test-shinyapps-deploy.R",
"chars": 453,
"preview": "test_that(\"can deploy to shinyapps.io and terminate\", {\n app_name <- paste0(\n run_prefix,\n \"-rsconnect-test-\",\n "
},
{
"path": "tests/testthat/_snaps/account-find.md",
"chars": 2181,
"preview": "# validates its arguments\n\n Code\n findAccount(1, NULL)\n Condition\n Error:\n ! `account` must be a si"
},
{
"path": "tests/testthat/_snaps/accounts.md",
"chars": 577,
"preview": "# secrets are hidden from casual inspection\n\n Code\n accountInfo(\"1\")$secret\n Output\n [1] \"SECRET... (red"
},
{
"path": "tests/testthat/_snaps/appDependencies.md",
"chars": 1153,
"preview": "# infers correct packages for each source\n\n Code\n inferRPackageDependencies(simulateMetadata(\"rmd-static\"))\n "
},
{
"path": "tests/testthat/_snaps/appMetadata-quarto.md",
"chars": 760,
"preview": "# quartoInspect requires quarto\n\n Code\n quartoInspect()\n Condition\n Error in `quartoInspect()`:\n ! "
},
{
"path": "tests/testthat/_snaps/appMetadata.md",
"chars": 1717,
"preview": "# validates quarto argument\n\n Code\n appMetadata(dir, c(\"foo.Rmd\"), quarto = 1)\n Condition\n Error in `app"
},
{
"path": "tests/testthat/_snaps/applications.md",
"chars": 159,
"preview": "# syncAppMetadata deletes deployment records if needed\n\n Code\n syncAppMetadata(app)\n Message\n Deleting d"
},
{
"path": "tests/testthat/_snaps/bundle.md",
"chars": 412,
"preview": "# removes renv/packrat activation\n\n Code\n tweakRProfile(path)\n writeLines(readLines(path))\n Output\n "
},
{
"path": "tests/testthat/_snaps/bundleFiles.md",
"chars": 3202,
"preview": "# can read all files from directory\n\n Code\n listDeploymentFiles(dir)\n Condition\n Error:\n ! No conte"
},
{
"path": "tests/testthat/_snaps/bundlePackage.md",
"chars": 1665,
"preview": "# can snapshot deps with renv\n\n Code\n pkgs <- bundlePackages(app_dir)\n Message\n i Capturing R dependenci"
},
{
"path": "tests/testthat/_snaps/bundlePackagePackrat.md",
"chars": 305,
"preview": "# uninstalled packages error\n\n Code\n snapshotPackratDependencies(app)\n Condition\n Error in `addPackratSn"
},
{
"path": "tests/testthat/_snaps/bundlePackageRenv.md",
"chars": 917,
"preview": "# large directories are analyzed\n\n Code\n deps <- snapshotRenvDependencies(app_dir)\n\n# errors when renv::snapshot"
},
{
"path": "tests/testthat/_snaps/bundlePython.md",
"chars": 236,
"preview": "# throws error if environment.py fails\n\n Code\n inferPythonEnv(\".\", pythonPathOrSkip())\n Condition\n Error"
},
{
"path": "tests/testthat/_snaps/client-connect.md",
"chars": 2620,
"preview": "# leading timestamps are stripped\n\n Code\n stripConnectTimestamps(c(\n \"2024/04/24 13:08:04.901698921 [rsc-"
},
{
"path": "tests/testthat/_snaps/cookies.md",
"chars": 395,
"preview": "# Invalid cookies fail parsing\n\n Code\n cookie <- parseCookie(\"x=1; Path=/something/else\", \"/path\")\n Condition"
},
{
"path": "tests/testthat/_snaps/deployApp.md",
"chars": 3013,
"preview": "# appDir must be an existing directory\n\n Code\n deployApp(1)\n Condition\n Error in `deployApp()`:\n ! "
},
{
"path": "tests/testthat/_snaps/deployDoc.md",
"chars": 179,
"preview": "# deployDoc correctly reports bad path\n\n Code\n deployDoc(\"doesntexist.Rmd\")\n Condition\n Error in `deploy"
},
{
"path": "tests/testthat/_snaps/deploymentTarget.md",
"chars": 4722,
"preview": "# errors if no accounts\n\n Code\n findDeploymentTarget()\n Condition\n Error:\n ! No accounts registered"
},
{
"path": "tests/testthat/_snaps/deployments-find.md",
"chars": 1161,
"preview": "# error when no deployments and no accounts\n\n Code\n findDeployment(app, appName = \"placeholder\")\n Condition\n "
},
{
"path": "tests/testthat/_snaps/deployments.md",
"chars": 1211,
"preview": "# addToDeploymentHistory() adds needed new lines\n\n Code\n addToDeploymentHistory(\"path\", list(x = 1))\n write"
},
{
"path": "tests/testthat/_snaps/http-libcurl.md",
"chars": 187,
"preview": "# can trace JSON\n\n Code\n . <- POST_JSON(service, list(), \"\", list(a = 1, b = 2))\n Output\n << {\n << "
},
{
"path": "tests/testthat/_snaps/http.md",
"chars": 2134,
"preview": "# authHeaders() picks correct method based on supplied fields\n\n Code\n str(authHeaders(list(secret = openssl::bas"
},
{
"path": "tests/testthat/_snaps/ide.md",
"chars": 756,
"preview": "# getAppById() fails where expected\n\n Code\n getAppById(\"123\", \"susan\", \"unknown\", \"unknown.com\")\n Condition\n "
},
{
"path": "tests/testthat/_snaps/lint.md",
"chars": 456,
"preview": "# lints give have useful print method\n\n Code\n lint(test_path(\"test-rmd-bad-case\"))\n Output\n ---------\n "
},
{
"path": "tests/testthat/_snaps/linters.md",
"chars": 789,
"preview": "# linter warns about absolute paths and relative paths\n\n Code\n result\n Output\n ---------------------\n "
},
{
"path": "tests/testthat/_snaps/secret.md",
"chars": 203,
"preview": "# print and str obfuscate output\n\n Code\n x <- secret(\"THIS IS MY PASSWORD: foo\")\n x\n Output\n [1] \"T"
},
{
"path": "tests/testthat/_snaps/servers.md",
"chars": 2892,
"preview": "# servers() redacts the certificate\n\n Code\n servers()\n Output\n name "
},
{
"path": "tests/testthat/_snaps/writeManifest.md",
"chars": 235,
"preview": "# Deploying a Quarto project without Quarto is an error\n\n Code\n makeManifest(appDir)\n Condition\n Error i"
},
{
"path": "tests/testthat/certs/example.com.pem",
"chars": 1517,
"preview": "-----BEGIN CERTIFICATE-----\nMIIEMzCCApugAwIBAgIQdirX302cRVUmWALMQvTOWTANBgkqhkiG9w0BAQsFADB7\nMR4wHAYDVQQKExVta2NlcnQgZGV"
},
{
"path": "tests/testthat/certs/invalid.crt",
"chars": 463,
"preview": "Do you remember still the falling stars\nthat like swift horses through the heavens raced\nand suddenly leaped across the "
},
{
"path": "tests/testthat/certs/localhost.pem",
"chars": 1517,
"preview": "-----BEGIN CERTIFICATE-----\nMIIEMjCCApqgAwIBAgIRAPKPR6q/usdid1xZCt4KDTswDQYJKoZIhvcNAQELBQAw\nezEeMBwGA1UEChMVbWtjZXJ0IGR"
},
{
"path": "tests/testthat/certs/sample.crt",
"chars": 1342,
"preview": "-----BEGIN CERTIFICATE-----\nMIIDszCCApugAwIBAgIJAJhKUgOoHiBhMA0GCSqGSIb3DQEBCwUAMIGAMQswCQYD\nVQQGEwJVUzELMAkGA1UECAwCV0E"
},
{
"path": "tests/testthat/certs/two-cas.crt",
"chars": 3140,
"preview": "Amazon Root CA 1\n================\n-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w"
},
{
"path": "tests/testthat/helper-content.R",
"chars": 5196,
"preview": "# A basic Quarto website that uses Python.\nquarto_website_py_files <- list(\n \"_quarto.yml\" = c(\n \"project:\",\n \" "
},
{
"path": "tests/testthat/helper-http.R",
"chars": 3418,
"preview": "cache <- new_environment()\n\nhttpbin_service <- function() {\n app <- env_cache(\n cache,\n \"test_app\",\n webfakes:"
},
{
"path": "tests/testthat/helper-paths.R",
"chars": 562,
"preview": "pythonPathOrSkip <- function() {\n skip_if_not_installed(\"reticulate\")\n\n if (!reticulate::py_available(TRUE)) {\n ski"
},
{
"path": "tests/testthat/helper.R",
"chars": 3257,
"preview": "showDcf <- function(df) {\n write.dcf(df, stdout())\n invisible()\n}\n\n# Create and use a directory as temporary replaceme"
},
{
"path": "tests/testthat/multibyte-characters/app.R",
"chars": 544,
"preview": "# 定义用户界面\nfluidPage(\n # 标题\n titlePanel(\"麻麻再也不用担心我的Shiny应用不能显示中文了\"),\n\n # 侧边栏布局\n sidebarLayout(\n sidebarPanel(\n "
},
{
"path": "tests/testthat/packages/latin1package/DESCRIPTION",
"chars": 237,
"preview": "Package: latin1package\nType: Package\nTitle: Provides some funky characters.\nVersion: 0.1.0\nAuthor: Jens Frhling\nMaintain"
}
]
// ... and 83 more files (download for full content)
About this extraction
This page contains the full source code of the rstudio/rsconnect GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 283 files (1.2 MB), approximately 425.4k tokens, and a symbol index with 20 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.