Skip to content

Download & Export ​

Overview ​

  1. Single Asset Download: The downloadAsset Vuex action routes through pages/get_assets.vue, which calls GET digital-assets/object-download to generate a signed URL, then delivers the file via StreamSaver streaming or a browser anchor redirect. This ensures auth headers are applied even for assets stored privately in S3.

  2. Bulk Zip Download: The downloadMultipleFiles Vuex action posts a list of asset IDs and folder IDs to generate-zip-data. The backend queues a Cloudflare Worker job and returns a streaming endpoint. The frontend fetches that endpoint and pipes the response to a writable stream via $streamSaver, delivering the zip without buffering the entire archive in memory. For very large archives, the backend omits the stream payload and sends a download link by email instead.

  3. Format Conversion: The FileConversionDialog lets users select a different format (e.g., PNG to JPG, MP4 to MOV) for a supported asset. Conversion is triggered via POST digital-assets/convert-asset; the job runs asynchronously on the backend and the converted file is delivered to the user by email.

  4. Resolution Conversion: From the same FileConversionDialog, users can select a reduced resolution for image assets. This calls POST /convert/resolution on the Nuxt server middleware, which resizes the image synchronously and returns a blob that is saved immediately with file-saver.

Architecture ​

All download operations are orchestrated through the dam Vuex store (store/dam.js). The store exposes four actions — downloadAsset, downloadFile, downloadMultipleFiles, and downloadMultipleSharedFiles — covering single authenticated asset download, streaming blob with progress tracking, bulk zip for workspace users, and bulk zip for public share links.

The downloadIndicator state object tracks in-flight downloads by ID, giving any component access to download name, progress percentage, loaded bytes, and a cancellation token without prop drilling. Components listen to EventBus events (download-file, download-all-folder, download-search-folder) to update their own download-state UI.

The FileConversionDialog component handles format and resolution selection independently of the Vuex download actions. Format conversion is an async backend job; the component only fires the API call and shows a success snackbar. Resolution conversion happens in the browser: the Nuxt server middleware resizes the image using Sharp and returns a binary blob, which file-saver writes to disk.

StreamSaver is initialized as a client-only plugin (plugins/streamsaver.client.js) and injected as this.$streamSaver. It uses a service worker to serve the file as a native browser download through the WritableStream API, bypassing the memory ceiling of URL.createObjectURL for large files.

File Structure ​

JavaScript Files (.js) ​

  • store/dam.js — Download Vuex actions and download-indicator state

    • downloadIndicator state tracks in-flight downloads as { files: { [id]: { name, progress, loaded, total, downloading, source } } }
    • downloadFile action routes to downloadAsset for single files or streams a blob with onDownloadProgress for multi-file cases
    • downloadAsset action generates a signed URL via the /get_assets proxy page and streams the response via StreamSaver
    • downloadMultipleFiles action posts to generate-zip-data, streams the zip via the Cloudflare Worker, or falls back to email
    • downloadMultipleSharedFiles action uses share-zip-data for the same flow on public share links
  • plugins/streamsaver.client.js — StreamSaver client-only plugin

    • Wraps StreamSaver.js for browser-native streaming downloads
    • Injected as this.$streamSaver.createWriteStream(filename) returning a WritableStream
    • Requires the StreamSaver service worker to be registered for cross-origin support
  • api/index.js — Express server middleware (Nuxt custom server routes)

    • POST /convert/resolution — synchronous image resize proxy
    • Receives { w, h, s, q, m } (width, height, source URL, quality, MIME type)
    • Returns the resized binary blob with a ext response header for filename construction

Vue Component Files (.vue) ​

  • components/dam/Dialogs/FileConversionDialog.vue — Download, format, and resolution dialog

    • Renders a format v-select populated from the downloadableFormats prop (original format always included)
    • Renders a resolution v-select from the otherResolutions prop (disabled when no MimeType in file metadata)
    • Format change → POST digital-assets/convert-asset (async, email delivery)
    • Resolution change → POST /convert/resolution (synchronous, immediate file-saver download)
    • Emits download-org when format or resolution matches the original, delegating to the parent
  • components/dam/BulkActionBar.vue — Contextual bulk action toolbar

    • Download button shown when canShareDownload or forceShowDownload is true
    • Emits a download event; the parent component dispatches the Vuex action
    • Displays a spinner and disables the button while downloading prop is true
  • pages/get_assets.vue — Asset download proxy page

    • Protected by onlyAuth middleware to ensure the auth token is present for the backend call
    • Calls GET digital-assets/object-download with asset ID, type, and workspace params
    • On success: issues a redirect(303, signedUrl) — browser follows the signed S3 URL directly
    • On error: calls error(e) to render the Nuxt error page

SVG Icon Components ​

  • components/svg/CollageDownloadIcon.vue — Download icon used in the bulk action bar and asset detail action menus

FileConversionDialog Component ​

File: components/dam/Dialogs/FileConversionDialog.vue

Modal dialog for selecting download format and resolution before initiating a download. Handles the format/resolution logic locally and delegates the actual download or conversion call to Vuex or the server middleware.

Features ​

  • Skeleton loading state while dialog data initializes
  • Format select populated from downloadableFormats prop with the original format always present
  • Resolution select populated from otherResolutions prop, disabled if file.file_meta_data.MimeType is absent
  • Format conversion gated behind subscription_features.asset_conversion.enable with an upgrade tooltip
  • Progress spinner on the Download button while downloading or converting is true
  • Amplitude tracking on both download and conversion events

Props ​

javascript
{
  dialog: {
    type: Boolean,
    required: true         // controls v-model on the underlying v-dialog
  },
  downloadableFormats: {
    type: Array,
    required: true         // array of MIME type strings, e.g. ['image/png', 'image/jpeg']
  },
  otherResolutions: {
    type: Array,
    required: true         // array of { id, name, width, height, quality }
  },
  file: {
    type: Object,
    required: true         // asset object: { id, file_type, display_file, display_file_name, file_meta_data, type }
  }
}

Events ​

javascript
{
  'update:dialog': (Boolean) => {},   // two-way binding; use :dialog.sync="showDialog"
  'close': () => {},                  // emitted when the X button is clicked
  'download-org': () => {},           // emitted when format/resolution matches original — parent handles actual download
}

Methods ​

javascript
{
  handleAction()              // decides between handleDownload() or handleConvertFile() based on selectedFormat
  handleConvertFile(format)   // POST digital-assets/convert-asset; shows snackbar on success
  handleDownload(resolution)  // POST /convert/resolution; saves blob via FileSaver; records history
  recordDownloadHistory(extra) // POST digital-assets/dashboard/file-download-history + Amplitude events
  onFormatChange(format)      // resets selectedResolution to 'original' when format changes
  onResolutionChange(id)      // resets selectedFormat to file.file_type when resolution changes
  extension(value)            // normalizes extension to lowercase for consistent Amplitude tracking
}

Usage ​

vue
<template>
  <FileConversionDialog
    :dialog.sync="showDownloadDialog"
    :downloadable-formats="asset.downloadable_formats"
    :other-resolutions="asset.other_resolutions"
    :file="asset"
    @close="showDownloadDialog = false"
    @download-org="downloadOriginal"
  />
</template>

<script>
export default {
  components: {
    FileConversionDialog: () =>
      import('~/components/dam/Dialogs/FileConversionDialog.vue'),
  },
  data() {
    return {
      showDownloadDialog: false,
    }
  },
  methods: {
    downloadOriginal() {
      this.$store.dispatch('dam/downloadAsset', {
        attachment_type: 'Digital Assets',
        assets_id: this.asset.id,
        workspace_id: this.$route.params.workspace_id,
        download_name: this.asset.display_file_name,
      })
    },
  },
}
</script>

BulkActionBar Component ​

File: components/dam/BulkActionBar.vue

Floating toolbar that appears when assets are selected. Emits a download event upward; the parent is responsible for dispatching the Vuex action and managing the loading state.

Features ​

  • Download button shown when canShareDownload or forceShowDownload is true
  • Independent showDownload prop controls visibility separately from the permission props
  • Loading state driven by downloading prop — shows spinner and disables the button
  • Responsive: action items collapse into a dropdown menu on narrow viewports

Props ​

javascript
{
  downloading: {
    type: Boolean,
    default: false         // true while bulk download is in progress
  },
  showDownload: {
    type: Boolean,
    default: true
  },
  canShareDownload: {
    type: Boolean,
    default: false         // derived from workspace permission check
  },
  forceShowDownload: {
    type: Boolean,
    default: false         // show download button regardless of canShareDownload
  },
  downloadOnly: {
    type: Boolean,
    default: false         // hides all non-download actions (tags, custom fields, etc.)
  }
}

Events ​

javascript
{
  'download': () => {}    // emitted when the download button is clicked
}

Methods ​

javascript
{
  // BulkActionBar is display-only; all logic lives in the parent via @download handler
}

Usage ​

vue
<template>
  <BulkActionBar
    :downloading="bulkDownloading"
    :show-download="activeSelection.length > 0"
    :can-share-download="canDownload"
    @download="triggerBulkDownload"
  />
</template>

<script>
export default {
  components: {
    BulkActionBar: () => import('~/components/dam/BulkActionBar.vue'),
  },
  data() {
    return { bulkDownloading: false }
  },
  methods: {
    async triggerBulkDownload() {
      this.bulkDownloading = true
      try {
        await this.$store.dispatch('dam/downloadMultipleFiles', {
          files: this.activeSelection.map((a) => a.id),
          folders: [],
          download_name: 'selected-assets',
        })
      } catch (e) {
        this.$snackbar.error(this.$getErrorMessage(e))
      } finally {
        this.bulkDownloading = false
      }
    },
  },
}
</script>

Workflows ​

Single Asset Download ​

1. User clicks Download on an asset
   Component: asset detail page or tile context menu
   Dispatches: dam/downloadFile({ id, url, name, file_type, workspace_id })
   ↓
2. downloadFile checks stateFiles[id].downloading
   If already downloading → bail out (idempotent)
   Single file, not useModernDownload (default):
   Delegates to: dam/downloadAsset({ assets_id, from, workspace_id, download_name })
   ↓
3. downloadAsset builds the proxy URL
   URL: window.location.origin + '/get_assets?' + queryString
   Params: { attachment_type, assets_id, from, workspace_id, url_workspace_id }
   ↓
4. pages/get_assets.vue handles the navigation (middleware: onlyAuth)
   GET digital-assets/object-download?<params>
   Response: signed S3 URL string
   redirect(303, signedUrl)
   ↓
5. If download_name provided:
   $streamSaver.createWriteStream(download_name)
   response.body.pipeTo(fileStream) or manual pump via reader/writer
   Else:
   Browser follows 303 redirect to signed URL — native browser download
   ↓
6. EventBus.$emit('download-file', assets_id)
   EventBus.$emit('download-file-collage', assets_id)
   EventBus.$emit('download-file-search')
   Listening components update their download-count display

Bulk Zip Download ​

1. User selects multiple assets and clicks Download
   BulkActionBar emits 'download' event
   Parent dispatches: dam/downloadMultipleFiles({
     files: [assetId, ...],
     folders: [folderId, ...],
     download_name: 'my-folder'
   })
   ↓
2. EventBus.$emit('download-all-folder', true)
   EventBus.$emit('download-search-folder', folders)
   Components enter loading state
   ↓
3. POST generate-zip-data
   Body: { workspace_id, assets_ids, category_ids }
   Response A (streaming available): { data: { zipFileName, ...workerPayload } }
   Response B (archive too large): { data: null } → email path
   ↓
4. Response A — stream path:
   zipUrl = $config.ZIP_DOWNLOAD_URL  (Cloudflare Worker origin)
   filename = response.data.zipFileName  OR  '<download_name>.zip'
   POST <zipUrl>download
   Body: { payload: response.data }
   ↓
5. Stream via StreamSaver:
   const fileStream = this.$streamSaver.createWriteStream(filename)
   writer = fileStream.getWriter()
   reader = res.body.getReader()
   Pump: read chunk → write chunk → repeat until done
   writer.close()
   ↓
6. EventBus.$emit('download-all-folder', false)
   Return { status: 'success', filename }

   Response B — email path:
   $snackbar.success('You will be receiving an email with the zip file download link shortly!')
   EventBus.$emit('download-all-folder', false)
   Return { status: 'email-notification' }

Format Conversion ​

1. User opens FileConversionDialog on an asset
   Dialog initializes: selectedFormat = file.file_type, selectedResolution = 'original'
   Parent passes: downloadableFormats, otherResolutions, file
   ↓
2. User selects a different format from the Format select
   onFormatChange() resets selectedResolution to 'original'
   ↓
3. User clicks Download
   handleAction() sees selectedFormat !== file.file_type
   Calls: handleConvertFile(selectedFormat)
   Subscription gate: !isAssetConversionAllowed → return early (tooltip shown in UI)
   ↓
4. POST digital-assets/convert-asset
   Body: { workspace_id, asset_id, asset_type: file.file_type, convert_type: format }
   Response: { message: 'Conversion started. You will receive an email...' }
   ↓
5. $snackbar.success(message)
   Amplitude: dispatchAnalytics + trackActivity with { from_format, to_format, media_type }
   closeDialog()
   Backend processes job asynchronously and delivers converted file via email

Resolution Conversion ​

1. User opens FileConversionDialog on an image asset
   Original format pre-selected; selectedResolution defaults to 'original'
   ↓
2. User selects a lower resolution from the Resolution select
   onResolutionChange() resets selectedFormat to file.file_type
   Resolution object contains: { id, name, width, height, quality }
   ↓
3. User clicks Download
   handleAction() sees selectedFormat === file.file_type
   Calls: handleDownload(selectedResolution)
   ↓
4. Short-circuit checks:
   isAudio or isVideo → $emit('download-org'), closeDialog()  (resize not applicable)
   quality === 100     → $emit('download-org'), closeDialog()  (original resolution)
   !resolution         → return
   ↓
5. POST /convert/resolution  (Nuxt server middleware, not backend API)
   Body: { w: width, h: height, s: display_file_url, q: quality, m: mimeType }
   responseType: 'blob'
   Returns binary image data + response headers { ext }
   ↓
6. FileSaver.saveAs(
     new Blob([response.data], { type: mimeType }),
     `${display_file_name}_${width}x${height}.${ext}`
   )
   ↓
7. recordDownloadHistory({
     download_type: 'resolution',
     download_format: ext,
     resolution_label: resolution.id,
     resolution_width: width,
     resolution_height: height,
     quality
   })
   POST digital-assets/dashboard/file-download-history
   Amplitude: dispatchAnalytics + trackActivity
   closeDialog()

API Integration ​

MethodEndpointPurpose
GETdigital-assets/object-downloadGenerate signed S3 URL for a single asset
POSTgenerate-zip-dataQueue bulk zip job; returns stream payload or triggers email
POSTshare-zip-dataSame zip flow for public share links
POSTdigital-assets/convert-assetQueue async format conversion job
POST/convert/resolutionSynchronous image resize on Nuxt server middleware
POSTdigital-assets/dashboard/file-download-historyRecord a download event for analytics

generate-zip-data Request / Response ​

json
// POST generate-zip-data
{
  "workspace_id": "42",
  "assets_ids": [101, 102, 103],
  "category_ids": [5]
}

// Response A — streaming available:
{
  "status": true,
  "data": {
    "zipFileName": "brand-assets.zip",
    "files": [
      { "key": "workspaces/42/asset-101.jpg", "name": "logo.jpg" }
    ],
    "bucket": "collage-assets",
    "region": "us-east-1"
  }
}

// Response B — archive too large, email delivery:
{
  "status": true,
  "data": null
}

digital-assets/convert-asset Request / Response ​

json
// POST digital-assets/convert-asset
{
  "workspace_id": "42",
  "asset_id": 101,
  "asset_type": "image/png",
  "convert_type": "image/jpeg"
}

// Response:
{
  "status": true,
  "message": "Conversion started. You will receive an email with the download link."
}

/convert/resolution Request / Response ​

json
// POST /convert/resolution  (Nuxt server — not the backend API)
{
  "w": 1920,
  "h": 1080,
  "s": "https://cdn.example.com/workspace/42/asset-101.png",
  "q": 80,
  "m": "image/jpeg"
}

// Response: binary blob
// Response headers:
//   Content-Type: image/jpeg
//   ext: jpg

Component Integration ​

vue
<template>
  <div>
    <BulkActionBar
      :downloading="bulkDownloading"
      :can-share-download="canDownload"
      :show-download="activeSelection.length > 0"
      @download="triggerBulkDownload"
    />

    <FileConversionDialog
      v-if="showConvertDialog"
      :dialog.sync="showConvertDialog"
      :downloadable-formats="selectedAsset.downloadable_formats"
      :other-resolutions="selectedAsset.other_resolutions"
      :file="selectedAsset"
      @close="showConvertDialog = false"
      @download-org="downloadOriginal(selectedAsset)"
    />
  </div>
</template>

<script>
import { EventBus } from '~/plugins/event-bus'

export default {
  components: {
    BulkActionBar: () => import('~/components/dam/BulkActionBar.vue'),
    FileConversionDialog: () =>
      import('~/components/dam/Dialogs/FileConversionDialog.vue'),
  },
  data() {
    return {
      bulkDownloading: false,
      showConvertDialog: false,
      selectedAsset: null,
    }
  },
  computed: {
    activeSelection() {
      return this.$store.state.dam.activeSelection
    },
    canDownload() {
      return this.$auth.user?.workspace_permissions?.download ?? false
    },
  },
  mounted() {
    EventBus.$on('download-all-folder', (loading) => {
      this.bulkDownloading = loading
    })
  },
  beforeDestroy() {
    EventBus.$off('download-all-folder')
  },
  methods: {
    async triggerBulkDownload() {
      try {
        await this.$store.dispatch('dam/downloadMultipleFiles', {
          files: this.activeSelection.map((a) => a.id),
          folders: [],
          download_name: 'selected-assets',
        })
      } catch (e) {
        this.$snackbar.error(this.$getErrorMessage(e))
      }
    },
    downloadOriginal(asset) {
      this.$store.dispatch('dam/downloadAsset', {
        attachment_type: 'Digital Assets',
        assets_id: asset.id,
        workspace_id: this.$route.params.workspace_id,
        download_name: asset.display_file_name,
      })
    },
  },
}
</script>