This package provides two basic Zope 3 content components, File and Image, and their ZMI-compliant browser views.

Contents

File objects

Adding Files

You can add File objects from the common tasks menu in the ZMI.

>>> print http(r"""
... GET /@@contents.html HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """)
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/html;charset=utf-8
<BLANKLINE>
...
    <title>Z3: </title>
...
                        <div class="box" id="commonTasks">
                            <h4>Add:</h4>
                            <div class="body">
...
    <div class="content...">
      <a href="http://localhost/@@+/action.html?type_name=zope.app.file.File"
         class="">File</a>
    </div>
...

Let's follow that link.

>>> print http(r"""
... GET /@@+/action.html?type_name=zope.app.file.File HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """, handle_errors=False)
HTTP/1.1 303 See Other
Content-Length: ...
Location: http://localhost/+/zope.app.file.File=
<BLANKLINE>

The file add form lets you specify the content type, the object name, and optionally upload the contents of the file.

>>> print http(r"""
... GET /+/zope.app.file.File= HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """)
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/html;charset=utf-8
<BLANKLINE>
...
    <title>Z3: +</title>
...
...
  <form action="http://localhost/+/zope.app.file.File%3D"
        method="post" enctype="multipart/form-data">
    <h3>Add a File</h3>
    ...<input class="textType" id="field.contentType"
              name="field.contentType" size="20" type="text" value="" />...
    ...<input class="fileType" id="field.data" name="field.data" size="20"
              type="file" />...
      <div class="controls"><hr />
        <input type="submit" value="Refresh" />
        <input type="submit" value="Add"
               name="UPDATE_SUBMIT" />
        &nbsp;&nbsp;<b>Object Name</b>&nbsp;&nbsp;
        <input type="text" name="add_input_name" value="" />
      </div>
...
  </form>
...

Binary Files

Let us upload a binary file.

>>> print http("""
... POST /+/zope.app.file.File%3D HTTP/1.1
... Authorization: Basic mgr:mgrpw
... Content-Type: multipart/form-data; boundary=---------------------------73793505419963331401738523176
...
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="field.contentType"
...
... application/octet-stream
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="field.data"; filename="hello.txt.gz"
... Content-Type: application/x-gzip
...
... \x1f\x8b\x08\x08\xcb\x48\xea\x42\x00\x03\x68\x65\x6c\x6c\x6f\x2e\
... \x74\x78\x74\x00\xcb\x48\xcd\xc9\xc9\xe7\x02\x00\x20\x30\x3a\x36\
... \x06\x00\x00\x00
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="UPDATE_SUBMIT"
...
... Add
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="add_input_name"
...
...
... -----------------------------73793505419963331401738523176--
... """)
HTTP/1.1 303 See Other
Content-Length: ...
Content-Type: text/html;charset=utf-8
Location: http://localhost/@@contents.html
<BLANKLINE>
...

Since we did not specify the object name in the form, Zope 3 will use the filename.

>>> response = http("""
... GET /hello.txt.gz HTTP/1.1
... """)
>>> print response
HTTP/1.1 200 OK
Content-Length: 36
Content-Type: application/octet-stream
<BLANKLINE>
...

Let's make sure the (binary) content of the file is correct

>>> response.getBody().encode('base64')
'H4sICMtI6kIAA2hlbGxvLnR4dADLSM3JyecCACAwOjYGAAAA\n'

Also, lets test a (bad) filename with full path that generates MS Internet Explorer, Zope should process it successfully and get the actual filename. Let's upload the same file with bad filename.

>>> print http("""
... POST /+/zope.app.file.File%3D HTTP/1.1
... Authorization: Basic mgr:mgrpw
... Content-Type: multipart/form-data; boundary=---------------------------73793505419963331401738523176
...
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="field.contentType"
...
... application/octet-stream
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="field.data"; filename="c:\\windows\\test.gz"
... Content-Type: application/x-gzip
...
... \x1f\x8b\x08\x08\xcb\x48\xea\x42\x00\x03\x68\x65\x6c\x6c\x6f\x2e\
... \x74\x78\x74\x00\xcb\x48\xcd\xc9\xc9\xe7\x02\x00\x20\x30\x3a\x36\
... \x06\x00\x00\x00
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="UPDATE_SUBMIT"
...
... Add
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="add_input_name"
...
...
... -----------------------------73793505419963331401738523176--
... """)
HTTP/1.1 303 See Other
Content-Length: ...
Content-Type: text/html;charset=utf-8
Location: http://localhost/@@contents.html
<BLANKLINE>
...

The file should be saved as "test.gz", let's check it name and contents.

>>> response = http("""
... GET /test.gz HTTP/1.1
... """)
>>> print response
HTTP/1.1 200 OK
Content-Length: 36
Content-Type: application/octet-stream
<BLANKLINE>
...
>>> response.getBody().encode('base64')
'H4sICMtI6kIAA2hlbGxvLnR4dADLSM3JyecCACAwOjYGAAAA\n'

Text Files

Let us now create a text file.

>>> print http(r"""
... POST /+/zope.app.file.File%3D HTTP/1.1
... Authorization: Basic mgr:mgrpw
... Content-Type: multipart/form-data; boundary=---------------------------167769037320366690221542301033
...
... -----------------------------167769037320366690221542301033
... Content-Disposition: form-data; name="field.contentType"
...
... text/plain
... -----------------------------167769037320366690221542301033
... Content-Disposition: form-data; name="field.data"; filename=""
... Content-Type: application/octet-stream
...
...
... -----------------------------167769037320366690221542301033
... Content-Disposition: form-data; name="UPDATE_SUBMIT"
...
... Add
... -----------------------------167769037320366690221542301033
... Content-Disposition: form-data; name="add_input_name"
...
... sample.txt
... -----------------------------167769037320366690221542301033--
... """)
HTTP/1.1 303 See Other
Content-Length: ...
Content-Type: text/html;charset=utf-8
Location: http://localhost/@@contents.html
<BLANKLINE>
...

The file is initially empty, since we did not upload anything.

>>> print http("""
... GET /sample.txt HTTP/1.1
... """)
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/plain
Last-Modified: ...
<BLANKLINE>

Since it is a text file, we can edit it directly in a web form.

>>> print http(r"""
... GET /sample.txt/edit.html HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """, handle_errors=False)
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/html;charset=utf-8
<BLANKLINE>
...
    <title>Z3: sample.txt</title>
...
    <form action="http://localhost/sample.txt/edit.html"
          method="post" enctype="multipart/form-data">
      <div>
        <h3>Change a file</h3>
...<input class="textType" id="field.contentType" name="field.contentType"
          size="20" type="text" value="text/plain"  />...
...<textarea cols="60" id="field.data" name="field.data" rows="15" ></textarea>...
...
        <div class="controls">
          <input type="submit" value="Refresh" />
          <input type="submit" name="UPDATE_SUBMIT"
                 value="Change" />
        </div>
...
    </form>
...

Files of type text/plain without any charset information can contain UTF-8 text. So you can use ASCII text.

>>> print http(r"""
... POST /sample.txt/edit.html HTTP/1.1
... Authorization: Basic mgr:mgrpw
... Content-Type: multipart/form-data; boundary=---------------------------165727764114325486311042046845
...
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="field.contentType"
...
... text/plain
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="field.data"
...
... This is a sample text file.
...
... It can contain US-ASCII characters.
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="UPDATE_SUBMIT"
...
... Change
... -----------------------------165727764114325486311042046845--
... """, handle_errors=False)
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/html;charset=utf-8
<BLANKLINE>
...
    <title>Z3: sample.txt</title>
...
    <form action="http://localhost/sample.txt/edit.html"
          method="post" enctype="multipart/form-data">
      <div>
        <h3>Change a file</h3>
<BLANKLINE>
        <p>Updated on ...</p>
<BLANKLINE>
      <div class="row">
...<input class="textType" id="field.contentType" name="field.contentType"
          size="20" type="text" value="text/plain"  />...
      <div class="row">
...<textarea cols="60" id="field.data" name="field.data" rows="15"
>This is a sample text file.
<BLANKLINE>
It can contain US-ASCII characters.</textarea></div>
...
        <div class="controls">
          <input type="submit" value="Refresh" />
          <input type="submit" name="UPDATE_SUBMIT"
                 value="Change" />
        </div>
...
    </form>
...

Here's the file

>>> print http(r"""
... GET /sample.txt HTTP/1.1
... """)
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/plain
Last-Modified: ...
<BLANKLINE>
This is a sample text file.
<BLANKLINE>
It can contain US-ASCII characters.

Non-ASCII Text Files

We can also use non-ASCII charactors in text file.

>>> print http("""
... POST /sample.txt/edit.html HTTP/1.1
... Authorization: Basic mgr:mgrpw
... Content-Type: multipart/form-data; boundary=---------------------------165727764114325486311042046845
...
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="field.contentType"
...
... text/plain
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="field.data"
...
... This is a sample text file.
...
... It can contain non-ASCII(UTF-8) characters, e.g. \xe2\x98\xbb (U+263B BLACK SMILING FACE).
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="UPDATE_SUBMIT"
...
... Change
... -----------------------------165727764114325486311042046845--
... """)
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/html;charset=utf-8
<BLANKLINE>
...
    <title>Z3: sample.txt</title>
...
    <form action="http://localhost/sample.txt/edit.html"
          method="post" enctype="multipart/form-data">
      <div>
        <h3>Change a file</h3>
<BLANKLINE>
        <p>Updated on ...</p>
<BLANKLINE>
      <div class="row">
...<input class="textType" id="field.contentType" name="field.contentType"
          size="20" type="text" value="text/plain"  />...
      <div class="row">
...<textarea cols="60" id="field.data" name="field.data" rows="15"
>This is a sample text file.
<BLANKLINE>
It can contain non-ASCII(UTF-8) characters, e.g. ... (U+263B BLACK SMILING FACE).</textarea></div>
...
        <div class="controls">
          <input type="submit" value="Refresh" />
          <input type="submit" name="UPDATE_SUBMIT"
                 value="Change" />
        </div>
...
    </form>
...

Here's the file

>>> response = http(r"""
... GET /sample.txt HTTP/1.1
... """)
>>> print response
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/plain
Last-Modified: ...
<BLANKLINE>
This is a sample text file.
<BLANKLINE>
It can contain non-ASCII(UTF-8) characters, e.g. ... (U+263B BLACK SMILING FACE).
>>> u'\u263B' in response.getBody().decode('UTF-8')
True

And you can explicitly specify the charset. Note that the browser form is always UTF-8.

>>> print http("""
... POST /sample.txt/edit.html HTTP/1.1
... Authorization: Basic mgr:mgrpw
... Content-Type: multipart/form-data; boundary=---------------------------165727764114325486311042046845
...
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="field.contentType"
...
... text/plain; charset=ISO-8859-1
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="field.data"
...
... This is a sample text file.
...
... It now contains Latin-1 characters, e.g. \xc2\xa7 (U+00A7 SECTION SIGN).
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="UPDATE_SUBMIT"
...
... Change
... -----------------------------165727764114325486311042046845--
... """)
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/html;charset=utf-8
<BLANKLINE>
...
    <title>Z3: sample.txt</title>
...
    <form action="http://localhost/sample.txt/edit.html"
          method="post" enctype="multipart/form-data">
      <div>
        <h3>Change a file</h3>
<BLANKLINE>
        <p>Updated on ...</p>
<BLANKLINE>
      <div class="row">
...<input class="textType" id="field.contentType" name="field.contentType"
          size="20" type="text" value="text/plain; charset=ISO-8859-1"  />...
      <div class="row">
...<textarea cols="60" id="field.data" name="field.data" rows="15"
>This is a sample text file.
<BLANKLINE>
It now contains Latin-1 characters, e.g. ... (U+00A7 SECTION SIGN).</textarea></div>
...
        <div class="controls">
          <input type="submit" value="Refresh" />
          <input type="submit" name="UPDATE_SUBMIT"
                 value="Change" />
        </div>
...
    </form>
...

Here's the file

>>> response = http(r"""
... GET /sample.txt HTTP/1.1
... """)
>>> print response
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/plain; charset=ISO-8859-1
Last-Modified: ...
<BLANKLINE>
This is a sample text file.
<BLANKLINE>
It now contains Latin-1 characters, e.g. ... (U+00A7 SECTION SIGN).

Body is actually encoded in ISO-8859-1, and not UTF-8

>>> response.getBody().splitlines()[-1]
'It now contains Latin-1 characters, e.g. \xa7 (U+00A7 SECTION SIGN).'

The user is not allowed to specify a character set that cannot represent all the characters.

>>> print http("""
... POST /sample.txt/edit.html HTTP/1.1
... Authorization: Basic mgr:mgrpw
... Content-Type: multipart/form-data; boundary=---------------------------165727764114325486311042046845
...
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="field.contentType"
...
... text/plain; charset=US-ASCII
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="field.data"
...
... This is a slightly changed sample text file.
...
... It now contains Latin-1 characters, e.g. \xc2\xa7 (U+00A7 SECTION SIGN).
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="UPDATE_SUBMIT"
...
... Change
... -----------------------------165727764114325486311042046845--
... """, handle_errors=False)
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/html;charset=utf-8
<BLANKLINE>
...
    <title>Z3: sample.txt</title>
...
    <form action="http://localhost/sample.txt/edit.html"
          method="post" enctype="multipart/form-data">
      <div>
        <h3>Change a file</h3>
<BLANKLINE>
        <p>The character set you specified (US-ASCII) cannot encode all characters in text.</p>
<BLANKLINE>
      <div class="row">
...<input class="textType" id="field.contentType" name="field.contentType" size="20" type="text" value="text/plain; charset=US-ASCII"  />...
      <div class="row">
...<textarea cols="60" id="field.data" name="field.data" rows="15" >This is a slightly changed sample text file.
<BLANKLINE>
It now contains Latin-1 characters, e.g. ... (U+00A7 SECTION SIGN).</textarea></div>
...
        <div class="controls">
          <input type="submit" value="Refresh" />
          <input type="submit" name="UPDATE_SUBMIT"
                 value="Change" />
        </div>
...
    </form>
...

Likewise, the user is not allowed to specify a character set that is not supported by Python.

>>> print http("""
... POST /sample.txt/edit.html HTTP/1.1
... Authorization: Basic mgr:mgrpw
... Content-Type: multipart/form-data; boundary=---------------------------165727764114325486311042046845
...
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="field.contentType"
...
... text/plain; charset=I-INVENT-MY-OWN
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="field.data"
...
... This is a slightly changed sample text file.
...
... It now contains just ASCII characters.
... -----------------------------165727764114325486311042046845
... Content-Disposition: form-data; name="UPDATE_SUBMIT"
...
... Change
... -----------------------------165727764114325486311042046845--
... """, handle_errors=False)
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/html;charset=utf-8
<BLANKLINE>
...
    <title>Z3: sample.txt</title>
...
    <form action="http://localhost/sample.txt/edit.html"
          method="post" enctype="multipart/form-data">
      <div>
        <h3>Change a file</h3>
<BLANKLINE>
        <p>The character set you specified (I-INVENT-MY-OWN) is not supported.</p>
<BLANKLINE>
      <div class="row">
...<input class="textType" id="field.contentType" name="field.contentType" size="20" type="text" value="text/plain; charset=I-INVENT-MY-OWN"  />...
      <div class="row">
...<textarea cols="60" id="field.data" name="field.data" rows="15" >This is a slightly changed sample text file.
<BLANKLINE>
It now contains just ASCII characters.</textarea></div>
...
        <div class="controls">
          <input type="submit" value="Refresh" />
          <input type="submit" name="UPDATE_SUBMIT"
                 value="Change" />
        </div>
...
    </form>
...

If you trick Zope and upload a file with a content type that does not match the file contents, you will not be able to access the edit view.

>>> print http(r"""
... GET /hello.txt.gz/@@edit.html HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """)
HTTP/1.1 200 OK
Content-Length: ...
Content-Type: text/html;charset=utf-8
<BLANKLINE>
...
   <li>The character set specified in the content type (UTF-8) does not match file content.</li>
...

Non-ASCII Filenames

Filenames are not restricted to ASCII.

>>> print http("""
... POST /+/zope.app.file.File%3D HTTP/1.1
... Authorization: Basic mgr:mgrpw
... Content-Type: multipart/form-data; boundary=---------------------------73793505419963331401738523176
...
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="field.contentType"
...
... application/octet-stream
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="field.data"; filename="bj\xc3\xb6rn.txt.gz"
... Content-Type: application/x-gzip
...
... \x1f\x8b\x08\x08\xcb\x48\xea\x42\x00\x03\x68\x65\x6c\x6c\x6f\x2e\
... \x74\x78\x74\x00\xcb\x48\xcd\xc9\xc9\xe7\x02\x00\x20\x30\x3a\x36\
... \x06\x00\x00\x00
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="UPDATE_SUBMIT"
...
... Add
... -----------------------------73793505419963331401738523176
... Content-Disposition: form-data; name="add_input_name"
...
...
... -----------------------------73793505419963331401738523176--
... """)
HTTP/1.1 303 See Other
Content-Length: ...
Content-Type: text/html;charset=utf-8
Location: http://localhost/@@contents.html
<BLANKLINE>
...

Since we did not specify the object name in the form, Zope 3 will use the filename.

>>> response = http("""
... GET /bj%C3%B6rn.txt.gz HTTP/1.1
... """)
>>> print response
HTTP/1.1 200 OK
Content-Length: 36
Content-Type: application/octet-stream
<BLANKLINE>
...

Special URL handling for DTML pages

When an HTML File page containing a head tag is visited, without a trailing slash, the base href isn't set. When visited with a slash, it is:

>>> print http(r"""
... POST /+/zope.app.file.File%3D HTTP/1.1
... Authorization: Basic mgr:mgrpw
... Content-Length: 610
... Content-Type: multipart/form-data; boundary=---------------------------32826232819858510771857533856
... Referer: http://localhost:8081/+/zope.app.file.File=
...
... -----------------------------32826232819858510771857533856
... Content-Disposition: form-data; name="field.contentType"
...
... text/html
... -----------------------------32826232819858510771857533856
... Content-Disposition: form-data; name="field.data"; filename=""
... Content-Type: application/octet-stream
...
...
... -----------------------------32826232819858510771857533856
... Content-Disposition: form-data; name="UPDATE_SUBMIT"
...
... Add
... -----------------------------32826232819858510771857533856
... Content-Disposition: form-data; name="add_input_name"
...
... file.html
... -----------------------------32826232819858510771857533856--
... """)
HTTP/1.1 303 See Other
...
>>> print http(r"""
... POST /file.html/edit.html HTTP/1.1
... Authorization: Basic mgr:mgrpw
... Content-Length: 507
... Content-Type: multipart/form-data; boundary=---------------------------10196264131256436092131136054
... Referer: http://localhost:8081/file.html/edit.html
...
... -----------------------------10196264131256436092131136054
... Content-Disposition: form-data; name="field.contentType"
...
... text/html
... -----------------------------10196264131256436092131136054
... Content-Disposition: form-data; name="field.data"
...
... <html>
... <head></head>
... <body>
... <a href="eek.html">Eek</a>
... </body>
... </html>
... -----------------------------10196264131256436092131136054
... Content-Disposition: form-data; name="UPDATE_SUBMIT"
...
... Change
... -----------------------------10196264131256436092131136054--
... """)
HTTP/1.1 200 OK
...
>>> print http(r"""
... GET /file.html HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """)
HTTP/1.1 200 OK
...
<html>
<head></head>
<body>
<a href="eek.html">Eek</a>
</body>
</html>
>>> print http(r"""
... GET /file.html/ HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """)
HTTP/1.1 200 OK
...
<html>
<head>
<base href="http://localhost/file.html" />
</head>
<body>
<a href="eek.html">Eek</a>
</body>
</html>

Changes

3.6.2 (unreleased)

  • Nothing changed yet.

3.6.1 (2010-09-17)

  • Removed ZPKG slugs and ZCML ones.
  • Moved a functional test here from zope.app.http.
  • Using Python's doctest instead of deprecated zope.testing.doctest.

3.6.0 (2010-08-19)

  • Updated ftesting.zcml to use the new permission names exported by zope.dublincore 3.7.
  • Using python's doctest instead of deprecated zope.testing.doctest.

3.5.1 (2010-01-08)

  • Fix ftesting.zcml due to zope.securitypolicy update.
  • Added missing dependency on transaction.
  • Import content-type parser from zope.contenttype, reducing zope.publisher to a test dependency.
  • Fix tests using a newer zope.publisher that requires zope.login.

3.5.0 (2009-01-31)

  • Replace zope.app.folder use by zope.site. Add missing dependency in setup.py.

3.4.6 (2009-01-27)

  • Remove zope.app.zapi dependency again. Previous release was wrong. We removed the zope.app.zapi uses before, so we don't need it anymore.

3.4.5 (2009-01-27)

  • added missing dependency: zope.app.zapi

3.4.4 (2008-09-05)

  • Bug: Get actual filename instead of full filesystem path when adding file/image using Internet Explorer.

3.4.3 (2008-06-18)

  • Using IDCTimes interface instead of IZopeDublinCore to determine the modification date of a file.

3.4.2 (2007-11-09)

3.4.1 (2007-10-31)

  • Resolve ZopeSecurityPolicy deprecation warning.

3.4.0 (2007-10-24)

  • Initial release independent of the main Zope tree.