6

I download a file using the get function of Python requests library. For storing the file, I'd like to determine the filename they way a web browser would for its 'save' or 'save as ...' dialog.

Easy, right? I can just get it from the Content-Disposition HTTP header, accessible on the response object:

import re
d = r.headers['content-disposition']
fname = re.findall("filename=(.+)", d)

But looking more closely at this topic, it isn't that easy:

According to RFC 6266 section 4.3, and the grammar in the section 4.1, the value can be an unquoted token (e.g. the_report.pdf) or a quoted string that can also contain whitespace (e.g. "the report.pdf") and escape sequences (the latter are discouraged, though, thus their handling isn't a hard requirement for me). Further,

when both "filename" and "filename*" are present in a single header field value, [we] SHOULD pick "filename*" and ignore "filename".

The value of filename*, though, is yet a bit more complicated than the one of filename.

Also, the RFC seems to allow for additional whitespace around the =.

Thus, for the examples listed in the RFC, I'd want the following results:

  • Content-Disposition: Attachment; filename=example.html
    
    filename: example.html
  • Content-Disposition: INLINE; FILENAME= "an example.html"
    
    filename: an example.html
  • Content-Disposition: attachment;
                         filename*= UTF-8''%e2%82%ac%20rates
    
    filename: € rates
  • Content-Disposition: attachment;
                         filename="EURO rates";
                         filename*=utf-8''%e2%82%ac%20rates
    
    filename: € rates here, too (not EURO rates, as filename* takes precedence)

I could implement the parsing of the Content-Disposition header I get from requests accordingly myself, but if I can avoid it and use an existing proven implementation instead, I'd prefer that.

Is there a Python library that can do this?

Requirements

The library would have to

  • provide a function that extracts and returns the proper filename (if there is one) from a passed requests response
    or
  • provide a function that extracts and returns the proper filename (if there is one) from a passed Content-Disposition header field value (a string)
    or
  • provide a function accepting the all the same parameters as requests.get that performs the request, and returns the response as well as the filename (if there is one)
    or
  • provides something similarly practical

Non-requirements

What it doesn't have to handle (but if it does, even better) as I can do that myself:

  • sanitize values so that they don't contain directory names or other path elements except for a single filename, so storing with that name won't cause files to be created or overwritten at arbitrary locations
  • produce "save" filename extensions "optimally matching the media type of the received payload" (see section 4.3)
  • sanitize filenames to prevent user confusion (section 4.3 mentions replacing "control characters and leading and trailing whitespace")
  • provide a fall-back

    • for when neither the filename nor the filename* disposition parameter are present or
    • for when the ones that are present cannot be parsed or
    • for when the complete Content-Disposition header is missing

    Though it should report that consistently (be it by raising or by returning None or ''), so that I can let my own fall-back kick in.

  • I think it would be a good idea asking in StackOverflow and not SoftwareRecommendation. – rwenz3l May 4 '16 at 14:56
  • @YoshiBotX even though I'm explicitly asking for a library recommendation? Those are off-topic on Stack Overflow. – das-g May 4 '16 at 16:02
  • Your question is specific enough "how to determine filename..." that I'd give it a try there. – rogerdpack May 5 '16 at 20:22
  • 1
    Asked on Stack Overflow, too: stackoverflow.com/questions/37060344/… – das-g May 5 '16 at 21:12
4

Check out rfc6266. It seems to do all what you want. It is licensed under the LGPL 3.0. The main fork may not be super active, and some other forks may have more goodies.

Here is the skinny:

>>> from rfc6266 import *

>>> parse_headers('Attachment; filename=example.html', relaxed=True)
ContentDisposition(u'Attachment', {u'filename': u'example.html'}, None)

>>> parse_headers('INLINE; FILENAME= "an example.html"', relaxed=True)
ContentDisposition(u'INLINE', {u'filename': u'an example.html'}, None)

>>> h='''attachment;
...                      filename*= UTF-8''%e2%82%ac%20rates'''
>>> parse_headers(h, relaxed=True)
ContentDisposition(u'attachment', {u'filename*': LangTagged(string=u'\u20ac rates', langtag=None)}, None)

>>> h='''attachment;
...                      filename="EURO rates";
...                      filename*=utf-8''%e2%82%ac%20rates'''
>>> parse_headers(h, relaxed=True)
ContentDisposition(u'attachment', {u'filename*': LangTagged(string=u'\u20ac rates', langtag=None), u'filename': u'EURO rates'}, None)
  • @das-g I am a moron, I had not seen you had cross-posted that on SO and that it was answered there .... Note to self for 2017: read the comments before answering! – Philippe Ombredanne Jan 1 '17 at 11:05
  • Eh, makes sense to have this answer at both places. (Here's the equivalent one at the cross-posted question.) – das-g Jan 1 '17 at 20:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.