s3

package module
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 13 Imported by: 0

README

S3 Source

go-scripts 提供 Amazon S3(以及任何 S3 兼容的对象存储)作为脚本来源的 Source 实现。

设计要点

  • 统一接口:实现 scriptEngine.Source,可作为 Engine 的脚本来源,或拼入 MultiSource 与其他源(File/Mem/HTTP/...)做 fallback / 并发择快。
  • AWS SDK v2:基于 aws-sdk-go-v2,支持完整的默认凭证链(环境变量 / shared config / IMDS / ECS / SSO)。
  • S3-compatible 友好:通过 WithEndpoint + WithPathStyle 即可接入 MinIO / Alibaba OSS / Tencent COS / Cloudflare R2 / LocalStack 等。
  • 热更新检测:基于 ETag(首选)和 LastModified(兜底)的版本比对,等价于 FileSource 对 mtime 的处理。
  • 并发安全:所有方法(Load / ReloadCheck / Close)都是 goroutine-safe;版本号比对用 sync.RWMutex 保护。
  • 可测试:内部定义 s3API 子集接口,测试用 httptest.NewServer 起一个最小 S3 兼容服务即可覆盖;不需要真实 bucket。

依赖

快速开始

1. 引入模块
import (
    "context"

    scriptEngine "github.com/tx7do/go-scripts"
    _ "github.com/tx7do/go-scripts/lua"   // 注册 Lua 引擎工厂
    s3src "github.com/tx7do/go-scripts/s3"
)
2. AWS S3(默认凭证链)
ctx := context.Background()

src, err := s3src.New(ctx, "my-prod-scripts",
    s3src.WithRegion("ap-northeast-1"),
    s3src.WithPrefix("lua/"), // bucket 内公共前缀
)
if err != nil {
    log.Fatal(err)
}
defer src.Close()

// 接入 Engine
eng, _ := scriptEngine.NewScriptEngine(scriptEngine.LuaType)
_ = eng.Init(ctx)
eng.SetSource(src)

// Load 实际是从 s3://my-prod-scripts/lua/main.lua 拉取
_, err = eng.ExecuteFromKey(ctx, "main.lua")

凭证按 AWS SDK 默认链解析:

  1. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_SESSION_TOKEN 环境变量
  2. AWS_PROFILE 指向的 shared config(~/.aws/credentials
  3. EC2 IMDS / ECS task role / IRSA / SSO
3. MinIO / 自建对象存储
src, err := s3src.New(ctx, "scripts",
    s3src.WithEndpoint("http://minio.minio.svc.cluster.local:9000"),
    s3src.WithPathStyle(),                              // MinIO 必需
    s3src.WithRegion("us-east-1"),                      // MinIO 不关心,但 SDK 需要
    s3src.WithStaticCredentials("AK", "SK", ""),
)
4. Cloudflare R2
src, err := s3src.New(ctx, "my-bucket",
    s3src.WithEndpoint("https://<ACCOUNT_ID>.r2.cloudflarestorage.com"),
    s3src.WithStaticCredentials(r2AccessKey, r2SecretKey, ""),
    s3src.WithRegion("auto"),   // R2 接受任意 region
)
5. 匿名访问公共 bucket
src, err := s3src.New(ctx, "public-scripts",
    s3src.WithRegion("us-east-1"),
    s3src.WithAnonymous(),  // 不带签名
)

核心 API

构造
函数 说明
New(ctx, bucket, opts...) 构造 *Sourcebucket 必填,其它都是可选 Option
Options
Option 作用 默认
WithRegion(region) AWS region 由默认链解析
WithEndpoint(url) 自定义 endpoint(MinIO/OSS/R2/LocalStack) AWS 默认
WithPathStyle() 强制 path-style 寻址(endpoint/bucket/key virtual-hosted
WithPrefix(prefix) key 前缀,自动去除首 / 并补尾 /
WithCredentials(p) 自定义 aws.CredentialsProvider 默认链
WithStaticCredentials(ak, sk, token) 静态凭证(封装 WithCredentials
WithAnonymous() 关闭签名(公共 bucket / 测试 fake)
Source 方法
方法 说明
Load(ctx, key) 从 S3 GET 对象,返回 body 字符串;记录当前 ETag/LastModified 作为版本基准
ReloadCheck(ctx, key) HEAD 对象,比对 ETag(首选)或 LastModified(兜底)
Close() 释放资源(AWS SDK v2 S3 client 无显式 Close,目前为 no-op)

错误处理

错误 说明
ErrNotFound 对象不存在(404 / NoSuchKey);用 errors.Is(err, s3src.ErrNotFound)s3src.IsNotFound(err) 识别
其它 包装 SDK 原始错误,前缀为 s3 source: ...,包含完整 object key
code, err := src.Load(ctx, key)
if errors.Is(err, s3src.ErrNotFound) {
    // 对象不存在
} else if err != nil {
    // 网络 / 权限 / 其它
}

与 Engine 集成

单引擎
eng, _ := scriptEngine.NewScriptEngine(scriptEngine.LuaType)
_ = eng.Init(ctx)
eng.SetSource(src)                  // 绑定 Source

_, _ = eng.ExecuteFromKey(ctx, "init.lua")
_, _ = eng.ExecuteFromKey(ctx, "main.lua")
引擎池
pool, _ := scriptEngine.NewEnginePool(8, scriptEngine.LuaType)
defer pool.Close()
pool.SetSource(src)                 // ⚠️ 警告:见下

_, _ = pool.ExecuteFromKey(ctx, "init.lua")

⚠️ 重要警告EnginePool.SetSource 只作用于"被 Acquire 的那一个 engine 实例"。池中其它 engine 仍然 source=nil。如果想让所有 engine 都用同一个 Source,请:

  • 在创建 engine 时预先 SetSource,再加入池;或
  • 自己遍历池内的 engine 逐个 SetSource;或
  • 直接用 pool.ExecuteFromKey(ctx, key),让每次调用都临时 SetSource(需要外层 lock 保护,更推荐前两种方案)。

当前 pool wrapper 的"per-call 局部状态语义"详见 engine_pool.go 注释

配合 MultiSource 做 fallback
mem := scriptEngine.NewMemSource()
mem.Set("main.lua", `-- fallback inline script`)

multi, _ := scriptEngine.NewFallbackSource(src, mem) // S3 优先,失败回退到内存
eng.SetSource(multi)

热更新检测

ReloadCheck 的工作流:

HEAD s3://bucket/prefix/key
  ↓
拿到 (ETag, LastModified)
  ↓
与 Load 时记录的版本比对
  ↓
不同 → changed=true
  • 首选 ETag:S3 对 PUT 的对象默认返回一个 MD5-based ETag(multipart upload 的对象 ETag 是合成 hash)。ETag 不同 → 内容一定变了。
  • 兜底 LastModified:当任一侧 ETag 为空(某些 S3 兼容服务不返回),退化到 LastModified 比对。注意 HTTP Last-Modified 精度为 1 秒,秒级内的连续修改可能被合并。
  • 未 Load 过的 keychanged=true(与 FileSource 行为一致)。

ReloadCheck 只是"通知有变化",不会自动重新加载。需要业务侧显式再调一次 Load / ExecuteFromKey

ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
    if changed, _ := src.ReloadCheck(ctx, "main.lua"); changed {
        _, _ = eng.ExecuteFromKey(ctx, "main.lua") // 热更新生效
    }
}

测试

cd source/s3
go test -v ./...

测试覆盖:

类别 用例
接口实现 TestSource_ImplementsInterface(编译期断言)
构造 TestNew_RequiresBucket / TestWithPrefix_Normalized(6 种 prefix 写法)
Load TestLoad_HappyPath / TestLoad_NotFound_WrapsSentinel / TestLoad_WithPrefix / TestLoad_ContextCanceled
ReloadCheck TestReloadCheck_NewKey_IsChanged / TestReloadCheck_AfterLoad_NotChanged / TestReloadCheck_AfterMutation_IsChanged / TestReloadCheck_NotFound / TestReloadCheck_FallsBackToLastModified
凭证 TestNew_WithStaticCredentials
并发 TestLoad_Concurrent(30 goroutine 并发 Load + 计数校验)
资源 TestClose_NoError

测试通过 httptest.NewServer 起一个最小 S3 兼容服务(path-style、忽略签名、返回 CRC32C checksum),不依赖任何真实 S3 bucket,CI 友好。

相关文档

Documentation

Overview

Package s3 provides a source.Reader implementation that reads scripts from an Amazon S3 bucket (or any S3-compatible object storage such as MinIO, Alibaba OSS, Tencent COS, Cloudflare R2, LocalStack, ...).

Construction:

src, err := s3.New(ctx, "my-bucket",
    s3.WithRegion("us-east-1"),
    s3.WithPrefix("scripts/lua/"),
)

Hot-reload detection compares the object's ETag and LastModified against the values recorded by the most recent Load; a subsequent ReloadCheck reports true when either changes.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("s3 source: object not found")

ErrNotFound is returned (wrapped) by Load / ReloadCheck when the requested object does not exist in the bucket. Detect with errors.Is(err, ErrNotFound) or the convenience helper IsNotFound.

Functions

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err represents a "404 / NoSuchKey" response from S3. Equivalent to errors.Is(err, ErrNotFound).

Types

type Option

type Option func(*configOptions)

Option configures a [Source]. Pass to New.

func WithAnonymous

func WithAnonymous() Option

WithAnonymous disables authentication entirely. Use for public buckets or for endpoints (such as test fakes) that don't validate signatures.

func WithCredentials

func WithCredentials(p aws.CredentialsProvider) Option

WithCredentials supplies an explicit credentials provider. By default the standard AWS credential chain (env vars / shared config / IMDS / ECS) is used.

func WithEndpoint

func WithEndpoint(endpoint string) Option

WithEndpoint overrides the AWS endpoint URL. Use this to point at MinIO, Alibaba OSS, Tencent COS, Cloudflare R2, LocalStack, etc.

func WithPathStyle

func WithPathStyle() Option

WithPathStyle forces path-style addressing (https://endpoint/bucket/key instead of https://bucket.endpoint/key). Required by MinIO and some other self-hosted S3-compatible servers.

func WithPrefix

func WithPrefix(prefix string) Option

WithPrefix sets a key prefix that is transparently prepended to every key before it is resolved against the bucket. Useful when all scripts share a common directory inside the bucket (e.g. WithPrefix("scripts/lua/")).

Leading slashes are stripped and a trailing slash is added automatically, so "scripts", "/scripts", "scripts/" all normalize to "scripts/".

func WithRegion

func WithRegion(region string) Option

WithRegion sets the AWS region (e.g. "us-east-1"). When omitted the default AWS SDK chain resolves the region from env / shared config.

func WithStaticCredentials

func WithStaticCredentials(accessKey, secretKey, token string) Option

WithStaticCredentials is a shortcut for WithCredentials with a static access-key / secret-key pair. Pass an empty token unless the credentials are temporary (STS).

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

Reader reads scripts from an S3 bucket.

All exported methods are safe for concurrent use. Reader implements the source.Reader interface.

func New

func New(ctx context.Context, bucket string, opts ...Option) (*Reader, error)

New creates an S3-backed Reader. `bucket` is required; all other settings are optional.

Credentials are loaded via the default AWS SDK v2 chain (env vars / shared config / IMDS / ...). Override with WithCredentials / WithStaticCredentials / WithAnonymous.

For S3-compatible services pass WithEndpoint("https://...") and (typically) WithPathStyle().

func (*Reader) Close

func (r *Reader) Close() error

Close releases any underlying resources. The AWS SDK v2 S3 client has no explicit Close method, so this is currently a no-op; the method exists so Reader satisfies source.Reader and so future implementations can release resources (custom HTTP transports, pools, ...) here.

func (*Reader) Load

func (r *Reader) Load(ctx context.Context, key string) (string, error)

Load fetches the object from S3 and returns its body as a string. Context cancellation propagates to the underlying request.

A "404 / NoSuchKey" response is reported as a wrapped ErrNotFound. Other errors are wrapped with the object key for easier debugging.

func (*Reader) Watch

func (r *Reader) Watch(ctx context.Context, key string) (<-chan struct{}, error)

Watch returns a channel that signals when the object identified by `key` changes. It polls the object's ETag and LastModified via HeadObject every 5 seconds and sends a signal on the channel when either value changes.

The returned channel is closed when the context is cancelled. Callers should re-Load the script after receiving from the channel.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL