web-dev-qa-db-fra.com

Python POST données binaires

J'écris du code pour faire l'interface avec Redmine et j'ai besoin de télécharger des fichiers dans le cadre du processus, mais je ne sais pas comment faire une demande de POST de python contenant un fichier binaire.

J'essaie d'imiter les commandes ici :

curl --data-binary "@image.png" -H "Content-Type: application/octet-stream" -X POST -u login:password http://redmine/uploads.xml

Dans python (ci-dessous), mais cela ne semble pas fonctionner. Je ne sais pas si le problème est lié au codage du fichier ou si quelque chose ne va pas avec les en-têtes.

import urllib2, os

FilePath = "C:\somefolder\somefile.7z"
FileData = open(FilePath, "rb")
length = os.path.getsize(FilePath)

password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(None, 'http://redmine/', 'admin', 'admin')
auth_handler = urllib2.HTTPBasicAuthHandler(password_manager)
opener = urllib2.build_opener(auth_handler)
urllib2.install_opener(opener)
request = urllib2.Request( r'http://redmine/uploads.xml', FileData)
request.add_header('Content-Length', '%d' % length)
request.add_header('Content-Type', 'application/octet-stream')
try:
    response = urllib2.urlopen( request)
    print response.read()
except urllib2.HTTPError as e:
    error_message = e.read()
    print error_message

J'ai accès au serveur et cela ressemble à une erreur d'encodage:

...
invalid byte sequence in UTF-8
Line: 1
Position: 624
Last 80 unconsumed characters:
7z¼¯'ÅÐз2^Ôøë4g¸R<süðí6kĤª¶!»=}jcdjSPúá-º#»ÄAtD»H7Ê!æ½]j):

(further down)

Started POST "/uploads.xml" for 192.168.0.117 at 2013-01-16 09:57:49 -0800
Processing by AttachmentsController#upload as XML
WARNING: Can't verify CSRF token authenticity
  Current user: anonymous
Filter chain halted as :authorize_global rendered or redirected
Completed 401 Unauthorized in 13ms (ActiveRecord: 3.1ms)
40
Mac

Fondamentalement, ce que vous faites est correct. En regardant les documents redmine auxquels vous avez lié, il semble que le suffixe après le point dans l'URL indique le type de données postées (.json pour JSON, .xml pour XML), ce qui est en accord avec la réponse que vous obtenez - Processing by AttachmentsController#upload as XML. Je suppose qu’il ya peut-être un bogue dans la documentation et que pour publier des données binaires, vous devriez essayer d’utiliser http://redmine/uploads url au lieu de http://redmine/uploads.xml.

Btw, je recommande fortement très bon et très populaire Requests bibliothèque pour http en Python. C'est beaucoup mieux que ce qui est dans la bibliothèque standard (urllib2). Il prend également en charge l'authentification, mais je l'ai ignoré par souci de concision.

import requests

data = open('./x.png', 'rb').read()
res = requests.post(url='http://httpbin.org/post',
                    data=data,
                    headers={'Content-Type': 'application/octet-stream'})

# let's check if what we sent is what we intended to send...
import json
import base64

assert base64.b64decode(res.json()['data'][len('data:application/octet-stream;base64,'):]) == data

MISE À JOUR

Pour savoir pourquoi cela fonctionne avec Requests mais pas avec urllib2, nous devons examiner la différence entre ce qui est envoyé. Pour voir ceci, j'envoie du trafic au proxy http (Fiddler) s'exécutant sur le port 8888:

Utiliser les demandes

import requests

data = 'test data'
res = requests.post(url='http://localhost:8888',
                    data=data,
                    headers={'Content-Type': 'application/octet-stream'})

nous voyons

POST http://localhost:8888/ HTTP/1.1
Host: localhost:8888
Content-Length: 9
Content-Type: application/octet-stream
Accept-Encoding: gzip, deflate, compress
Accept: */*
User-Agent: python-requests/1.0.4 CPython/2.7.3 Windows/Vista

test data

et en utilisant urllib2

import urllib2

data = 'test data'    
req = urllib2.Request('http://localhost:8888', data)
req.add_header('Content-Length', '%d' % len(data))
req.add_header('Content-Type', 'application/octet-stream')
res = urllib2.urlopen(req)

on a

POST http://localhost:8888/ HTTP/1.1
Accept-Encoding: identity
Content-Length: 9
Host: localhost:8888
Content-Type: application/octet-stream
Connection: close
User-Agent: Python-urllib/2.7

test data

Je ne vois aucune différence qui justifierait un comportement différent de celui que vous observez. Cela dit, il n’est pas rare que les serveurs http inspectent User-Agent en-tête et modifie le comportement en fonction de sa valeur. Essayez de changer les en-têtes envoyés par les requêtes une par une en les rendant identiques à ceux envoyés par urllib2 et de voir quand cela cesse de fonctionner.

49
Piotr Dobrogost

Cela n'a rien à voir avec un téléchargement mal formé. L'erreur HTTP indique clairement 401 non autorisé et vous indique que le jeton CSRF n'est pas valide. Essayez d'envoyer un jeton CSRF valide avec le téléchargement.

Plus d'informations sur les jetons csrf ici:

Qu'est-ce qu'un jeton CSRF? Quelle est son importance et comment fonctionne-t-il?

2
Josh Liptzin

Vous pouvez utiliser nirest , il fournit une méthode simple pour poster une demande. `

import unirest

def callback(response):
 print "code:"+ str(response.code)
 print "******************"
 print "headers:"+ str(response.headers)
 print "******************"
 print "body:"+ str(response.body)
 print "******************"
 print "raw_body:"+ str(response.raw_body)

# consume async post request
def consumePOSTRequestASync():
 params = {'test1':'param1','test2':'param2'}

 # we need to pass a dummy variable which is open method
 # actually unirest does not provide variable to shift between
 # application-x-www-form-urlencoded and
 # multipart/form-data

 params['dummy'] = open('dummy.txt', 'r')
 url = 'http://httpbin.org/post'
 headers = {"Accept": "application/json"}
 # call get service with headers and params
 unirest.post(url, headers = headers,params = params, callback = callback)


# post async request multipart/form-data
consumePOSTRequestASync()

`

Vous pouvez vérifier l'exemple complet à l'adresse http://stackandqueue.com/?p=57

0
gvir

vous devez ajouter un en-tête Content-Disposition, comme ceci (bien que j'utilise mod-python ici, mais le principe devrait être le même):

request.headers_out['Content-Disposition'] = 'attachment; filename=%s' % myfname
0
mrkafk