یک S3 Client قابل اتکا در Go بسازید
نمونهٔ Go پیکربندی نسخهٔ دوم AWS SDK، Endpoint سفارشی و path-style را در یک نقطه جمع میکند. هر عملیات با Context محدود اجرا میشود و بستن درست Response Body هم در کد قابل اجرا دیده میشود.

نمونه یک Object کوچک را زیر Prefix برابر tutorial/ میفرستد، متادیتایش را میخواند، دانلودش میکند و در آخر آن را پاک میکند.
فرمانها را بهترتیب اجرا کنید. در این حالت، اگر head یا download خطا دهد، میدانید مشکل پس از مرحلهٔ آپلود رخ داده است.
پیشنیاز و ساختار پروژه
Go 1.20 یا جدیدتر برای Module همراه مستندات در نظر گرفته شده است.
go versionexamples/go/
├── .env # پس از کپیکردن فایل نمونه ساخته میشود
├── go.mod # Module و وابستگیهای مستقیم
├── go.sum # Checksum وابستگیها
└── main.go # Client و هفت فرمان مدیریت فایلدریافت وابستگیهای SDK v2
کتابخانههای AWS لازم برای اجرای نمونه را دانلود میکند.
(cd examples/go && go mod download)config، credentials و service/s3 در AWS SDK for Go v2 استفاده میکند. نسخه قدیمی SDK v1 در این نمونه بهکار نرفته است.ساخت فایل .env
پنج مقدار را از پنل فضای ابری پارسپک کپی کنید. معنی هر متغیر و شکل معمول Endpoint در صفحه شروع آمده است.
upload با خطای امضا میخوابد.cp examples/.env.example examples/go/.envPARSPACK_S3_ENDPOINT=https://c123456.parspack.net
PARSPACK_S3_BUCKET=c123456
PARSPACK_S3_REGION=us-east-1
PARSPACK_S3_ACCESS_KEY=YOUR_ACCESS_KEY
PARSPACK_S3_SECRET_KEY=YOUR_SECRET_KEY| متغیر | چه چیزی در آن میگذارید | اگر اشتباه باشد |
|---|---|---|
PARSPACK_S3_ENDPOINT | آدرس HTTPS پنل، بدون / در انتها؛ شکل معمول https://c123456.parspack.net | Timeout یا خطای امضا |
PARSPACK_S3_BUCKET | نام باکت؛ اغلب با شناسه داخل Endpoint یکی است | NoSuchBucket |
PARSPACK_S3_REGION | فقط برای امضای Signature V4 است. اگر پنل Region نداد، us-east-1 را ثابت نگه دارید | SignatureDoesNotMatch |
PARSPACK_S3_ACCESS_KEY / SECRET_KEY | کلیدهای API از پنل؛ فقط روی سرور | AccessDenied یا خطای امضا |
فایل .env را Commit نکنید. Access Key و Secret Key فقط روی سرور میمانند؛ مرورگر فقط لینک کوتاهعمر میگیرد.
اتصال برنامه به فضای پارسپک
Client را یک بار با مقدارهای .env بسازید. همین شیء برای هر هفت فرمان کافی است.
BaseEndpoint همان آدرس HTTPS پنل است؛ اسلش انتها را بردارید. Region موقعیت سرور نیست؛ اگر پنل چیزی نگفت us-east-1 را عوض نکنید. UsePathStyle باید true باشد تا URL به شکل /bucket/key ساخته شود.Context مهلت یا لغو درخواست را همراه عملیات میبرد؛ نمونه کامل برای کل فرمان حداکثر دو دقیقه میگذارد.cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(os.Getenv("PARSPACK_S3_REGION")),
config.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(
os.Getenv("PARSPACK_S3_ACCESS_KEY"),
os.Getenv("PARSPACK_S3_SECRET_KEY"),
"",
),
),
config.WithRetryMaxAttempts(3),
)
if err != nil {
return fmt.Errorf("load AWS config: %w", err)
}
client := s3.NewFromConfig(cfg, func(options *s3.Options) {
options.BaseEndpoint = aws.String(
strings.TrimRight(os.Getenv("PARSPACK_S3_ENDPOINT"), "/"),
)
options.UsePathStyle = true
})آپلود فایل با PutObject
اولین فرمان سناریو: فایل محلی را با یک نام مشخص در باکت بگذارید.
./hello.txt است؛ داخل باکت با نام tutorial/hello.txt ذخیره میشود. اسلش پوشه واقعی نمیسازد، فقط بخشی از نام است. ETag اثر انگشت محتواست. اگر همین نام از قبل در باکت باشد، محتوای قبلی عوض میشود. نتیجه os.Open و PutObject را همیشه بررسی کنید.file, err := os.Open("./hello.txt")
if err != nil {
return err
}
defer file.Close()
result, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String("tutorial/hello.txt"),
Body: file,
ContentType: aws.String("text/plain"),
})
if err != nil {
return fmt.Errorf("upload object: %w", err)
}
fmt.Println(aws.ToString(result.ETag))| فیلد | مثال | نقش |
|---|---|---|
Bucket | از .env | محل ذخیره Object |
Key | tutorial/hello.txt | نام کامل Object |
Body | *os.File | جریان بایتهای فایل |
ContentType | text/plain | نوع محتوای فایل |
printf 'hello ParsPack\n' > hello.txt
(cd examples/go && go run . upload ../../hello.txt tutorial/hello.txt)اگر همین Key وجود داشته باشد، PutObject محتوای آن را جایگزین میکند. همچنین نتیجه os.Open و PutObject را همیشه بررسی کنید.
فهرست فایلها و مشخصات یک فایل
بعد از آپلود، اول ببینید فایل در فهرست هست؛ بعد حجم و نوع را بدون دانلود بخوانید.
list tutorial/ خود فایل را نمیآورد؛ فقط نام، حجم و زمان تغییر فایلهایی را برمیگرداند که با این Prefix شروع میشوند. اگر آرایه خالی بود، یا آپلود انجام نشده یا Prefix را اشتباه نوشتهاید.head وقتی لازم است که فقط بخواهید بدانید فایل هست یا نه، یا حجم و نوعش چیست. اگر Key نباشد معمولاً 404 میگیرید. این فرمان را با دانلود عوض نکنید؛ بدنه فایل را منتقل نمیکند.paginator := s3.NewListObjectsV2Paginator(client, &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Prefix: aws.String("tutorial/"),
})
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil { return err }
for _, object := range page.Contents {
fmt.Println(aws.ToString(object.Key), aws.ToInt64(object.Size))
}
}info, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(bucket),
Key: aws.String("tutorial/hello.txt"),
})
if err != nil { return err }
fmt.Println(aws.ToInt64(info.ContentLength))
fmt.Println(aws.ToString(info.ContentType))(cd examples/go && go run . list tutorial/)(cd examples/go && go run . head tutorial/hello.txt)دانلود روی سرور
فایل را از باکت بگیرید و روی دیسک همین برنامه بنویسید. این کار لینک برای کاربر نمیسازد.
result, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String("tutorial/hello.txt"),
})
if err != nil { return err }
defer result.Body.Close()
destination, err := os.Create("./downloads/hello.txt")
if err != nil { return err }
defer destination.Close()
_, err = io.Copy(destination, result.Body)(cd examples/go && go run . download tutorial/hello.txt ../../downloads/hello.txt)لینک موقت دانلود برای کاربر
این فرمان را با دانلود روی سرور قاطی نکنید. اینجا فایل روی دیسک شما نوشته نمیشود؛ فقط یک آدرس زماندار ساخته میشود. جریان کامل در صفحه شروع است.
<a href> بگذارید، یا با curl --output file.pdf "$URL" بگیرید. Header اضافه لازم نیست. 900 یعنی پانزده دقیقه؛ بعد از آن آدرس کار نمیکند.presign := s3.NewPresignClient(client)
result, err := presign.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String("tutorial/hello.txt"),
}, func(options *s3.PresignOptions) {
options.Expires = 15 * time.Minute
})(cd examples/go && go run . presign-get tutorial/hello.txt 900)curl --output invoice.pdf "$SIGNED_DOWNLOAD_URL"لینک را کوتاه نگه دارید؛ بین ۱ ثانیه تا ۷ روز (۶۰۴۸۰۰ ثانیه). فقط همان Key را میخواند. لینک را فقط بعد از ورود کاربر و چک مجوز بسازید.
لینک موقت آپلود مستقیم
کاربر فایل را از مرورگر به باکت میفرستد. فایل از سرور شما رد نمیشود و کلید API هم به کاربر نمیرسد.
Content-Type را دقیقاً همان مقدار زمان ساخت لینک بگذارید؛ وگرنه معمولاً خطای امضا میگیرید. از مرورگر فقط وقتی کار میکند که CORS باکت سایت شما را مجاز کرده باشد؛ وگرنه از curl یا سرور خودتان استفاده کنید.presign := s3.NewPresignClient(client)
result, err := presign.PresignPutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String("tutorial/upload.txt"),
ContentType: aws.String("text/plain"),
}, func(options *s3.PresignOptions) {
options.Expires = 15 * time.Minute
})(cd examples/go && go run . presign-put tutorial/upload.txt text/plain 900)curl -X PUT -H 'Content-Type: text/plain' \
--upload-file ./hello.txt "$SIGNED_UPLOAD_URL"await fetch(signed.url, {
method: "PUT",
headers: { "Content-Type": signed.headers["Content-Type"] },
body: file,
});اگر موقع ساخت لینک text/plain گذاشتهاید، هنگام ارسال هم باید همان را بفرستید؛ وگرنه معمولاً خطای امضا میگیرید. از مرورگر این کار فقط وقتی درست است که CORS باکت سایت شما را مجاز کرده باشد؛ وگرنه از curl یا سرور خودتان استفاده کنید.
حذف فایل با DeleteObject
نام را از باکت برمیدارد. لازم نیست اول دانلودش کنید. فایل روی سیستم شما دست نمیخورد.
_, err := client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String("tutorial/hello.txt"),
})
if err != nil {
return fmt.Errorf("delete object: %w", err)
}(cd examples/go && go run . delete tutorial/hello.txt)خطای S3 را از timeout جدا کنید
نمونه برای هر اجرا دو دقیقه مهلت دارد و خطا را روی stderr مینویسد. اگر Context منقضی شده، شبکه و اندازهٔ فایل را بررسی کنید؛ برای پاسخ S3 سراغ کد و پیام خود سرویس بروید.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
if err := run(ctx); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}| خطا | علت معمول | بررسی |
|---|---|---|
open ... no such file | مسیر فایل محلی اشتباه است | توجه به اجرای Go داخل examples/go |
AccessDenied | کلید یا دسترسی Bucket کافی نیست | مقدارهای پنل و نوع دسترسی کاربر فضای ابری |
SignatureDoesNotMatch | امضای درخواست با تنظیمات یکی نیست | Slash انتهای Endpoint، Region، کلیدها و Content-Type در PUT |
NoSuchBucket | نام Bucket با مقدار پنل یکی نیست | PARSPACK_S3_BUCKET |
context deadline exceeded | درخواست در مهلت تمام نشد | شبکه، Endpoint و Timeout |
اجرای کامل همین سناریو
نمونه همراه، Loader کوچک .env، اعتبارسنجی ورودی، تشخیص Content-Type و خروجی JSON دارد. اگر خروجی هر فرمان شبیه نمونههای بالا بود، میتوانید Key و مسیر را با نام فایلهای برنامه خودتان عوض کنید.
(cd examples/go && go mod download)
cp examples/.env.example examples/go/.env
# مقدارهای examples/go/.env را کامل کنید
printf 'hello ParsPack\n' > hello.txt
(cd examples/go && go run . upload ../../hello.txt tutorial/hello.txt)
(cd examples/go && go run . list tutorial/)
(cd examples/go && go run . head tutorial/hello.txt)
(cd examples/go && go run . download tutorial/hello.txt ../../downloads/hello.txt)
(cd examples/go && go run . presign-get tutorial/hello.txt 900)
(cd examples/go && go run . presign-put tutorial/upload.txt text/plain 900)
(cd examples/go && go run . delete tutorial/hello.txt)r.Context() را به متد Storage بدهید.Upload(ctx, reader, key) و Delete(ctx, key) را روی یک Struct قرار دهید.