Skip to content

fix(s3): preserve trailing-slash objects - #3303

Open
Suyunmeng wants to merge 8 commits into
mainfrom
issue-3186-fix-s3-compatibility
Open

Suyunmeng wants to merge 8 commits into
mainfrom
issue-3186-fix-s3-compatibility

Conversation

@Suyunmeng

Copy link
Copy Markdown
Contributor

Summary / 摘要

  • Persist zero-byte S3 objects with trailing slashes as durable markers so HEAD, GET, LIST, DELETE, and COPY behave consistently. Remove synthetic empty-directory objects and preserve stable marker metadata and modification times.

  • 将以 / 结尾的零字节 S3 对象作为持久化 marker 保存,使 HEAD、GET、LIST、DELETE 和 COPY 行为一致。移除虚假的空目录对象,并保持 marker 元数据及修改时间稳定。

  • Migrate legacy marker hashes and indexes to a unique bucket-and-key identity; bind multipart operations to their original bucket and object. Listing also preserves the root / marker.

  • 将旧 marker hash 和索引迁移为 bucket 与 key 的唯一标识;multipart 操作绑定原始 bucket 和对象。列表同时支持根路径 / marker。

  • This PR has breaking changes.
    / 此 PR 包含破坏性变更。

  • This PR changes public API, config, storage format, or migration behavior.
    / 此 PR 修改了公开 API、配置、存储格式或迁移行为。

  • This PR requires corresponding changes in related repositories.
    / 此 PR 需要关联仓库同步修改。

Related repository PRs / 关联仓库 PR:

  • OpenList-Frontend: Not applicable
  • OpenList-Docs: Not applicable

Related Issues / 关联 Issue

Fixes #3186

Testing / 测试

  • Passed / 已通过: go test ./internal/db ./server/s3 -count=1
  • go test ./... (not run; full-repository validation was outside the requested scope)
  • Manual test / 手动测试: Not run

Checklist / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING。
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    / 我已按适用情况使用 gofmt、go fmt 或 prettier 格式化变更代码。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者或代码所有者审查。

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.
    / 此 PR 包含 AI 辅助内容。

Tools used / 使用工具:

  • ChatGPT
  • Codex
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明): Lunelle ( Experimental AI )

Usage scope / 使用范围:

  • Code generation / 代码生成

  • Refactoring / 重构

  • Documentation / 文档

  • Tests / 测试

  • Translation / 翻译

  • Review assistance / 审查辅助

  • I have reviewed and validated all AI-assisted content included in this PR.
    / 我已审核并验证此 PR 中的所有 AI 辅助内容。

  • I have ensured that all AI-assisted commits include Co-Authored-By attribution.
    / 我已确保所有 AI 辅助提交都包含 Co-Authored-By 归属信息。

  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。

Suyunmeng and others added 4 commits October 27, 2026 19:42
- Persist zero-byte trailing-slash objects as S3 markers
- Remove synthetic empty-directory objects from listings
- Validate multipart upload bucket and object identity
- Add marker lifecycle and listing regression tests

Co-authored-by: LunelleEverdawn <lunelle+suyunmeng@oplist.org>
- Backfill bucket-aware marker key hashes before AutoMigrate
- Retain the latest marker for duplicate bucket and key pairs
- Replace the legacy composite index with the unique hash index
- Cover legacy marker migration and repeat startup

Co-authored-by: LunelleEverdawn <lunelle+suyunmeng@oplist.org>
- Preserve the root slash marker during bucket listing
- Add a regression test for the root marker key

Co-authored-by: LunelleEverdawn <lunelle+suyunmeng@oplist.org>
@Suyunmeng
Suyunmeng requested a review from a team October 27, 2026 13:45

@jyxjjj jyxjjj left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] marker 的精确 key 没有贯通 HTTP 路由,实际 HEAD、GET、DELETE 仍然访问不到 PUT 保存的对象。

server/s3/backend.go:123

当前固定使用的 gofakes3 v0.8.2 会先裁掉请求路径首尾的 /,只对普通 PUT 补回末尾的 /。因此 PUT /mp/dir/ 保存的是 dir/,但 HEAD、GET、DELETE /mp/dir/ 传入后端的是 dir,匹配不到保存的 marker。只有 marker、没有物理目录时,HEAD 和 GET 会返回找不到对象,DELETE 也不会删除 marker。

要让各方法经过路由后仍然保留同一个对象 key,并补经过 HTTP handler 的生命周期测试。现在的测试直接调用后端方法,绕过了这层问题。

[P2] 带 delimiter 的列表把子目录 marker 放进了 Contents,目录分组和分页计数会出错。

server/s3/list.go:239

例如只有 nested/marker/,请求 prefix=nested/&delimiter=/ 时,应返回 CommonPrefixes 中的 nested/marker/。这里先执行 marker 的 addContent,随后在没有物理目录时直接 continue,返回的却是一个普通对象。如果同时存在同名物理目录,还会把同一个 key 同时加入 Contents 和 CommonPrefixes,并占用两次 MaxKeys。

AWS ListObjectsV2 文档要求,被 delimiter 折叠的 key 只出现在 CommonPrefixes,每组只计为一项。要根据请求 prefix 和 delimiter 决定 marker 是否需要折叠成 CommonPrefixes。TestS3NestedMarkerListing 当前把这个错误结果写成了预期,也要一起调整。


Generated by ChatGPT-6.1-Sol Medium

- Recover trailing-slash marker keys lost by gofakes3 routing
- Return delimiter-folded markers as common prefixes
- Add HTTP lifecycle and delimiter listing regression tests

Co-authored-by: LunelleEverdawn <lunelle+suyunmeng@oplist.org>
@Suyunmeng
Suyunmeng requested a review from jyxjjj October 28, 2026 14:57

@jyxjjj jyxjjj left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 下载重定向会绕过 marker 判断,GET 和 HEAD 可能返回不同对象。

server/s3/server.go:42

redirectHandler 在 trailingSlashHandler 和 S3 后端之前执行。如果桶里同时存在普通文件 a 和 marker a/,GET /桶/a/ 会先被 directObjectURL 通过 path.Join 解析成普通文件 a。当驱动允许直链时,请求直接重定向到文件内容,后端没有机会返回零字节 marker。HEAD 则进入后端,返回 marker。

要让尾斜杠对象先按精确 key 判断,避免被普通文件的直链截走,并覆盖普通文件和同名 marker 并存的 HTTP 测试。

[P2] 分片上传仍然丢失尾斜杠,完成后保存的不是 marker。

server/s3/server.go:16

当前固定使用的 gofakes3 v0.8.2 路由会先裁掉路径首尾的 /,补回尾斜杠的逻辑只在普通 PUT 路由里执行。POST /桶/dir/?uploads、UploadPart 和 CompleteMultipartUpload 传入后端的 object 都是 dir,最终 putStream 会走普通文件上传分支,不会保存 dir/ marker。

新增的 TestS3MarkerMultipartCompletion 直接调用后端,绕过了这个路由行为。要让分片上传各阶段保留同一个精确 key,并补经过 HTTP handler 的测试。

之前指出的普通 HEAD、GET、DELETE 尾斜杠问题,以及 delimiter 分组问题,最新修改已经处理,不再重复提出。


Reviewed or refined by GPT-6.1-Sol Medium

- Skip direct download redirects for trailing-slash object keys
- Flag trailing-slash requests for every method, including multipart routes
- Restore exact marker keys in multipart create, part, complete and abort
- Add HTTP tests for same-name file redirects and multipart markers

Co-authored-by: LunelleEverdawn <lunelle+suyunmeng@oplist.org>

Suyunmeng commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

最新评审中指出的两点问题已在 2701e29 中得到修复:

  • 直接下载重定向现在会跳过末尾带有斜杠(/)的键;因此,执行 GET /bucket/a/ 时,系统会进行标记(marker)查找,而不会重定向至文件 a。
  • 分段上传操作(创建、上传分段、完成及中止)现在会保留末尾的斜杠;因此,完成对 dir/ 的上传操作时,系统能够正确保存该标记。

上述两种情况现均已配备 HTTP 级别的测试用例。

@Suyunmeng
Suyunmeng requested a review from jyxjjj October 31, 2026 03:56

@jyxjjj jyxjjj left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] marker 删除后再次 DELETE,会落到同名普通文件,造成误删。

server/s3/backend.go:419–434

例如桶里同时存在普通文件 a 和 marker a/。第一次 DELETE /bucket/a/ 会删除 marker,第二次相同请求查不到 marker 后,会继续进入 path.Join(bucketPath, objectName) 和 fs.Remove,最终删除普通文件 a。HEAD、GET 在 marker 不存在时也会回退到 a,返回另一个对象。

这里沿用了旧的路径归一化行为,但新增 marker 生命周期只处理了“marker 存在”的分支,没有处理删除后的精确 key 隔离。要让尾斜杠 key 查找失败后仍按该对象处理,DELETE 不存在的 marker 应成功结束,不能继续删除去掉斜杠后的文件。补一个 a 与 a/ 并存、连续删除两次 a/ 后 a 仍保留的 HTTP 测试。

之前指出的下载重定向、分片上传尾斜杠丢失和 delimiter 分组问题,最新代码已经处理,重定向和分片上传也补了 HTTP 测试,不再重复提出。


Reviewed or refined by GPT-6.1-Sol Medium

Suyunmeng and others added 2 commits November 8, 2026 14:32
- Prevent trailing-slash reads from falling back to same-name files
- Keep idempotent marker deletes from removing ordinary objects
- Cover marker and ordinary key coexistence through HTTP

Co-authored-by: LunelleEverdawn <lunelle+suyunmeng@oplist.org>

Copy link
Copy Markdown
Contributor Author

已确认该问题确实会发生,并已在提交 c9b61ec6a 修复:marker 不存在时,尾斜杠请求不会再回退到同名普通文件;重复删除也不会误删普通文件。已补充同名 key 共存的 HTTP 回归测试。

@Suyunmeng
Suyunmeng requested a review from jyxjjj November 8, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] OpenList S3 API is incompatible with most external S3 clients

2 participants