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  }