S3 命令行工具 Docker 容器运行

源代码

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
相关推荐
.柒宇.2 小时前
Linux 时间同步服务:Chrony 深度笔记
linux·运维·服务器
zjeweler2 小时前
云服务器centos7.6搭建个人网站教程
运维·服务器
米高梅狮子2 小时前
04.yaml和Kubernetes Pod精讲
云原生·容器·kubernetes
PGCCC2 小时前
PostgreSQL DBA 进阶:从日常运维到生产级性能与高可用实战
运维·postgresql·dba
2301_792674862 小时前
java学习day31 (docker)
java·学习·docker
观测云2 小时前
观测云 x AI Agent:运维智能化的范式跃迁实践
大数据·运维·人工智能
NINGMENGb2 小时前
被误读的“传播力”——Infoseek如何量化媒体投放的“质量”而非“数量”
运维·人工智能·媒体·ai监测·舆情监测·舆情监测系统
Elivs.Xiang2 小时前
centos9中安装Jenkins
linux·运维·centos·jenkins
gjc5922 小时前
MySQL运维避坑:你的MySQL总是关机慢、启动卡?
运维·数据库·mysql