Attachment download as ZIP
This documentation provides comprehensive guidance for external AL developers on how to make effective use of the DXP Freeze attachment download functionality in their Business Central extensions.
Overview
The DXP Freeze Result Management codeunit provides two main methods for downloading archived attachments as ZIP files:
DownloadAttachmentsAsZipWithPagination- Downloads attachments from search query results with pagination.DownloadAttachmentsAsZipFromRecord- Downloads attachments from specific Business Central records.
Both methods organize attachments into structured ZIP archives with comprehensive metadata for auditing and traceability purposes.
Method 1: DownloadAttachmentsAsZipWithPagination
Purpose
Downloads all attachments from a search query result set and processes the results page by page to handle large data sets efficiently. The attachments of each record are organized into individual ZIP files inside a main ZIP archive.
Available overloads
1. Basic usage
procedure DownloadAttachmentsAsZipWithPagination(SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer): Boolean
2. With filters
procedure DownloadAttachmentsAsZipWithPagination(SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer; FilenameFilter: Text; FileExtensionFilter: Text): Boolean
3. Full control
procedure DownloadAttachmentsAsZipWithPagination(SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer; FilenameFilter: Text; FileExtensionFilter: Text; StoreApiLink: Text; RecordsPerPage: Integer; SuppressDialog: Boolean): Boolean
4. Pre-populated records (advanced)
procedure DownloadAttachmentsAsZipWithPagination(var TempFrzResultQueryHeader: Record "DXP FRZ Query Result Header" temporary; var TempFrzResultRecordHeader: Record "DXP FRZ Record Result Header" temporary; var TempFrzResultRecordField: Record "DXP FRZ Result Record-Field" temporary; var TempFrzAttachmentResult: Record "DXP FRZ Attachment Result" temporary; SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer; FilenameFilter: Text; FileExtensionFilter: Text; SuppressDialog: Boolean): Boolean
Parameters
| Parameter | Type | Description |
|---|---|---|
SearchQuery |
Text | Freeze search query string. If empty in the pre-populated overload, the existing query is used or the search is run again. |
ZipArchive |
Codeunit "Data Compression" | ZIP archive object that will contain the downloaded files. |
AttachmentCount |
Integer (var) | Returns the total number of downloaded attachments. |
FilenameFilter |
Text | Filter for attachment file names (for example '*.pdf', 'invoice*'). |
FileExtensionFilter |
Text | Filter for file extensions (for example 'pdf', 'docx'). |
StoreApiLink |
Text | Optional, specific store API link. |
RecordsPerPage |
Integer | Number of records per page (default: 100). |
SuppressDialog |
Boolean | Whether the progress dialog should be suppressed. |
Return value
Boolean:trueif attachments were found and downloaded;falseotherwise.
Usage examples
Basic download
procedure DownloadSearchResults()
var
ResultMgt: Codeunit "DXP FRZ Result Mgt.";
ZipArchive: Codeunit "Data Compression";
FileMgt: Codeunit "File Management";
TempBlob: Codeunit "Temp Blob";
AttachmentCount: Integer;
InStr: InStream;
OutStr: OutStream;
SearchQuery: Text;
begin
SearchQuery := 'invoice AND 2024';
if ResultMgt.DownloadAttachmentsAsZipWithPagination(SearchQuery, ZipArchive, AttachmentCount) then begin
// Save ZIP to file
TempBlob.CreateOutStream(OutStr);
ZipArchive.SaveZipArchive(OutStr);
TempBlob.CreateInStream(InStr);
FileMgt.DownloadFromStreamHandler(InStr, '', '', '', 'SearchResults.zip');
Message('Successfully downloaded %1 attachments.', AttachmentCount);
end else
Message('No attachments found for the search query.');
end;
With filters
procedure DownloadPDFInvoices()
var
ResultMgt: Codeunit "DXP FRZ Result Mgt.";
ZipArchive: Codeunit "Data Compression";
AttachmentCount: Integer;
SearchQuery: Text;
begin
SearchQuery := 'type:invoice';
if ResultMgt.DownloadAttachmentsAsZipWithPagination(
SearchQuery,
ZipArchive,
AttachmentCount,
'*.pdf', // PDF files only
'pdf' // File extension filter
) then begin
// Process ZIP archive
ProcessDownloadedFiles(ZipArchive, AttachmentCount);
end;
end;
Using pre-populated records
procedure DownloadFromExistingResults()
var
ResultMgt: Codeunit "DXP FRZ Result Mgt.";
TempFrzResultQueryHeader: Record "DXP FRZ Query Result Header" temporary;
TempFrzResultRecordHeader: Record "DXP FRZ Record Result Header" temporary;
TempFrzResultRecordField: Record "DXP FRZ Result Record-Field" temporary;
TempFrzAttachmentResult: Record "DXP FRZ Attachment Result" temporary;
ZipArchive: Codeunit "Data Compression";
AttachmentCount: Integer;
begin
// Assume these records are already populated from a previous search
PopulateSearchResults(TempFrzResultQueryHeader, TempFrzResultRecordHeader, TempFrzResultRecordField, TempFrzAttachmentResult);
// Download using existing results without running the search again
if ResultMgt.DownloadAttachmentsAsZipWithPagination(
TempFrzResultQueryHeader,
TempFrzResultRecordHeader,
TempFrzResultRecordField,
TempFrzAttachmentResult,
'invoicesearch', // SearchQuery - if empty, the search is run again
ZipArchive,
AttachmentCount,
'', // No file name filter
'', // No extension filter
true // Suppress dialog
) then begin
ProcessDownloadedFiles(ZipArchive, AttachmentCount);
end;
end;
ZIP structure (pagination)
SearchResults.zip
├── export-metadata.json
├── Invoice_001_V1_20241201_1430.zip
│ ├── {GUID}_invoice.pdf
│ └── {GUID}_supporting-document.docx
├── Order_002_V2_20241202_0900.zip
│ └── {GUID}_order-document.pdf
└── Contract_003_V1_20241203_1200.zip
├── {GUID}_contract.pdf
└── {GUID}_addendum.pdf
Method 2: DownloadAttachmentsAsZipFromRecord
Purpose
Downloads attachments from specific Business Central records. The attachments of each selected record are organized into individual ZIP files inside a main ZIP archive.
Available overloads
1. Basic usage
procedure DownloadAttachmentsAsZipFromRecord(var SelectedRecord: RecordRef; var ZipArchive: Codeunit "Data Compression"): Boolean
2. With filters
procedure DownloadAttachmentsAsZipFromRecord(var SelectedRecord: RecordRef; var ZipArchive: Codeunit "Data Compression"; FilenameFilter: Text; FileExtensionFilter: Text): Boolean
Parameters
| Parameter | Type | Description |
|---|---|---|
SelectedRecord |
RecordRef (var) | RecordRef holding the selected Business Central records. |
ZipArchive |
Codeunit "Data Compression" | ZIP archive object that will contain the downloaded files. |
FilenameFilter |
Text | Filter for attachment file names. |
FileExtensionFilter |
Text | Filter for file extensions. |
Return value
Boolean:trueif attachments were found and downloaded;falseotherwise.
Usage examples
Downloading sales invoices
procedure DownloadInvoiceAttachments()
var
SalesInvoiceHeader: Record "Sales Invoice Header";
ResultMgt: Codeunit "DXP FRZ Result Mgt.";
ZipArchive: Codeunit "Data Compression";
RecordRef: RecordRef;
HasAttachments: Boolean;
begin
// Select specific invoices
SalesInvoiceHeader.SetRange("Posting Date", DMY2Date(1, 1, 2024), DMY2Date(31, 12, 2024));
SalesInvoiceHeader.SetFilter("Sell-to Customer No.", '10000|20000');
if SalesInvoiceHeader.FindSet() then begin
RecordRef.GetTable(SalesInvoiceHeader);
HasAttachments := ResultMgt.DownloadAttachmentsAsZipFromRecord(RecordRef, ZipArchive);
if HasAttachments then
SaveZipFile(ZipArchive, 'InvoiceAttachments.zip')
else
Message('No attachments found for the selected invoices.');
end;
end;
Downloading with filters from a page
// In a page extension
action(DownloadAttachmentsFiltered)
{
Caption = 'Download filtered attachments';
Image = ExportFile;
trigger OnAction()
var
ResultMgt: Codeunit "DXP FRZ Result Mgt.";
ZipArchive: Codeunit "Data Compression";
RecordRef: RecordRef;
FilenameFilter: Text;
FileExtensionFilter: Text;
begin
// Show filter dialog
if ShowFilterDialog(FilenameFilter, FileExtensionFilter) then begin
CurrPage.SetSelectionFilter(Rec);
RecordRef.GetTable(Rec);
if ResultMgt.DownloadAttachmentsAsZipFromRecord(
RecordRef,
ZipArchive,
FilenameFilter,
FileExtensionFilter
) then
DownloadZipFile(ZipArchive, 'FilteredAttachments.zip');
end;
end;
}
ZIP structure (records)
RecordAttachments.zip
├── export-metadata.json
├── Sales_Invoice_Header_Company_SI-001.zip
│ ├── {GUID}_invoice.pdf
│ └── {GUID}_terms.pdf
├── Sales_Invoice_Header_Company_SI-002.zip
│ └── {GUID}_invoice.pdf
└── Sales_Invoice_Header_Company_SI-003.zip
├── {GUID}_invoice.pdf
├── {GUID}_delivery-note.pdf
└── {GUID}_receipt.jpg
Metadata structure
Both methods generate comprehensive metadata in export-metadata.json:
Pagination export metadata
{
"exportInfo": {
"exportTimestamp": "2024-12-19T10:13:52.248Z",
"exportedBy": "USER001",
"searchQuery": "type:invoice AND year:2024",
"totalRecordsFound": 150,
"totalPages": 15,
"exportType": "paginated-search",
"description": "Freeze search query export"
},
"appliedFilters": {
"filenameFilter": "*.pdf",
"fileExtensionFilter": "pdf"
},
"statistics": {
"pagesProcessed": 15,
"totalRecordsProcessed": 150,
"recordsWithAttachments": 120,
"recordsWithoutAttachments": 30,
"totalAttachments": 245,
"exportCompletedAt": "2024-12-19T10:15:33.021Z"
},
"records": [
{
"recordId": "{GUID}",
"title": "Invoice REG-2024-001",
"version": 1,
"archivedAt": "2024-12-01T09:30:00Z",
"archivedBy": "SYSTEM",
"type": "Sales Invoice",
"masterId": "{GUID}",
"attachmentCount": 3,
"hasAttachments": true,
"zipFile": "Invoice_REG-2024-001_V1_20241201_0930.zip"
}
]
}
Record export metadata
{
"exportTimestamp": "2024-12-19T14:30:00Z",
"exportedBy": "USER001",
"totalRecordsProcessed": 25,
"description": "DXP Freeze attachment export",
"sourceTable": {
"tableNumber": 112,
"tableName": "Sales Invoice Header",
"tableCaption": "Posted Sales Invoice"
},
"appliedFilters": {
"filenameFilter": "*.pdf",
"fileExtensionFilter": "pdf"
},
"statistics": {
"totalAttachments": 45,
"recordsWithAttachments": 20,
"recordsWithoutAttachments": 5,
"totalZipFiles": 20
},
"records": [
{
"recordId": "Sales Invoice Header: Company, SI-001",
"systemId": "{GUID}",
"primaryKey": {
"fields": [
{
"fieldName": "No.",
"fieldValue": "SI-001",
"fieldType": "Code"
}
]
},
"hasAttachments": true,
"attachmentCount": 2,
"zipFile": "Sales_Invoice_Header_Company_SI-001.zip"
}
]
}
Performance considerations
Pagination method
- Large result sets: Automatic pagination handling to process large data sets efficiently.
- Memory management: Processes one page at a time and clears memory between pages.
- Progress tracking: Shows real-time progress for longer-running operations.
- Recommended for: Search queries that can return hundreds or thousands of records.
Record method
- Selected records: Processes only the specifically selected records.
- Direct processing: No pagination overhead for smaller data sets.
- Batch processing: Efficient for processing specific record sets.
- Recommended for: Targeted downloads of specific Business Central records.
Error handling
Both methods include comprehensive error handling:
Common scenarios
- No results found: Returns
falsewhen no records or attachments are found. - Permission issues: Automatically excludes records the user cannot access.
- API errors: Reliable handling of API communication errors.
- Empty filters: Handles empty or invalid filter parameters.
Best practices
// Always check the return value
if not ResultMgt.DownloadAttachmentsAsZipWithPagination(SearchQuery, ZipArchive, AttachmentCount) then begin
Message('No attachments found or download failed.');
exit;
end;
// Validate the attachment count
if AttachmentCount = 0 then begin
Message('Search completed, but no attachments match the criteria.');
exit;
end;
// Handle large downloads
if AttachmentCount > 1000 then
if not Confirm('This will download %1 attachments. Continue?', false, AttachmentCount) then
exit;
Integration events
Both methods support integration events for customization:
Available events
OnBeforeDownloadAttachmentsAsZip: Modify behavior before the download starts.OnBeforeProcessAttachmentForZip: Skip or modify individual attachments.OnAfterGetAttachmentBase64: Modify attachment content after retrieval.OnAfterAddAttachmentToZip: Perform actions after adding to the ZIP.OnNoAttachmentsFound: Handle the no-attachments scenario.
Integration example
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP FRZ Result Mgt.", 'OnBeforeProcessAttachmentForZip', '', false, false)]
local procedure OnBeforeProcessAttachmentForZip(var TempFrzAttachmentResult: Record "DXP FRZ Attachment Result" temporary; var IsHandled: Boolean)
begin
// Skip attachments larger than 10MB
if TempFrzAttachmentResult.Filesize > 10485760 then
IsHandled := true;
end;
File name conventions
Automatic sanitization
All file names are automatically sanitized with the SanitizeFileName method:
- Invalid characters (
< > : " / \ | ? *) are replaced with underscores. - Spaces are replaced with underscores.
- Maximum file name lengths are enforced.
Unique naming
- Individual files: Include an attachment GUID prefix to ensure uniqueness.
- ZIP files: Include record information and a timestamp.
- No conflicts: Guarantees unique names within each ZIP archive.
No comments to display
No comments to display