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 }