Automating Attachment Download – Bulk Attachment Downloads from List View
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
6 hours ago
List View — Multi-Record Attachment Download
We’ve all faced it — opening multiple records, downloading attachments one by one, and repeating the same clicks endlessly.
Tedious, right?
Let’s simplify that too.
This enhancement adds a Download Attachments button right on the List View.
Select a few records (up to 10), click once, and every record’s attachments are collected, zipped, and ready for download — all in one go.
No background jobs, no leftover files, just a direct and clean download.
Why Build This
The goal was clear:
Enable downloading attachments for multiple records with a single click.
Keep it simple, native, and secure.
Ensure it works smoothly without temporary tables or extra storage.
With just one UI Action and one Processor, the job is done beautifully — again, fully in-memory and zero-cleanup.
What’s Inside
Component | Type | Purpose |
Download Attachments | UI Action (List View) | The button that initiates the bulk download |
exportListAttachmentsToZip | Processor (Global Scope) | Collects attachments from multiple records, zips them, and streams to the browser |
UI Action Setup
Action Script:
function list_attachment() {
var selected = g_list.getChecked();
if (!selected) {
alert("Please select at least one record.");
return;
} else if (selected.split(',').length > 10) {
alert("Please select records less than 10.");
return;
}
var url = 'exportListAttachmentsToZip.do?sysparm_sys_id_list=' + selected + '&sysparm_table=' + g_list.getTableName();
top.window.open(url,'_blank');
}
Processor Logic — exportListAttachmentsToZip
(function process(g_request, g_response, g_processor) {
// Retrieve the table name and comma-separated sys_id list from request parameters
var table = g_request.getParameter('sysparm_table');
var idList = g_request.getParameter('sysparm_sys_id_list');
// Validate inputs – exit early if either table name or sys_id list is missing
if (!idList || !table) {
gs.info("No records selected.");
return;
}
// Convert comma-separated sys_id list into an array and remove extra spaces
var sysIds = idList.split(',').map(function(id) { return id.trim(); });
// Initialize GlideSysAttachment API for reading file bytes
var sa = new GlideSysAttachment();
// Name of the main ZIP file that will contain all per-record ZIPs
var mainZipName = 'Attachments.zip';
// -----------------------------------------
// Configure HTTP response headers for file download
// -----------------------------------------
g_response.setHeader('Pragma', 'public');
g_response.addHeader('Cache-Control', 'max-age=0');
g_response.setContentType('application/octet-stream');
g_response.addHeader('Content-Disposition', 'attachment;filename=' + mainZipName);
// Obtain the response output stream
var outputStream = g_response.getOutputStream();
// Create a new ZIP output stream for writing the main ZIP
var mainZip = new Packages.java.util.zip.ZipOutputStream(outputStream);
// -----------------------------------------
// Preload metadata (record number + caller info) for all records
// -----------------------------------------
var recordMap = {};
var gr = new GlideRecord(table);
gr.addQuery('sys_id', 'IN', sysIds);
gr.query();
while (gr.next()) {
var callerName = gr.caller_id ? gr.caller_id.getDisplayValue().replace(/\s+/g, "_") : "Unknown";
var num = gr.getValue('number') || gr.getDisplayValue();
recordMap[gr.getUniqueValue()] = num + "_" + callerName + ".zip";
}
// -----------------------------------------
// Query all attachments for the selected records
// -----------------------------------------
var att = new GlideRecord('sys_attachment');
att.addQuery('table_sys_id', 'IN', sysIds);
att.addQuery('table_name', table);
att.orderBy('table_sys_id');
att.query();
// Variables used to manage nested ZIP creation
var currentSysId = null;
var zipOut = null;
var baos = null;
var zipName = null;
var hadAnyEntryForCurrent = false;
// Track filenames to prevent duplicates per record
var usedFileNames = {};
// -----------------------------------------
// Iterate through each attachment and group them by record
// -----------------------------------------
while (att.next()) {
var sysid = att.getValue('table_sys_id');
if (!recordMap[sysid]) continue;
// If sys_id changes, close the previous record ZIP and start a new one
if (currentSysId !== sysid) {
if (zipOut) {
zipOut.close();
if (hadAnyEntryForCurrent) {
var incidentBytes = baos.toByteArray();
mainZip.putNextEntry(new Packages.java.util.zip.ZipEntry(zipName));
mainZip.write(incidentBytes, 0, incidentBytes.length);
mainZip.closeEntry();
}
}
// Start a new ZIP for the current record
currentSysId = sysid;
zipName = recordMap[sysid];
baos = new Packages.java.io.ByteArrayOutputStream();
zipOut = new Packages.java.util.zip.ZipOutputStream(baos);
hadAnyEntryForCurrent = false;
// Reset filename tracking for the new record
usedFileNames = {};
}
try {
var fileBytes = sa.getBytes(att);
if (!fileBytes || fileBytes.length === 0) continue;
var originalName = att.getValue('file_name') || "attachment";
var fileName = originalName;
var counter = 1;
// Duplicate Prevention Logic
while (usedFileNames[fileName]) {
var extIndex = originalName.lastIndexOf(".");
if (extIndex > -1) {
fileName = originalName.substring(0, extIndex) + "_" + counter + originalName.substring(extIndex);
} else {
fileName = originalName + "_" + counter;
}
counter++;
}
// Mark this file name as used
usedFileNames[fileName] = true;
// Add this attachment into the current record ZIP
zipOut.putNextEntry(new Packages.java.util.zip.ZipEntry(fileName));
zipOut.write(fileBytes, 0, fileBytes.length);
zipOut.closeEntry();
hadAnyEntryForCurrent = true;
} catch (e) {
gs.error("Error adding attachment " + att.getValue('file_name') + ": " + e);
}
}
// -----------------------------------------
// Finalize the last record ZIP
// -----------------------------------------
if (zipOut) {
zipOut.close();
if (hadAnyEntryForCurrent) {
var finalBytes = baos.toByteArray();
mainZip.putNextEntry(new Packages.java.util.zip.ZipEntry(zipName));
mainZip.write(finalBytes, 0, finalBytes.length);
mainZip.closeEntry();
}
}
// -----------------------------------------
// Final step: Close the main ZIP stream
// -----------------------------------------
mainZip.close();
})(g_request, g_response, g_processor);
This processor loops through all selected records, gathers attachments from each, and compresses them into one ZIP file.
It prevents duplicate filenames, streams the ZIP directly to the browser, and cleans up instantly after download.
No persistence. No mess. Just results.
How It All Comes Together
Record Loop – Iterates through all selected records.
Attachment Lookup – Finds and processes attachments per record.
Smart Naming – Prefixes each file with its record number.
Duplicate Prevention – Safely handles repeated filenames.
Direct Streaming – Sends ZIP immediately to the browser.
Zero Cleanup – Once downloaded, everything’s cleared from memory.
Why This Works So Well
- Gracefully handles empty or duplicate attachments.
- Requires no background or scheduled jobs.
- Logic easily scales for future email or automation flows.
- Keeps your instance clean — no temporary files, no artifacts.
Performance Thoughts
Efficient, lightweight, and memory-based — ideal for small to medium workloads.
It can handle a minimum number of records at once with no delay.
If you ever plan to scale it for larger sets, a background or asynchronous approach would better balance performance and stability.
Final Outcome
A single click.
A single ZIP.
All attachments from multiple records — instantly downloaded and gone without a trace.
What’s Next
This use case makes it easy to download attachments from multiple records right from the List View.
Next, we’ll take this further — sending the generated ZIP by email automatically and implementing a cleanup mechanism to clear old files, keeping your instance lightweight and efficient.
Hope this helps!
Regards,
Yashwanth Pepakayala
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
26m ago
Want to see other approaches for downloading attachments? Check out:
Form View → Download attachments one record at a time directly from the form for precise control: Form View Immediate Download
List View (Email) → Automate delivery of attachments to your email without manual intervention: List View Email