Skip to main content

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:

  1. DownloadAttachmentsAsZipWithPagination - Downloads attachments from search query results with pagination.
  2. 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: true if attachments were found and downloaded; false otherwise.

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: true if attachments were found and downloaded; false otherwise.

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 false when 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.