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

     1  package output
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/Jeffail/benthos/v3/internal/bloblang/field"
    12  	"github.com/Jeffail/benthos/v3/internal/codec"
    13  	"github.com/Jeffail/benthos/v3/internal/docs"
    14  	"github.com/Jeffail/benthos/v3/internal/interop"
    15  	"github.com/Jeffail/benthos/v3/internal/shutdown"
    16  	"github.com/Jeffail/benthos/v3/lib/log"
    17  	"github.com/Jeffail/benthos/v3/lib/metrics"
    18  	"github.com/Jeffail/benthos/v3/lib/output/writer"
    19  	"github.com/Jeffail/benthos/v3/lib/types"
    20  )
    21  
    22  //------------------------------------------------------------------------------
    23  
    24  func init() {
    25  	Constructors[TypeFile] = TypeSpec{
    26  		constructor: fromSimpleConstructor(NewFile),
    27  		Summary: `
    28  Writes messages to files on disk based on a chosen codec.`,
    29  		Description: `
    30  Messages can be written to different files by using [interpolation functions](/docs/configuration/interpolation#bloblang-queries) in the path field. However, only one file is ever open at a given time, and therefore when the path changes the previously open file is closed.
    31  
    32  ` + multipartCodecDoc,
    33  		FieldSpecs: docs.FieldSpecs{
    34  			docs.FieldCommon(
    35  				"path", "The file to write to, if the file does not yet exist it will be created.",
    36  				"/tmp/data.txt",
    37  				"/tmp/${! timestamp_unix() }.txt",
    38  				`/tmp/${! json("document.id") }.json`,
    39  			).IsInterpolated().AtVersion("3.33.0"),
    40  			codec.WriterDocs.AtVersion("3.33.0"),
    41  			docs.FieldDeprecated("delimiter"),
    42  		},
    43  		Categories: []Category{
    44  			CategoryLocal,
    45  		},
    46  	}
    47  }
    48  
    49  //------------------------------------------------------------------------------
    50  
    51  // FileConfig contains configuration fields for the file based output type.
    52  type FileConfig struct {
    53  	Path  string `json:"path" yaml:"path"`
    54  	Codec string `json:"codec" yaml:"codec"`
    55  	Delim string `json:"delimiter" yaml:"delimiter"`
    56  }
    57  
    58  // NewFileConfig creates a new FileConfig with default values.
    59  func NewFileConfig() FileConfig {
    60  	return FileConfig{
    61  		Path:  "",
    62  		Codec: "lines",
    63  		Delim: "",
    64  	}
    65  }
    66  
    67  //------------------------------------------------------------------------------
    68  
    69  // NewFile creates a new File output type.
    70  func NewFile(conf Config, mgr types.Manager, log log.Modular, stats metrics.Type) (Type, error) {
    71  	if len(conf.File.Delim) > 0 {
    72  		conf.File.Codec = "delim:" + conf.File.Delim
    73  	}
    74  	f, err := newFileWriter(conf.File.Path, conf.File.Codec, mgr, log, stats)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	w, err := NewAsyncWriter(TypeFile, 1, f, log, stats)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	if aw, ok := w.(*AsyncWriter); ok {
    83  		aw.SetNoCancel()
    84  	}
    85  	return w, nil
    86  }
    87  
    88  //------------------------------------------------------------------------------
    89  
    90  type fileWriter struct {
    91  	log   log.Modular
    92  	stats metrics.Type
    93  
    94  	path      *field.Expression
    95  	codec     codec.WriterConstructor
    96  	codecConf codec.WriterConfig
    97  
    98  	handleMut  sync.Mutex
    99  	handlePath string
   100  	handle     codec.Writer
   101  
   102  	shutSig *shutdown.Signaller
   103  }
   104  
   105  func newFileWriter(pathStr, codecStr string, mgr types.Manager, log log.Modular, stats metrics.Type) (*fileWriter, error) {
   106  	codec, codecConf, err := codec.GetWriter(codecStr)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	path, err := interop.NewBloblangField(mgr, pathStr)
   111  	if err != nil {
   112  		return nil, fmt.Errorf("failed to parse path expression: %w", err)
   113  	}
   114  	return &fileWriter{
   115  		codec:     codec,
   116  		codecConf: codecConf,
   117  		path:      path,
   118  		log:       log,
   119  		stats:     stats,
   120  		shutSig:   shutdown.NewSignaller(),
   121  	}, nil
   122  }
   123  
   124  //------------------------------------------------------------------------------
   125  
   126  func (w *fileWriter) ConnectWithContext(ctx context.Context) error {
   127  	return nil
   128  }
   129  
   130  func (w *fileWriter) WriteWithContext(ctx context.Context, msg types.Message) error {
   131  	err := writer.IterateBatchedSend(msg, func(i int, p types.Part) error {
   132  		path := filepath.Clean(w.path.String(i, msg))
   133  
   134  		w.handleMut.Lock()
   135  		defer w.handleMut.Unlock()
   136  
   137  		if w.handle != nil && path == w.handlePath {
   138  			return w.handle.Write(ctx, p)
   139  		}
   140  		if w.handle != nil {
   141  			if err := w.handle.Close(ctx); err != nil {
   142  				return err
   143  			}
   144  		}
   145  
   146  		flag := os.O_CREATE | os.O_RDWR
   147  		if w.codecConf.Append {
   148  			flag |= os.O_APPEND
   149  		}
   150  		if w.codecConf.Truncate {
   151  			flag |= os.O_TRUNC
   152  		}
   153  
   154  		if err := os.MkdirAll(filepath.Dir(path), os.FileMode(0o777)); err != nil {
   155  			return err
   156  		}
   157  
   158  		file, err := os.OpenFile(path, flag, os.FileMode(0o666))
   159  		if err != nil {
   160  			return err
   161  		}
   162  
   163  		w.handlePath = path
   164  		handle, err := w.codec(file)
   165  		if err != nil {
   166  			return err
   167  		}
   168  
   169  		if err = handle.Write(ctx, p); err != nil {
   170  			handle.Close(ctx)
   171  			return err
   172  		}
   173  
   174  		if !w.codecConf.CloseAfter {
   175  			w.handle = handle
   176  		} else {
   177  			handle.Close(ctx)
   178  		}
   179  		return nil
   180  	})
   181  	if err != nil {
   182  		return err
   183  	}
   184  
   185  	if msg.Len() > 1 {
   186  		w.handleMut.Lock()
   187  		if w.handle != nil {
   188  			w.handle.EndBatch()
   189  		}
   190  		w.handleMut.Unlock()
   191  	}
   192  	return nil
   193  }
   194  
   195  // CloseAsync shuts down the File output and stops processing messages.
   196  func (w *fileWriter) CloseAsync() {
   197  	go func() {
   198  		w.handleMut.Lock()
   199  		if w.handle != nil {
   200  			w.handle.Close(context.Background())
   201  			w.handle = nil
   202  		}
   203  		w.handleMut.Unlock()
   204  		w.shutSig.ShutdownComplete()
   205  	}()
   206  }
   207  
   208  // WaitForClose blocks until the File output has closed down.
   209  func (w *fileWriter) WaitForClose(timeout time.Duration) error {
   210  	select {
   211  	case <-w.shutSig.HasClosedChan():
   212  	case <-time.After(timeout):
   213  		return types.ErrTimeout
   214  	}
   215  	return nil
   216  }