Using API to upload media files?

What I wanted was to add form-media for a particular form, using an API endpoint. My program can then call this API every day and attach the CSV file. I tried linking the CSV file in this area after uploading it to a public folder:

But it doesn’t work. I guess it only works for images.

Maybe you could make a post to /api/v1/metadata/ with the following payload:

{
    xform: <xform ID>,  # can be retrieve from `/api/v1/forms.json`
   data_value: "<URL to your CSV file>",
   data_type: "media",
}
1 Like

I’m not able to access the endpoint /api/v1/metadata

Hi @ks_1,

Try /api/v1/metadata.json instead.

2 Likes

Thanks @Kal_Lam and @nolive, I was able to add the URL using the API. But this just created a media URL link in the media section, as I tried manually earlier. And this approach doesn’t help to pull the data from the remote CSV file.

Can you help me to change this payload to attach a file and upload it from the local system?

{
    xform: <xform ID>,  # can be retrieve from `/api/v1/forms.json`
   data_value: "<URL to your CSV file>",
   data_type: "media",
}

Hi @ks_1, here are two ways you can achieve this for a file upload:

  1. Using curl:
curl https://<kc_url>/api/v1/metadata.json -H "Authorization: Token <token>" \
-F "xform=<xform_id>" \
-F "data_value=<filename>" \
-F "data_type=media" \
-F "data_file=@<path/to/file>"
  1. Using a Python script:
import requests

URL = 'https://<kc_url>/api/v1/metadata.json'
TOKEN = '<token>'
FILE_PATH = '<path/to/file>'
FILENAME = '<filename>'
MIME = 'text/csv'
XFORM = '<xform_id>'

headers = {'Authorization': f'Token {TOKEN}'}
files = {'data_file': (FILENAME, open(f'{FILE_PATH}{FILENAME}', 'rb').read(), MIME)}
data = {
    'data_value': FILENAME,
    'xform': XFORM,
    'data_type': 'media',
    'data_file_type': MIME,
}
response = requests.post(URL, data=data, files=files, headers=headers)
2 Likes

@Josh Thank you so much!! The python script worked great! I have a couple more questions:

  1. How do I use the API to delete a file that I’ve uploaded? When I tried to upload the changed file again, I get the error:
    {'non_field_errors': ['The fields xform, data_type, data_value must make a unique set.']}
    So I would need to delete the previous uploaded file before attempting to reupload.

  2. I am guessing that ODK collect will not download the changed CSV unless I redeploy the form. So how can I redeploy this form with the API?

Once again, thanks a lot.

@ks_1, great!

  1. Yes, you have to have unique files. If you look at the return data in the response from the upload, you will see an id field. You can do a DELETE request to /api/v1/metadata/<id> to remove the file. You can get all our form’s data in the response from a GET to /api/v1/forms.json
  2. I’m not certain about how ODK will treat the new CSV, but you can redeploy through the API to check

To redeploy your form:

curl -X PATCH https://<kf url>/api/v2/assets/<form uid>/deployment/?format=json \
-d 'active=true' \
-d 'version_id=<form version id>' \
-H 'Authorization: Token <token>'
2 Likes

You can get your form’s version_id by doing the following:

curl 'https:/<kf url>/api/v2/assets/<form uid>.json' \                                                      
-H 'Authorization: Token <token>'

If you have jq installed, you can find it easily by piping to jq:

curl -s 'https:/<kf url>/api/v2/assets/<form uid>.json' \                                                      
-H 'Authorization: Token <token>' | jq '.version_id'
2 Likes

@Josh It works beautifully! Many thanks.
I did some experimenting and I found that ODK will download the new CSV file if you sync manually, even if you don’t redeploy the form. Here’s the final python script for anyone who wants to programmatically upload the CSV file for pulldata purposes:

    import requests
    import json

    KC_URL = 'https://<kc_url>/api/v1/'
    KF_URL = 'https://<kf_url>/api/v2/'

    TOKEN = '<token>'      
    XFORM = <xform_id>

    FILE_FOLDER = r"<file_folder>"
    FILENAME = '<file_name>'
    MIME = 'text/csv'


    headers = {'Authorization': f'Token {TOKEN}'}
    files = {'data_file': (FILENAME, open(fr'{FILE_FOLDER}\{FILENAME}', 'rb').read(), MIME)}
    data = {
        'data_value': FILENAME,
        'xform': XFORM,
        'data_type': 'media',
        'data_file_type': MIME,
    }

    # Download metadata.json
    response = requests.get(fr"{KC_URL}/metadata.json", headers=headers)
    dict_response = json.loads(response.text)

    # Delete appropriate entry in the metadata.json (delete old file)
    for each in dict_response:
        if each['xform'] == XFORM and each['data_value'] == FILENAME:
            del_id = each['id']
            response = requests.delete(fr"{KC_URL}/metadata/{del_id}", headers=headers)
            break

    # Upload the changed file
    response = requests.post(fr"{KC_URL}/metadata.json", data=data, files=files, headers=headers)
4 Likes

Awesome! Thanks for sharing :muscle:

1 Like

My pleasure. Is there any detailed documentation about all the available API endpoints?

Hi @ks_1,

We don’t have a great central location with all the API endpoints and their methods (something that we’ll hopefully get sorted soon), but you can have a look at some of these locations to get started:

2 Likes

One of the posts in the community shows how to upload media files (csv).

Using API to upload media files?

I would like to ask if I upload a csv file as an update to a file already in the server, would it overwrite that file? If not then how do I delete media files in the server using API so that I can upload an update?

Thanks

@raffy_m
You need to delete the old file. I have commented the section in the code where it does this.

1 Like

I have been trying to post a csv file in c#. Here is my code:

    private async Task<HttpResponseMessage> UploadMediaToServer(FileInfo file)
    {
        HttpResponseMessage result = null;
        string baseURL = "https://kc.kobotoolbox.org/api/v1/metadata.json";


        using (var httpClient = new HttpClient())
        {
            using (var request = new HttpRequestMessage(new HttpMethod("POST"), baseURL))
            {
                request.Headers.TryAddWithoutValidation("Authorization", "Token <token>");
                var multipartContent = new MultipartFormDataContent();
                multipartContent.Headers.Add("xform", _formID);
                multipartContent.Headers.Add("data_value", file.Name);
                multipartContent.Headers.Add("data_type", "media");
                multipartContent.Headers.ContentType = MediaTypeHeaderValue.Parse("text/csv");
                multipartContent.Add(new ByteArrayContent(File.ReadAllBytes(file.FullName)), "data_file", file.Name);
                request.Content = multipartContent;

                result = await httpClient.SendAsync(request);
            }
        }
        return result;
    }

I keep on getting a result with status code 415 - unsupported media type.

Any help is greatly appreciated. Thanks

@raffy_m Try adding

‘data_file_type’: ‘text/csv’

to the header

1 Like

When i add
'data_file_type’: ‘text/csv'

I get a 400 status code or Bad Request.

Do you think it has something to do with how the payload is encoded using ByteArrayContent?

I also tried this:

byte[] bytes = File.ReadAllBytes(file.FullName);
HttpContent fileContent = new ByteArrayContent(bytes);
multipartContent.Add(fileContent);
request.Content = multipartContent;

Thanks

Hi @raffy_m,

My guess is that your issue lies here:

multipartContent.Add(new ByteArrayContent(File.ReadAllBytes(file.FullName)), "data_file", file.Name);

Note the structure of this from the Python code — the content of data_file is within a tuple:

{'data_file': (<filename>, <bites>, <mime>)}

i.e.

{'data_file': (FILENAME, open(fr'{FILE_FOLDER}\{FILENAME}', 'rb').read(), MIME)}
1 Like

I was able to solve it.

@Josh posted this curl script earlier:

curl https://<kc_url>/api/v1/metadata.json -H “Authorization: Token ”
-F “xform=<xform_id>”
-F “data_value=”
-F “data_type=media”
-F “data_file=@<path/to/file>”

I used this online C# to curl converter and this is the code i got:

using (var httpClient = new HttpClient())
{
using (var request = new HttpRequestMessage(new HttpMethod("POST"), "http://https//<kc_url>/api/v1/metadata.json"))
{
request.Headers.TryAddWithoutValidation("Authorization", "Token <token>");

    var multipartContent = new MultipartFormDataContent();
    multipartContent.Add(new StringContent(File.ReadAllText("xform_id>")), "xform");
    multipartContent.Add(new StringContent(File.ReadAllText("filename>")), "data_value");
    multipartContent.Add(new StringContent("media"), "data_type");
    multipartContent.Add(new ByteArrayContent(File.ReadAllBytes("<path/to/file>")), "data_file", Path.GetFileName("<path/to/file>"));
    request.Content = multipartContent; 

    var response = await httpClient.SendAsync(request);
}

}

Some lines did not translate well:

multipartContent.Add(new StringContent(File.ReadAllText("xform_id>")), "xform");
multipartContent.Add(new StringContent(File.ReadAllText("filename>")), "data_value");

These should have been

multipartContent.Add(new StringContent("xform_id>"), "xform");
multipartContent.Add(new StringContent("filename>"), "data_value");

This is the working code that I use now:

    private async Task<HttpResponseMessage> UploadMediaToServer(FileInfo file)
    {
        HttpResponseMessage result = null;
        string baseURL = "https://kc.kobotoolbox.org/api/v1/metadata.json";
        using (var httpClient = new HttpClient())
        {
            using (var request = new HttpRequestMessage(new HttpMethod("POST"), baseURL))
            {
                var base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_user}:{_password}"));
                request.Headers.TryAddWithoutValidation("Authorization", $"Basic {base64authorization}");

                var multipartContent = new MultipartFormDataContent();
                multipartContent.Add(new StringContent(_formID), "xform");
                multipartContent.Add(new StringContent(file.Name), "data_value");
                multipartContent.Add(new StringContent("media"), "data_type");
                multipartContent.Add(new StringContent("text/csv"), "data_file_type");
                multipartContent.Add(new ByteArrayContent(File.ReadAllBytes(file.FullName)), "data_file", file.Name);
                request.Content = multipartContent;

                result = await httpClient.SendAsync(request);
            }
        }
        return result;
    }

The status code returned is 201, meaning it works perfectly.

Requerying the form metadata, the `text/csv’ attribute of the updated CSV file is not recognized as shown by the JSON text below:

{"url":"https://kc.kobotoolbox.org/api/v1/metadata/1331767?format=json",
"id":1331767,
"xform":<xformID>,
"data_value":"enumerator_select.csv",
"data_type":"media",
"data_file":"https://kobocat-s3.s3.amazonaws.com/<form_username>/form-media/<long random string>",
"data_file_type":"", CSV ATTRIBUTE IS NOT RECOGNIZED
"file_hash":"md5:be30dc27789387659818e428b63d867a"},

This is the json text of a csv file that was not updated. The text/csv value is still there.

{"url":"https://kc.kobotoolbox.org/api/v1/metadata/1299805?format=json", "id":1299805,
"xform":<xformID>,
"data_value":"gear.csv",
"data_type":"media",
"data_file":"https://kobocat-s3.s3.amazonaws.com/<form_username>/form-media/<long random string>",
"data_file_type":"text/csv",
"file_hash":"md5:e89a4c6e61e958a5e7b3a624fc7fa8f1"},

Is the missing attribute something that should be of concern?

Thanks

1 Like