github.com/Jeffail/benthos/v3@v3.65.0/lib/output/writer/azure_blob_storage.go (about)

     1  //go:build !wasm
     2  // +build !wasm
     3  
     4  package writer
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"net/url"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/Azure/azure-sdk-for-go/storage"
    16  	"github.com/Azure/go-autorest/autorest/azure"
    17  	"github.com/Jeffail/benthos/v3/internal/bloblang/field"
    18  	"github.com/Jeffail/benthos/v3/internal/interop"
    19  	"github.com/Jeffail/benthos/v3/lib/log"
    20  	"github.com/Jeffail/benthos/v3/lib/metrics"
    21  	"github.com/Jeffail/benthos/v3/lib/types"
    22  )
    23  
    24  //------------------------------------------------------------------------------
    25  
    26  // AzureBlobStorage is a benthos writer. Type implementation that writes messages to an
    27  // Azure Blob Storage storage account.
    28  type AzureBlobStorage struct {
    29  	conf        AzureBlobStorageConfig
    30  	container   *field.Expression
    31  	path        *field.Expression
    32  	blobType    *field.Expression
    33  	accessLevel *field.Expression
    34  	client      storage.BlobStorageClient
    35  	log         log.Modular
    36  	stats       metrics.Type
    37  }
    38  
    39  // NewAzureBlobStorage creates a new AzureBlobStorage writer.Type.
    40  // Deprecated
    41  func NewAzureBlobStorage(
    42  	conf AzureBlobStorageConfig,
    43  	log log.Modular,
    44  	stats metrics.Type,
    45  ) (*AzureBlobStorage, error) {
    46  	return NewAzureBlobStorageV2(types.NoopMgr(), conf, log, stats)
    47  }
    48  
    49  // NewAzureBlobStorageV2 creates a new AzureBlobStorage writer.Type.
    50  func NewAzureBlobStorageV2(
    51  	mgr types.Manager,
    52  	conf AzureBlobStorageConfig,
    53  	log log.Modular,
    54  	stats metrics.Type,
    55  ) (*AzureBlobStorage, error) {
    56  	if conf.StorageAccount == "" && conf.StorageConnectionString == "" {
    57  		return nil, errors.New("invalid azure storage account credentials")
    58  	}
    59  	var client storage.Client
    60  	var err error
    61  	if len(conf.StorageConnectionString) > 0 {
    62  		if strings.Contains(conf.StorageConnectionString, "UseDevelopmentStorage=true;") {
    63  			client, err = storage.NewEmulatorClient()
    64  		} else {
    65  			client, err = storage.NewClientFromConnectionString(conf.StorageConnectionString)
    66  		}
    67  	} else if len(conf.StorageAccessKey) > 0 {
    68  		client, err = storage.NewBasicClient(conf.StorageAccount, conf.StorageAccessKey)
    69  	} else {
    70  		// The SAS token in the Azure UI is provided as an URL query string with
    71  		// the '?' prepended to it which confuses url.ParseQuery
    72  		token, err := url.ParseQuery(strings.TrimPrefix(conf.StorageSASToken, "?"))
    73  		if err != nil {
    74  			return nil, fmt.Errorf("invalid azure storage SAS token: %v", err)
    75  		}
    76  		client = storage.NewAccountSASClient(conf.StorageAccount, token, azure.PublicCloud)
    77  	}
    78  	if err != nil {
    79  		return nil, fmt.Errorf("invalid azure storage account credentials: %v", err)
    80  	}
    81  	a := &AzureBlobStorage{
    82  		conf:   conf,
    83  		log:    log,
    84  		stats:  stats,
    85  		client: client.GetBlobService(),
    86  	}
    87  	if a.container, err = interop.NewBloblangField(mgr, conf.Container); err != nil {
    88  		return nil, fmt.Errorf("failed to parse container expression: %v", err)
    89  	}
    90  	if a.path, err = interop.NewBloblangField(mgr, conf.Path); err != nil {
    91  		return nil, fmt.Errorf("failed to parse path expression: %v", err)
    92  	}
    93  	if a.blobType, err = interop.NewBloblangField(mgr, conf.BlobType); err != nil {
    94  		return nil, fmt.Errorf("failed to parse blob type expression: %v", err)
    95  	}
    96  	if a.accessLevel, err = interop.NewBloblangField(mgr, conf.PublicAccessLevel); err != nil {
    97  		return nil, fmt.Errorf("failed to parse public access level expression: %v", err)
    98  	}
    99  	return a, nil
   100  }
   101  
   102  // ConnectWithContext attempts to establish a connection to the target Blob Storage Account.
   103  func (a *AzureBlobStorage) ConnectWithContext(ctx context.Context) error {
   104  	return a.Connect()
   105  }
   106  
   107  // Connect attempts to establish a connection to the target Blob Storage Account.
   108  func (a *AzureBlobStorage) Connect() error {
   109  	return nil
   110  }
   111  
   112  // Write attempts to write message contents to a target Azure Blob Storage container as files.
   113  func (a *AzureBlobStorage) Write(msg types.Message) error {
   114  	return a.WriteWithContext(context.Background(), msg)
   115  }
   116  
   117  func (a *AzureBlobStorage) uploadBlob(b *storage.Blob, blobType string, message []byte) error {
   118  	if blobType == "APPEND" {
   119  		exists, err := b.Exists()
   120  		if err != nil {
   121  			return err
   122  		}
   123  		if !exists {
   124  			if err := b.PutAppendBlob(nil); err != nil {
   125  				return err
   126  			}
   127  		}
   128  		return b.AppendBlock(message, nil)
   129  	}
   130  	return b.CreateBlockBlobFromReader(bytes.NewReader(message), nil)
   131  }
   132  
   133  func (a *AzureBlobStorage) createContainer(c *storage.Container, accessLevel string) error {
   134  	opts := storage.CreateContainerOptions{}
   135  	switch accessLevel {
   136  	case "BLOB":
   137  		opts.Access = storage.ContainerAccessTypeBlob
   138  	case "CONTAINER":
   139  		opts.Access = storage.ContainerAccessTypeContainer
   140  	}
   141  	return c.Create(&opts)
   142  }
   143  
   144  // WriteWithContext attempts to write message contents to a target storage account as files.
   145  func (a *AzureBlobStorage) WriteWithContext(_ context.Context, msg types.Message) error {
   146  	return IterateBatchedSend(msg, func(i int, p types.Part) error {
   147  		c := a.client.GetContainerReference(a.container.String(i, msg))
   148  		b := c.GetBlobReference(a.path.String(i, msg))
   149  		if err := a.uploadBlob(b, a.blobType.String(i, msg), p.Get()); err != nil {
   150  			if containerNotFound(err) {
   151  				if cerr := a.createContainer(c, a.accessLevel.String(i, msg)); cerr != nil {
   152  					a.log.Debugf("error creating container: %v.", cerr)
   153  					return cerr
   154  				}
   155  				err = a.uploadBlob(b, a.blobType.String(i, msg), p.Get())
   156  				if err != nil {
   157  					a.log.Debugf("error retrying to upload  blob: %v.", err)
   158  				}
   159  			}
   160  			return err
   161  		}
   162  		return nil
   163  	})
   164  }
   165  
   166  func containerNotFound(err error) bool {
   167  	if serr, ok := err.(storage.AzureStorageServiceError); ok {
   168  		return serr.Code == "ContainerNotFound"
   169  	}
   170  	return false
   171  }
   172  
   173  // CloseAsync begins cleaning up resources used by this reader asynchronously.
   174  func (a *AzureBlobStorage) CloseAsync() {
   175  }
   176  
   177  // WaitForClose will block until either the reader is closed or a specified
   178  // timeout occurs.
   179  func (a *AzureBlobStorage) WaitForClose(time.Duration) error {
   180  	return nil
   181  }
   182  
   183  //------------------------------------------------------------------------------