refactor: split simples3.go into logical modules

Split monolithic simples3.go (1064 LOC) into 5 focused modules:
- simples3.go (141 LOC) - Core S3 struct and configuration
- helpers.go (97 LOC) - Utility functions
- iam.go (202 LOC) - IAM credential handling
- list.go (260 LOC) - List operations
- object.go (409 LOC) - File operations

Split simples3_test.go (912 LOC) into 4 test modules:
- simples3_test.go (118 LOC) - Shared test helpers
- iam_test.go (110 LOC) - IAM tests
- list_test.go (321 LOC) - List operation tests
- object_test.go (381 LOC) - File operation tests

Zero functional changes. All tests pass identically. 72.5% test coverage maintained.

"Review v0.10.2 code refactor"
This commit is contained in:
Rohan Verma
2025-11-26 14:57:15 +05:30
parent 61124858fb
commit 2334cb5429
9 changed files with 1780 additions and 1717 deletions
-794
View File
@@ -1,17 +1,8 @@
package simples3
import (
"bytes"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
)
type tConfig struct {
@@ -21,613 +12,6 @@ type tConfig struct {
Region string
}
func TestS3_FileUploadPostAndPut(t *testing.T) {
testTxt, err := os.Open("testdata/test.txt")
if err != nil {
t.Fatalf("Failed to open test.txt: %v", err)
}
defer testTxt.Close()
// Note: cannot re-use the same file descriptor due to seeking!
testTxtSpecialChars, err := os.Open("testdata/test.txt")
if err != nil {
t.Fatalf("Failed to open test.txt for special chars: %v", err)
}
defer testTxtSpecialChars.Close()
testPng, err := os.Open("testdata/avatar.png")
if err != nil {
t.Fatalf("Failed to open avatar.png: %v", err)
}
defer testPng.Close()
type args struct {
u UploadInput
}
tests := []struct {
name string
fields tConfig
args args
testDetails bool
wantErr bool
}{
{
name: "Upload test.txt",
fields: tConfig{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
Region: os.Getenv("AWS_S3_REGION"),
},
args: args{
UploadInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "test.txt",
ContentType: "text/plain",
FileName: "test.txt",
Body: testTxt,
},
},
wantErr: false,
},
{
name: "Upload test.txt with custom metadata",
fields: tConfig{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
Region: os.Getenv("AWS_S3_REGION"),
},
args: args{
UploadInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "test_metadata.txt",
ContentType: "text/plain",
FileName: "test.txt",
Body: testTxt,
CustomMetadata: map[string]string{
"test-metadata": "foo-bar",
},
},
},
wantErr: false,
testDetails: true,
},
{
name: "Upload avatar.png",
fields: tConfig{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
Region: os.Getenv("AWS_S3_REGION"),
},
args: args{
UploadInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "xyz/image.png",
ContentType: "image/png",
FileName: "avatar.png",
Body: testPng,
},
},
wantErr: false,
},
{
name: "Upload special filename txt",
fields: tConfig{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
Region: os.Getenv("AWS_S3_REGION"),
},
args: args{
UploadInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "xyz/example file%with$special&chars(1)?.txt",
ContentType: "text/plain",
FileName: "example file%with$special&chars(1)?.txt",
Body: testTxtSpecialChars,
},
},
wantErr: false,
},
}
for _, testcase := range tests {
tt := testcase
t.Run(tt.name+"_post", func(t *testing.T) {
s3 := New(tt.fields.Region, tt.fields.AccessKey, tt.fields.SecretKey)
s3.SetEndpoint(tt.fields.Endpoint)
resp, err := s3.FileUpload(tt.args.u)
if (err != nil) != tt.wantErr {
t.Errorf("S3.FileUpload() error = %v, wantErr %v", err, tt.wantErr)
}
// reset file, to reuse in further tests.
tt.args.u.Body.Seek(0, 0)
// check for empty response
if (resp == UploadResponse{}) {
t.Errorf("S3.FileUpload() returned empty response, %v", resp)
}
if tt.testDetails {
dResp, err := s3.FileDetails(DetailsInput{
Bucket: tt.args.u.Bucket,
ObjectKey: tt.args.u.ObjectKey,
})
if (err != nil) != tt.wantErr {
t.Errorf("S3.FileDetails() error = %v, wantErr %v", err, tt.wantErr)
}
if len(dResp.AmzMeta) != len(tt.args.u.CustomMetadata) {
t.Errorf("S3.FileDetails() returned incorrect metadata, got: %#v", dResp)
}
}
})
t.Run(tt.name+"_put", func(t *testing.T) {
s3 := New(tt.fields.Region, tt.fields.AccessKey, tt.fields.SecretKey)
s3.SetEndpoint(tt.fields.Endpoint)
resp, err := s3.FilePut(tt.args.u)
if (err != nil) != tt.wantErr {
t.Errorf("S3.FileUpload() error = %v, wantErr %v", err, tt.wantErr)
}
// reset file, to reuse in further tests.
tt.args.u.Body.Seek(0, 0)
// check for empty response
if resp.ETag == "" {
t.Errorf("S3.FileUpload() returned empty response, %v", resp)
}
if tt.testDetails {
dResp, err := s3.FileDetails(DetailsInput{
Bucket: tt.args.u.Bucket,
ObjectKey: tt.args.u.ObjectKey,
})
if (err != nil) != tt.wantErr {
t.Errorf("S3.FileUpload() error = %v, wantErr %v", err, tt.wantErr)
}
if len(dResp.AmzMeta) != len(tt.args.u.CustomMetadata) {
t.Errorf("S3.FileDetails() returned incorrect metadata, got: %#v", dResp)
}
}
})
}
}
func TestS3_FileDownload(t *testing.T) {
testTxt, err := os.Open("testdata/test.txt")
if err != nil {
t.Fatal(err)
}
defer testTxt.Close()
testTxtData, err := io.ReadAll(testTxt)
if err != nil {
t.Fatal(err)
}
testPng, err := os.Open("testdata/avatar.png")
if err != nil {
t.Fatal(err)
}
defer testPng.Close()
testPngData, err := io.ReadAll(testPng)
if err != nil {
t.Fatal(err)
}
type args struct {
u DownloadInput
}
tests := []struct {
name string
fields tConfig
args args
wantErr bool
wantResponse []byte
}{
{
name: "txt",
fields: tConfig{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
Region: os.Getenv("AWS_S3_REGION"),
},
args: args{
u: DownloadInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "test.txt",
},
},
wantErr: false,
wantResponse: testTxtData,
},
{
name: "png",
fields: tConfig{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
Region: os.Getenv("AWS_S3_REGION"),
},
args: args{
u: DownloadInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "xyz/image.png",
},
},
wantErr: false,
wantResponse: testPngData,
},
{
name: "txt-special-filename",
fields: tConfig{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
Region: os.Getenv("AWS_S3_REGION"),
},
args: args{
u: DownloadInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "xyz/example file%with$special&chars(1)?.txt",
},
},
wantErr: false,
wantResponse: testTxtData,
},
}
for _, testcase := range tests {
tt := testcase
t.Run(tt.name, func(t *testing.T) {
s3 := New(tt.fields.Region, tt.fields.AccessKey, tt.fields.SecretKey)
s3.SetEndpoint(tt.fields.Endpoint)
resp, err := s3.FileDownload(tt.args.u)
if (err != nil) != tt.wantErr {
t.Fatalf("S3.FileDownload() error = %v, wantErr %v", err, tt.wantErr)
}
got, err := io.ReadAll(resp)
if err != nil {
t.Fatalf("error = %v", err)
}
resp.Close()
if !bytes.Equal(got, tt.wantResponse) {
t.Fatalf("S3.FileDownload() = %v, want %v", got, tt.wantResponse)
}
})
}
}
func TestS3_FileDelete(t *testing.T) {
type args struct {
u DeleteInput
}
tests := []struct {
name string
fields tConfig
args args
wantErr bool
}{
{
name: "Delete test.txt",
fields: tConfig{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
Region: os.Getenv("AWS_S3_REGION"),
},
args: args{
DeleteInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "test.txt",
},
},
wantErr: false,
},
{
name: "Delete test_metadata.txt",
fields: tConfig{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
Region: os.Getenv("AWS_S3_REGION"),
},
args: args{
DeleteInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "test_metadata.txt",
},
},
wantErr: false,
},
{
name: "Delete avatar.png",
fields: tConfig{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
Region: os.Getenv("AWS_S3_REGION"),
},
args: args{
DeleteInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "xyz/image.png",
},
},
wantErr: false,
},
{
name: "Delete special filename txt",
fields: tConfig{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Endpoint: os.Getenv("AWS_S3_ENDPOINT"),
Region: os.Getenv("AWS_S3_REGION"),
},
args: args{
DeleteInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "xyz/example file%with$special&chars(1)?.txt",
},
},
wantErr: false,
},
}
for _, testcase := range tests {
tt := testcase
t.Run(tt.name, func(t *testing.T) {
s3 := New(tt.fields.Region, tt.fields.AccessKey, tt.fields.SecretKey)
s3.SetEndpoint(tt.fields.Endpoint)
if err := s3.FileDelete(tt.args.u); (err != nil) != tt.wantErr {
t.Errorf("S3.FileDelete() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestS3_NewUsingIAM(t *testing.T) {
var (
iam = `test-new-s3-using-iam`
resp = `{"Code" : "Success","LastUpdated" : "2018-12-24T10:18:01Z",
"Type" : "AWS-HMAC","AccessKeyId" : "abc",
"SecretAccessKey" : "abc","Token" : "abc",
"Expiration" : "2018-12-24T16:24:59Z"}`
respIMDSToken = `AQAEAJWopi8yvjKYXyWJbzESE0cms-OoTnptJzS3M9g5iNcl06UEkQ==`
)
tsFail := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
}))
defer tsFail.Close()
genServerHandlerFunc := func(failIMDS bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
if !failIMDS {
// check if token is present
if r.Header.Get(imdsTokenHeader) == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}
}
url := securityCredentialsURI
if r.URL.EscapedPath() == url {
w.WriteHeader(http.StatusOK)
io.WriteString(w, iam)
}
if r.URL.EscapedPath() == url+iam {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, resp)
}
case "PUT":
if failIMDS {
w.WriteHeader(http.StatusNotFound)
return
}
if r.URL.EscapedPath() == imdsTokenURI {
if r.Header.Get(imdsTokenTtlHeader) != "60" {
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
io.WriteString(w, respIMDSToken)
}
default:
t.Errorf("Expected 'GET' or 'PUT' request, got '%s'", r.Method)
}
}
}
ts := httptest.NewServer(http.HandlerFunc(genServerHandlerFunc(false)))
defer ts.Close()
tsFailIMDS := httptest.NewServer(http.HandlerFunc(genServerHandlerFunc(true)))
defer tsFailIMDS.Close()
cl := &http.Client{Timeout: 1 * time.Second}
// Test for timeout.
_, err := newUsingIAM(cl, tsFail.URL, "abc")
if err == nil {
t.Errorf("Expected error, got nil")
} else {
var timeoutError net.Error
if errors.As(err, &timeoutError) && !timeoutError.Timeout() {
t.Errorf("newUsingIAM() timeout check. got error = %v", err)
}
}
// Test for successful IAM fetch.
s3, err := newUsingIAM(cl, ts.URL, "abc")
if err != nil {
t.Errorf("newUsingIAM() error = %v", err)
}
if s3 == nil {
t.Errorf("newUsingIAM() got = %v", s3)
}
if s3.AccessKey != "abc" || s3.SecretKey != "abc" || s3.Region != "abc" {
t.Errorf("S3.FileDelete() got = %v", s3)
}
// Test for failed IMDS token fetch.
_, err = newUsingIAM(cl, tsFailIMDS.URL, "abc")
if err == nil {
t.Errorf("Expected error, got nil")
}
}
func TestListAll(t *testing.T) {
// Setup test environment
s3 := New(
os.Getenv("AWS_S3_REGION"),
os.Getenv("AWS_S3_ACCESS_KEY"),
os.Getenv("AWS_S3_SECRET_KEY"),
)
s3.SetEndpoint(os.Getenv("AWS_S3_ENDPOINT"))
// Test cases
tests := []struct {
name string
setup func() // Setup function to prepare test data
input ListInput
validate func([]Object, error)
cleanup func() // Cleanup function
}{
{
name: "Empty Bucket Iterator",
input: ListInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
},
validate: func(objects []Object, err error) {
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(objects) != 0 {
t.Errorf("Expected empty bucket, got %d objects", len(objects))
}
},
},
{
name: "Basic Iterator Listing",
setup: func() {
// Upload test files
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{"iter_file1.txt", "iter_file2.txt", "iter_file3.txt"})
},
input: ListInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
},
validate: func(objects []Object, err error) {
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(objects) != 3 {
t.Errorf("Expected 3 objects, got %d", len(objects))
}
// Check that all expected files are present
expectedKeys := map[string]bool{
"iter_file1.txt": false,
"iter_file2.txt": false,
"iter_file3.txt": false,
}
for _, obj := range objects {
if _, exists := expectedKeys[obj.Key]; exists {
expectedKeys[obj.Key] = true
}
}
for key, found := range expectedKeys {
if !found {
t.Errorf("Expected object %s not found in iterator results", key)
}
}
},
cleanup: func() {
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{"iter_file1.txt", "iter_file2.txt", "iter_file3.txt"})
},
},
{
name: "Iterator with Prefix",
setup: func() {
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
"iter_docs/file1.txt",
"iter_docs/file2.txt",
"iter_images/image1.jpg",
"iter_images/image2.jpg",
})
},
input: ListInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
Prefix: "iter_docs/",
},
validate: func(objects []Object, err error) {
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(objects) != 2 {
t.Errorf("Expected 2 objects with iter_docs/ prefix, got %d", len(objects))
}
for _, obj := range objects {
if !strings.HasPrefix(obj.Key, "iter_docs/") {
t.Errorf("Object %s doesn't have expected prefix", obj.Key)
}
}
},
cleanup: func() {
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
"iter_docs/file1.txt",
"iter_docs/file2.txt",
"iter_images/image1.jpg",
"iter_images/image2.jpg",
})
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.setup != nil {
tt.setup()
}
// Collect all objects from the iterator and check for errors
var objects []Object
seq, finish := s3.ListAll(tt.input)
for obj := range seq {
objects = append(objects, obj)
}
// Check for any iteration errors
if err := finish(); err != nil {
t.Fatalf("Iterator error: %v", err)
}
if tt.validate != nil {
tt.validate(objects, nil) // No error from iterator
}
if tt.cleanup != nil {
tt.cleanup()
}
})
}
}
func TestCustomEndpoint(t *testing.T) {
s3 := New("us-east-1", "AccessKey", "SuperSecretKey")
@@ -705,184 +89,6 @@ func TestGetURL(t *testing.T) {
}
}
func TestList(t *testing.T) {
// Setup test environment
s3 := New(
os.Getenv("AWS_S3_REGION"),
os.Getenv("AWS_S3_ACCESS_KEY"),
os.Getenv("AWS_S3_SECRET_KEY"),
)
s3.SetEndpoint(os.Getenv("AWS_S3_ENDPOINT"))
// Test cases
tests := []struct {
name string
setup func() // Setup function to prepare test data
input ListInput
validate func(ListResponse, error)
cleanup func() // Cleanup function
}{
{
name: "Empty Bucket",
input: ListInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
},
validate: func(result ListResponse, err error) {
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(result.Objects) != 0 {
t.Errorf("Expected empty bucket, got %d objects", len(result.Objects))
}
},
},
{
name: "Basic Listing",
setup: func() {
// Upload test files
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{"file1.txt", "file2.txt", "file3.txt"})
},
input: ListInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
},
validate: func(result ListResponse, err error) {
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(result.Objects) != 3 {
t.Errorf("Expected 3 objects, got %d", len(result.Objects))
}
},
cleanup: func() {
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{"file1.txt", "file2.txt", "file3.txt"})
},
},
{
name: "Pagination Test",
setup: func() {
// Upload many files for pagination testing
var filenames []string
for i := 1; i <= 25; i++ {
filenames = append(filenames, fmt.Sprintf("file%03d.txt", i))
}
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), filenames)
},
input: ListInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
MaxKeys: 10,
},
validate: func(result ListResponse, err error) {
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(result.Objects) != 10 {
t.Errorf("Expected 10 objects, got %d", len(result.Objects))
}
if !result.IsTruncated {
t.Errorf("Expected IsTruncated=true for pagination")
}
if result.NextContinuationToken == "" {
t.Errorf("Expected NextContinuationToken for pagination: %v", result.NextContinuationToken)
}
},
cleanup: func() {
var filenames []string
for i := 1; i <= 25; i++ {
filenames = append(filenames, fmt.Sprintf("file%03d.txt", i))
}
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), filenames)
},
},
{
name: "Prefix Filtering",
setup: func() {
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
"documents/file1.txt",
"documents/file2.txt",
"images/image1.jpg",
"images/image2.jpg",
})
},
input: ListInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
Prefix: "documents/",
},
validate: func(result ListResponse, err error) {
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(result.Objects) != 2 {
t.Errorf("Expected 2 objects with documents/ prefix, got %d", len(result.Objects))
}
for _, obj := range result.Objects {
if !strings.HasPrefix(obj.Key, "documents/") {
t.Errorf("Object %s doesn't have expected prefix", obj.Key)
}
}
},
cleanup: func() {
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
"documents/file1.txt",
"documents/file2.txt",
"images/image1.jpg",
"images/image2.jpg",
})
},
},
{
name: "Delimiter Grouping",
setup: func() {
uploadTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
"documents/important/file1.txt",
"documents/backup/file2.txt",
"images/landscape/image1.jpg",
"images/portrait/image2.jpg",
})
},
input: ListInput{
Bucket: os.Getenv("AWS_S3_BUCKET"),
Delimiter: "/",
Prefix: "documents/",
},
validate: func(result ListResponse, err error) {
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
// Should return common prefixes instead of individual files
if len(result.CommonPrefixes) != 2 {
t.Errorf("Expected 2 common prefixes, got %d", len(result.CommonPrefixes))
}
},
cleanup: func() {
cleanupTestFiles(t, s3, os.Getenv("AWS_S3_BUCKET"), []string{
"documents/important/file1.txt",
"documents/backup/file2.txt",
"images/landscape/image1.jpg",
"images/portrait/image2.jpg",
})
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.setup != nil {
tt.setup()
}
result, err := s3.List(tt.input)
if tt.validate != nil {
tt.validate(result, err)
}
if tt.cleanup != nil {
tt.cleanup()
}
})
}
}
// Helper functions
func uploadTestFiles(t *testing.T, s3 *S3, bucket string, filenames []string) {
for _, filename := range filenames {