news 2026/9/16 19:42:21

Polar 文件服务(File Service)详解:基于 AWS S3 的分片上传、预签名下载与恶意文件检测实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Polar 文件服务(File Service)详解:基于 AWS S3 的分片上传、预签名下载与恶意文件检测实现

Polar 文件服务(File Service)详解:基于 AWS S3 的分片上传、预签名下载与恶意文件检测实现

【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar

导读

Polar 是一个面向"智能时代"的开源计费平台,其文件服务(File Service)承载着托管下载权益(downloadables benefit)、产品媒体图、组织头像、工单附件等全部文件类能力。本篇文章以 server/polar/file/README.md 为主体骨架,完整还原其本地开发环境从零配置 AWS S3 的四个步骤(建桶 → 配 CORS → 建 IAM 用户与策略 → 配置 Polar 环境变量),并深入到仓库源码层(file/service.pys3.pyendpoints.pyintegrations/aws/s3/service.py以及 Web 端Upload.ts),剖析其分片上传、预签名 URL、四类文件服务类型与 GuardDuty 恶意文件检测的完整实现原理。读完本文,你将掌握如何在本地为 Polar 配置 AWS S3 存储,并理解其文件生命周期从创建到下载的完整数据流。

一、文件服务的定位与架构概览

从 server/polar/file/README.md 开头可以看到,文件服务的职责非常聚焦:在 AWS S3 上完成文件的存储、管理与访问,目前主要用于 Polar 托管的下载权益(hosted downloads benefit)。

在仓库中,文件服务以独立 Python 模块形式存在于 server/polar/file/,由以下核心文件构成:

  • service.py:业务编排层,负责列出、查询、创建、完成上传、下载与删除文件;
  • s3.py:S3 服务注册表,按文件服务类型映射到不同的存储桶;
  • endpoints.py:FastAPI 路由层,暴露/v1/files/*的 REST 接口;
  • schemas.py:Pydantic 请求/响应模型,含四种文件类型的差异化校验;
  • repository.py:基于 SQLAlchemy 的持久化查询层;
  • tasks.py:异步任务,处理 AWS GuardDuty 恶意文件扫描结果;
  • auth.py:接口鉴权依赖(files_read/files_write两个 OAuth scope)。

从数据模型看,文件元数据持久化在 PostgreSQL 的files表(见 server/polar/models/file.py),而文件内容本身存放在 S3 对象存储中;数据库字段(pathchecksum_etagchecksum_sha256_base64storage_version等)与 S3 对象一一对应,是一种典型的"元数据入库、二进制入桶"的双存储架构。

四类文件服务类型

server/polar/models/file.py 通过FileServiceTypes枚举定义了四种服务类型,它们分别映射到不同的存储桶与访问策略:

服务类型用途存储桶(默认)访问方式
downloadable托管下载权益(付费后发放的下载文件)polar-s3(私有桶)预签名 URL
product_media商品媒体图polar-s3-public(公开桶)公开 URL
organization_avatar组织头像polar-s3-public(公开桶)公开 URL
support_case_attachment工单附件(敏感,不对外开放)polar-s3(私有桶)仅预签名 URL,禁止经文件 API 下载/修改/删除

这种类型到桶的映射定义在 server/polar/file/s3.py:私有数据(可下载文件、工单附件)统一进私有桶,通过短期预签名 URL 访问;公开媒体(产品图、头像)进公开桶,可直接拼出公开 URL 供 CDN/浏览器加载。从源码注释可以看到,support_case_attachment属于"敏感附件",必须走预签名 URL,不能暴露公开链接。

二、本地开发环境配置 AWS S3(README 原文步骤完整展开)

README 的"Development Configuration"章节给出了一套可选的本地开发配置流程。下面按原文的四个步骤完整展开,并补充必要的细节说明。

第 1 步:创建 AWS 账号

这是所有后续步骤的前提。你需要一个 AWS 账号来完成 S3 桶、IAM 用户与访问密钥的创建。该步骤在 README 中仅一行带过,但它是本地联调 S3 文件上传/下载的硬性前置条件。

第 2 步:创建 S3 存储桶(Bucket)

创建存储桶时,README 给出了两个关键要求:

  1. Block all public access(阻止所有公共访问):这是安全默认项。即使你的文件最终通过"公开 URL"访问,桶级别也应默认全部封闭,通过对象级策略或预签名 URL 精确控制访问范围,避免桶被整体暴露。
  2. 配置 CORS(跨域资源共享):浏览器端分片上传(PUT请求)是跨域直连 S3 的,因此桶必须允许来自本地开发服务器的跨域请求。

README 提供的 CORS 配置如下(原样):

[ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "PUT", "HEAD" ], "AllowedOrigins": [ "http://127.0.0.1:3000" ], "ExposeHeaders": [ "ETag" ] } ]

逐项说明这张配置的含义:

  • AllowedMethods只开放PUTHEADPUT用于把分片直传 S3,HEAD用于服务端完成上传后校验对象元数据(见 integrations/aws/s3/service.py 的get_head_or_raise)。这里刻意不放GET,因为读取一律走预签名 URL 或公开 URL,不需要也不应该开启浏览器直读。
  • AllowedOrigins指向http://127.0.0.1:3000:这是本地 Web 开发服务器(Next.js)的默认地址。若你的前端跑在其他端口,需要同步修改。
  • ExposeHeaders暴露ETag:Web 端完成每个分片的PUT后,需要从响应头读取ETag作为该分片的校验标记(见下文 Web 端上传流程),因此必须显式暴露该响应头,否则浏览器脚本读不到。

第 3 步:创建 IAM 用户与策略

创建 IAM 策略(Policy)

README 要求先创建一个 IAM 策略,策略名称与 S3 桶名保持一致,策略 JSON 如下(原样):

{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObjectAttributes", "s3:GetObject", "s3:GetObjectVersion", "s3:GetObjectVersionAttributes", "s3:DeleteObject", "s3:DeleteObjectVersion" ], "Resource": "arn:aws:s3:::<S3_BUCKET_NAME>/*" } ] }

注意Resource中的<S3_BUCKET_NAME>需要替换为你的真实桶名。对照源码,这些 Action 与 Polar 文件服务的实际调用一一对应:

  • s3:PutObject:直传分片(upload_part)以及服务端直接上传(service.py 的upload,用于发票、导出文件等后端生成文件)都会用到;
  • s3:GetObject/s3:GetObjectVersion:预签名下载 URL 与公开 URL 的底层操作;
  • s3:DeleteObject:删除文件时调用(service.py 的delete_file);
  • s3:GetObjectAttributes/s3:GetObjectVersionAttributes:读取对象属性,完成上传后获取ETagLastModified等元数据。

策略作用范围限定在arn:aws:s3:::<S3_BUCKET_NAME>/*(桶内所有对象),遵循最小权限原则。

创建 IAM 用户(User)
  • 名称:与 S3 桶名保持一致;
  • 策略:选择上一步创建的策略直接附加;
  • 凭据:用户创建完成后,进入Security credentials(安全凭据)页面,滚动到Access keys(访问密钥)区域生成访问密钥对(Access Key ID + Secret Access Key)。

生成的访问密钥对将填入 Polar 的环境变量中,作为服务端访问 S3 的凭证。

第 4 步:配置 Polar 设置(.env

最后一步是把上面创建的桶名、访问密钥等写入 Polar 的.env,覆盖POLAR_AWS_*相关配置。README 原文如下:

Update your.envfor thePOLAR_AWS_*settings to contain the credentials, bucket name etc from above.

结合 server/polar/config.py 的源码,这一组配置项的实际名称与默认值如下:

环境变量默认值说明
POLAR_AWS_ACCESS_KEY_IDpolar-developmentIAM 用户的 Access Key ID
POLAR_AWS_SECRET_ACCESS_KEYpolar123456789IAM 用户的 Secret Access Key
POLAR_AWS_REGIONus-east-2S3 桶所在区域,需与桶保持一致
POLAR_AWS_SIGNATURE_VERSIONv4AWS 签名版本(SigV4)
POLAR_S3_FILES_BUCKET_NAMEpolar-s3私有文件桶名
POLAR_S3_FILES_PUBLIC_BUCKET_NAMEpolar-s3-public公开文件桶名
POLAR_S3_FILES_PRESIGN_TTL3600(60 分钟)预签名 URL 有效期(秒)
POLAR_S3_ENDPOINT_URL本地可指向 MinIO(http://127.0.0.1:9000)等 S3 兼容服务
POLAR_S3_PUBLIC_ENDPOINT_URL生成给浏览器使用的预签名 URL 的端点;本地开发时需与S3_ENDPOINT_URL区分(见下文说明)

需要特别说明的是环境变量前缀机制:Polar 的Settings类通过SettingsConfigDict(env_prefix="polar_", case_sensitive=False)读取配置(见 config.py),所以源码中的S3_FILES_BUCKET_NAME.env里对应POLAR_S3_FILES_BUCKET_NAME,依此类推。本地默认值可以直接连通一个开箱即用的本地 S3 环境polar-development/polar123456789),这意味着仓库还提供了基于 MinIO 的本地联调方案。

关于S3_ENDPOINT_URLS3_PUBLIC_ENDPOINT_URL的区别,config.py 的注释解释得很清楚:在本地用 Docker 起 MinIO 时,服务端在容器网络内访问 MinIO 用http://minio:9000,而生成给浏览器用的预签名 URL 必须让宿主机也能访问,因此要用http://localhost:9000。生产环境两者都是真实的 AWS 端点,无需区分。

三、文件生命周期:从创建到下载的完整链路

配置好 AWS 之后,文件服务在运行时是如何工作的?结合 endpoints.py 与 service.py 可以还原完整的数据流。

阶段一:创建文件(生成预签名分片上传)

客户端调用POST /v1/files/(endpoints.py),携带FileCreate负载。服务端 generate_presigned_upload 的执行流程为:

  1. create_schema.serviceS3_SERVICES选出对应的 S3 服务;
  2. 调用create_multipart_upload向 S3 发起多段上传初始化(integrations/aws/s3/service.py),生成对象路径{namespace}/{organization_id}/{file_uuid}/{name}——每个组织一个目录、每个文件一个 UUID 目录,因此允许不同文件同名共存;
  3. 为客户端声明的每个分片生成一个预签名 PUT URLgenerate_presigned_upload_parts,有效期为presign_ttl);
  4. files表写入一条is_uploaded=False的元数据记录,随响应返回上传 ID 与分片 URL 列表。

该步骤要求调用方具备products_manage组织权限(assert_organization_permission),且 OAuth 需要files_writescope(见 auth.py)。

阶段二:客户端直传分片到 S3

拿到预签名 URL 后,浏览器绕过 Polar 后端,直接把文件分片PUT到 S3(这就是第 2 步配置 CORS 的原因)。Web 端实现位于 clients/apps/web/src/components/FileUpload/Upload.ts:

  • CHUNK_SIZE = 10MB将文件切块(Upload.ts),逐块计算 SHA-256;
  • 在浏览器端用 hash-wasm 计算整文件 SHA-256 与每个分片的 SHA-256(getMultiparts);
  • 串行上传各分片(uploadMultiparts)。注释明确解释了为何不能并发:S3 会对 SHA-256 做校验,且按文档要求分片必须按顺序接收,乱序会返回 400(Upload.ts);
  • 每个分片成功后从响应头读取ETag,与分片号、分片 SHA-256 一起保存,最后调用POST /v1/files/{id}/uploaded完成整个上传。

阶段三:完成上传(服务端校验并落库)

POST /v1/files/{id}/uploaded(endpoints.py)调用 complete_upload:

  1. 通过complete_multipart_upload告知 S3 所有分片已就绪(integrations/aws/s3/service.py),S3 据此组装完整对象;
  2. 服务端head_object取回最终对象元数据,把ETagLastModifiedstorage_version写回files表;
  3. is_uploaded置为True——此后文件才对外可见可下载(list接口默认只返回is_uploaded的文件,见 service.py)。

对应地,server/tests/file/test_endpoints.py 验证了"未完成上传时 S3 对象不可用"的行为:只创建不上传、不 complete,读取对象会抛出S3FileError

阶段四:下载与公开访问

  • 私有文件(downloadable):调用GET /v1/files/{id}/download(endpoints.py)获得一个预签名下载 URL。服务端 generate_presigned_download_url 在签名参数中携带ResponseContentDisposition(保证浏览器以下载而非内联方式处理,正确处理非 ASCII 文件名)与ResponseContentType,URL 有效期默认 60 分钟(POLAR_S3_FILES_PRESIGN_TTL)。
  • 公开文件(product_media / organization_avatar):响应模型通过public_url计算字段直接生成公开 URL(见 schemas.py)。底层用UNSIGNED签名客户端生成ExpiresIn=0的 URL(integrations/aws/s3/service.py),本质是拼出一个不带签名的公开访问地址。

值得注意的安全细节:下载接口在返回 URL 前还会检查flagged_malicious_at,被判定为恶意的文件直接拒绝下载(endpoints.py)。

阶段五:更新与删除

  • PATCH /v1/files/{id}仅允许修改nameversion两个元数据字段(service.py);
  • DELETE /v1/files/{id}执行软删除(置deleted_at),同时删除ProductMedia关联记录并调用 S3delete_object清理对象(service.py);
  • 以上两个接口均通过_assert_mutable拒绝工单附件(support_case_attachment)的修改与删除——工单附件属于工单记录的一部分,只能经工单接口创建,不通过文件 API 变更(endpoints.py)。

四、四种文件类型的差异化校验规则

不同服务类型对 MIME 类型和大小有着完全不同的约束,这些约束以 Pydantic 校验形式定义在 schemas.py,创建文件时后端即会强校验:

服务类型允许的 MIME 类型大小上限
downloadable不限不限(仅受 S3 与分片数约束)
product_media仅图片:image/jpegimage/pngimage/gifimage/webpimage/svg+xml10 MB
organization_avatar仅图片:image/jpegimage/pngimage/gifimage/webpimage/svg+xml1 MB
support_case_attachment图片、视频(mp4/quicktime/webm)、PDF、CSV、纯文本、Word、Excel250 MB

这些规则分别定义在 schemas.py(商品媒体图)、L49-L64(头像)与 L67-L92(工单附件)。此外,创建请求对分片数量也有上限约束——test_endpoints.py 验证了提交超过 1 万个分片会被拒绝(HTTP 422,too_long校验错误),防止恶意构造超大分片列表。

FileCreateFileRead均采用 Pydantic判别联合(Discriminated Union):以service字段作为判别器,客户端传什么service,就自动套用对应的校验规则与响应模型(schemas.py)。数据模型层面,File基类通过polymorphic_on: "service"实现了 SQLAlchemy 单表继承,四个子类(DownloadableFileProductMediaFileOrganizationAvatarFileSupportCaseAttachmentFile)共用files表(models/file.py)。

五、安全机制:GuardDuty 恶意文件检测

文件服务内置了与 AWS GuardDuty(S3 恶意文件检测)联动的闭环:

  1. GuardDuty 扫描上传到 S3 桶的对象,发现风险后投递扫描结果事件;
  2. Polar worker 通过异步任务file.guardduty_scan_result(tasks.py)接收事件;
  3. 任务首先校验事件中的桶名是否属于POLAR_S3_FILES_BUCKET_NAMEPOLAR_S3_FILES_PUBLIC_BUCKET_NAME(防止伪造/误投递),再按对象 key 反查files表;
  4. 命中后调用 flag_malicious,将flagged_malicious_at与扫描详情写入数据库,并向组织成员发送maintainer_file_flagged_malicious通知。

文件被标记后即被"禁用消费":下载接口直接拒绝(HTTP 403),商品媒体选择接口也通过flagged_malicious_at.is_(None)条件排除被标记文件(service.py)。该任务以TaskPriority.MEDIUM优先级投递(tasks.py),对应的异步任务测试位于 server/tests/file/test_tasks.py。

六、接口与权限一览

文件服务的所有接口都以/v1/files为前缀(endpoints.py),汇总如下:

方法路径摘要权限
GET/v1/files/列出文件(支持按组织、按 ID 过滤与分页)files_read/files_write
POST/v1/files/创建文件并返回预签名分片上传参数files_write+products_manage
POST/v1/files/{id}/uploaded完成分片上传files_write+products_manage
GET/v1/files/{id}/download获取预签名下载 URLfiles_read/files_write
PATCH/v1/files/{id}更新文件名/版本(工单附件除外)files_write+products_manage
DELETE/v1/files/{id}软删除并清理 S3 对象(工单附件除外)files_write+products_manage

鉴权上,auth.py 定义了两个依赖:FileRead需要files_readfiles_writescope 之一,FileWrite严格要求files_write;主体(subject)可以是UserOrganization。写操作还需组织级products_manage权限(endpoints.py),未认证请求一律 401(见 test_endpoints.py)。列表接口还通过get_accessible_org_ids做数据隔离,普通用户只能看到自己有权限组织的文件(service.py)。

结语

Polar 的文件服务是一个典型的"小而完整"的 S3 集成案例:四个步骤即可在本地完成 AWS 配置(建桶 → CORS → IAM →.env),运行时则通过服务端签发预签名分片 URL + 浏览器直传 S3 + 服务端完成校验的架构,既避免了文件流量经过应用服务器,又通过 SHA-256 校验与 GuardDuty 检测保障了数据完整性与安全性。如果你想深入阅读实现细节,建议从 server/polar/file/s3.py(桶映射)和 clients/apps/web/src/components/FileUpload/Upload.ts(浏览器端分片上传)这两个端点入手,配合 server/tests/file/test_endpoints.py 中的测试用例理解完整行为。

【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 19:42:14

国产Linux系统上用pyenv实现Python多版本隔离管理实战

先说一个我自己的踩坑经历。去年在一台统信UOS 20机器上部署数据分析服务&#xff0c;项目依赖锁文件要求Python 3.10&#xff0c;机器自带的是Python 3.7。当时图省事&#xff0c;直接改了系统Python的软链接&#xff0c;结果重启后桌面环境直接起不来了。后来才搞清楚&#x…

作者头像 李华
网站建设 2026/9/16 19:41:32

智能算法在栅格地图路径规划中的对比与应用

1. 项目背景与核心价值在机器人导航、物流配送和自动驾驶等领域&#xff0c;路径规划始终是核心问题之一。二维栅格地图作为最常见的环境建模方式&#xff0c;其路径优化效果直接影响系统性能。传统算法如A*、Dijkstra在复杂环境中容易陷入局部最优&#xff0c;而智能优化算法因…

作者头像 李华
网站建设 2026/9/16 19:40:12

从零构建AI Agent工具调用系统的核心技术与实践

1. 项目概述&#xff1a;为什么要从零构建Agent工具调用系统&#xff1f;最近两年&#xff0c;AI Agent技术呈现爆发式增长。根据2023年行业报告&#xff0c;全球超过67%的企业正在或计划部署Agent系统。但现成的框架往往存在两个致命问题&#xff1a;一是黑箱化严重&#xff0…

作者头像 李华
网站建设 2026/9/16 19:39:56

混合动作空间强化学习:P-DQN与MPDQN算法核心机制全解析

做强化学习的人&#xff0c;可能都遇到过这种让人发疯的情况&#xff1a;环境里的动作要么是离散的&#xff08;左转、右转、抓取、放下&#xff09;&#xff0c;要么是连续的&#xff08;方向盘转角、关节力矩、功率大小&#xff09;&#xff0c;但真实世界从来不按教科书出牌…

作者头像 李华