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