跳转到主要内容

为Zope应用程序提供高效文件实现

项目描述

zope.file包提供用于存储文件的内容对象。该接口支持高效的上传和下载。

文件对象

zope.file包提供用于存储文件的内容对象。该接口支持高效的上传和下载。让我们创建一个实例

>>> from zope.file.file import File
>>> f = File()

该对象提供有限数量的数据属性。mimeType属性用于存储数据的首选MIME内容类型值

>>> f.mimeType
>>> f.mimeType = "text/plain"
>>> f.mimeType
'text/plain'
>>> f.mimeType = "application/postscript"
>>> f.mimeType
'application/postscript'

parameters属性是一个映射,用于存储内容类型参数。当适用(并且可用)时,可以在此处找到编码信息

>>> f.parameters
{}
>>> f.parameters["charset"] = "us-ascii"
>>> f.parameters["charset"]
'us-ascii'

在创建File对象时,parametersmimeType也可以选择性地设置

>>> f2 = File(mimeType = "application/octet-stream",
...           parameters = dict(charset = "utf-8"))
>>> f2.mimeType
'application/octet-stream'
>>> f2.parameters["charset"]
'utf-8'

文件对象还提供了一个size属性,该属性提供了文件中的字节数

>>> f.size
0

该对象通过提供通过提供访问器对象访问内容数据(Python文件API的子集)来支持高效的上传和下载。

尚未写入的文件是空的。我们可以通过调用 open() 来获取一个读取器。注意,所有blob都是二进制格式,因此模式总是包含一个‘b’。

>>> r = f.open("r")
>>> r.mode
'rb'

可以调用 read() 方法,并传入一个非负整数来指定要读取的字节数,或者传入一个负数或省略的参数来读取文件末尾的数据。

>>> r.read(10)
''
>>> r.read()
''
>>> r.read(-1)
''

一旦访问器被关闭,我们就无法再从它读取。

>>> r.close()
>>> r.read()
Traceback (most recent call last):
ValueError: I/O operation on closed file

一旦文件对象中有数据,读取器就更有趣了。

可以使用写入器添加数据,写入器也是通过在文件上调用 open() 方法创建的,但请求写入文件模式。

>>> w = f.open("w")
>>> w.mode
'wb'

write() 方法用于向文件添加数据,但请注意,数据可能会在写入器中被缓冲。

>>> _ = w.write(b"some text ")
>>> _ = w.write(b"more text")

flush() 方法确保已写入的数据被写入文件对象。

>>> w.flush()

在确定文件大小之前,我们需要先关闭文件。

>>> w.close()
>>> f.size
19

现在我们可以使用读取器来查看数据是否已写入文件。

>>> w = f.open("w")
>>> _ = w.write(b'some text more text')
>>> _ = w.write(b" still more")
>>> w.close()
>>> f.size
30

现在创建一个新的读取器,让我们执行一些查找操作。

>>> r = f.open()

读取器还有一个 seek() 方法,可以用来在数据流中回退或跳进。只需传入一个偏移量参数,我们就可以看到当前位置被移动到从文件开始处的该偏移量。

>>> _ = r.seek(20)
>>> r.read()
'still more'

这相当于传递0作为 whence 参数。

>>> _ = r.seek(20, 0)
>>> r.read()
'still more'

我们可以通过传递1给 whence 来相对于当前位置向前或向后跳过。

>>> _ = r.seek(-10, 1)
>>> r.read(5)
'still'
>>> _ = r.seek(2, 1)
>>> r.read()
'ore'

我们可以通过传递2给 whence 来跳转到从文件末尾开始的一个位置。

>>> _ = r.seek(-10, 2)
>>> r.read()
'still more'
>>> _ = r.seek(0)
>>> _ = r.seek(-4, 2)
>>> r.read()
'more'
>>> r.close()

尝试向已关闭的写入器写入将引发异常。

>>> w = f.open('w')
>>> w.close()
>>> w.write(b'foobar')
Traceback (most recent call last):
ValueError: I/O operation on closed file

同样,在已关闭的读取器上使用 seek()tell() 也会引发异常。

>>> r.close()
>>> _ = r.seek(0)
Traceback (most recent call last):
ValueError: I/O operation on closed file
>>> r.tell()
Traceback (most recent call last):
ValueError: I/O operation on closed file

下载文件对象

文件内容类型提供了一个用于下载文件的视图,不管浏览器对内容类型的默认行为如何。这依赖于浏览器对 Content-Disposition 头的支持。

下载支持由两个不同的对象提供:一个视图,它使用内容对象中的信息提供下载支持,以及一个结果对象,它可以用来通过其他视图实现文件下载。视图可以覆盖浏览器使用标准 IResponse.setHeader 方法建议的内容类型或文件名。

请注意,结果对象旨在使用一次后丢弃。

让我们首先创建一个文件对象,我们可以用它来演示下载支持。

>>> import transaction
>>> from zope.file.file import File
>>> f = File()
>>> getRootFolder()['file'] = f
>>> transaction.commit()

头部

现在,让我们获取此文件的标题。我们使用一个名为 getHeaders 的实用函数。

>>> from zope.file.download import getHeaders
>>> headers = getHeaders(f, contentDisposition='attachment')

由于文件上没有建议的下载文件名,Content-Disposition 头不指定一个,但指示将响应体作为要保存的文件而不是应用内容类型的默认处理程序。

>>> sorted(headers)
[('Content-Disposition', 'attachment; filename="file"'),
 ('Content-Length', '0'),
 ('Content-Type', 'application/octet-stream')]

请注意,使用的是默认内容类型‘application/octet-stream’。

如果文件对象指定了内容类型,则默认情况下在标题中使用该类型。

>>> f.mimeType = "text/plain"
>>> headers = getHeaders(f, contentDisposition='attachment')
>>> sorted(headers)
[('Content-Disposition', 'attachment; filename="file"'),
 ('Content-Length', '0'),
 ('Content-Type', 'text/plain')]

或者,可以将内容类型指定给 getHeaders

>>> headers = getHeaders(f, contentType="text/xml",
...                      contentDisposition='attachment')
>>> sorted(headers)
[('Content-Disposition', 'attachment; filename="file"'),
 ('Content-Length', '0'),
 ('Content-Type', 'text/xml')]

提供给浏览器的文件名也可以类似地控制。如果内容对象提供了一个,则默认使用该文件名。

>>> headers = getHeaders(f, contentDisposition='attachment')
>>> sorted(headers)
[('Content-Disposition', 'attachment; filename="file"'),
 ('Content-Length', '0'),
 ('Content-Type', 'text/plain')]

getHeaders 提供一个备用名称将覆盖文件中的下载名称。

>>> headers = getHeaders(f, downloadName="foo.txt",
...                      contentDisposition='attachment')
>>> sorted(headers)
[('Content-Disposition', 'attachment; filename="foo.txt"'),
 ('Content-Length', '0'),
 ('Content-Type', 'text/plain')]

可以通过向 getHeaders 提供一个参数来覆盖默认的 Content-Disposition 头。

>>> headers = getHeaders(f, contentDisposition="inline")
>>> sorted(headers)
[('Content-Disposition', 'inline; filename="file"'),
 ('Content-Length', '0'),
 ('Content-Type', 'text/plain')]

如果未提供 contentDisposition 参数,则不会在标题中包含任何内容。

>>> headers = getHeaders(f)
>>> sorted(headers)
[('Content-Length', '0'),
 ('Content-Type', 'text/plain')]

主体

我们使用 DownloadResult 来将内容传递给浏览器。由于此文件中没有数据,因此没有体块。

>>> transaction.commit()
>>> from zope.file.download import DownloadResult
>>> result = DownloadResult(f)
>>> list(result)
[]

我们仍然需要了解如何处理非空文件。让我们向我们的文件对象写入一些数据。

>>> with f.open("w") as w:
...    _ = w.write(b"some text")
...    w.flush()
>>> transaction.commit()

现在我们可以创建一个结果对象并查看我们是否得到预期的数据。

>>> result = DownloadResult(f)
>>> L = list(result)
>>> b"".join(L)
'some text'

如果体内容非常大,迭代器可能会提供多个数据块。

>>> with f.open("w") as w:
...   _ = w.write(b"*" * 1024 * 1024)
...   w.flush()
>>> transaction.commit()
>>> result = DownloadResult(f)
>>> L = list(result)
>>> len(L) > 1
True

当迭代体完成迭代后,进一步的迭代将不会产生额外的数据

>>> list(result)
[]

下载视图

现在我们已经看到了getHeaders函数和结果对象,让我们看看使用它们的简单下载视图。我们需要添加一个文件对象,以便我们可以通过浏览器访问它

>>> f = File()
>>> f.mimeType = "text/plain"
>>> with f.open("w") as w:
...    _ = w.write(b"some text")
>>> transaction.commit()
>>> getRootFolder()["abcdefg"] = f
>>> transaction.commit()

现在,让我们请求文件对象的下载视图并检查结果

>>> print(http(b"""
... GET /abcdefg/@@download HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """, handle_errors=False))
HTTP/1.0 200 Ok
Content-Disposition: attachment; filename="abcdefg"
Content-Length: 9
Content-Type: text/plain
<BLANKLINE>
some text

内嵌视图

此外,有时查看数据而不是下载它也是有用的。为此提供了一个基本的内联视图。请注意,当使用此视图且没有加载到页面时,浏览器可能会决定不显示图片:如果此视图是通过URL直接引用的,浏览器可能显示不出任何内容

>>> print(http(b"""
... GET /abcdefg/@@inline HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """, handle_errors=False))
HTTP/1.0 200 Ok
Content-Disposition: inline; filename="abcdefg"
Content-Length: 9
Content-Type: text/plain
<BLANKLINE>
some text

默认显示视图

此视图类似于下载和内联视图,但未指定任何内容处置。这允许浏览器在当前上下文中默认处理数据

>>> print(http(b"""
... GET /abcdefg/@@display HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """, handle_errors=False))
HTTP/1.0 200 Ok
Content-Length: 9
Content-Type: text/plain
<BLANKLINE>
some text

大Unicode字符

我们需要能够支持比Latin-1(WSGI使用的编码)所能支持的更多的Unicode字符在文件名中。

让我们将一个文件重命名为包含高Unicode字符,并尝试下载它;文件名将被编码

>>> getRootFolder()["abcdefg"].__name__ = u'Big \U0001F4A9'
>>> transaction.commit()
>>> print(http(b"""
... GET /abcdefg/@@download HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """, handle_errors=False))
HTTP/1.0 200 Ok
Content-Disposition: attachment; filename="Big 💩"
Content-Length: 9
Content-Type: text/plain
<BLANKLINE>
some text

上传新文件

有一个简单的视图用于上传新文件。让我们试试

>>> from io import BytesIO as StringIO
>>> sio = StringIO(b"some text")
>>> from zope.testbrowser.wsgi import Browser
>>> browser = Browser()
>>> browser.handleErrors = False
>>> browser.addHeader("Authorization", "Basic mgr:mgrpw")
>>> browser.addHeader("Accept-Language", "en-US")
>>> browser.open("https://127.0.0.1/@@+/zope.file.File")
>>> ctrl = browser.getControl(name="form.data")
>>> ctrl.add_file(
...     sio, "text/plain; charset=utf-8", "plain.txt")
>>> browser.getControl("Add").click()

现在,让我们请求文件对象的下载视图并检查结果

>>> print(http(b"""
... GET /plain.txt/@@download HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """, handle_errors=False))
HTTP/1.0 200 Ok
Content-Disposition: attachment; filename="plain.txt"
Content-Length: 9
Content-Type: text/plain;charset=utf-8
<BLANKLINE>
some text

我们将检查数据库以确保对象实现了预期的MIME类型接口

>>> from zope.mimetype import types
>>> ob = getRootFolder()["plain.txt"]
>>> types.IContentTypeTextPlain.providedBy(ob)
True

我们还可以将新数据上传到我们的文件对象中

>>> sio = StringIO(b"new text")
>>> browser.open("https://127.0.0.1/plain.txt/@@edit.html")
>>> ctrl = browser.getControl(name="form.data")
>>> ctrl.add_file(
...     sio, "text/plain; charset=utf-8", "stuff.txt")
>>> browser.getControl("Edit").click()

现在,让我们请求文件对象的下载视图并检查结果

>>> print(http(b"""
... GET /plain.txt/@@download HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """, handle_errors=False))
HTTP/1.0 200 Ok
Content-Disposition: attachment; filename="plain.txt"
Content-Length: 8
Content-Type: text/plain;charset=utf-8
<BLANKLINE>
new text

如果我们上传一个内容类型信息不精确的文件(正如我们通常从浏览器和MSIE那里期望的那样),我们可以看到MIME类型机制将尽可能改进信息

>>> sio = StringIO(b"<?xml version='1.0' encoding='utf-8'?>\n"
...                b"<html>...</html>\n")
>>> browser.open("https://127.0.0.1/@@+/zope.file.File")
>>> ctrl = browser.getControl(name="form.data")
>>> ctrl.add_file(
...     sio, "text/html; charset=utf-8", "simple.html")
>>> browser.getControl("Add").click()

再次,我们将请求文件对象的下载视图并检查结果

>>> print(http(b"""
... GET /simple.html/@@download HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """, handle_errors=False))
HTTP/1.0 200 Ok
Content-Disposition: attachment; filename="simple.html"
Content-Length: 56
Content-Type: application/xhtml+xml;charset=utf-8
<BLANKLINE>
<?xml version='1.0' encoding='utf-8'?>
<html>...</html>
<BLANKLINE>

此外,如果浏览器不好,发送完整的路径作为文件名(如许多浏览器中有时发生的那样),名称将被正确截断并更改。

>>> sio = StringIO(b"<?xml version='1.0' encoding='utf-8'?>\n"
...                b"<html>...</html>\n")
>>> browser.open("https://127.0.0.1/@@+/zope.file.File")
>>> ctrl = browser.getControl(name="form.data")
>>> ctrl.add_file(
...     sio, "text/html; charset=utf-8", r"C:\Documents and Settings\Joe\naughty name.html")
>>> browser.getControl("Add").click()

再次,我们将请求文件对象的下载视图并检查结果

>>> print(http(b"""
... GET /naughty%20name.html/@@download HTTP/1.1
... Authorization: Basic mgr:mgrpw
... """, handle_errors=False))
HTTP/1.0 200 Ok
Content-Disposition: attachment; filename="naughty name.html"
Content-Length: 56
Content-Type: application/xhtml+xml;charset=utf-8
<BLANKLINE>
<?xml version='1.0' encoding='utf-8'?>
<html>...</html>
<BLANKLINE>

在zope.file <= 0.5.0中,在Upload视图中触发了多余的ObjectCreatedEvent。我们将演示这种情况不再存在。

>>> import zope.component
>>> from zope.file.interfaces import IFile
>>> from zope.lifecycleevent import IObjectCreatedEvent

我们将为IObjectCreatedEvent注册一个订阅者,该订阅者只是增加一个计数器。

>>> count = 0
>>> def inc(*args):
...   global count; count += 1
>>> zope.component.provideHandler(inc, (IFile, IObjectCreatedEvent))
>>> browser.open("https://127.0.0.1/@@+/zope.file.File")
>>> ctrl = browser.getControl(name="form.data")
>>> sio = StringIO(b"some data")
>>> ctrl.add_file(
...     sio, "text/html; charset=utf-8", "name.html")
>>> browser.getControl("Add").click()

订阅者只被调用了一次。

>>> print(count)
1

内容类型和编码控制

文件提供了一个视图,支持控制MIME内容类型和(适用的情况下)内容编码。内容编码适用于文件的具体内容类型。

让我们用一个简单的片段内容来演示表单的行为。我们将上传一些HTML作为样本文档

>>> from io import BytesIO
>>> sio = BytesIO(b"A <sub>little</sub> HTML."
...               b"  There's one 8-bit Latin-1 character: \xd8.")
>>> from zope.testbrowser.wsgi import Browser
>>> browser = Browser()
>>> browser.handleErrors = False
>>> browser.addHeader("Authorization", "Basic mgr:mgrpw")
>>> browser.addHeader("Accept-Language", "en-US")
>>> browser.open("https://127.0.0.1/@@+/zope.file.File")
>>> ctrl = browser.getControl(name="form.data")
>>> ctrl.add_file(
...     sio, "text/html", "sample.html")
>>> browser.getControl("Add").click()

我们可以看到MIME处理程序已将其标记为HTML内容

>>> import zope.mimetype.interfaces
>>> import zope.mimetype.mtypes
>>> file = getRootFolder()[u"sample.html"]
>>> zope.mimetype.mtypes.IContentTypeTextHtml.providedBy(file)
True

重要的是要注意,这也意味着内容是编码文本

>>> zope.mimetype.interfaces.IContentTypeEncoded.providedBy(file)
True

“内容类型”页面将显示所选的MIME类型和编码

>>> browser.getLink("sample.html").click()
>>> browser.getLink("Content Type").click()
>>> browser.getControl(name="form.mimeType").value
['zope.mimetype.mtypes.IContentTypeTextHtml']

空字符串值表示我们没有编码信息

>>> ctrl = browser.getControl(name="form.encoding")
>>> print(ctrl.value)
['']

现在,让我们将编码值设置为旧喜爱的Latin-1

>>> ctrl.value = ["iso-8859-1"]
>>> browser.handleErrors = False
>>> browser.getControl("Save").click()

我们现在在表单中看到更新后的值,并可以在对象的MIME内容类型参数中检查值

>>> ctrl = browser.getControl(name="form.encoding")
>>> print(ctrl.value)
['iso-8859-1']
>>> file = getRootFolder()["sample.html"]
>>> file.parameters
{'charset': 'iso-8859-1'}

更有趣的是,我们现在可以使用非编码的内容类型,并且编码字段将从表单中删除

>>> ctrl = browser.getControl(name="form.mimeType")
>>> ctrl.value = ["zope.mimetype.mtypes.IContentTypeImageTiff"]
>>> browser.getControl("Save").click()
>>> browser.getControl(name="form.encoding")
Traceback (most recent call last):
  ...
LookupError: name 'form.encoding'
...

如果我们切换回编码类型,我们看到我们的编码没有丢失

>>> ctrl = browser.getControl(name="form.mimeType")
>>> ctrl.value = ["zope.mimetype.mtypes.IContentTypeTextHtml"]
>>> browser.getControl("Save").click()
>>> browser.getControl(name="form.encoding").value
['iso-8859-1']

另一方面,如果我们尝试将编码设置为无法解码输入数据的东西,我们会收到一条错误消息,表明这不会起作用,并且不会保存任何更改

>>> ctrl = browser.getControl(name="form.encoding")
>>> ctrl.value = ["utf-8"]
>>> browser.getControl("Save").click()
>>> print(browser.contents)
<...Selected encoding cannot decode document...

表示适配器

对象大小

文件的大小是通过适配器使用zope.size.interfaces.ISized接口在容器的内容视图中提供的。此类适配器适用于文件对象。

让我们进行一些导入并创建一个新的文件对象

>>> from zope.file.file import File
>>> from zope.file.browser import Sized
>>> from zope.size.interfaces import ISized
>>> f = File()
>>> f.size
0
>>> s = Sized(f)
>>> ISized.providedBy(s)
True
>>> s.sizeForSorting()
('byte', 0)
>>> s.sizeForDisplay()
u'0 KB'

让我们向文件添加一些内容

>>> with f.open('w') as w:
...    _ =  w.write(b"some text")

大小适配器现在反映了更新后的大小

>>> s.sizeForSorting()
('byte', 9)
>>> s.sizeForDisplay()
u'1 KB'

让我们尝试使用更大的文件大小

>>> with f.open('w') as w:
...    _ = w.write(b"x" * (1024*1024+10))
>>> s.sizeForSorting()
('byte', 1048586)
>>> m = s.sizeForDisplay()
>>> m
u'${size} MB'
>>> m.mapping
{'size': '1.00'}

并且大小仍然更大

>>> with f.open('w') as w:
...    _ = w.write(b"x" * 3*512*1024)
>>> s.sizeForSorting()
('byte', 1572864)
>>> m = s.sizeForDisplay()
>>> m
u'${size} MB'
>>> m.mapping
{'size': '1.50'}

变更记录

1.2.0 (2020-03-06)

  • 支持 Python 3.7 和 3.8

  • 取消对 Python 3.4 的支持。

1.1.0 (2017-09-30)

  • 将更多浏览器依赖项移动到 browser 额外。

  • 开始在 Travis CI 上测试 PyPy3。

1.0.0 (2017-04-25)

  • 移除不必要的测试依赖项 zope.app.server, zope.app.component, zope.app.container 以及其他。

  • 更新以与 zope.testbrowser 5 兼容。

  • 添加 PyPy 支持。

  • 添加对 Python 3.4、3.5 和 3.6 的支持。见 PR 5

0.6.2 (2012-06-04)

  • 将面向菜单的注册移动到新的 menus.zcml。现在仅在 zope.app.zcmlfiles 可用的情况下加载。

  • 提高测试覆盖率。

0.6.1 (2012-01-26)

  • 声明更多依赖项。

0.6.0 (2010-09-16)

  • 错误修复:在 zope.file.upload.Upload 中移除 ObjectCreatedEvent 的重复触发(该事件已在其基类 zope.formlib.form.AddForm 中触发)。

  • 将浏览器相关的 zcml 移动到 browser.zcml,以便应用程序更容易排除它。

  • 从 zope.contenttype 导入内容类型解析器,添加对该包的依赖。

  • 移除未声明的 zope.app.container 依赖项,依赖 zope.browser。

  • 使用 Python 的 doctest 模块而不是已弃用的 zope.testing.doctest

0.5.0 (2009-07-23)

  • 将包的邮件列表地址更改为 zope-dev at zope.org,而不是已退休的地址。

  • 使测试与 ZODB 3.9 兼容。

  • 移除不必要的安装要求声明。

0.4.0 (2009-01-31)

  • openDetached 现在由 zope.View 保护,而不是 zope.ManageContent。

  • 使用 zope.container 而不是 zope.app.container。

0.3.0 (2007-11-01)

  • 包数据更新。

0.2.0 (2007-04-18)

  • 修复与 Publisher 版本 3.4 兼容的代码。

0.1.0 (2007-04-18)

  • 初始发布。

下载文件

下载您平台上的文件。如果您不确定选择哪一个,请了解更多关于 安装包 的信息。

源分发

zope.file-1.2.0.tar.gz (36.2 kB 查看散列)

上传时间

构建分发

zope.file-1.2.0-py2.py3-none-any.whl (40.1 kB 查看散列)

上传时间 Python 2 Python 3

由以下支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面