【问题标题】:Images in Blobstore: inefficient to get metadata?Blobstore 中的图像:获取元数据效率低下?
【发布时间】:2016-02-25 19:32:54
【问题描述】:

总结:我正在使用 Blobstore 让用户上传要提供的图像。我想阻止用户上传无效图像或尺寸过大的文件。我正在使用 App Engine 的图像服务来获取相关的元数据。但是,为了从图像服务获取有关图像类型或尺寸的任何信息,您必须首先执行转换,将转换后的图像提取到 App Engine 服务器。我让它进行无操作裁剪并编码为质量非常低的 JPEG 图像,但它仍在获取实际图像,我想要的只是尺寸和文件类型。这是我能做的最好的吗?图片数据的内部传输(从 Blobstore 到 App Engine 服务器)会花费我吗?

详情:

Blobstore 似乎经过精心设计,可以有效地提供来自 App Engine 的图像。另一方面,某些操作似乎会让你跳过低效的圈子。我希望有人能告诉我有更有效的方法,或者让我相信我正在做的事情并没有我想象的那么浪费。

我让用户上传图片以作为其他用户生成内容的一部分。 Blobstore 使上传和服务变得非常容易。不幸的是,它允许用户上传他们想要的任何文件,我想施加限制。

(旁注:Blobstore 确实允许您限制上传的文件大小,但此功能的文档很少。事实证明,如果用户尝试超过限制,Blobstore 将返回 413“实体太大”,并且根本不调用 App Engine 处理程序。)

我只想允许有效的 JPEG、GIF 和 PNG 文件,并且我想限制尺寸。这样做的方法似乎是在上传后检查文件,如果不允许则将其删除。这是我得到的:

class ImageUploadHandler(blobstore_handlers.BlobstoreUploadHandler):
  def post(self):
    try:
      # TODO: Check that user is logged in and has quota; xsrfToken.
      uploads = self.get_uploads()
      if len(uploads) != 1:
        logging.error('{} files uploaded'.format(len(uploads)))
        raise ServerError('Must be exactly 1 image per upload')
      image = images.Image(blob_key=uploads[0].key())
      # Do a no-op transformation; otherwise execute_transforms()
      # doesn't work and you can't get any image metadata.
      image.crop(0.0, 0.0, 1.0, 1.0)
      image.execute_transforms(output_encoding=images.JPEG, quality=1)
      if image.width > 640 or image.height > 640:
        raise ServerError('Image must be 640x640 or smaller')
      resultUrl = images.get_serving_url(uploads[0].key())
      self.response.headers['Content-Type'] = 'application/json'
      self.response.body = jsonEncode({'status': 0, 'imageUrl': resultUrl})
    except Exception as e:
      for upload in uploads:
        blobstore.delete(upload.key()) # TODO: delete in parallel with delete_async
      self.response.headers['Content-Type'] = 'text/plain'
      self.response.status = 403
      self.response.body = e.args[0]

代码中的注释突出了问题。

我知道图片可以在投放时动态调整大小(使用 get_serving_url),但我宁愿强制用户首先上传较小的图片,以避免耗尽存储空间。后来,我可能不想限制原始图像的尺寸,而是让它在上传时自动缩小,但我仍然需要在缩小之前找出它的尺寸和类型。

我错过了更简单或更有效的方法吗?

【问题讨论】:

  • 你见过get_serving_url吗? cloud.google.com/appengine/docs/python/refdocs/… 可能会解决一些调整大小等问题。
  • @PaulCollingwood 是的,如果你仔细看的话,我在上面的代码中使用了 get_serving_url。它不能解决我要问的问题。
  • 原来如此。太多而无法详细查看,删除与问题无关的所有内容?您是否考虑过在客户端执行此操作?你信任你的用户吗?我不确定你真正在问什么。如果你想检查某个东西是否真的是一个图像,你必须做类似的事情。您还可以检查数据的前几个字节以查看它是否可能是图像:docs.python.org/2/library/imghdr.html
  • 我相信这几乎都是相关的,但你说得对,这个问题需要消化很多。我将重新组织以突出中心点。
  • @PaulCollingwood,我将最重要的内容移到顶部并删除了 3 行错误的代码,以及一些问题文本。用户通常不受信任。我不相信客户端检查会提供我想要的安全级别。

标签: image google-app-engine blobstore


【解决方案1】:

当您上传到 Google Cloud Storage (GCS) 而不是 blobstore 时,您可以更好地控制对象上传条件,例如名称、类型和大小。策略文件控制用户条件。如果用户不满足这些上传条件,该对象将被拒绝。

文档here.

例子:

{"expiration": "2010-06-16T11:11:11Z",
 "conditions": [
  ["starts-with", "$key", "" ],
  {"acl": "bucket-owner-read" },
  {"bucket": "travel-maps"},
  {"success_action_redirect":"http://www.example.com/success_notification.html" },
  ["eq", "$Content-Type", "image/jpeg" ],
  ["content-length-range", 0, 1000000]
  ]
}

超过内容长度时的 POST 响应:

<Error>
    <Code>EntityTooLarge</Code>
    <Message>
        Your proposed upload exceeds the maximum allowed object size.
    </Message>
    <Details>Content-length exceeds upper bound on range</Details>
</Error>

发送 PDF 时的 POST 响应:

<Error>
    <Code>InvalidPolicyDocument</Code>
    <Message>
        The content of the form does not meet the conditions specified in the policy document.
    </Message>
    <Details>Policy did not reference these fields: filename</Details>
</Error>

here 你可以找到我的 Python 代码直接上传到 GCS。

【讨论】:

  • 是的,我也一直想知道 GCS,尽管我不确定它对我的情况有多大影响。它看起来不会在上传时解析图像文件并能够根据尺寸或无效文件内容拒绝它,对吗?文档似乎没有提到这一点。我很确定对文件类型施加限制只是检查 Content-Type 标头,而不是实际的文件内容。不过,我可能稍后会切换到 GCS 以利用用户访问控制。
  • 我已经更新了我的答案。 GCS 检查上例中的内容长度。内容类型保存为 GCS 元数据,但未检查。
  • 未选中。您可以发送内容类型为 image/png 的 png。但是如果你发送一个 PDF GCS 会返回一个错误。
  • @voscausa 我注意到当使用没有任何 ContentType 限制的策略时,某些文件扩展名会被拒绝:.jpg 可以上传,但 .jpeg 不能(也不能 .pdf )。你知道GCS对上传哪些文件有限制吗?
  • 创建一个新问题。我没有答案。
【解决方案2】:

实际上,Blobstore 并没有针对提供图像进行完全优化,它可以对任何类型的数据进行操作。 The BlobReader class 可用于管理原始 blob 数据。

The GAE Images service 可用于管理图像(包括在 BlobStore 中存储为 blob 的图像)。您是对的,该服务仅在对其执行转换后才提供有关上传图像的信息,这无助于在处理之前删除不需要的 blob 图像。

您可以做的是使用覆盖在 BlobReader 类顶部的 the Image module from the PIL library(在 GAE's Runtime-Provided Libraries 之间可用)。

PIL Image formatsize 方法可在读取整个图像之前获取您寻找的信息并清理图像数据:

>>> image = Image.open('Spain-rail-map.jpg')
>>> image.format
'JPEG'
>>> image.size
(410, 317)

这些方法应该非常有效,因为它们只需要 open 方法加载的 blob 中的图像头信息:

打开并识别给定的图像文件。这是一个惰性操作; 该函数读取文件头,但实际图像数据不是 从文件中读取,直到您尝试处理数据(调用 load 方法强制加载)。

这就是在您的ImageUploadHandler 中进行叠加的方式:

  from PIL import Image
  with blobstore.BlobReader(uploads[0].key()) as fd:
      image = Image.open(fd)
      logging.error('format=%s' % image.format)
      logging.error('size=%dx%d' % image.size)

【讨论】:

  • 好的,我觉得我很喜欢这个。据推测,您可以在 BlobReader 上调用 Image.open 而不是文件名。然后它只会根据需要从 blob 中读取尽可能多的数据(嗯,四舍五入到缓冲区大小的倍数;我猜最佳缓冲区大小可能很小)。
  • 我上面的代码不会做的一件事是验证整个图像文件。要使用 PIL Image 和 BlobReader 做到这一点,它必须获取整个 blob,这比获取转换后的低质量 JPEG 使用更多的带宽。
  • 我有点惊讶 Blobstore 没有提供在没有(转换后的)图像数据的情况下获取元数据的方法。我认为这符合 Google 的利益,因为(据我所知)他们不会为此向我收费(仍然不确定这是否属实)。
  • 我可能需要更新我的答案,因为我在实现中没有看到 size 方法:cloud.google.com/appengine/docs/python/refdocs/modules/google/…
  • 这是来自 App Engine Images 服务的 Image 类,完全独立于 PIL Image 类。我认为你的答案仍然是正确的。 (虽然没试过。)
最近更新 更多