File Transfers

client.transfers() uploads local files and streams, and downloads files to local paths. Each transfer runs with its client's settings as they were when it started, and returns when it is complete.

An upload is sent in parts, sized by the SDK where Files.com's instructions for the upload let it, and as Files.com plans them everywhere else (see Part Sizes). When Files.com allows it, several parts are sent at the same time, as many as the upload measures to help (see Concurrency); otherwise they are sent one at a time, in order. The parts go to the storage URLs Files.com provides, with the method and headers it gives, and carry no Files.com credentials. Once every part has been accepted, one final request completes the upload.

Example Request

import com.files.FilesClient;
import com.files.exceptions.*;
import com.files.exceptions.ApiErrorException.*;
import com.files.net.UploadOptions;
import com.files.net.UploadResult;
import java.nio.file.Paths;

try {
  UploadResult result = client.transfers().upload(Paths.get("report.pdf"), "reports/2026/report.pdf",
      UploadOptions.builder().mkdirParents(true).build());
  System.out.println("Uploaded " + result.getSize() + " bytes in " + result.getParts().size() + " parts");
} catch (NotAuthenticatedException e) {
  System.out.println("Authentication Error Occurred (" + e.getClass().getName() + "): " + e.getMessage());
} catch (SdkException e) {
  System.out.println("Unknown Error Occurred (" + e.getClass().getName() + "): " + e.getMessage());
}

Files and Streams

A local file is opened once, and every part is read from it as it is sent, so its contents are never held in memory. Each part is also read once before it is first sent, to record a digest of its bytes: an attempt whose bytes no longer match, because the file changed in place, even with the same size and modification time, is stopped before it is complete, and the upload fails. A part is only ever accepted with the bytes it had when it was first read. The length uploaded is the file's length when the upload starts, and its modification time is recorded unless you set modifiedTime. If the file's length or modification time changes during the upload, the upload is not completed either.

A stream is read forward, from its current position, one part at a time. Each part is kept in memory until the storage server accepts it, so a retry sends exactly the same bytes. Give the number of bytes to upload, and the upload fails with EOFException if the stream ends first. Or leave the length out, and the stream is uploaded until it ends: it may hold at most maxStreamLength bytes (5 TiB by default), and one that holds more fails before its upload is completed, without sending a byte past that bound. The stream is never closed, reset or skipped.

Example Request

import com.files.net.UploadOptions;
import java.io.InputStream;

try (InputStream stream = openExport()) {
  client.transfers().upload(stream, exportLength, "exports/latest.csv", UploadOptions.defaults());
}

try (InputStream stream = openPipe()) {
  client.transfers().upload(stream, "exports/piped.csv",
      UploadOptions.builder().maxStreamLength(64L * 1024 * 1024 * 1024).build());
}

Part Sizes

Files.com plans every upload's parts. The SDK chooses their sizes only where Files.com's instructions let it: an ordinary multipart upload to S3, to Files.com's own storage or a bucket of yours, whose signed URLs fix neither a part's length nor its bytes. Anywhere else, such as a Files Agent, or where the URLs require a length or checksum, the upload sends the parts Files.com plans. Where the SDK chooses, every part but the last holds at least 5 MiB and at most 1 GiB, and the upload has at most 10,000 parts, or fewer if Files.com allows fewer. If Files.com later gives a part's URL for another S3 upload, or one that fixes its length, the upload stops before sending that part.

PartSizing.automatic(), the default, starts at 8 MiB for files up to 256 MiB, 16 MiB up to 4 GiB, 32 MiB up to 64 GiB and 64 MiB above that, smaller so that a file has at least 16 parts and larger where 10,000 parts would not hold it. A stream of unknown length starts at 8 MiB, with parts made larger as needed for 10,000 of them to hold maxStreamLength. Since a stream's parts are held in memory, where its limiter's getMaxBufferedBytes() cannot hold parts that large, the stream gets the parts Files.com plans instead: give a lower maxStreamLength for the SDK to size them. PartSizing.serverPlanned() always sends the parts Files.com plans, and PartSizing.fixed(size) sends parts of one size, failing before anything is sent where the SDK may not choose sizes or the size breaks the limits above. UploadResult.getPartSizeSource() says which sizes an upload used.

Example Request

import com.files.net.PartSizing;
import com.files.net.UploadOptions;
import com.files.net.UploadResult;
import java.nio.file.Paths;

UploadResult result = client.transfers().upload(Paths.get("video.mov"), "videos/video.mov",
    UploadOptions.builder().partSizing(PartSizing.fixed(64L * 1024 * 1024)).build());
System.out.println(result.getParts().size() + " parts, sized " + result.getPartSizeSource());

Default vs. Aggressive Startup

For automatic uploads, choose how quickly the SDK starts sending parts in parallel. Use default startup when you have not measured your network's capacity. Choose aggressive startup for a known fast connection, including uploads from a cloud VM.

Default startup begins an S3 upload with one part at a time and gradually increases concurrency as measurements show an improvement. Use UploadOptions.defaults() or leave startup unset in UploadOptions.builder().

Aggressive startup begins with a target of 150 concurrent parts for S3 uploads. Set startup(UploadStartup.AGGRESSIVE) to use it. The actual number of parts in flight is limited by maxConcurrency, the transfer limiter, the connection pool and the number of available parts.

Both modes adjust concurrency using the same measurements after startup and can reach the same throughput. Aggressive startup avoids the gradual increase at the beginning of the upload. These startup options apply to uploads. Downloads use the behavior described in Downloads.

Choose one of the following examples with your configured client.

Default Startup

import com.files.net.UploadOptions;
import java.nio.file.Paths;

client.transfers().upload(Paths.get("backup.tar"), "backups/backup.tar",
    UploadOptions.defaults());

Aggressive Startup

import com.files.net.UploadOptions;
import com.files.net.UploadStartup;
import java.nio.file.Paths;

client.transfers().upload(Paths.get("backup.tar"), "backups/backup.tar",
    UploadOptions.builder().startup(UploadStartup.AGGRESSIVE).build());

Concurrency

By default an upload adapts how many parts it sends at once to what its parts deliver, following the policy for where they go, as Files.com's answers show it. Parts to S3, Files.com's own storage or a bucket of yours, follow the scaled S3 policy: up to 150 parts at once, and up to 1024 once 64 GiB have been delivered at 96 MiB/s, growing by 4 parts when a window of at least 32 answers shows the gain, and lowered by 35% after a temporary failure, by 10% when the storage server throttles, and by 8% when requests queue while throughput falls. Parts to a Files Agent start at 2 at once and go up to 8. Files.com's answers do not yet identify a remote server integration, so its uploads follow the S3 policy.

With default startup, an S3 upload increases concurrency by half when a measurement window shows enough improvement without queueing, or by one after reducing concurrency. A window that delivers 96 MiB/s increases concurrency to the policy's starting target.

Give the transfers of one job the same TransferScope (scope(job) in their options) to have them share request limits and concurrency measurements. Compatible automatic uploads share one concurrency target, and compatible automatic downloads share another. The SDK adjusts each target using throughput, temporary failures and throttling responses from upload parts and download attempts for whole files or bound ranges. An upload to S3 warms up once for the job. The scope also shares one limiter for requests for file contents and their buffered bytes, unless the options specify a different limiter. Upload planning and finalization API calls do not use that limiter.

Run the job's blocking transfer calls on a bounded pool of threads that your application owns. The number of calls you run at once limits how many files the SDK transfers at once, even when the shared target allows more part requests. For downloads, submit each file for download as you receive its listing page so transfers can begin while the rest of the folder is being listed. Limit the number of queued transfer calls as well. Wait for every call to finish before closing the scope. Concurrency targets and measurements are not retained between scopes.

For automatic uploads in a scope, default startup also controls how many files begin. The SDK starts a few uploads and gradually starts more as the shared concurrency target increases or existing uploads provide too few parts to use it. This avoids starting every file's API requests at once. Aggressive startup admits the supplied files immediately. Default and aggressive startup can use the same number of callers and reach the same throughput. Default startup takes longer to increase concurrency. Requests for file contents still follow the limiter, shared target and connection pool limits either way.

maxConcurrency caps how many parts go at once, as do the limiter's request slots and the transport's connections to the storage host. concurrencyMode(ConcurrencyMode.STATIC) sends maxConcurrency parts at once (4 if it is not set) instead of adapting. Each progress report's getConcurrency() says what the upload does and why: its target, the parts in flight, its last decision, and where its time went.

Example Request

import com.files.net.TransferConcurrency;
import com.files.net.UploadOptions;
import com.files.net.UploadResult;
import java.nio.file.Paths;

UploadResult result = client.transfers().upload(Paths.get("backup.tar"), "backups/backup.tar",
    UploadOptions.defaults());
TransferConcurrency concurrency = result.getFinalProgress().getConcurrency();
System.out.println(concurrency.getPeakActiveAttempts() + " parts at once at most, last " + concurrency.getLastDecision());

Retries and Limits

A part that fails with a connection failure, a request timeout (408), throttling (429) or a server error (5xx other than 501) is sent again with the same bytes, when Files.com allows retrying parts, up to the client's maxRetries. It waits the client's retry delay first; after 429 or 503 it waits the storage server's whole Retry-After instead, or not at all without one; a part whose Retry-After asks for longer than the SDK can wait, some 24 days, is not sent again. An automatic upload also sends no new part until the Retry-After has passed. While parts are sent in parallel, a part waiting to be sent again lets other parts be sent meanwhile. A part whose URL is about to expire gets a new one for the same part of the same upload. Parts already accepted are never sent again.

Transfers that share a client's connection pool send no more parts at once than the pool can lease connections for: to one storage host, at most its limit for that host (HttpTransport.Builder.maxConnectionsPerRoute, 1024 by default), and to all storage hosts together, at most its total (maxConnections, 2048 by default). For a pool you supply, its limit for the host's route, including one you set for that route, and its total are read whenever a part waits, and never changed. A part waits its turn rather than failing for want of a connection. Requests to the Files.com API, and anything else using the pool, are not counted: while they hold connections, a part, or they, can still wait for one, up to the client's connectionAcquisitionTimeout.

A TransferLimiter limits how many part requests transfers send at once and how many bytes of file contents they hold in memory. Give several uploads the same limiter, even uploads of different clients, and they take turns within one budget without sharing credentials. A part of a stream larger than its limiter's getMaxBufferedBytes() fails the upload before it is read.

Example Request

import com.files.net.TransferLimiter;
import com.files.net.UploadOptions;
import java.nio.file.Path;

TransferLimiter limiter = new TransferLimiter(8, 256L * 1024 * 1024);
UploadOptions options = UploadOptions.builder()
    .maxConcurrency(4)
    .limiter(limiter)
    .progressListener(progress -> System.out.println(progress.getTransferredBytes() + " bytes"))
    .build();

for (Path file : files) {
  client.transfers().upload(file, "incoming/" + file.getFileName(), options);
}

Completion

The final request is never repeated once it may have reached Files.com: Files.com forgets an upload once it completes it, so a repeat could not tell a completed upload from a missing one. Only an attempt that could not be sent, such as one that could not connect, is retried. If the request fails in a way that does not show whether Files.com completed the file, such as a lost connection, the upload throws UploadCompletionUnknownException. Check the file at its getPath() before uploading it again. A refusal from Files.com is thrown as its API error, and a request that never reached Files.com as its connection error: then the upload was not completed.

Example Request

import com.files.exceptions.UploadCompletionUnknownException;
import com.files.models.File;
import java.nio.file.Paths;

try {
  client.transfers().upload(Paths.get("big.iso"), "images/big.iso");
} catch (UploadCompletionUnknownException e) {
  File file = client.files().find(e.getPath(), null);
  System.out.println("Uploaded " + file.getSize() + " of " + e.getSize() + " bytes");
}

Checkpoints

Give an upload of a local file a checkpointDirectory, and it keeps a record there that a later run of the same upload continues from. The record holds Files.com's reference for the upload and each part's number, offset, length and, once accepted, ETag. It holds no credentials, URLs or queries. A later run sends nothing from it except the reference, and sends that only where its own call goes. Parts recorded as accepted are not sent again, and are reported by TransferProgress.getResumedBytes(). A part recorded as planned may already have been sent, so it is sent again with exactly the same bytes, and only where Files.com's answer to the later run allows retrying parts (retry_parts). Files.com must still recognize the upload, and, where the SDK chose the part sizes, give the same S3 upload, before any part is sent. Otherwise the run throws TransferCheckpointException with reason UPLOAD_CHANGED, and you must discard the checkpoint and upload the file again from the start. The record is deleted once Files.com completes or refuses the upload.

The record is bound to the API root and workspace of the call, the remote path, the local file's real path, and the file's identity, length and modification time. It keeps only a SHA-256 of the API root, with its scheme, host and port, so no user information, query or fragment in a configured API root is written. If the file has changed, the upload starts over. A run with different part sizing is refused (OTHER_TRANSFER), and so is a second run of the same upload while one holds the record (IN_USE). A record in a format this version of the SDK does not read is refused (UNREADABLE); discard it to start over.

Before the final request is sent, the record notes that it is about to be sent, and that note is made durable first. If it cannot be made durable, the request is not sent (NOT_DURABLE). A later run that finds the note never sends the request again. Neither does a run that finds Files.com no longer recognizes the upload. Both throw UploadCompletionUnknownException without sending anything. Check the file, then call discardUploadCheckpoint before uploading it again.

The directory must be yours and private to you. On Linux and macOS its permissions must be exactly rwx------, with no ACL on macOS. On Windows its protected ACL must grant only you full control. A directory that does not exist yet is created that way. One that exists but is not private, or that is a link, is refused (UNTRUSTED) before anything in it is read. Stream uploads and continueUpload refuse options that set a checkpoint directory. The Windows store has not been run natively in the SDK's own tests. Windows cannot make a new record's directory entry durable, so after a crash there the upload may start over.

Example Request

import com.files.exceptions.UploadCompletionUnknownException;
import com.files.net.UploadOptions;
import java.nio.file.Path;
import java.nio.file.Paths;

Path source = Paths.get("big.iso");
UploadOptions options = UploadOptions.builder()
    .checkpointDirectory(Paths.get(System.getProperty("user.home"), ".files-checkpoints"))
    .build();
try {
  // Run again after a failure or a crash to continue where it stopped.
  client.transfers().upload(source, "images/big.iso", options);
} catch (UploadCompletionUnknownException e) {
  // Check the file at e.getPath(), then start over if it is incomplete.
  client.transfers().discardUploadCheckpoint(source, "images/big.iso", options);
}

Cancellation

Cancel an upload's cancellation token, set its timeout, interrupt the thread that called the upload, or close its client, and the upload stops wherever it is: waiting for the limiter, reading, sending, waiting for an answer or waiting to retry. Its requests are aborted, and once every part in flight has stopped and the upload's own threads have ended, it throws OperationCancelledException. Nothing more is sent, so the upload is not completed, unless its final request had already been sent, which UploadCompletionUnknownException reports.

A read of your own stream that blocks cannot be interrupted in Java 8; the upload stops when it returns. The progress listener runs on the thread that called the upload.

Example Request

import com.files.CancellationToken;
import com.files.exceptions.OperationCancelledException;
import com.files.net.UploadOptions;
import java.nio.file.Paths;
import java.time.Duration;

CancellationToken token = new CancellationToken();
UploadOptions options = UploadOptions.builder()
    .cancellation(token)
    .timeout(Duration.ofHours(2))
    .build();
try {
  client.transfers().upload(Paths.get("archive.zip"), "archive.zip", options);
} catch (OperationCancelledException e) {
  System.out.println("Stopped: " + e.getReason());
}

Downloads

download writes a remote file to a local path. The contents go into a new file in a directory of the download's own, created beside the destination and readable only by you while it holds unchecked bytes. The SDK replaces the destination only after every byte has been received, checked and flushed to storage, and after confirming that the file is still the one the download created. A file put in its place meanwhile is never published, written or deleted. This flush makes the file's contents durable. It does not guarantee that the destination's directory entry survives a power loss.

A file larger than one part (partSize, 8 MiB by default) is first asked for its first part alone. Where the storage server's answer binds the download to one version of the file, with the file's size and a strong entity tag, the rest comes in parts, many at once, as for uploads (concurrencyMode and maxConcurrency; an automatic download from S3 starts at 150 parts at once, within its caps), each written at its own place in the file. Every part is asked for with that tag in If-Match, and must come back as exactly its bytes of exactly that version; otherwise the file changed while it was being downloaded, and the download fails. A part cut short is asked for again from its first byte not yet written. Where the answer binds nothing, the file comes in one response. getFinalProgress() and isVersionBound() on the result say how it went.

Every request for file contents uses the limiter, including a whole-file request, the first request that binds a version, and a checkpoint's version check. Its copy buffer counts against the limiter's memory budget. Whole-file requests count against the scope's shared automatic target, and the SDK uses their results to adjust the target for all compatible downloads. The first version-binding request and a checkpoint's version check run outside that adaptive target, within the limiter.

A response that is shorter than it said, that is not the file's size, or that comes from the Files.com web application fails the download: what it created is deleted and the destination, and every other file, is left as it was. A failed attempt that may be temporary is tried again, up to the client's maxRetries, and the download's timeout covers all of it, describing the file included.

A downloaded file keeps the permissions and group of the file it replaces; a new file gets the permissions new files in that directory normally get. On file systems with ACLs rather than POSIX permissions, such as Windows, a replaced file's ACL is kept and a new file keeps the owner-only access it was staged with. A destination you could not write, or that belongs to another user, is not replaced.

On macOS and Windows, the download creates and holds its directory and file through the operating system, with JNA (net.java.dev.jna:jna, which the SDK depends on): Java's own file API can neither create a macOS directory that inherits no ACL entries nor create a Windows file or directory whose ACL inherits nothing. On macOS, a replaced file's ACL is kept too, and a new file gets the ACL entries that new files in its directory inherit. Where JNA cannot be loaded, a download throws an IOException before it creates anything; the SDK's other calls are unaffected. On macOS and Windows, a download to a volume that does not enforce file ownership or keep ACLs, such as a FAT drive or a macOS volume set to ignore ownership, also throws an IOException, since nothing on it can be kept private. On Java 24 and later, JNA's native access prints a warning unless your application allows it: with --enable-native-access=ALL-UNNAMED when JNA is on the class path, --enable-native-access=com.sun.jna when it is on the module path, or the manifest attribute Enable-Native-Access: ALL-UNNAMED in an executable JAR.

On Windows, JNA writes its native library to a temporary directory and loads it from there, so anyone who could change that directory could run code as your application. If you set that directory with jna.tmpdir, or turn the writing off with jna.nounpack or jna.noclasspath, JNA follows your settings. Otherwise the SDK first checks the directory JNA would write to, and lets JNA write its library only to one that only your application's account and administrators can change: java.io.tmpdir if it is such a directory, or else the account's own AppData\Local\Temp if that one is. The SDK sets jna.tmpdir to the real path of the directory it checked, for the rest of the JVM's life. jna.boot.library.path and jna.nosys=false are kept as you set them, but the directory is checked anyway, since JNA writes its library there when it does not find it where they point. A library found through either setting is loaded as supplied, without being checked by the SDK. Where neither directory qualifies, as can happen when java.io.tmpdir is a shared directory such as C:\Windows\Temp, downloads throw an IOException: set jna.tmpdir to a directory only that account and administrators can change, or, if jna.boot.library.path supplies JNA's library, set jna.nounpack=true.

File.saveAsLocalFile downloads the same way, with the client that returned the File.

Example Request

import com.files.net.DownloadOptions;
import java.nio.file.Paths;

client.transfers().download("reports/2026/report.pdf", Paths.get("report.pdf"));

client.transfers().download("reports/2026/report.pdf", Paths.get("copy.pdf"),
    DownloadOptions.builder().replaceExisting(false).build());

Download Checkpoints

Give a download a checkpointDirectory (the same kind of private directory as an upload's), and a download fetched in parts keeps its partial file when it stops before it is complete. A later download of the same remote file to the same destination, with the same options, continues it. The record holds the partial file's directory and the identities of that directory and its file. It also holds the file's version (its strong entity tag, its size and a SHA-256 of its storage resource) and the ranges the partial file holds. It holds no credentials, URLs or queries, and binds the API root only as a SHA-256. Each later run describes the file again for a fresh download URL.

A later run reopens the partial file only if it is still the one the download created, by its recorded identities, and still private to you. Anything else at its name is refused (TransferCheckpointException with reason STAGE_CHANGED) and left as it is. If it is gone, the download starts over.

The version is confirmed before anything is written. The first missing range, or the last byte, is asked for with If-Match, and must come back as exactly those bytes of exactly that version. If the file changed, the checkpoint forgets the old version before the partial file is emptied, and the download starts over, so bytes of two versions are never mixed. Only the missing ranges are then fetched, and TransferProgress.getResumedBytes() reports the bytes the partial file already held.

While parts are written, what they hold is made durable and recorded at most once a second, and again when the download stops, so a crash costs at most about a second of parts. The record stays small however many parts there are: it holds coalesced ranges, and it is compacted as it grows. A file that comes in one request keeps no progress.

Once the file is published, the checkpoint is deleted. If a run stopped after publishing but before deleting it, the next run finds the destination is the partial file itself. If Files.com still describes the same version, the run finishes without fetching anything; if not, the file is downloaded again under your replaceExisting setting. discardDownloadCheckpoint deletes a checkpoint and the partial file it records, if that is still the download's own.

Some partial files are not removed by the SDK, and you must delete them by hand: one left by a crash before the checkpoint recorded it, one refused as STAGE_CHANGED, and one whose checkpoint became unreadable, as a power loss can leave it. Each is in a hidden directory beside the destination, named after it. The partial file's directory has no Windows directory flush, and the Windows code has not been run natively in the SDK's tests.

Example Request

import com.files.net.DownloadOptions;
import java.nio.file.Paths;

DownloadOptions options = DownloadOptions.builder()
    .checkpointDirectory(Paths.get(System.getProperty("user.home"), ".files-checkpoints"))
    .build();
// Run again after a failure or a crash to continue where it stopped.
client.transfers().download("images/big.iso", Paths.get("big.iso"), options);

// Or start over.
client.transfers().discardDownloadCheckpoint("images/big.iso", Paths.get("big.iso"), options);