XF 2.1 File upload via API

developr

Active member
I want to add avatars to users via the xenforo api.

PHP:
function apiCall($action,$scope,$post,$upload=0) {
     $headers = array(
        'Content-type: application/x-www-form-urlencoded',
        'XF-Api-User: 1',
        'XF-Api-Key: '.APIKEY
     );
     if(!$upload) {
          $post = @http_build_query($post);
     }
     $url = 'https://127.0.0.1/api'.$scope;
     $ch = curl_init();
     if($upload) {
          $file = $post['avatar'];
          $mime = mime_content_type($file);
          $info = pathinfo($file);
          $name = $info['basename'];
          $output = new CURLFile($file, $mime, $name);
          $post['avatar'] = $output;
     }
     curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
     curl_setopt($ch, CURLOPT_URL, $url);
     curl_setopt($ch, CURLOPT_POSTFIELDS,$post);
     curl_setopt($ch, CURLOPT_CUSTOMREQUEST,$action);
     curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
     $json = curl_exec($ch);
     curl_close($ch);
     return json_decode($json);
}

The API will response with "success" but it doesn't work.

The API require "file" in postdata. But how? A link to the file doesn't work. A resource doesn't work. Without an error it's hard to find a solution.

 
Solution
Try and error... now here is the solution.

PHP:
function apiattach($action,$scope,$post) {
     $headers = array(
        'XF-Api-User: 1',
        'XF-Api-Key: '.APIKEY
     );
     $url = 'https://127.0.0.1/api'.$scope;
     $ch = curl_init();
     $headers = array(
        'Content-type: multipart/form-data',
        'XF-Api-User: 1',
        'XF-Api-Key: '.APIKEY
     );
     $file = $post['avatarurl'];
     $mime = mime_content_type($file);
     $info = pathinfo($file);
     $name = $info['basename'];
     $file = new CURLFile($file, $mime, $name);
     $post['avatar'] = $file;
     curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
     curl_setopt($ch, CURLOPT_URL, $url);
     curl_setopt($ch, CURLOPT_POSTFIELDS,$post)...
Try and error... now here is the solution.

PHP:
function apiattach($action,$scope,$post) {
     $headers = array(
        'XF-Api-User: 1',
        'XF-Api-Key: '.APIKEY
     );
     $url = 'https://127.0.0.1/api'.$scope;
     $ch = curl_init();
     $headers = array(
        'Content-type: multipart/form-data',
        'XF-Api-User: 1',
        'XF-Api-Key: '.APIKEY
     );
     $file = $post['avatarurl'];
     $mime = mime_content_type($file);
     $info = pathinfo($file);
     $name = $info['basename'];
     $file = new CURLFile($file, $mime, $name);
     $post['avatar'] = $file;
     curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
     curl_setopt($ch, CURLOPT_URL, $url);
     curl_setopt($ch, CURLOPT_POSTFIELDS,$post);
     curl_setopt($ch, CURLOPT_CUSTOMREQUEST,$action);
     curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
     $json = curl_exec($ch);
     curl_close($ch);
     return json_decode($json);
}
 
Solution
Try and error... now here is the solution.

PHP:
function apiattach($action,$scope,$post) {
     $headers = array(
        'XF-Api-User: 1',
        'XF-Api-Key: '.APIKEY
     );
     $url = 'https://127.0.0.1/api'.$scope;
     $ch = curl_init();
     $headers = array(
        'Content-type: multipart/form-data',
        'XF-Api-User: 1',
        'XF-Api-Key: '.APIKEY
     );
     $file = $post['avatarurl'];
     $mime = mime_content_type($file);
     $info = pathinfo($file);
     $name = $info['basename'];
     $file = new CURLFile($file, $mime, $name);
     $post['avatar'] = $file;
     curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
     curl_setopt($ch, CURLOPT_URL, $url);
     curl_setopt($ch, CURLOPT_POSTFIELDS,$post);
     curl_setopt($ch, CURLOPT_CUSTOMREQUEST,$action);
     curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
     $json = curl_exec($ch);
     curl_close($ch);
     return json_decode($json);
}
Thank you!
 
Hi.

I am trying to call this function


from python code but apparently it does not work.

Here is my code:
Python:
import os, requests
import json
import requests

MY_API_KEY = '...'
FORUM_URL = "https://forums.indiegala.com"

headers             = {     'Content-Type'     : 'multipart/form-data',
                            'XF-Api-User'      : '1',
                            'XF-Api-Key'       : MY_API_KEY }

avatar_url = 'https://www.indiegalacdn.com/imgs/users/1074019/avatar/1588699740.png'

def replace_user_avatar( user_id, avatar ):
    url_to_call             = FORUM_URL +  "/api/users/" + str(user_id) + "/avatar"

    #read raw file here
    response = requests.get(avatar, stream=True)

    data_to_send = {'avatar' : response.raw }
  
    print("calling url: %s, with headers: %s posting data = %s", url_to_call, headers, data_to_send )
    response = requests.post(url_to_call, headers=headers, data=data_to_send  ) 

    print("result = %s", response.text)
              
    if response.status_code == 200:
        response        = json.loads(response.content)
        if response['success']:
            print("replace_user_avatar avatar ok.")
    else:
        print("problem in replace_user_avatar: status code: %s", response.status_code)
        if response.content:
            response_json        = json.loads(response.content)
            print("response : %s", response_json)

test_user_id = 2020
replace_user_avatar(test_user_id, avatar_url )

The problem is that i always get success : true as reply but nothing happens.

Any help would be much much appreciated.
( i tested also with files instead of data in request post but no luck )
 
Last edited by a moderator:
On Python and requests library, you need pass uploadable files as additional arguments files. Example is provided here. You can combine data with files.
Thank you for your reply.

As i said i tried also this 'file' approach:

Python:
....

headers             = {     'Content-Type'     : 'multipart/form-data',
                            'XF-Api-User'      : '1',
                            'XF-Api-Key'       : MY_API_KEY }

avatar_url = 'https://www.indiegalacdn.com/imgs/users/1074019/avatar/1588699740.png'

def replace_user_avatar( user_id, avatar ):
   
    url_to_call             = FORUM_URL +  "/api/users/" + str(user_id) + "/avatar"

    #read raw file here
    response = requests.get(avatar, stream=True)
    #guess mime type
    mime = MimeTypes()
    mime_type = mime.guess_type(avatar)
    #this does not work
    #data_to_send = {'avatar' : response.raw }
    #this neither
    files = {'avatar': ('avatar_name', response.raw, mime_type ) }
             
    print("calling url: %s, with headers: %s posting data = %s", url_to_call, headers, files )
    response = requests.post(url_to_call, headers=headers, files=files  )  

    print("result = %s", response.text)
                 
    if response.status_code == 200:
        response        = json.loads(response.content)
        if response['success']:
            print("replace_user_avatar avatar ok.")
    else:
        print("problem in replace_user_avatar: status code: %s", response.status_code)
        if response.content:
            response_json        = json.loads(response.content)
            print("response : %s", response_json)

test_user_id = 2020

replace_user_avatar(test_user_id, avatar_url )

But the result is the same.
Success = "true"
But no change in user avatar...
 
Note: in documentation example developers used result of executing open() function. In extended documentation, developers noticed on requirement pass "file-like" object to this array. You just pass already fully read content.
Try something like this. I don't write code on Python, but judging by the documentation and requests internal code, this can work:
Python:
headers             = {     'Content-Type'     : 'multipart/form-data',
                            'XF-Api-User'      : '1',
                            'XF-Api-Key'       : MY_API_KEY }

avatar_url = 'https://www.indiegalacdn.com/imgs/users/1074019/avatar/1588699740.png'

def replace_user_avatar( user_id, avatar ):
  
    url_to_call             = FORUM_URL +  "/api/users/" + str(user_id) + "/avatar"

    #read raw file here
    response = requests.get(avatar, stream=True)
    #guess mime type
    mime = MimeTypes()
    mime_type = mime.guess_type(avatar)
    #this does not work
    #data_to_send = {'avatar' : response.raw }
    #this neither
    files = {'avatar': ('avatar_name', response.raw.fileno(), mime_type ) }
            
    print("calling url: %s, with headers: %s posting data = %s", url_to_call, headers, files )
    response = requests.post(url_to_call, headers=headers, files=files  ) 

    print("result = %s", response.text)
                
    if response.status_code == 200:
        response        = json.loads(response.content)
        if response['success']:
            print("replace_user_avatar avatar ok.")
    else:
        print("problem in replace_user_avatar: status code: %s", response.status_code)
        if response.content:
            response_json        = json.loads(response.content)
            print("response : %s", response_json)

test_user_id = 2020

replace_user_avatar(test_user_id, avatar_url )

Or you can just downloaded avatar write somewhere (in /tmp directory, for example), open file in "read-binary" mode and received file handle give to requests as "file content".
 
Note: in documentation example developers used result of executing open() function. In extended documentation, developers noticed on requirement pass "file-like" object to this array. You just pass already fully read content.
Try something like this. I don't write code on Python, but judging by the documentation and requests internal code, this can work:
Python:
headers             = {     'Content-Type'     : 'multipart/form-data',
                            'XF-Api-User'      : '1',
                            'XF-Api-Key'       : MY_API_KEY }

avatar_url = 'https://www.indiegalacdn.com/imgs/users/1074019/avatar/1588699740.png'

def replace_user_avatar( user_id, avatar ):
 
    url_to_call             = FORUM_URL +  "/api/users/" + str(user_id) + "/avatar"

    #read raw file here
    response = requests.get(avatar, stream=True)
    #guess mime type
    mime = MimeTypes()
    mime_type = mime.guess_type(avatar)
    #this does not work
    #data_to_send = {'avatar' : response.raw }
    #this neither
    files = {'avatar': ('avatar_name', response.raw.fileno(), mime_type ) }
           
    print("calling url: %s, with headers: %s posting data = %s", url_to_call, headers, files )
    response = requests.post(url_to_call, headers=headers, files=files  )

    print("result = %s", response.text)
               
    if response.status_code == 200:
        response        = json.loads(response.content)
        if response['success']:
            print("replace_user_avatar avatar ok.")
    else:
        print("problem in replace_user_avatar: status code: %s", response.status_code)
        if response.content:
            response_json        = json.loads(response.content)
            print("response : %s", response_json)

test_user_id = 2020

replace_user_avatar(test_user_id, avatar_url )

Or you can just downloaded avatar write somewhere (in /tmp directory, for example), open file in "read-binary" mode and received file handle give to requests as "file content".

Unfortunately it does not work that way, either
(if i put files in data request parameter i get success but not working, if i put files in 'files' param i get an error - 'long' does not have buffer interface ).


Also, following explicitly requests documentation and sending a file this way, from disk:

Python:
files = {'avatar': open(FILE_ON_DISK, 'rb')}

response = requests.post(url_to_call, headers=headers, files=files )

lends to success, but no change in avatar.


I think the API is bugged at this point, or at least is impossible to make that REST api call with python.
But i could not make it neither with reqbin or other online tools.
I mean, i got "success" : "True" reply, yes, but nothing happened.

IF someone of the xenforo dev team is reading this, you may want to look into that api call:
 
Try and error... now here is the solution.

PHP:
function apiattach($action,$scope,$post) {
     $headers = array(
        'XF-Api-User: 1',
        'XF-Api-Key: '.APIKEY
     );
     $url = 'https://127.0.0.1/api'.$scope;
     $ch = curl_init();
     $headers = array(
        'Content-type: multipart/form-data',
        'XF-Api-User: 1',
        'XF-Api-Key: '.APIKEY
     );
     $file = $post['avatarurl'];
     $mime = mime_content_type($file);
     $info = pathinfo($file);
     $name = $info['basename'];
     $file = new CURLFile($file, $mime, $name);
     $post['avatar'] = $file;
     curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
     curl_setopt($ch, CURLOPT_URL, $url);
     curl_setopt($ch, CURLOPT_POSTFIELDS,$post);
     curl_setopt($ch, CURLOPT_CUSTOMREQUEST,$action);
     curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
     $json = curl_exec($ch);
     curl_close($ch);
     return json_decode($json);
}

Hi @developr

Can you just briefly summarize how this script works.

I mean, Is this meant for more than one user at a time? Does it read a file with the user info first? Are the Avatars in a separate folder?

I'm not that fluent in php yet.

Thanks.
 
Back
Top Bottom