Casey Houser

Home Resume Articles

Upload to File.io With C and Libcurl

File.io offers a simple web service for uploading files and sharing links to those files.

This article walks through the functionality of a short C script I wrote that connects to the File.io API to upload a file from my terminal.

Along the way, I describe my use of the libcurl URL transfer library and cJSON JSON parsing library that do the heavy lifting of web connection and data presentation in my script.

How the File.io API Works

Using libcurl to POST

Using cJSON to Parse

Full C Script

How the File.io API Works

In addition to the File.io web interface, File.io opens its API to developers. The company offers a free service tier that anyone can access without a signup, and in that tier, you can use the API to upload a file and (optionally) modify its expiration date (from the default of 12 days to any time between 1 day and 1 year).

You could easily do this with the curl command line utility. This example code would upload a file to File.io with an expiration date of 1 day. The file parameter must be the full file path you wish to upload, and the expiration date should match the format ^[1-9]d*[y|Q|M|w|d|h|m|s]$, like 1d as 1 day or 1M as 1 month:

curl -X POST https://file.io -F file=@/path/to/file.txt -F expires=1d

With -X, you specify the request type. This POST to File.io says you want to upload data to that site.

With -F, you specify the MIME data that curl should send. Two uses of -F list our file and expiration.

This request would return a JSON string like:

{"success":true,"status":200,"id":"12345678-abcd-1234-ijkl-abcdef123456","key":"a1b2c3d4e5f6,"path":"/","nodeType":"file","name":"file.txt","title":null,"description":null,"size":100,"link":"file.io/a1b2c3d4e5f6","private":false,"expires":"2024-11-20T02:30:10.277Z","downloads":0,"maxDownloads":1,"autoDelete":true,"planId":0,"screeningStatus":"pending","mimeType":"text/plain","created":"2024-11-19T02:30:10.277Z","modified":"2024-11-19T02:30:10.277Z"}

File.io gives you two base options when completing a POST operation:

  • file (required), string => file path, e.g. “/path/to/file.txt”
  • expires, string => matches pattern ^[1-9]d*[y|Q|M|w|d|h|m|s]$, e.g. “1d”

Paid plans offer two more options:

  • maxDownloads, integer => e.g. 5
  • autoDelete, boolean => e.g. “false”

A more extensive request would include more options, but its return would look similar to the example shown above.

curl -X POST https://file.io -F file=@/path/to/file.txt -F expires=1d -F maxDownloads=5 -F autoDelete=false

Using libcurl to POST (back to top)

The C programming language doesn’t include a library for reaching the internet. Developers often use libcurl for that functionality. This is what my C script uses.

Now that you’ve seen what curl can do, it’s easy enough to see the jump to libcurl.

The subheaders throughout the rest of this article match the comments provided in my script.

Keep in mind that this script is run like ./myscript , or more concretely, ./myscript /path/to/file.txt 1d. It takes two (required) command line arguments.

Initialize Libcurl

/** INITIALIZE LIBCURL **/
CURL *easy_handle = curl_easy_init(); /* handle; used as input for other "easy curl" functions */

To complete any kind of process with libcurl, you first need to initialize it. For one-time requests like I discussed above with curl, you can use the curl_easy_init() function.

Libcurl offers a choice between the easy or multi interface. This script uses the easy type, so you will see that “easy” name referenced in many parts of the code.

Here, I tell curl_easy_init() to initialize a special object type, CURL, that libcurl provides.

Use of the CURL type tells libcurl to create a handle that you will use later as an input for other easy curl operations.

This handle is an abstracted reference to many networking operations libcurl is capable of completing. Think of it like a messenger; you can tell CURL which operations to perform, and you don’t need to know how to setup and tear down a network transfer or deal with any other particulars.

My script names this CURL handle easy_handle.

Create HTTP Form

/** CREATE HTTP FORM **/
curl_mime *mime;                         /* empty mime struct */
curl_mimepart *file;                     /* empty mimepart struct */
curl_mimepart *expires;                  /* empty mimepart struct */

mime = curl_mime_init(easy_handle);      /* assign handle to the struct */
  
file = curl_mime_addpart(mime);          /* assign part to our mime handle */
curl_mime_filedata(file, argv[1]);       /* add data to part */
curl_mime_name(file, "file");            /* name data */

expires = curl_mime_addpart(mime);       /* assign part to our mime handle */
curl_mime_data(expires, argv[2], CURL_ZERO_TERMINATED);
curl_mime_name(expires, "expires");

In order to complete a POST request, we need to build our MIME data. A few variables begin the process:

  • mime uses the libcurl built-in type curl_mime to create a struct that will carry all the MIME data libcurl needs to complete a POST
  • file and expires use the built-in type curl_mimepart to create structs that hold details of a subsection of the overall MIME data

Again, libcurl makes initialization simple with the curl_mime_init function, which assigns a handle to the struct. The variable mime is that handle.

Now the script builds its MIME parts. file uses curl_mime_addpart to create a new part in the mime struct. The libcurl functions curl_mime_filedata and curl_mime_name, respectively, set the file data and name of the file data. This script accepts the command line argument argv[1] for the file location. Each of those libcurl functions take two arguments: the MIME part and the data that part should reference.

Similarly, expires uses curl_mime_data to create a second MIME part. curl_mime_data takes three arguments: the MIME part, file data, and the size of the data (with CURL_ZERO_TERMINATED indicating that this is a null-terminated string).

Initialize Buffer Struct

/** INITIALIZE BUFFER STRUCT **/
struct memory fileio_obj = {0};

This script needs memory to hold information returned from the File.io server.

In a separate part of the script, you will find this struct and the only function outside of main(): custom_callback.

Memory Struct and Callback

struct memory {
  char *response;
  size_t size;
};
 
static size_t custom_callback(char *data, size_t size, size_t nmemb, void *clientp) {
  size_t realsize = size * nmemb;
  struct memory *mem = (struct memory *)clientp;
 
  char *ptr = realloc(mem->response, mem->size + realsize + 1);
  if(!ptr)
    return 0;  /* out of memory */
 
  mem->response = ptr;
  memcpy(&(mem->response[mem->size]), data, realsize);
  mem->size += realsize;
  mem->response[mem->size] = 0;
 
  return realsize;
}

This part of the script is taken verbatim from the CURLOPT_WRITEFUNCTION section of the libcurl API. Libcurl generously writes example code for most of its functions, and this one in particular doesn’t need any alterations from me.

Libcurl will, by default, gather return data from the server and print it to standard out. You would see everything appear in the terminal window. In the case of sending a POST to File.io, you would see the returned JSON without formatting. This is useful, but it isn't pretty.

This function (I’ve renamed custom_callback) lets you alter what happens to the server's data after it has been received.

custom_callback gathers response data from the remote server in data, copies that data to clientp, and returns the size of the data.

Set Curl Options for Output

/** SET CURL OPTIONS FOR OUTPUT **/
/*  all transaction data is sent to a callback function
 *  this function allows our custom_callback function to process that data */
curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, custom_callback);
/* our custom_callback copies transaction data to our struct: fileio_obj */
curl_easy_setopt(easy_handle, CURLOPT_WRITEDATA, (void *)&fileio_obj);

Changing the libcurl option of CURLOPT_WRITEFUNCTION reroutes all input to custom_callback. Then with a second option change to CURLOPT_WRITEDATA, I tell custom_callback to transfer all the server data to an object I control, fileio_obj.

Remember that fileio_obj is the memory struct defined above that contains parts for the response data and its size.

My script now edits those libcurl options with use of the built-in curl_easy_setopt function.

Uses of this function require three arguments: The CURL handle created at the beginning of the script, the libcurl option to change, and a parameter that the option can work with.

Set Curl Options for Data POST

/** SET CURL OPTIONS FOR DATA POST **/
/* data to post */
curl_easy_setopt(easy_handle, CURLOPT_MIMEPOST, mime);
/* url to reach */
curl_easy_setopt(easy_handle, CURLOPT_URL, "https://file.io/");

The script also changes a few other options.

It tells libcurl that its request should be a POST that references the mime handle that includes all the MIME data necessary for the request. It also tells the POST to reach the File.io URL.

Send Data to URL

/** SEND DATA TO URL **/
curl_easy_perform(easy_handle);

Finally, the script can complete the request. It uses the built-in function curl_easy_perform with a single argument --- the handle --- to start the POST.

Using cJSON to Parse (back to top)

With a successful return, the script can now parse the data into useful output to screen.

The server will return data in the JSON format, but C has no included JSON parsing capability.

I used cJSON to manage it.

Parse JSON

/** PARSE JSON **/
cJSON *parsed_response = cJSON_Parse(fileio_obj.response);
const cJSON *file_name = cJSON_GetObjectItemCaseSensitive(parsed_response, "name");
const cJSON *file_link = cJSON_GetObjectItemCaseSensitive(parsed_response, "link");
char *name = cJSON_Print(file_name);
char *link = cJSON_Print(file_link);

cJSON, like libcurl, also makes its own types and provides several helper functions.

The parsed_response, file_name, and file_link variables here all become cJSON structs.

cJSON_Parse creates a tree of items from the input --- the function’s only argument.

cJSON_GetObjectItemCaseSensitive looks for an item by its key and returns its value.

cJSON_Print creates a string from the result of a value lookup.

Print to Screen

/** PRINT TO SCREEN **/
printf("Filename: %s\n", name);
printf("Link: %s\n", link);

Now it’s possible to print some values to screen.

This script doesn’t do much at this point. After libcurl completes the request and cJSON parses the return, the user sees only the uploaded file and link where it can be accessed.

There’s no error handling here, so if it request goes south, the script only reports the name “ApiError” as its Filename.

Housekeeping

/** FREE THE BUFFER **/
free(fileio_obj.response);
  
/** CURL CLEANUP **/
curl_easy_cleanup(easy_handle);
curl_mime_free(mime);

The only responsibility left is for the script to free its memory buffer and tell libcurl that its tasks are complete.

curl_easy_cleanup blanks the networking handle, and curl_mime_free blanks the handle for its MIME data.

Full C Script (back to top)

#include <stdio.h>
#include <curl/curl.h>
#include <cjson/cJSON.h>

/* custom_callback function uses these */
#include <stdlib.h>
#include <string.h>

struct memory {
  char *response;
  size_t size;
};
 
static size_t custom_callback(char *data, size_t size, size_t nmemb, void *clientp) {
  size_t realsize = size * nmemb;
  struct memory *mem = (struct memory *)clientp;
 
  char *ptr = realloc(mem->response, mem->size + realsize + 1);
  if(!ptr)
    return 0;  /* out of memory */
 
  mem->response = ptr;
  memcpy(&(mem->response[mem->size]), data, realsize);
  mem->size += realsize;
  mem->response[mem->size] = 0;
 
  return realsize;
}

int main(int argc, char *argv[]) {
  /** QUICK CHECK FOR COMMAND SYNTAX **/
  if (argc != 3) {
    printf("Usage: Uploader <filename> <expiration>\n");
    exit(-1);
  }


  /** INITIALIZE LIBCURL **/
  CURL *easy_handle = curl_easy_init(); /* handle; used as input for other "easy curl" functions */


  /** CREATE HTTP FORM **/
  curl_mime *mime;                         /* empty mime struct */
  curl_mimepart *file;                     /* empty mimepart struct */
  curl_mimepart *expires;                  /* empty mimepart struct */

  mime = curl_mime_init(easy_handle);      /* assign handle to the struct */
  file = curl_mime_addpart(mime);          /* assign part to our mime handle */

  curl_mime_filedata(file, argv[1]);       /* add data to part */
  curl_mime_name(file, "file");            /* name data */

  expires = curl_mime_addpart(mime);       /* assign part to our mime handle */
  curl_mime_data(expires, argv[2], CURL_ZERO_TERMINATED);
  curl_mime_name(expires, "expires");


  /** INITIALIZE BUFFER STRUCT **/
  struct memory fileio_obj = {0};


  /** SET CURL OPTIONS FOR OUTPUT **/
  /*  all transaction data is sent to a callback function
   *  this function allows our custom_callback function to process that data */
  curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, custom_callback);
  /* our custom_callback copies transaction data to our struct: fileio_obj */
  curl_easy_setopt(easy_handle, CURLOPT_WRITEDATA, (void *)&fileio_obj);


  /** SET CURL OPTIONS FOR DATA POST **/
  /* data to post */
  curl_easy_setopt(easy_handle, CURLOPT_MIMEPOST, mime);
  /* url to reach */
  curl_easy_setopt(easy_handle, CURLOPT_URL, "https://file.io/");
  

  /** SEND DATA TO URL **/
  curl_easy_perform(easy_handle);


  /** PARSE JSON **/
  cJSON *parsed_response = cJSON_Parse(fileio_obj.response);
  const cJSON *file_name = cJSON_GetObjectItemCaseSensitive(parsed_response, "name");
  const cJSON *file_link = cJSON_GetObjectItemCaseSensitive(parsed_response, "link");
  char *name = cJSON_Print(file_name);
  char *link = cJSON_Print(file_link);


  /** PRINT TO SCREEN **/
  printf("Filename: %s\n", name);
  printf("Link: %s\n", link);


  /** FREE THE BUFFER **/
  free(fileio_obj.response);


  /** CURL CLEANUP **/
  curl_easy_cleanup(easy_handle);
  curl_mime_free(mime);

  return 0;
}