<img> 提供图像

作者:

pkel

在本操作指南中,您将学习如何创建一个端点,用于向 HTML <img> 标签提供图像,而无需客户端 JavaScript。事实上,所介绍的技术不仅适用于提供图像,还适用于提供任意文件。

我们将从一个最小示例开始,该示例突出显示了总体概念。之后,我们将介绍一个更详细的解决方案,该解决方案修复了第一种方法的一些缺点。

警告

在数据库中保存二进制文件时要小心,在大多数情况下,最好为这些文件使用单独的存储服务。请参阅 在数据库中存储二进制文件.

最小示例

首先,我们需要一个用于存储文件的公共表。

create table files(
  id   int primary key
, blob bytea
);

假设这张表包含一张两张可爱小猫的图片,id 为 42。我们可以使用 媒体类型处理器 从我们的 PostgREST API 中以二进制格式检索这张图片。

create domain "application/octet-stream" as bytea;

create or replace function file(id int) returns "application/octet-stream" as $$
  select blob from files where id = file.id;
$$ language sql;

现在我们可以使用 Accept: application/octet-stream 头部请求 RPC 端点 /rpc/file?id=42

curl "localhost:3000/rpc/file?id=42" -H "Accept: application/octet-stream"

不幸的是,将 URL 放入 <img> 标签的 src 属性中并不会起作用。这是因为浏览器不会发送所需的 Accept: application/octet-stream 头部。相反,许多 Web 浏览器默认会发送 Accept: image/webp 头部。

幸运的是,我们可以像这样在函数中更改接受的媒体类型

create domain "image/webp" as bytea;

create or replace function file(id int) returns "image/webp" as $$
  select blob from files where id = file.id;
$$ language sql;

现在,图片将显示在 HTML 页面中

<img src="http://localhost:3000/file?id=42" alt="Cute Kittens"/>

改进版本

基本解决方案有一些缺点

  1. 响应 Content-Type 头部被设置为 image/webp。如果您想为文件指定不同的格式,这可能是一个问题。

  2. /files?select=blob&id=eq.42 的下载请求(例如,右键单击 -> 另存为图片)将建议 files 作为文件名。这可能会让用户感到困惑。

  3. 对二进制端点的请求不会被缓存。这会导致数据库上的不必要负载。

以下改进版本解决了这些问题。首先,除了最小示例之外,我们需要在数据库中存储文件的媒体类型和名称。

alter table files
  add column type text generated always as (byteamagic_mime(substr(blob, 0, 4100))) stored,
  add column name text;

这使用了来自 pg_byteamagic 扩展byteamagic_mime() 函数来自动生成 files 表中的类型。要猜测文件的类型,通常查看文件开头就足够了,这更有效率。

接下来,我们修改函数以设置内容类型和文件名。我们利用这个机会配置一些基本的客户端缓存。在生产环境中,您可能需要配置额外的缓存,例如在 反向代理 上。

create domain "*/*" as bytea;

create function file(id int) returns "*/*" as
$$
  declare headers text;
  declare blob bytea;
  begin
    select format(
      '[{"Content-Type": "%s"},'
       '{"Content-Disposition": "inline; filename=\"%s\""},'
       '{"Cache-Control": "max-age=259200"}]'
      , files.type, files.name)
    from files where files.id = file.id into headers;
    perform set_config('response.headers', headers, true);
    select files.blob from files where files.id = file.id into blob;
    if FOUND -- special var, see https://postgresql.ac.cn/docs/current/plpgsql-statements.html#PLPGSQL-STATEMENTS-DIAGNOSTICS
    then return(blob);
    else raise sqlstate 'PT404' using
      message = 'NOT FOUND',
      detail = 'File not found',
      hint = format('%s seems to be an invalid file id', file.id);
    end if;
  end
$$ language plpgsql;

有了这个,我们可以从 /rpc/file?id=42 获取猫的图片。因此,生成的 HTML 将是

<img src="http://localhost:3000/rpc/file?id=42" alt="Cute Kittens"/>