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

developr

Active member
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

BubbaLovesCheese

Active member
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!
 

Indiegala

Member
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:

Kruzya

Well-known member
On Python and requests library, you need pass uploadable files as additional arguments files. Example is provided here. You can combine data with files.
 

Indiegala

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

Kruzya

Well-known member
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".
 

Indiegala

Member
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:
 

BubbaLovesCheese

Active member
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.
 
Top