Formato correcto multipart / form-cuerpo de datos

Introducción

Fondo

Estoy escribiendo una secuencia de comandos para cargar elementos, incluidos los archivos que utilizan el tipo de contenido de multipart/form-data definido en RFC 2388 . A largo plazo, estoy tratando de proporcionar un script Python simple para realizar cargas de paquetes binarios para github , que involucra el envío de datos similares a formularios a Amazon S3.

Relacionado

Esta pregunta ya se ha preguntado acerca de cómo hacer esto, pero no tiene una respuesta aceptada hasta ahora, y la más útil de las dos respuestas actualmente tiene puntos para estas recetas, que a su vez generan el mensaje completo manualmente. Estoy algo preocupado por este enfoque, particularmente con respecto a los conjuntos de caracteres y el contenido binario.

También está esta pregunta , con su respuesta actual con la puntuación más alta que sugiere el módulo MultipartPostHandler . Pero eso no es muy diferente de las recetas que mencioné, y por lo tanto mis preocupaciones también se aplican a eso.

Preocupaciones

Contenido binario

La Sección 4.3 de RFC 2388 establece explícitamente que se espera que el contenido sea de 7 bits a menos que se declare lo contrario y, por lo tanto, se puede requerir un Content-Transfer-Encoding . ¿Eso significa que tendría que codificar en base64 el contenido de un archivo binario? ¿O sería Content-Transfer-Encoding: 8bit sería suficiente para archivos arbitrarios? ¿O debería leer Content-Transfer-Encoding: binary ?

Conjunto de caracteres para campos de encabezado

Los campos de encabezado en general, y el campo de encabezado de filename en particular, son ASCII solo de manera predeterminada. Me gustaría que mi método también pueda pasar nombres de archivos que no sean ASCII. Sé que para mi aplicación actual de cargar cosas para github, probablemente no lo necesitaré, ya que el nombre del archivo aparece en un campo separado. Pero me gustaría que mi código fuera reutilizable, por lo que prefiero codificar el parámetro de nombre de archivo de forma conforme. RFC 2388 La sección 4.4 informa sobre el formato introducido en RFC 2231 , por ejemplo, filename*=utf-8''t%C3%A4st.txt .

Mi acercamiento

Usando bibliotecas de python

Como multipart/form-data es esencialmente un tipo MIME, pensé que debería ser posible usar el paquete de email de las bibliotecas estándar de Python para redactar mi publicación. El manejo bastante complicado de los campos de encabezado no ASCII en particular es algo que me gustaría delegar.

Trabajar hasta ahora

Así que escribí el siguiente código:

 #!/usr/bin/python3.2 import email.charset import email.generator import email.header import email.mime.application import email.mime.multipart import email.mime.text import io import sys class FormData(email.mime.multipart.MIMEMultipart): def __init__(self): email.mime.multipart.MIMEMultipart.__init__(self, 'form-data') def setText(self, name, value): part = email.mime.text.MIMEText(value, _charset='utf-8') part.add_header('Content-Disposition', 'form-data', name=name) self.attach(part) return part def setFile(self, name, value, filename, mimetype=None): part = email.mime.application.MIMEApplication(value) part.add_header('Content-Disposition', 'form-data', name=name, filename=filename) if mimetype is not None: part.set_type(mimetype) self.attach(part) return part def http_body(self): b = io.BytesIO() gen = email.generator.BytesGenerator(b, False, 0) gen.flatten(self, False, '\r\n') b.write(b'\r\n') b = b.getvalue() pos = b.find(b'\r\n\r\n') assert pos >= 0 return b[pos + 4:] fd = FormData() fd.setText('foo', 'bar') fd.setText('täst', 'Täst') fd.setFile('file', b'abcdef'*50, 'Täst.txt') sys.stdout.buffer.write(fd.http_body()) 

El resultado se ve así:

 --===============6469538197104697019== Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: base64 Content-Disposition: form-data; name="foo" YmFy --===============6469538197104697019== Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: base64 Content-Disposition: form-data; name*=utf-8''t%C3%A4st VMOkc3Q= --===============6469538197104697019== Content-Type: application/octet-stream MIME-Version: 1.0 Content-Transfer-Encoding: base64 Content-Disposition: form-data; name="file"; filename*=utf-8''T%C3%A4st.txt YWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJj ZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVm YWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJj ZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVm YWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJj ZGVmYWJjZGVmYWJjZGVm --===============6469538197104697019==-- 

Parece manejar bastante bien los encabezados. El contenido del archivo binario se codificará en base64, lo que puede evitarse pero debería funcionar lo suficientemente bien. Lo que me preocupa son los campos de texto intermedios. También están codificados en base64. Creo que de acuerdo con el estándar, esto debería funcionar lo suficientemente bien, pero preferiría tener texto sin formato allí, en caso de que algún marco estúpido tenga que lidiar con los datos en un nivel intermedio y no conozca los datos codificados en Base64.

Preguntas

  • ¿Puedo usar datos de 8 bits para mis campos de texto y seguir cumpliendo con las especificaciones?
  • ¿Puedo obtener el paquete de correo electrónico para serializar mis campos de texto como datos de 8 bits sin encoding adicional?
  • Si tengo que atenerme a una encoding de 7 bits, ¿puedo hacer que la implementación use la parte imprimible entre comillas para aquellas partes de texto donde esa encoding sea más corta que la base64?
  • ¿Puedo evitar también la encoding base64 para el contenido de archivos binarios?
  • Si puedo evitarlo, ¿debo escribir la Content-Transfer-Encoding como 8 8bit o como binary ?
  • Si yo mismo tuviera que serializar el cuerpo, ¿cómo podría usar el paquete email.header solo para formatear los valores de encabezado? ( email.utils.encode_rfc2231 hace esto).
  • ¿Hay alguna implementación que ya hizo todo lo que estoy tratando de hacer?

Estas preguntas están estrechamente relacionadas y podrían resumirse como “cómo implementaría esto” . En muchos casos, responder una pregunta responde u obsoleta a otra. Así que espero que estén de acuerdo en que una sola publicación para todos ellos es apropiada.

Esta es una respuesta de marcador de posición, que describe lo que hice mientras esperaba una entrada autoritaria a algunas de mis preguntas. Estaré encantado de aceptar una respuesta diferente si demuestra que este enfoque es incorrecto o inadecuado en al menos una de las decisiones de diseño.

Aquí está el código que usé para hacer este trabajo de acuerdo a mi gusto por ahora. Tomé las siguientes decisiones:

¿Puedo usar datos de 8 bits para mis campos de texto y seguir cumpliendo con las especificaciones?

Decidí hacerlo. Al menos para esta aplicación, funciona.

¿Puedo obtener el paquete de correo electrónico para serializar mis campos de texto como datos de 8 bits sin encoding adicional?

No encontré ninguna manera, así que estoy haciendo mi propia serialización, al igual que todas las otras recetas que vi en esto.

¿Puedo evitar también la encoding base64 para el contenido de archivos binarios?

El simple hecho de enviar el contenido del archivo en binario parece funcionar lo suficientemente bien, al menos en mi única aplicación.

Si puedo evitarlo, ¿debo escribir la encoding de transferencia de contenido como 8 bits o como binario?

Como indica la Sección 2.8 de RFC 2045 , que los datos de 8 8bit están sujetos a una limitación de longitud de línea de 998 octetos entre pares CRLF, decidí que el binary es la descripción más general y, por lo tanto, la más apropiada aquí.

Si yo mismo tuviera que serializar el cuerpo, ¿cómo podría usar el paquete email.header solo para formatear los valores de encabezado?

Como ya he editado en mi pregunta, email.utils.encode_rfc2231 es muy útil para esto. Intento codificar usando ascii primero, pero uso ese método en caso de datos no ascii o caracteres ascii que están prohibidos dentro de una cadena de comillas dobles.

¿Hay alguna implementación que ya hizo todo lo que estoy tratando de hacer?

No que yo supiese. Sin embargo, otras implementaciones están invitadas a adoptar ideas de mi código .


Editar:

Gracias a este comentario , ahora soy consciente de que el uso de RFC 2231 para encabezados no es universalmente aceptado: el borrador actual de HTML 5 prohíbe su uso . También se ha visto que causa problemas en la naturaleza . Pero como los encabezados POST no siempre se corresponden con un documento HTML específico (por ejemplo, las API web, por ejemplo), tampoco estoy seguro de que confíe en ese borrador. Quizás la mejor manera de hacerlo es dar tanto el nombre codificado como el no codificado, como sugiere RFC 5987 Sección 4.2 . Pero ese RFC es para encabezados HTTP, mientras que un encabezado multipart / form-data es técnicamente un cuerpo HTTP. Por lo tanto, ese RFC no se aplica, y no conozco ningún RFC que permita explícitamente (o incluso fomente) el uso de ambos formularios simultáneamente para multiparte / formulario de datos.

Es posible que desee consultar Enviar archivo usando POST desde una pregunta del script de Python que apunta a la biblioteca de solicitudes que se está convirtiendo en la biblioteca de Python más utilizada para http. En caso de que no encuentre allí toda la funcionalidad necesaria y decida implementarla usted mismo, lo invito a contribuir a este proyecto.