While working on this I had some difficulty finding exactly what I needed so, here’s this post for whoever may need it.

The function comment is quite clear on what the function does. It uploads the content of a directory into an AWS S3 bucket.

Some details

LoadDefaultConfig is loading the AWS CLI config from the default path ~/.aws.

awsConfig, err := config.LoadDefaultConfig(context.TODO())

It makes use of an errgroup to ease goroutine synchronization and error propagation. Since this function was created as part of the internals of a CLI/API, I wanted to be able to get an error from it instead of a panic/os.Exit()/log.Fatal() and leave it to the top layers to handle it as they see fit.

errGrp := new(errgroup.Group)

Walk the file tree rooted at sourcePath. Skip the upload of directories because only files can be uploaded to S3. In the end the tree will be the same because of the objectKey (see function comment doc).

_ = filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error {
    if info.IsDir() {
		return nil
    }

The entire thing

// UploadToBucket uploads a the contents of a directory into a bucket
// where rootKey is the root of every object.
// sourcePath is the path to the directory to upload
// UploadToBucket assumes the last part of the sourcePath to be the root of the object key.
// Example:
//   - /home/bugsbunny/.acme/carrots
//     - ./red_carrot.txt
//     - ./favs/red_carrot.txt
//
// UploadToBucket("/home/bugsbunny/.acme/carrots") will upload the txt files with the following keys:
//   - carrots/red_carrot.txt
//   - carrots/favs/red_carrot.txt
//
func UploadToBucket(sourcePath string) error {
	var bucket = viper.GetString("aws_s3_bucket")

	awsConfig, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		return err
	}

	client := s3.NewFromConfig(awsConfig)
	rootKey := path.Base(sourcePath)
	pathPrefix := strings.TrimSuffix(sourcePath, "/")
	pathPrefix = strings.TrimSuffix(sourcePath, rootKey)
	errGrp := new(errgroup.Group)

	_ = filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error {
		if info.IsDir() {
			return nil
		}

		errGrp.Go(func() error {
			objectKey := strings.TrimPrefix(path, pathPrefix)

			file, err := os.Open(path)
			if err != nil {
				return err
			}
			defer file.Close()

			if _, err = client.PutObject(context.Background(), &s3.PutObjectInput{
				Bucket: &bucket,
				Body: 	file,
				Key:    &objectKey,
			}); err != nil {
				return err
			}

			return nil
		})

		return nil
	})

	if err := errGrp.Wait(); err != nil {
		return err
	}

	return nil
}