File Attachments

Some operations take a file as a parameter: a user's avatar_file, a behavior's attachment_file, a bundle's watermark_attachment_file, a site's logo_file, its icons and its login page background, and a style's file. Give such a parameter a FileAttachment:

  • FileAttachment.FromFile(path) sends a local file, under its own name unless you give another.
  • FileAttachment.FromBytes(bytes, fileName) sends bytes in memory. The array is not copied. Leave it unchanged until the operation ends.
  • FileAttachment.FromStream(stream, fileName) sends the rest of a stream of yours, from its position when the operation starts.

Each also takes the file's media type, such as image/png; without one, it is sent as application/octet-stream. The file name is sent exactly as given. Files.com still validates the file and its name as it does for any upload.

A model's file property, such as User.AvatarFile, takes a FileAttachment too, which SaveAsync sends with the object's other changes.

Example Request

using FilesCom;
using FilesCom.Models;

await using FilesClient client = FilesClient.Create(config);

User user = await client.Users.UpdateAsync(userId, new Dictionary<string, object>
{
    { "avatar_file", FileAttachment.FromFile("avatar.png", contentType: "image/png") },
    { "notes", "New picture" },
});

Site site = await client.Sites.UpdateAsync(new Dictionary<string, object>
{
    { "logo_file", FileAttachment.FromBytes(logoBytes, "logo.png", "image/png") },
    { "icon16_file", FileAttachment.FromStream(iconStream, "icon16.png", "image/png") },
});

How Files Are Sent

The files go in the same request as the operation's other parameters, and every other parameter keeps the exact value it would have without a file: false, 0, empty text, null inside an object, empty lists and objects, and numbers or booleans inside an object alike.

  • When form fields can represent every value exactly, and the file names unchanged, the request is multipart/form-data, with each file as a part of its own.
  • Otherwise the request is JSON, in which each file is the API's file object, with its bytes as Base64 in encoded_content, its name in filename and its media type in type. The request is then about a third larger than the files. That is the case for an empty list or object, null or a number or boolean inside an object, a list of objects, and a file name with %, \, / or ", which a form would change.

Either way, files are read in chunks as the request is sent, and never held whole in memory for it, and the request states its exact length. A large request is timed as an upload is: WriteTimeout limits how long it may go without sending any data, however long it takes in all.

A file parameter also takes the API's file object itself, a Dictionary<string, object> with encoded_content, filename and type, which is sent as it is. A FileAttachment outside a file parameter, including one nested in another parameter's dictionary, throws InvalidParameterException before the request is sent.

Exact Values With A File

// Sent as JSON, since a form cannot carry an empty list or a number inside an object.
await client.Bundles.CreateAsync(new Dictionary<string, object>
{
    { "paths", new string[0] },
    { "watermark_value", new Dictionary<string, object> { { "transparency", 25 } } },
    { "watermark_attachment_file", FileAttachment.FromFile("watermark.png") },
});

Retries, Redirects and Failures

A request that is sent again, after a failure that may be temporary, a redirect that repeats it, or a move to the site's region, sends the same bytes. A request with files that can change something, such as an update, is sent again only when it certainly was not carried out, such as after a move to the site's region: after a lost answer, or an error such as 503, it is not, and says it may have taken effect (see Maximum Network Retries under Configuration). So files and streams must keep their contents and length until the operation ends:

  • A local file is opened when the operation starts and read through that one handle for every attempt. A file whose length changes fails the operation with an IOException before it is sent again.
  • A stream that can seek is read in place, from its starting position, for each attempt, and must keep its length.
  • A stream that cannot seek is first copied, up to maxBytes (64 MiB unless you choose otherwise), to a temporary file of the operation's own, readable only by you where the system allows. A longer stream fails the operation with InvalidParameterException before anything is sent.

A failure to read a file or stream, such as an IOException, is thrown as it is, and the request is not sent again, even when retries remain. A file that cannot be opened fails the same way, before anything is sent, with .NET's own exception, such as System.IO.FileNotFoundException. FilesCom.FileNotFoundException represents the API error. If both namespaces are in scope, use the fully qualified exception name in your catch clause, or catch IOException for local file failures.

The operation closes files it opened and deletes its temporary files when it ends, whether it succeeds or fails. A deletion failure is logged and does not replace the operation's result or exception. Streams you provide remain open. Their final position is determined by the SDK.

The SDK's DEBUG log shows the parameters of a request with files, with each file's length in place of its bytes, which are never logged.