github.com/zntrio/harp/v2@v2.0.9/pkg/sdk/cmdutil/io.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package cmdutil 19 20 import ( 21 "bufio" 22 "context" 23 "errors" 24 "fmt" 25 "io" 26 "os" 27 "path/filepath" 28 "time" 29 ) 30 31 const ( 32 // MaxReaderLimitSize is the upper limit for reader capability. 33 MaxReaderLimitSize = 250 * 1024 * 1024 34 // ReaderTimeout is a time limit during the reader is waiting for data. 35 ReaderTimeout = 1 * time.Minute 36 ) 37 38 // Reader creates a reader instance according to the given name value 39 // Use "" or "-" for Stdin reader, else use a filename. 40 func Reader(name string) (io.Reader, error) { 41 var ( 42 reader io.Reader 43 err error 44 ) 45 46 // Clean input filename 47 name = filepath.Clean(name) 48 49 // Create input reader 50 switch name { 51 case "", ".", "-": 52 // Check stdin 53 info, errStat := os.Stdin.Stat() 54 if errStat != nil { 55 return nil, fmt.Errorf("unable to retrieve stdin information: %w", errStat) 56 } 57 if info.Mode()&os.ModeCharDevice != 0 { 58 return nil, fmt.Errorf("the command expects stdin input but nothing seems readable") 59 } 60 61 // Stdin 62 reader = bufio.NewReader(os.Stdin) 63 reader = NewTimeoutReader(reader, ReaderTimeout) 64 default: 65 reader, err = os.Open(filepath.Clean(name)) 66 if err != nil { 67 return nil, fmt.Errorf("unable to open %q for read: %w", name, err) 68 } 69 } 70 71 // Limit the reader 72 limitedReader := &io.LimitedReader{R: reader, N: MaxReaderLimitSize} 73 74 // No error 75 return limitedReader, nil 76 } 77 78 // LineReader creates a reder and returns content read line by line. 79 func LineReader(name string) ([]string, error) { 80 out := []string{} 81 82 // Create input reader 83 reader, err := Reader(name) 84 if err != nil { 85 return nil, fmt.Errorf("unable to initialize a content reader: %w", err) 86 } 87 88 // Read line by line 89 scanner := bufio.NewScanner(reader) 90 for scanner.Scan() { 91 out = append(out, scanner.Text()) 92 } 93 94 // Check scanner error 95 if err = scanner.Err(); err != nil { 96 return nil, fmt.Errorf("error occurs during scanner usage: %w", err) 97 } 98 99 // No error 100 return out, nil 101 } 102 103 // Writer creates a writer according to the given name value 104 // Use "" or "-" for Stdout writer, else use a filename. 105 func Writer(name string) (io.WriteCloser, error) { 106 var ( 107 writer *os.File 108 err error 109 ) 110 111 // Clean input filename 112 name = filepath.Clean(name) 113 114 // Create output writer 115 switch name { 116 case "", ".", "-": 117 // Stdout 118 writer = os.Stdout 119 default: 120 // Open output file 121 //nolint:gosec // expected behavior 122 writer, err = os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o400) 123 if err != nil { 124 return nil, fmt.Errorf("unable to open %q for write: %w", name, err) 125 } 126 } 127 128 // No error 129 return writer, nil 130 } 131 132 // ----------------------------------------------------------------------------- 133 134 // TimeoutReader implemnts a timelimited reader. 135 type TimeoutReader struct { 136 reader io.Reader 137 timeout time.Duration 138 } 139 140 // NewTimeoutReader create a timed-out limited reader instance. 141 func NewTimeoutReader(reader io.Reader, timeout time.Duration) io.Reader { 142 ret := new(TimeoutReader) 143 ret.reader = reader 144 ret.timeout = timeout 145 return ret 146 } 147 148 // Read implements io.Reader interface. 149 func (r *TimeoutReader) Read(buf []byte) (n int, err error) { 150 ch := make(chan bool, 1) 151 n = 0 152 err = nil 153 go func() { 154 n, err = r.reader.Read(buf) 155 ch <- true 156 }() 157 select { 158 case <-ch: 159 return 160 case <-time.After(r.timeout): 161 return 0, errors.New("Reader timeout") 162 } 163 } 164 165 // ----------------------------------------------------------------------------- 166 167 // NewClosedWriter returns a io.WriteCloser instance which always fails when 168 // writing data. (Used for testing purpose). 169 func NewClosedWriter() io.WriteCloser { 170 return &closedWriter{} 171 } 172 173 type closedWriter struct{} 174 175 func (c *closedWriter) Write(_ []byte) (int, error) { 176 return 0, io.EOF 177 } 178 179 func (c *closedWriter) Close() error { 180 return nil 181 } 182 183 // ----------------------------------------------------------------------------- 184 185 // FileReader returns lazy evaluated reader. 186 func FileReader(filename string) func(context.Context) (io.Reader, error) { 187 return func(_ context.Context) (io.Reader, error) { 188 reader, err := Reader(filename) 189 if err != nil { 190 return nil, fmt.Errorf("unable to open file %q for reading: %w", filename, err) 191 } 192 193 // No error 194 return reader, nil 195 } 196 } 197 198 // FileWriter returns lazy evaluated writer. 199 func FileWriter(filename string) func(context.Context) (io.Writer, error) { 200 return func(_ context.Context) (io.Writer, error) { 201 writer, err := Writer(filename) 202 if err != nil { 203 return nil, fmt.Errorf("unable to open file %q for writing: %w", filename, err) 204 } 205 206 // No error 207 return writer, nil 208 } 209 } 210 211 // StdoutWriter returns lazy evaluated writer. 212 func StdoutWriter() func(context.Context) (io.Writer, error) { 213 return func(_ context.Context) (io.Writer, error) { 214 // No error 215 return os.Stdout, nil 216 } 217 } 218 219 // DiscardWriter returns discard writer. 220 func DiscardWriter() func(context.Context) (io.Writer, error) { 221 return func(_ context.Context) (io.Writer, error) { 222 // No error 223 return io.Discard, nil 224 } 225 } 226 227 // DirectWriter returns the given writer. 228 func DirectWriter(w io.Writer) func(context.Context) (io.Writer, error) { 229 return func(_ context.Context) (io.Writer, error) { 230 // No error 231 return w, nil 232 } 233 }