1. Working with ZIP, FTP and SFTP files

  1. Working with ZIP, FTP and SFTP files
    1. Opening and closing archives
      1. ZIP
      2. FTP
      3. SFTP
    2. JSON options for FTP and SFTP
    3. Supported file-system functions
    4. Path rules
    5. Current paths
    6. Copying files and folders
    7. Moving files and folders
    8. Reading and writing remote files
    9. Listing files
    10. Checking and creating folders
    11. Complete ZIP example
    12. Packaging and uploading a release
    13. Working with EXCEL .xlsx files from FileMaker
    14. Good practices

This page is valid for ACF-Plugin version 1.7.9.1 and upwards.

ACF extends its regular file-system functions so they can also work inside ZIP archives and on remote FTP/SFTP servers. This means that the same ACF code style can be used for local files, files inside a ZIP archive, and files on a remote server.

The feature is based on path prefixes. When you open an archive or a remote connection, ACF returns a prefix string. Add that prefix in front of paths that belong to the archive or remote server.

Examples:

string zip  = open_zip("~/Desktop/reports.zip");
string ftp  = open_ftp("ftp.example.com", JSON("username", "user", "password", "secret"));
string sftp = open_sftp("sftp.example.com", JSON("username", "user", "privateKey", "~/keys/id_ed25519"));

copy_file("~/Desktop/report.pdf", zip + "/Reports/report.pdf");
copy_file("~/Desktop/report.pdf", ftp + "/incoming/report.pdf");
copy_file("~/Desktop/report.pdf", sftp + "~/incoming/report.pdf");

The returned prefixes look like this:

ZIP<0>:
FTP<1>:
SFTP<2>:

The number can vary. Always use the value returned by open_zip, open_ftp or open_sftp. Do not construct these prefixes manually.

1.1. Opening and closing archives

1.1.1. ZIP

string zip = open_zip(zipFilePath);
close_zip(zip);

open_zip opens an existing ZIP archive or creates a new one if the file does not exist. Always call close_zip after writing to a ZIP archive. ZIP archives are finalized when they are closed, and changes may not be complete before that.

1.1.2. FTP

string ftp = open_ftp(server, optionsJSON);
close_ftp(ftp);

The server parameter can be a hostname, host:port, or a full FTP URL.

Examples:

string ftp = open_ftp(
    "ftp.example.com",
    JSON(
        "username", "deploy",
        "password", "secret",
        "createDirectories", true
    )
);
string ftp = open_ftp(
    "ftp.example.com:2121",
    JSON(
        "username", "deploy",
        "password", "secret"
    )
);

1.1.3. SFTP

string sftp = open_sftp(server, optionsJSON);
close_sftp(sftp);

SFTP can use username/password authentication or SSH private key authentication.

Username/password example:

string sftp = open_sftp(
    "sftp.example.com",
    JSON(
        "username", "deploy",
        "password", "secret",
        "createDirectories", true
    )
);

Private key example:

string sftp = open_sftp(
    "sftp.example.com",
    JSON(
        "username", "deploy",
        "privateKey", "~/keys/id_ed25519",
        "passphrase", "optional key passphrase",
        "knownHosts", "~/keys/known_hosts",
        "createDirectories", true
    )
);

For SFTP paths, ~/ is normally understood by the remote server as the remote user's home directory. ACF sends this path to the server; it is not expanded locally.

1.2. JSON options for FTP and SFTP

The second parameter to open_ftp and open_sftp is a JSON object.

Option Type FTP SFTP Description
username string yes yes Login username.
password string yes yes Login password. For SFTP this is used for password authentication unless a private key is used.
port integer yes yes Optional port number. You can also include the port in the server string, for example "localhost:2121".
timeoutSeconds number yes yes Optional timeout for remote operations.
createDirectories boolean yes yes When true, upload operations may create missing destination folders where supported. This defaults to true for uploads.
privateKey string no yes Path to an SSH private key file for SFTP authentication.
passphrase string no yes Optional passphrase for the private key.
knownHosts string no yes Optional path to a known hosts file used for SSH host verification.

Keep credentials outside source code when possible. In FileMaker solutions, connection settings are often better stored in configuration tables, encrypted fields, or environment-specific setup records.

1.3. Supported file-system functions

The following file-system functions can work with normal OS paths and prefixed ZIP, FTP or SFTP paths:

The markdown and document generation functions are not archive-aware by themselves, but their output can be written or copied to ZIP, FTP or SFTP paths afterward.

1.4. Path rules

Use / as the directory separator in ZIP, FTP and SFTP paths.

copy_file("~/Desktop/report.pdf", zip + "/Reports/report.pdf");
copy_file("~/Desktop/report.pdf", ftp + "/incoming/report.pdf");
copy_file("~/Desktop/report.pdf", sftp + "~/incoming/report.pdf");

For local OS paths, ACF also uses POSIX-style paths on macOS and Unix-like systems. Windows drive-letter and UNC paths are treated as absolute Windows paths.

For ZIP paths, folders are virtual. A ZIP archive is really a list of file entries, where each entry can contain / in its name. Creating a directory inside a ZIP archive is therefore not required. If you write a file to ZIP<0>:/Reports/report.pdf, the Reports path is part of the file entry name.

For FTP and SFTP paths, folders are real remote directories. Use create_directory or set "createDirectories", true when uploading into folders that may not exist.

1.5. Current paths

set_current_path(path) sets the current working path for the active runtime. get_current_path() returns it.

There is one current path for normal OS paths, and each open ZIP, FTP or SFTP connection can also have its own current path.

string zip = open_zip("~/Desktop/reports.zip");

set_current_path(zip + "/Reports");
int f = open(zip + "summary.txt", "w");
write(f, "Monthly summary");
close(f);

print get_current_path(); // /Reports

close_zip(zip);

For archive and remote paths, get_current_path() returns the path inside the archive or remote connection, not the prefix.

Examples:

set_current_path(zip + "/Reports");
// get_current_path() returns "/Reports"

set_current_path(sftp + "~/www/files");
// get_current_path() returns "~/www/files"

Absolute paths still behave as absolute paths. On macOS and Linux, paths starting with / are absolute. On Windows, drive-letter and UNC paths are absolute. A local path starting with ~ is expanded as the user's home directory. On SFTP, ~ is sent to the remote server.

1.6. Copying files and folders

copy_file works between local paths, ZIP paths, FTP paths and SFTP paths.

Upload a local file to FTP:

string ftp = open_ftp(
    "ftp.example.com",
    JSON("username", "deploy", "password", "secret", "createDirectories", true)
);

string res = copy_file("~/Desktop/report.pdf", ftp + "/incoming/report.pdf");
close_ftp(ftp);

return res;

Upload a folder into a ZIP archive:

string zip = open_zip("~/Desktop/release.zip");

string res = copy_file("~/Desktop/MyReleaseFolder", zip + "/Release/MyReleaseFolder");

close_zip(zip);
return res;

Download a folder from SFTP:

string sftp = open_sftp(
    "sftp.example.com",
    JSON("username", "deploy", "privateKey", "~/keys/id_ed25519")
);

string res = copy_file(sftp + "~/www/files", "~/Desktop/files");

close_sftp(sftp);
return res;

When copying folders, ACF copies the folder tree recursively.

1.7. Moving files and folders

move_file works like copy_file, but removes the source after a successful copy.

Move a local file into a ZIP archive:

string zip = open_zip("~/Desktop/archive.zip");

string res = move_file("~/Desktop/bigfile.dmg", zip + "/Downloads/bigfile.dmg");

close_zip(zip);
return res;

When moving files into a ZIP archive, ACF waits until close_zip has successfully completed before deleting the original local file. This protects the source file if the ZIP archive cannot be finalized.

Move a file on the same SFTP connection:

string sftp = open_sftp(
    "sftp.example.com",
    JSON("username", "deploy", "privateKey", "~/keys/id_ed25519")
);

string res = move_file(
    sftp + "~/www/incoming/report.pdf",
    sftp + "~/www/archive/report.pdf"
);

close_sftp(sftp);
return res;

When source and destination are on the same FTP or SFTP connection, ACF uses a remote rename operation instead of downloading and uploading the file.

1.8. Reading and writing remote files

You can use open, read, readline, write and close with ZIP, FTP and SFTP paths.

Write a text file to SFTP:

string sftp = open_sftp(
    "sftp.example.com",
    JSON("username", "deploy", "privateKey", "~/keys/id_ed25519")
);

int f = open(sftp + "~/www/hello.txt", "w");
write(f, "Hello world\n");
write(f, "Line 2\n");
close(f);

close_sftp(sftp);

Append to a remote file:

int f = open(sftp + "~/www/hello.txt", "wa");
write(f, "Added line\n");
close(f);

Read a whole remote file:

int f = open(sftp + "~/www/hello.txt", "r");
string content = read(f);
close(f);

Read a remote file line by line:

int f = open(sftp + "~/www/hello.txt", "r");
string line;
string content;

repeat
    line = readline(f);
    if (line != "{{EOF}}") then
        content += line + "\n";
    end if
until (line == "{{EOF}}");

close(f);

1.9. Listing files

Use list_files(path, option) to list local, ZIP, FTP or SFTP paths.

string ftp = open_ftp(
    "ftp.example.com",
    JSON("username", "deploy", "password", "secret")
);

string listing = list_files(ftp + "/incoming", 0);

close_ftp(ftp);
return listing;

The returned listing can be used to inspect remote folders before copying or deleting content.

1.10. Checking and creating folders

if (! directory_exists(sftp + "~/www/archive")) then
    string res = create_directory(sftp + "~/www/archive");
    if (res != "OK") then
        throw res;
    end if
end if

For ZIP archives, create_directory returns OK because explicit directory objects are not required. For FTP and SFTP, it creates the remote directory.

1.11. Complete ZIP example

The example below creates a ZIP archive, writes files into it, then adds a user-selected file.

function test_zip_archive (string archive, string filename, string content)
    int x, y, z;
    string path, xcontent;

    string zip = open_zip(archive);

    if (left(zip, 3) == "ZIP") then

        x = open(zip + filename, "w");
        y = open(zip + "ZipDirectory/Mytestfile6.txt", "w");

        write(x, content);
        write(y, "content into file6");
        write(x, "\nSome more content in line two...");
        write(y, "\nSome more content in line two into file6");

        close(x);
        close(y);

        path = select_file("Select a file to include");

        if (path != "") then
            z = open(path, "r");
            xcontent = read(z);
            path = Get_FilenameFromPath(path);
            close(z);

            z = open(zip + "ZipDirectory/" + path, "w");
            write(z, xcontent);
            close(z);
        end if

        close_zip(zip);
    else
        alert(zip);
    end if

    return "OK";
end

1.12. Packaging and uploading a release

A common workflow is to prepare files locally, pack them into a ZIP archive, then upload the completed archive to FTP or SFTP.

function PackAndUploadRelease (string folderPath, string zipPath)

    string zip = open_zip(zipPath);
    if (left(zip, 3) != "ZIP") then
        throw "Could not open ZIP archive: " + zip;
    end if

    string res = copy_file(folderPath, zip + "/Release");
    close_zip(zip);

    if (res != "OK") then
        throw res;
    end if

    string ftp = open_ftp(
        "ftp.example.com",
        JSON(
            "username", "deploy",
            "password", "secret",
            "createDirectories", true
        )
    );

    if (left(ftp, 3) != "FTP") then
        throw "Could not open FTP connection: " + ftp;
    end if

    res = copy_file(zipPath, ftp + "/downloads/" + Get_FilenameFromPath(zipPath));

    close_ftp(ftp);
    return res;

end

1.13. Working with EXCEL .xlsx files from FileMaker

EXCEL spreadsheet files in the .xlsx format are ZIP archives. This means that you can open them with open_zip and inspect or modify the XML files inside the workbook.

If we use list_files on an EXCEL file:

string path = "~/Desktop/MyExcelFile.xlsx";
string zip = open_zip(path);
print list_files(zip, 0);
close_zip(zip);

This returns a list similar to:

[Content_Types].xml
_rels/.rels
xl/_rels/workbook.xml.rels
xl/workbook.xml
xl/sharedStrings.xml
xl/styles.xml
xl/worksheets/sheet1.xml
xl/theme/theme1.xml
docProps/app.xml
docProps/core.xml
xl/calcChain.xml

The file xl/worksheets/sheet1.xml contains the worksheet data. The data is stored in XML, with a structure similar to this:

<worksheet>
    <sheetData>
        <row>
            <c><v>some value</v></c>
            <c><v>some value</v></c>
            <c><v>some value</v></c>
        </row>
    </sheetData>
</worksheet>

The following snippet extracts values from row 5 to row 9, column 5:

string path = "~/Desktop/MyExcelFile.xlsx";
int x, i;
string content;
XML sheet1;

string zip = open_zip(path);
x = open(zip + "xl/worksheets/sheet1.xml", "r");
content = read(x);
close(x);
close_zip(zip);

sheet1 = content;

ARRAY STRING colE5_9;
for (i = 5, 9)
    colE5_9[] = string(sheet1["worksheet.sheetData.row[" + i + "].c[5].v"]);
end for

return implode(",", colE5_9);

Example result:

6300,239519,74335,14500,108521

This technique is useful for advanced workflows where you need direct access to the internal XML files in an .xlsx package. For ordinary Excel report generation, the dedicated ACF Excel functions are usually easier and safer.

1.14. Good practices