Appearance
Download & Export ​
Overview ​
Single Asset Download: The
downloadAssetVuex action routes throughpages/get_assets.vue, which callsGET digital-assets/object-downloadto 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.Bulk Zip Download: The
downloadMultipleFilesVuex action posts a list of asset IDs and folder IDs togenerate-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.Format Conversion: The
FileConversionDialoglets users select a different format (e.g., PNG to JPG, MP4 to MOV) for a supported asset. Conversion is triggered viaPOST digital-assets/convert-asset; the job runs asynchronously on the backend and the converted file is delivered to the user by email.Resolution Conversion: From the same
FileConversionDialog, users can select a reduced resolution for image assets. This callsPOST /convert/resolutionon the Nuxt server middleware, which resizes the image synchronously and returns a blob that is saved immediately withfile-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 statedownloadIndicatorstate tracks in-flight downloads as{ files: { [id]: { name, progress, loaded, total, downloading, source } } }downloadFileaction routes todownloadAssetfor single files or streams a blob withonDownloadProgressfor multi-file casesdownloadAssetaction generates a signed URL via the/get_assetsproxy page and streams the response via StreamSaverdownloadMultipleFilesaction posts togenerate-zip-data, streams the zip via the Cloudflare Worker, or falls back to emaildownloadMultipleSharedFilesaction usesshare-zip-datafor the same flow on public share links
plugins/streamsaver.client.js— StreamSaver client-only plugin- Wraps
StreamSaver.jsfor 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
- Wraps
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
extresponse header for filename construction
Vue Component Files (.vue) ​
components/dam/Dialogs/FileConversionDialog.vue— Download, format, and resolution dialog- Renders a format
v-selectpopulated from thedownloadableFormatsprop (original format always included) - Renders a resolution
v-selectfrom theotherResolutionsprop (disabled when noMimeTypein file metadata) - Format change →
POST digital-assets/convert-asset(async, email delivery) - Resolution change →
POST /convert/resolution(synchronous, immediate file-saver download) - Emits
download-orgwhen format or resolution matches the original, delegating to the parent
- Renders a format
components/dam/BulkActionBar.vue— Contextual bulk action toolbar- Download button shown when
canShareDownloadorforceShowDownloadis true - Emits a
downloadevent; the parent component dispatches the Vuex action - Displays a spinner and disables the button while
downloadingprop is true
- Download button shown when
pages/get_assets.vue— Asset download proxy page- Protected by
onlyAuthmiddleware to ensure the auth token is present for the backend call - Calls
GET digital-assets/object-downloadwith 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
- Protected by
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
downloadableFormatsprop with the original format always present - Resolution select populated from
otherResolutionsprop, disabled iffile.file_meta_data.MimeTypeis absent - Format conversion gated behind
subscription_features.asset_conversion.enablewith an upgrade tooltip - Progress spinner on the Download button while
downloadingorconvertingis 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
canShareDownloadorforceShowDownloadis true - Independent
showDownloadprop controls visibility separately from the permission props - Loading state driven by
downloadingprop — 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 displayBulk 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 emailResolution 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 ​
| Method | Endpoint | Purpose |
|---|---|---|
GET | digital-assets/object-download | Generate signed S3 URL for a single asset |
POST | generate-zip-data | Queue bulk zip job; returns stream payload or triggers email |
POST | share-zip-data | Same zip flow for public share links |
POST | digital-assets/convert-asset | Queue async format conversion job |
POST | /convert/resolution | Synchronous image resize on Nuxt server middleware |
POST | digital-assets/dashboard/file-download-history | Record 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: jpgComponent 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>Related Documentation ​
- File Upload — Upload system that shares StreamSaver infrastructure
- Authentication Methods — Auth middleware used by the
get_assetsdownload proxy - Store — dam.js — Full Vuex store including download state, actions, and mutations
- Plugins — StreamSaver — Client-only plugin wrapping the WritableStream download API