Skip to contents

Working with CDN Assets

This vignette covers artutils path functions for constructing CDN URLs and managing asset workflows. These functions are pure string operations - no database queries, no network calls - making them safe to use in loops and vectorized operations.

Understanding the CDN Architecture

Artalytics uses DigitalOcean Spaces (S3-compatible) for asset storage with three buckets:

art-public/     → Public thumbnails, site images (no auth required)
art-data/       → Processed assets (frames, graphs, variants)
art-vault/      → Original uploads, canvas files (restricted access)

Bucket Security Model

Bucket Access Contents URL Type
art-public Public read Thumbnails, avatars, certificates Direct HTTPS
art-data Private Replay frames, analytics graphs Direct HTTPS
art-vault Private Canvas files, original uploads Presigned URLs

All artutils path functions return direct HTTPS URLs for art-public and art-data. For art-vault assets, use artcore’s presigning functions.

Asset Types & Path Functions

Artist Assets

library(artutils)

artist <- "746b8207-72f5-4ab6-8d19-a91d03daec3d"

# Avatar/thumbnail (art-public bucket)
avatar_url <- path_artist_thumb(artist)
cat("Avatar:", avatar_url, "\n")
# Returns: https://art-public.nyc3.cdn.digitaloceanspaces.com/artists/{uuid}/thumb.jpeg

# Path construction is pure - works in data.table operations
artists_dt <- data.table::data.table(
  artist_uuid = c("uuid1", "uuid2", "uuid3"),
  artist_name = c("Alice", "Bob", "Carol")
)
artists_dt[, avatar_url := path_artist_thumb(artist_uuid)]
print(artists_dt)

Artwork Thumbnails & Images

artist <- "746b8207-72f5-4ab6-8d19-a91d03daec3d"
artwork <- "99a61148-1d3b-4340-8cf6-92ad26046b0f"

# Thumbnail for gallery grids (art-public)
thumb_url <- path_artwork_thumb(artist, artwork)
cat("Thumbnail:", thumb_url, "\n")

# Full-size main image (art-public)
main_url <- path_art_main_image(artist, artwork)
cat("Main image:", main_url, "\n")

# Original canvas file (art-vault - use presigned URL)
canvas_url <- path_art_canvas(artist, artwork)
cat("Canvas file:", canvas_url, "\n")
# This returns a path - actual download requires presigning via artcore

Replay Player Assets

The replay player displays a time-lapse sequence of frames and corresponding analytics graphs.

artist <- "746b8207-72f5-4ab6-8d19-a91d03daec3d"
artwork <- "99a61148-1d3b-4340-8cf6-92ad26046b0f"

# Frame sequence (art-data bucket)
# Frames are numbered 1, 2, 3, ..., N
frame_1_url <- path_replay_frame(artist, artwork, which = 1)
frame_50_url <- path_replay_frame(artist, artwork, which = 50)

cat("Frame 1:", frame_1_url, "\n")
cat("Frame 50:", frame_50_url, "\n")

# Analytics graphs (art-data bucket)
graph_1_url <- path_replay_graph(artist, artwork, which = 1)
cat("Graph 1:", graph_1_url, "\n")

# Prefix for listing all frames (used with artcore::cdn_list_keys)
frames_prefix <- prefix_replay_frames(artist, artwork)
cat("Frames prefix:", frames_prefix, "\n")
# Returns: replays/{artist}/{artwork}/frames/

Certificate Assets

Certificates of authenticity are issued as both PDF and JPEG.

artist <- "746b8207-72f5-4ab6-8d19-a91d03daec3d"
artwork <- "99a61148-1d3b-4340-8cf6-92ad26046b0f"
cert_id <- "cert-20240115-abc123"

# PDF certificate for download
pdf_url <- path_artwork_cert(artist, artwork, cert_id, type = "pdf")
cat("Certificate PDF:", pdf_url, "\n")

# JPEG certificate for preview/display
jpeg_url <- path_artwork_cert(artist, artwork, cert_id, type = "jpeg")
cat("Certificate JPEG:", jpeg_url, "\n")

# Certificate thumbnail (smaller JPEG)
cert_thumb_url <- path_artwork_cert_thumb(artist, artwork, cert_id)
cat("Certificate thumb:", cert_thumb_url, "\n")

# Certificate frame asset (used in certificate generation)
frame_asset_url <- path_cert_frame_asset(artist, artwork, which = 1)
cat("Cert frame 1:", frame_asset_url, "\n")

Gallery variants are alternative crops, formats, or styles of an artwork.

artist <- "746b8207-72f5-4ab6-8d19-a91d03daec3d"
artwork <- "99a61148-1d3b-4340-8cf6-92ad26046b0f"

# Specific variant file
variant_url <- path_gallery_asset(artist, artwork, "square-crop.jpeg")
cat("Square crop:", variant_url, "\n")

# Common variant patterns
portrait_url <- path_gallery_asset(artist, artwork, "portrait.jpeg")
landscape_url <- path_gallery_asset(artist, artwork, "landscape.jpeg")
social_url <- path_gallery_asset(artist, artwork, "social-1200x630.jpeg")

# Prefix for listing all variants
gallery_prefix <- prefix_gallery(artist, artwork)
cat("Gallery prefix:", gallery_prefix, "\n")

Workflow 1: Building a Replay Player

The replay player needs frame images and corresponding analytics graphs for each frame.

build_replay_player_data <- function(artist_uuid, artwork_uuid) {
  # Step 1: Get frame count from database
  frames <- get_frame_analytics(artist_uuid, artwork_uuid)
  n_frames <- nrow(frames)

  if (n_frames == 0) {
    return(list(error = "no_frames"))
  }

  # Step 2: Generate all frame URLs (vectorized)
  frame_numbers <- seq_len(n_frames)

  frame_urls <- sapply(frame_numbers, function(i) {
    path_replay_frame(artist_uuid, artwork_uuid, which = i)
  })

  graph_urls <- sapply(frame_numbers, function(i) {
    path_replay_graph(artist_uuid, artwork_uuid, which = i)
  })

  # Step 3: Combine with frame analytics
  player_data <- data.table::data.table(
    frame = frame_numbers,
    elapsed_hours = frames$elapsed_hours,
    cumulative_strokes = frames$cumulative_strokes,
    unique_colors = frames$unique_colors,
    estimated_bpm = frames$estimated_bpm,
    technique_phase = frames$technique_phase,
    frame_url = frame_urls,
    graph_url = graph_urls
  )

  # Step 4: Calculate playback metadata
  total_duration_secs <- max(frames$elapsed_hours) * 3600
  playback_speed_options <- c(0.5, 1, 2, 4, 8)  # Relative to real-time

  list(
    frames = player_data,
    metadata = list(
      n_frames = n_frames,
      total_duration_hours = max(frames$elapsed_hours),
      total_duration_secs = total_duration_secs,
      avg_frame_duration = total_duration_secs / n_frames,
      playback_speeds = playback_speed_options,
      # Prefixes for batch operations
      frames_prefix = prefix_replay_frames(artist_uuid, artwork_uuid),
      graphs_prefix = prefix_replay_graphs(artist_uuid, artwork_uuid)
    )
  )
}

# Use it
player <- build_replay_player_data(
  "746b8207-72f5-4ab6-8d19-a91d03daec3d",
  "99a61148-1d3b-4340-8cf6-92ad26046b0f"
)

if (is.null(player$error)) {
  cat("Replay player ready\n")
  cat("Frames:", player$metadata$n_frames, "\n")
  cat("Duration:", round(player$metadata$total_duration_hours, 2), "hours\n")
  cat("First frame:", player$frames$frame_url[1], "\n")
  cat("Last frame:", player$frames$frame_url[nrow(player$frames)], "\n")
}

Build a responsive gallery grid with lazy loading and multiple image sizes.

build_gallery_grid <- function(artist_uuid, limit = 20, offset = 0) {
  cn <- artcore::dbc()
  on.exit(artcore::dbd(cn))

  # Step 1: Get artworks for grid
  recent <- get_artist_recent_works(artist_uuid, limit = limit, cn = cn)

  if (nrow(recent) == 0) {
    return(list(items = list(), total = 0))
  }

  # Step 2: Enrich with multiple image sizes
  # Vectorized path generation
  recent[, `:=`(
    # Thumbnail for initial grid load (small, fast)
    thumb_url = path_artwork_thumb(artist_uuid, art_uuid),

    # Main image for lightbox/detail view (large)
    main_url = path_art_main_image(artist_uuid, art_uuid),

    # Social sharing image (optimized dimensions)
    social_url = path_gallery_asset(
      artist_uuid, art_uuid, "social-1200x630.jpeg"
    ),

    # Square crop for uniform grid
    square_url = path_gallery_asset(
      artist_uuid, art_uuid, "square-crop.jpeg"
    )
  )]

  # Step 3: Build lazy loading metadata
  recent[, `:=`(
    # Preload thumbnail, lazy load others
    loading_strategy = "lazy",
    thumbnail_priority = ifelse(
      seq_len(.N) <= 6,  # First 6 items
      "high",
      "low"
    )
  )]

  list(
    items = recent,
    total = nrow(recent),
    grid_config = list(
      columns = 3,  # Responsive: 1 on mobile, 2 on tablet, 3 on desktop
      gap = "1rem",
      aspect_ratio = "1 / 1",  # Square grid cells
      lazy_threshold = 6  # Preload first 6, lazy load rest
    )
  )
}

# Use it in Shiny
output$gallery_grid <- renderUI({
  grid <- build_gallery_grid(rv$artist_uuid, limit = 20)

  # Map to HTML img tags
  img_tags <- lapply(seq_len(nrow(grid$items)), function(i) {
    item <- grid$items[i, ]

    tags$div(
      class = "grid-item",
      tags$img(
        src = item$thumb_url,
        alt = item$art_title,
        loading = if (i <= grid$grid_config$lazy_threshold) "eager" else "lazy",
        `data-full-url` = item$main_url,
        onclick = paste0("openLightbox('", item$art_uuid, "')")
      ),
      tags$p(class = "caption", item$art_title)
    )
  })

  tags$div(class = "gallery-grid", img_tags)
})

Workflow 3: Certificate Download Handler

Provide certificate downloads with proper content types and filenames.

# In Shiny server
output$download_certificate <- downloadHandler(
  filename = function() {
    # Get artwork title for filename
    appdata <- rv$appdata
    title_slug <- stringr::str_replace_all(
      tolower(appdata$artwork$info$basic$art_title),
      "[^a-z0-9]+",
      "-"
    )
    cert_id <- appdata$certificate$cert_id

    paste0("certificate-", title_slug, "-", cert_id, ".pdf")
  },
  content = function(file) {
    # Get certificate URL
    cert_url <- path_artwork_cert(
      rv$artist_uuid,
      rv$artwork_uuid,
      rv$appdata$certificate$cert_id,
      type = "pdf"
    )

    # Download from CDN to temp file, then serve
    download.file(cert_url, file, mode = "wb", quiet = TRUE)
  },
  contentType = "application/pdf"
)

# JPEG version for preview
output$certificate_preview <- renderImage({
  req(rv$appdata$certificate$cert_id)

  jpeg_url <- path_artwork_cert(
    rv$artist_uuid,
    rv$artwork_uuid,
    rv$appdata$certificate$cert_id,
    type = "jpeg"
  )

  # Shiny can render URLs directly
  list(src = jpeg_url, contentType = "image/jpeg", alt = "Certificate preview")
}, deleteFile = FALSE)

Workflow 4: Batch Asset Verification

Verify that expected assets exist on CDN before displaying UI elements.

verify_artwork_assets <- function(artist_uuid, artwork_uuid) {
  # This requires artcore functions for actual existence checks
  # artutils only provides path construction

  required_assets <- list(
    thumbnail = path_artwork_thumb(artist_uuid, artwork_uuid),
    main_image = path_art_main_image(artist_uuid, artwork_uuid),
    replay_prefix = prefix_replay_frames(artist_uuid, artwork_uuid)
  )

  verification_results <- list()

  for (asset_name in names(required_assets)) {
    asset_path <- required_assets[[asset_name]]

    # Determine bucket and key from URL
    if (grepl("art-public", asset_path)) {
      bucket <- "art-public"
      key <- sub(".*art-public.*/", "", asset_path)

      exists <- artcore::has_object(bucket, key)
      verification_results[[asset_name]] <- exists

    } else if (grepl("art-data", asset_path)) {
      bucket <- "art-data"
      key <- sub(".*art-data.*/", "", asset_path)

      # For prefixes, check if any objects exist
      if (grepl("/$", key)) {
        exists <- artcore::has_prefix(bucket, key)
      } else {
        exists <- artcore::has_object(bucket, key)
      }
      verification_results[[asset_name]] <- exists
    }
  }

  # Determine what UI features can be enabled
  list(
    results = verification_results,
    features_available = list(
      can_show_thumbnail = verification_results$thumbnail,
      can_show_main_image = verification_results$main_image,
      can_enable_replay = verification_results$replay_prefix
    )
  )
}

# Use in Shiny reactive
artwork_features <- reactive({
  req(rv$artist_uuid, rv$artwork_uuid)
  verify_artwork_assets(rv$artist_uuid, rv$artwork_uuid)
})

# Conditionally render UI
output$replay_button <- renderUI({
  if (artwork_features()$features_available$can_enable_replay) {
    actionButton("show_replay", "Play Timelapse", icon = icon("play"))
  } else {
    tags$p("Replay not available for this artwork")
  }
})

Common Patterns & Best Practices

Pattern: Vectorized Path Generation

# GOOD - Vectorized (fast)
artworks_dt <- get_artist_recent_works(artist_uuid, limit = 50)
artworks_dt[, thumb_url := path_artwork_thumb(artist_uuid, art_uuid)]
artworks_dt[, main_url := path_art_main_image(artist_uuid, art_uuid)]

# AVOID - Loop (slow and verbose)
thumb_urls <- character(nrow(artworks_dt))
for (i in seq_len(nrow(artworks_dt))) {
  thumb_urls[i] <- path_artwork_thumb(
    artworks_dt$artist_uuid[i],
    artworks_dt$art_uuid[i]
  )
}

Pattern: Conditional Asset Loading

# Load different assets based on viewport
load_artwork_image <- function(artist, artwork, viewport_width) {
  if (viewport_width < 768) {
    # Mobile - use thumbnail
    path_artwork_thumb(artist, artwork)
  } else if (viewport_width < 1200) {
    # Tablet - use square crop
    path_gallery_asset(artist, artwork, "square-crop.jpeg")
  } else {
    # Desktop - use main image
    path_art_main_image(artist, artwork)
  }
}

Pattern: Fallback Images

get_avatar_with_fallback <- function(artist_uuid) {
  primary <- path_artist_thumb(artist_uuid)

  # Check if exists (requires artcore)
  if (artcore::has_object("art-public", extract_key_from_url(primary))) {
    return(primary)
  }

  # Fallback to default avatar
  path_image_asset("default-avatar.jpeg")
}

Performance Considerations

Path Functions Are Free

All artutils path functions are pure string operations with negligible cost:

# Benchmark: 1 million path constructions
system.time({
  for (i in seq_len(1e6)) {
    path_artwork_thumb("746b8207-72f5-4ab6-8d19-a91d03daec3d",
                       "99a61148-1d3b-4340-8cf6-92ad26046b0f")
  }
})
# Typical result: ~0.5 seconds for 1M calls
# = 0.0000005 seconds per call

Compare to database queries: - Database query: 5-50ms per call - Path function: 0.0005ms per call - 10,000x faster

Use Paths Liberally

Because path functions are so cheap, you can: - Generate paths in tight loops without concern - Add multiple path columns to data.tables freely - Recalculate paths on every render (no caching needed)

Next Steps