github.com/enetx/g@v1.0.80/file.go (about)

     1  package g
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"io/fs"
     8  	"net/http"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	"github.com/enetx/g/internal/filelock"
    13  )
    14  
    15  // NewFile returns a new File instance with the given name.
    16  func NewFile(name String) *File { return &File{name: name} }
    17  
    18  // Lines returns a new iterator instance that can be used to read the file
    19  // line by line.
    20  //
    21  // Example usage:
    22  //
    23  //	// Open a new file with the specified name "text.txt"
    24  //	g.NewFile("text.txt").
    25  //		Lines().                 // Read the file line by line
    26  //		Unwrap().                // Unwrap the Result type to get the underlying iterator
    27  //		Skip(3).                 // Skip the first 3 lines
    28  //		Exclude(f.Zero).         // Exclude lines that are empty or contain only whitespaces
    29  //		Dedup().                 // Remove consecutive duplicate lines
    30  //		Map(g.String.Upper).     // Convert each line to uppercase
    31  //		ForEach(                 // For each line, print it
    32  //			func(s g.String) {
    33  //				s.Print()
    34  //			})
    35  //
    36  //	// Output:
    37  //	// UPPERCASED_LINE4
    38  //	// UPPERCASED_LINE5
    39  //	// UPPERCASED_LINE6
    40  func (f *File) Lines() Result[SeqSlice[String]] {
    41  	if f.file == nil {
    42  		if r := f.Open(); r.IsErr() {
    43  			return Err[SeqSlice[String]](r.Err())
    44  		}
    45  	}
    46  
    47  	return Ok(SeqSlice[String](func(yield func(String) bool) {
    48  		defer f.Close()
    49  
    50  		scanner := bufio.NewScanner(f.file)
    51  		scanner.Split(bufio.ScanLines)
    52  
    53  		for scanner.Scan() {
    54  			if !yield(String(scanner.Text())) {
    55  				return
    56  			}
    57  		}
    58  	}))
    59  }
    60  
    61  // Chunks returns a new iterator instance that can be used to read the file
    62  // in fixed-size chunks of the specified size in bytes.
    63  //
    64  // Parameters:
    65  //
    66  // - size (int): The size of each chunk in bytes.
    67  //
    68  // Example usage:
    69  //
    70  //	// Open a new file with the specified name "text.txt"
    71  //	g.NewFile("text.txt").
    72  //		Chunks(100).              // Read the file in chunks of 100 bytes
    73  //		Unwrap().                 // Unwrap the Result type to get the underlying iterator
    74  //		Map(g.String.Upper).      // Convert each chunk to uppercase
    75  //		ForEach(                  // For each chunk, print it
    76  //			func(s g.String) {
    77  //				s.Print()
    78  //			})
    79  //
    80  //	// Output:
    81  //	// UPPERCASED_CHUNK1
    82  //	// UPPERCASED_CHUNK2
    83  //	// UPPERCASED_CHUNK3
    84  func (f *File) Chunks(size Int) Result[SeqSlice[String]] {
    85  	if f.file == nil {
    86  		if r := f.Open(); r.IsErr() {
    87  			return Err[SeqSlice[String]](r.Err())
    88  		}
    89  	}
    90  
    91  	return Ok(SeqSlice[String](func(yield func(String) bool) {
    92  		defer f.Close()
    93  
    94  		buffer := make([]byte, size)
    95  
    96  		for {
    97  			n, err := f.file.Read(buffer)
    98  			if err != nil && err != io.EOF {
    99  				return
   100  			}
   101  
   102  			if n == 0 {
   103  				break
   104  			}
   105  
   106  			if !yield(String(buffer[:n])) {
   107  				return
   108  			}
   109  		}
   110  	}))
   111  }
   112  
   113  // Append appends the given content to the file, with the specified mode (optional).
   114  // If no FileMode is provided, the default FileMode (0644) is used.
   115  // Don't forget to close the file!
   116  func (f *File) Append(content String, mode ...os.FileMode) Result[*File] {
   117  	if f.file == nil {
   118  		if r := f.createAll(); r.IsErr() {
   119  			return r
   120  		}
   121  
   122  		fmode := FileDefault
   123  		if len(mode) != 0 {
   124  			fmode = mode[0]
   125  		}
   126  
   127  		if r := f.OpenFile(os.O_APPEND|os.O_CREATE|os.O_WRONLY, fmode); r.IsErr() {
   128  			return r
   129  		}
   130  	}
   131  
   132  	if _, err := f.file.WriteString(content.Std()); err != nil {
   133  		return Err[*File](err)
   134  	}
   135  
   136  	return Ok(f)
   137  }
   138  
   139  // Chmod changes the mode of the file.
   140  func (f *File) Chmod(mode os.FileMode) Result[*File] {
   141  	var err error
   142  	if f.file != nil {
   143  		err = f.file.Chmod(mode)
   144  	} else {
   145  		err = os.Chmod(f.name.Std(), mode)
   146  	}
   147  
   148  	if err != nil {
   149  		return Err[*File](err)
   150  	}
   151  
   152  	return Ok(f)
   153  }
   154  
   155  // Chown changes the owner of the file.
   156  func (f *File) Chown(uid, gid int) Result[*File] {
   157  	var err error
   158  	if f.file != nil {
   159  		err = f.file.Chown(uid, gid)
   160  	} else {
   161  		err = os.Chown(f.name.Std(), uid, gid)
   162  	}
   163  
   164  	if err != nil {
   165  		return Err[*File](err)
   166  	}
   167  
   168  	return Ok(f)
   169  }
   170  
   171  // Seek sets the file offset for the next Read or Write operation. The offset
   172  // is specified by the 'offset' parameter, and the 'whence' parameter determines
   173  // the reference point for the offset.
   174  //
   175  // The 'offset' parameter specifies the new offset in bytes relative to the
   176  // reference point determined by 'whence'. If 'whence' is set to io.SeekStart,
   177  // io.SeekCurrent, or io.SeekEnd, the offset is relative to the start of the file,
   178  // the current offset, or the end of the file, respectively.
   179  //
   180  // If the file is not open, this method will attempt to open it. If the open
   181  // operation fails, an error is returned.
   182  //
   183  // If the Seek operation fails, the file is closed, and an error is returned.
   184  //
   185  // Example:
   186  //
   187  //	file := g.NewFile("example.txt")
   188  //	result := file.Seek(100, io.SeekStart)
   189  //	if result.Err() != nil {
   190  //	    log.Fatal(result.Err())
   191  //	}
   192  //
   193  // Parameters:
   194  //   - offset: The new offset in bytes.
   195  //   - whence: The reference point for the offset (io.SeekStart, io.SeekCurrent, or io.SeekEnd).
   196  //
   197  // Don't forget to close the file!
   198  func (f *File) Seek(offset int64, whence int) Result[*File] {
   199  	if f.file == nil {
   200  		if r := f.Open(); r.IsErr() {
   201  			return r
   202  		}
   203  	}
   204  
   205  	if _, err := f.file.Seek(offset, whence); err != nil {
   206  		f.Close()
   207  		return Err[*File](err)
   208  	}
   209  
   210  	return Ok(f)
   211  }
   212  
   213  // Close closes the File and unlocks its underlying file, if it is not already closed.
   214  func (f *File) Close() error {
   215  	if f.file == nil {
   216  		return &ErrFileClosed{f.name.Std()}
   217  	}
   218  
   219  	var err error
   220  
   221  	if f.guard {
   222  		err = filelock.Unlock(f.file)
   223  	}
   224  
   225  	if closeErr := f.file.Close(); closeErr != nil {
   226  		err = closeErr
   227  	}
   228  
   229  	f.file = nil
   230  
   231  	return err
   232  }
   233  
   234  // Copy copies the file to the specified destination, with the specified mode (optional).
   235  // If no mode is provided, the default FileMode (0644) is used.
   236  func (f *File) Copy(dest String, mode ...os.FileMode) Result[*File] {
   237  	if r := f.Open(); r.IsErr() {
   238  		return r
   239  	}
   240  
   241  	defer f.Close()
   242  
   243  	return NewFile(dest).WriteFromReader(f.file, mode...)
   244  }
   245  
   246  // Create is similar to os.Create, but it returns a write-locked file.
   247  // Don't forget to close the file!
   248  func (f *File) Create() Result[*File] {
   249  	return f.OpenFile(os.O_RDWR|os.O_CREATE|os.O_TRUNC, FileCreate)
   250  }
   251  
   252  // Dir returns the directory the file is in as an Dir instance.
   253  func (f *File) Dir() Result[*Dir] {
   254  	dirPath := f.dirPath()
   255  	if dirPath.IsErr() {
   256  		return Err[*Dir](dirPath.Err())
   257  	}
   258  
   259  	return Ok(NewDir(dirPath.Ok()))
   260  }
   261  
   262  // Exist checks if the file exists.
   263  func (f *File) Exist() bool {
   264  	if f.dirPath().IsOk() {
   265  		filePath := f.filePath()
   266  		if filePath.IsOk() {
   267  			_, err := os.Stat(filePath.Ok().Std())
   268  			return !os.IsNotExist(err)
   269  		}
   270  	}
   271  
   272  	return false
   273  }
   274  
   275  // Ext returns the file extension.
   276  func (f *File) Ext() String { return String(filepath.Ext(f.name.Std())) }
   277  
   278  // Guard sets a lock on the file to protect it from concurrent access.
   279  // It returns the File instance with the guard enabled.
   280  func (f *File) Guard() *File {
   281  	f.guard = true
   282  	return f
   283  }
   284  
   285  // MimeType returns the MIME type of the file as an String.
   286  func (f *File) MimeType() Result[String] {
   287  	if r := f.Open(); r.IsErr() {
   288  		return Err[String](r.Err())
   289  	}
   290  
   291  	defer f.Close()
   292  
   293  	const bufferSize = 512
   294  
   295  	buff := make([]byte, bufferSize)
   296  
   297  	bytesRead, err := f.file.ReadAt(buff, 0)
   298  	if err != nil && err != io.EOF {
   299  		return Err[String](err)
   300  	}
   301  
   302  	buff = buff[:bytesRead]
   303  
   304  	return Ok(String(http.DetectContentType(buff)))
   305  }
   306  
   307  // Move function simply calls [File.Rename]
   308  func (f *File) Move(newpath String) Result[*File] { return f.Rename(newpath) }
   309  
   310  // Name returns the name of the file.
   311  func (f *File) Name() String {
   312  	if f.file != nil {
   313  		return String(filepath.Base(f.file.Name()))
   314  	}
   315  
   316  	return String(filepath.Base(f.name.Std()))
   317  }
   318  
   319  // Open is like os.Open, but returns a read-locked file.
   320  // Don't forget to close the file!
   321  func (f *File) Open() Result[*File] { return f.OpenFile(os.O_RDONLY, 0) }
   322  
   323  // OpenFile is like os.OpenFile, but returns a locked file.
   324  // If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked
   325  // otherwise, it is read-locked.
   326  // Don't forget to close the file!
   327  func (f *File) OpenFile(flag int, perm fs.FileMode) Result[*File] {
   328  	file, err := os.OpenFile(f.name.Std(), flag&^os.O_TRUNC, perm)
   329  	if err != nil {
   330  		return Err[*File](err)
   331  	}
   332  
   333  	if f.guard {
   334  		switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) {
   335  		case os.O_WRONLY, os.O_RDWR:
   336  			err = filelock.Lock(file)
   337  		default:
   338  			err = filelock.RLock(file)
   339  		}
   340  
   341  		if err != nil {
   342  			file.Close()
   343  			return Err[*File](err)
   344  		}
   345  	}
   346  
   347  	if flag&os.O_TRUNC == os.O_TRUNC {
   348  		if err := file.Truncate(0); err != nil {
   349  			if fi, statErr := file.Stat(); statErr != nil || fi.Mode().IsRegular() {
   350  				if f.guard {
   351  					filelock.Unlock(file)
   352  				}
   353  
   354  				file.Close()
   355  
   356  				return Err[*File](err)
   357  			}
   358  		}
   359  	}
   360  
   361  	f.file = file
   362  
   363  	return Ok(f)
   364  }
   365  
   366  // Path returns the absolute path of the file.
   367  func (f *File) Path() Result[String] { return f.filePath() }
   368  
   369  // Print prints the content of the File to the standard output (console)
   370  // and returns the File unchanged.
   371  func (f *File) Print() *File { fmt.Println(f); return f }
   372  
   373  // Read opens the named file with a read-lock and returns its contents.
   374  func (f *File) Read() Result[String] {
   375  	if r := f.Open(); r.IsErr() {
   376  		return Err[String](r.Err())
   377  	}
   378  
   379  	defer f.Close()
   380  
   381  	content, err := io.ReadAll(f.file)
   382  	if err != nil {
   383  		return Err[String](err)
   384  	}
   385  
   386  	return Ok(String(content))
   387  }
   388  
   389  // Remove removes the file.
   390  func (f *File) Remove() Result[*File] {
   391  	if err := os.Remove(f.name.Std()); err != nil {
   392  		return Err[*File](err)
   393  	}
   394  
   395  	return Ok(f)
   396  }
   397  
   398  // Rename renames the file to the specified new path.
   399  func (f *File) Rename(newpath String) Result[*File] {
   400  	if !f.Exist() {
   401  		return Err[*File](&ErrFileNotExist{f.name.Std()})
   402  	}
   403  
   404  	nf := NewFile(newpath).createAll()
   405  
   406  	if err := os.Rename(f.name.Std(), newpath.Std()); err != nil {
   407  		return Err[*File](err)
   408  	}
   409  
   410  	return nf
   411  }
   412  
   413  // Split splits the file path into its directory and file components.
   414  func (f *File) Split() (*Dir, *File) {
   415  	path := f.Path()
   416  	if path.IsErr() {
   417  		return nil, nil
   418  	}
   419  
   420  	dir, file := filepath.Split(path.Ok().Std())
   421  
   422  	return NewDir(String(dir)), NewFile(String(file))
   423  }
   424  
   425  // Stat returns the fs.FileInfo of the file.
   426  // It calls the file's Stat method if the file is open, or os.Stat otherwise.
   427  func (f *File) Stat() Result[fs.FileInfo] {
   428  	if f.file != nil {
   429  		return ResultOf(f.file.Stat())
   430  	}
   431  
   432  	return ResultOf(os.Stat(f.name.Std()))
   433  }
   434  
   435  // Lstat retrieves information about the symbolic link represented by the *File instance.
   436  // It returns a Result[fs.FileInfo] containing details about the symbolic link's metadata.
   437  // Unlike Stat, Lstat does not follow the link and provides information about the link itself.
   438  func (f *File) Lstat() Result[fs.FileInfo] {
   439  	return ResultOf(os.Lstat(f.name.Std()))
   440  }
   441  
   442  // IsDir checks if the file is a directory.
   443  func (f *File) IsDir() bool {
   444  	stat := f.Stat()
   445  	return stat.IsOk() && stat.Ok().IsDir()
   446  }
   447  
   448  // IsLink checks if the file is a symbolic link.
   449  func (f *File) IsLink() bool {
   450  	stat := f.Lstat()
   451  	return stat.IsOk() && stat.Ok().Mode()&os.ModeSymlink != 0
   452  }
   453  
   454  // Std returns the underlying *os.File instance.
   455  // Don't forget to close the file with g.File().Close()!
   456  func (f *File) Std() *os.File { return f.file }
   457  
   458  // CreateTemp creates a new temporary file in the specified directory with the
   459  // specified name pattern and returns a Result, which contains a pointer to the File
   460  // or an error if the operation fails.
   461  // If no directory is specified, the default directory for temporary files is used.
   462  // If no name pattern is specified, the default pattern "*" is used.
   463  //
   464  // Parameters:
   465  //
   466  // - args ...String: A variadic parameter specifying the directory and/or name
   467  // pattern for the temporary file.
   468  //
   469  // Returns:
   470  //
   471  // - *File: A pointer to the File representing the temporary file.
   472  //
   473  // Example usage:
   474  //
   475  //	f := g.NewFile("")
   476  //	tmpfile := f.CreateTemp()                     // Creates a temporary file with default settings
   477  //	tmpfileWithDir := f.CreateTemp("mydir")       // Creates a temporary file in "mydir" directory
   478  //	tmpfileWithPattern := f.CreateTemp("", "tmp") // Creates a temporary file with "tmp" pattern
   479  func (*File) CreateTemp(args ...String) Result[*File] {
   480  	dir := ""
   481  	pattern := "*"
   482  
   483  	if len(args) != 0 {
   484  		if len(args) > 1 {
   485  			pattern = args[1].Std()
   486  		}
   487  
   488  		dir = args[0].Std()
   489  	}
   490  
   491  	tmpfile, err := os.CreateTemp(dir, pattern)
   492  	if err != nil {
   493  		return Err[*File](err)
   494  	}
   495  
   496  	ntmpfile := NewFile(String(tmpfile.Name()))
   497  	ntmpfile.file = tmpfile
   498  
   499  	defer ntmpfile.Close()
   500  
   501  	return Ok(ntmpfile)
   502  }
   503  
   504  // Write opens the named file (creating it with the given permissions if needed),
   505  // then write-locks it and overwrites it with the given content.
   506  func (f *File) Write(content String, mode ...os.FileMode) Result[*File] {
   507  	return f.WriteFromReader(content.Reader(), mode...)
   508  }
   509  
   510  // WriteFromReader takes an io.Reader (scr) as input and writes the data from the reader into the file.
   511  // If no FileMode is provided, the default FileMode (0644) is used.
   512  func (f *File) WriteFromReader(scr io.Reader, mode ...os.FileMode) Result[*File] {
   513  	if f.file == nil {
   514  		if r := f.createAll(); r.IsErr() {
   515  			return r
   516  		}
   517  	}
   518  
   519  	fmode := FileDefault
   520  	if len(mode) != 0 {
   521  		fmode = mode[0]
   522  	}
   523  
   524  	filePath := f.filePath()
   525  	if filePath.IsErr() {
   526  		return Err[*File](filePath.Err())
   527  	}
   528  
   529  	f = NewFile(filePath.Ok())
   530  
   531  	if r := f.OpenFile(os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fmode); r.IsErr() {
   532  		return Err[*File](r.Err())
   533  	}
   534  
   535  	defer f.Close()
   536  
   537  	_, err := io.Copy(f.file, scr)
   538  	if err != nil {
   539  		return Err[*File](err)
   540  	}
   541  
   542  	err = f.file.Sync()
   543  	if err != nil {
   544  		return Err[*File](err)
   545  	}
   546  
   547  	return Ok(f)
   548  }
   549  
   550  // dirPath returns the absolute path of the directory containing the file.
   551  func (f *File) dirPath() Result[String] {
   552  	var (
   553  		path string
   554  		err  error
   555  	)
   556  
   557  	if f.IsDir() {
   558  		path, err = filepath.Abs(f.name.Std())
   559  	} else {
   560  		path, err = filepath.Abs(filepath.Dir(f.name.Std()))
   561  	}
   562  
   563  	if err != nil {
   564  		return Err[String](err)
   565  	}
   566  
   567  	return Ok(String(path))
   568  }
   569  
   570  // filePath returns the full file path, including the directory and file name.
   571  func (f *File) filePath() Result[String] {
   572  	dirPath := f.dirPath()
   573  	if dirPath.IsErr() {
   574  		return Err[String](dirPath.Err())
   575  	}
   576  
   577  	if f.IsDir() {
   578  		return dirPath
   579  	}
   580  
   581  	return Ok(String(filepath.Join(dirPath.Ok().Std(), filepath.Base(f.name.Std()))))
   582  }
   583  
   584  func (f *File) createAll() Result[*File] {
   585  	dirPath := f.dirPath()
   586  	if dirPath.IsErr() {
   587  		return Err[*File](dirPath.Err())
   588  	}
   589  
   590  	if !f.Exist() {
   591  		if err := os.MkdirAll(dirPath.Ok().Std(), DirDefault); err != nil {
   592  			return Err[*File](err)
   593  		}
   594  	}
   595  
   596  	return Ok(f)
   597  }