源代码
bash
package main
import (
"bytes"
"context"
"flag"
"fmt"
"io"
"log"
"os"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)
// S3Client 封装 AWS S3 客户端,提供面向对象接口
type S3Client struct {
ctx context.Context
svc *s3.S3
logger *log.Logger
}
// NewS3Client 创建一个新的 S3Client 实例
func NewS3Client(ctx context.Context, endpoint, accessKey, secretKey, region string, disableSSL, forcePathStyle bool, logger *log.Logger) (*S3Client, error) {
conf := &aws.Config{
Endpoint: aws.String(endpoint),
S3ForcePathStyle: aws.Bool(forcePathStyle),
DisableSSL: aws.Bool(disableSSL),
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
Region: aws.String(region),
}
sess, err := session.NewSessionWithOptions(session.Options{Config: *conf})
if err != nil {
return nil, fmt.Errorf("创建 AWS session 失败: %v", err)
}
return &S3Client{
ctx: ctx,
svc: s3.New(sess),
logger: logger,
}, nil
}
// CreateBucket 创建存储桶
func (c *S3Client) CreateBucket(bucketName string) error {
input := &s3.CreateBucketInput{
Bucket: aws.String(bucketName),
}
_, err := c.svc.CreateBucket(input)
if err != nil {
return fmt.Errorf("创建存储桶失败: %v", err)
}
c.logger.Printf("存储桶 %s 创建成功", bucketName)
return nil
}
// ListBuckets 列出所有存储桶
func (c *S3Client) ListBuckets() error {
resp, err := c.svc.ListBuckets(&s3.ListBucketsInput{})
if err != nil {
return fmt.Errorf("列出存储桶失败: %v", err)
}
if len(resp.Buckets) == 0 {
fmt.Println("没有找到任何存储桶")
return nil
}
fmt.Println("存储桶列表:")
for _, b := range resp.Buckets {
fmt.Printf(" - %s (创建时间: %s)\n", aws.StringValue(b.Name), aws.TimeValue(b.CreationDate).Format(time.RFC3339))
}
return nil
}
// ListObjects 列出指定存储桶中的对象
func (c *S3Client) ListObjects(bucketName string) error {
input := &s3.ListObjectsInput{
Bucket: aws.String(bucketName),
}
resp, err := c.svc.ListObjects(input)
if err != nil {
return fmt.Errorf("列出对象失败: %v", err)
}
if len(resp.Contents) == 0 {
fmt.Printf("存储桶 %s 中没有对象\n", bucketName)
return nil
}
fmt.Printf("存储桶 %s 中的对象:\n", bucketName)
for _, obj := range resp.Contents {
fmt.Printf(" - %s (大小: %d bytes, 修改时间: %s)\n",
aws.StringValue(obj.Key), aws.Int64Value(obj.Size), aws.TimeValue(obj.LastModified).Format(time.RFC3339))
}
return nil
}
// PutObject 上传对象到存储桶
func (c *S3Client) PutObject(bucketName, objectName, filePath string) error {
data, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("读取文件失败: %v", err)
}
ctx, cancel := context.WithTimeout(c.ctx, 30*time.Second)
defer cancel()
_, err = c.svc.PutObjectWithContext(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectName),
Body: bytes.NewReader(data),
ACL: aws.String("public-read"),
})
if err != nil {
return fmt.Errorf("上传对象失败: %v", err)
}
c.logger.Printf("对象 %s 已上传到 %s", objectName, bucketName)
return nil
}
// GetObject 从存储桶下载对象
func (c *S3Client) GetObject(bucketName, objectName, outputFile string) error {
ctx, cancel := context.WithTimeout(c.ctx, 30*time.Second)
defer cancel()
out, err := c.svc.GetObjectWithContext(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectName),
})
if err != nil {
return fmt.Errorf("下载对象失败: %v", err)
}
defer out.Body.Close()
data, err := io.ReadAll(out.Body)
if err != nil {
return fmt.Errorf("读取对象内容失败: %v", err)
}
if outputFile == "" {
fmt.Print(string(data))
} else {
if err := os.WriteFile(outputFile, data, 0644); err != nil {
return fmt.Errorf("写入文件失败: %v", err)
}
c.logger.Printf("对象 %s 已保存到 %s", objectName, outputFile)
}
return nil
}
// GetObjectAcl 获取对象的 ACL 并打印
func (c *S3Client) GetObjectAcl(bucketName, objectName string) error {
out, err := c.svc.GetObjectAcl(&s3.GetObjectAclInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectName),
})
if err != nil {
return fmt.Errorf("获取对象 ACL 失败: %v", err)
}
fmt.Printf("对象 %s 的 ACL:\n", objectName)
fmt.Printf(" 所有者: %s\n", aws.StringValue(out.Owner.DisplayName))
for _, grant := range out.Grants {
grantee := grant.Grantee
permission := aws.StringValue(grant.Permission)
if grantee.DisplayName != nil {
fmt.Printf(" - 授权给 %s: %s\n", aws.StringValue(grantee.DisplayName), permission)
} else if grantee.URI != nil {
fmt.Printf(" - 授权给 %s: %s\n", aws.StringValue(grantee.URI), permission)
} else if grantee.ID != nil {
fmt.Printf(" - 授权给 %s: %s\n", aws.StringValue(grantee.ID), permission)
}
}
return nil
}
// 命令行参数结构
type config struct {
endpoint string
accessKey string
secretKey string
region string
disableSSL bool
forcePathStyle bool
}
func parseFlags() *config {
cfg := &config{}
flag.StringVar(&cfg.endpoint, "endpoint", getEnv("S3_ENDPOINT", ""), "S3 endpoint URL")
flag.StringVar(&cfg.accessKey, "access-key", getEnv("AWS_ACCESS_KEY_ID", ""), "AWS access key ID")
flag.StringVar(&cfg.secretKey, "secret-key", getEnv("AWS_SECRET_ACCESS_KEY", ""), "AWS secret access key")
flag.StringVar(&cfg.region, "region", getEnv("AWS_REGION", "cn"), "AWS region")
flag.BoolVar(&cfg.disableSSL, "disable-ssl", getEnvBool("S3_DISABLE_SSL", true), "disable SSL")
flag.BoolVar(&cfg.forcePathStyle, "force-path-style", getEnvBool("S3_FORCE_PATH_STYLE", false), "force path style")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "用法: %s [全局选项] <子命令> [子命令参数]\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\n全局选项:\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\n子命令:\n")
fmt.Fprintf(os.Stderr, " create-bucket <bucket> 创建存储桶\n")
fmt.Fprintf(os.Stderr, " list-buckets 列出所有存储桶\n")
fmt.Fprintf(os.Stderr, " list-objects <bucket> 列出存储桶中的对象\n")
fmt.Fprintf(os.Stderr, " put-object --bucket <bucket> --key <key> --file <path> 上传文件\n")
fmt.Fprintf(os.Stderr, " get-object --bucket <bucket> --key <key> [--output <file>] 下载对象(不指定输出则打印到 stdout)\n")
fmt.Fprintf(os.Stderr, " get-object-acl --bucket <bucket> --key <key> 获取对象 ACL\n")
fmt.Fprintf(os.Stderr, "\n环境变量:\n")
fmt.Fprintf(os.Stderr, " S3_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, S3_DISABLE_SSL, S3_FORCE_PATH_STYLE\n")
}
flag.Parse()
return cfg
}
func getEnv(key, defaultValue string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultValue
}
func getEnvBool(key string, defaultValue bool) bool {
if val := os.Getenv(key); val != "" {
return val == "true" || val == "1"
}
return defaultValue
}
func main() {
// S3_ENDPOINT="https://..." AWS_ACCESS_KEY_ID="..." AWS_SECRET_ACCESS_KEY="..." go run main.go put-object --bucket my-bucket --key myfile.jpg --file ./photo.jpg
cfg := parseFlags()
// 验证必需参数
if cfg.endpoint == "" {
log.Fatal("错误: 必须指定 endpoint (通过 -endpoint 或环境变量 S3_ENDPOINT)")
}
if cfg.accessKey == "" {
log.Fatal("错误: 必须指定 access-key (通过 -access-key 或环境变量 AWS_ACCESS_KEY_ID)")
}
if cfg.secretKey == "" {
log.Fatal("错误: 必须指定 secret-key (通过 -secret-key 或环境变量 AWS_SECRET_ACCESS_KEY)")
}
// 创建日志器和上下文
logger := log.New(os.Stderr, "[S3] ", log.LstdFlags)
ctx := context.Background()
// 实例化客户端
client, err := NewS3Client(ctx, cfg.endpoint, cfg.accessKey, cfg.secretKey, cfg.region, cfg.disableSSL, cfg.forcePathStyle, logger)
if err != nil {
log.Fatalf("初始化 S3 客户端失败: %v", err)
}
// 获取子命令
args := flag.Args()
if len(args) == 0 {
flag.Usage()
os.Exit(1)
}
cmd := args[0]
subArgs := args[1:]
switch cmd {
case "create-bucket":
if len(subArgs) < 1 {
log.Fatal("用法: create-bucket <bucket>")
}
if err := client.CreateBucket(subArgs[0]); err != nil {
log.Fatal(err)
}
case "list-buckets":
if err := client.ListBuckets(); err != nil {
log.Fatal(err)
}
case "list-objects":
if len(subArgs) < 1 {
log.Fatal("用法: list-objects <bucket>")
}
if err := client.ListObjects(subArgs[0]); err != nil {
log.Fatal(err)
}
case "put-object":
fs := flag.NewFlagSet("put-object", flag.ExitOnError)
bucket := fs.String("bucket", "", "存储桶名称")
key := fs.String("key", "", "对象键名")
file := fs.String("file", "", "要上传的文件路径")
fs.Parse(subArgs)
if *bucket == "" || *key == "" || *file == "" {
log.Fatal("put-object 需要 --bucket, --key, --file 参数")
}
if err := client.PutObject(*bucket, *key, *file); err != nil {
log.Fatal(err)
}
case "get-object":
fs := flag.NewFlagSet("get-object", flag.ExitOnError)
bucket := fs.String("bucket", "", "存储桶名称")
key := fs.String("key", "", "对象键名")
output := fs.String("output", "", "输出文件路径(可选)")
fs.Parse(subArgs)
if *bucket == "" || *key == "" {
log.Fatal("get-object 需要 --bucket 和 --key 参数")
}
if err := client.GetObject(*bucket, *key, *output); err != nil {
log.Fatal(err)
}
case "get-object-acl":
fs := flag.NewFlagSet("get-object-acl", flag.ExitOnError)
bucket := fs.String("bucket", "", "存储桶名称")
key := fs.String("key", "", "对象键名")
fs.Parse(subArgs)
if *bucket == "" || *key == "" {
log.Fatal("get-object-acl 需要 --bucket 和 --key 参数")
}
if err := client.GetObjectAcl(*bucket, *key); err != nil {
log.Fatal(err)
}
default:
log.Fatalf("未知子命令: %s\n", cmd)
flag.Usage()
os.Exit(1)
}
}
Dockerfile
bash
# 使用多阶段构建 (Multi-stage build) 以减小最终镜像体积
# 第一阶段:构建阶段
FROM golang:1.20-alpine AS builder
# 设置 Go 环境变量:启用 Go Modules 并使用国内代理
ENV GO111MODULE=on \
GOPROXY=https://goproxy.cn,direct
# (可选)更换 Alpine 软件源为国内镜像,加速后续可能出现的 apk 操作
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
# 设置工作目录
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
# 复制源代码
COPY . .
# 构建可执行文件,关闭 CGO 以生成静态链接的二进制文件
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o s3-tool main.go
# 第二阶段:运行阶段
FROM alpine:latest
# 更换 Alpine 软件源为国内镜像(中科大源)
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
# 安装 CA 证书,以便在 HTTPS 请求中验证 SSL 证书
RUN apk --no-cache add ca-certificates
# 设置工作目录
WORKDIR /root/
# 从构建阶段复制可执行文件
COPY --from=builder /app/s3-tool .
# 设置容器启动时的默认命令,显示帮助信息
ENTRYPOINT ["./s3-tool"]
容器方式运行
构建镜像
bash
docker build -t s3-tool:latest .
方法一:使用环境变量文件(推荐)
创建一个 s3.env 文件:
text
S3_ENDPOINT=https://your-s3-endpoint.com
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_REGION=us-east-1
S3_DISABLE_SSL=false
S3_FORCE_PATH_STYLE=false
- 列出所有存储桶
bash
docker run --rm --env-file s3.env s3-tool:latest list-buckets
- 创建新存储桶
bash
docker run --rm --env-file s3.env s3-tool:latest create-bucket my-new-bucket
- 上传文件(需要挂载宿主机文件)
bash
docker run --rm \
--env-file s3.env \
-v /path/to/your/local/file:/tmp/upload-file \
s3-tool:latest put-object --bucket my-bucket --key remote-key --file /tmp/upload-file
说明:使用 -v 将宿主机文件挂载到容器内可访问的路径。
- 下载对象到宿主机
bash
docker run --rm \
--env-file s3.env \
-v /path/to/save/on/host:/tmp/download \
s3-tool:latest get-object --bucket my-bucket --key remote-key --output /tmp/download/local-file.txt
说明:下载的文件会出现在宿主机的 /path/to/save/on/host/local-file.txt。
方法二:逐个传递环境变量
- 列出所有存储桶
bash
docker run --rm \
-e S3_ENDPOINT="https://your-s3-endpoint.com" \
-e AWS_ACCESS_KEY_ID="your-access-key" \
-e AWS_SECRET_ACCESS_KEY="your-secret-key" \
-e AWS_REGION="us-east-1" \
s3-tool:latest list-buckets