Files
phishingclub/backend/vendor/github.com/enetx/g/file.go
T
Ronni Skansing 07c8adaf76 update vendor deps
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
2025-11-06 23:31:08 +01:00

744 lines
17 KiB
Go

package g
import (
"bufio"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"github.com/enetx/g/internal/filelock"
)
// NewFile returns a new File instance with the given name.
func NewFile[T ~string](name T) *File { return &File{name: String(name)} }
// Lines returns a new iterator instance that can be used to read the file
// line by line.
//
// Example usage:
//
// // Open a new file with the specified name "text.txt"
// g.NewFile("text.txt").
// Lines(). // Read the file line by line
// Skip(3). // Skip the first 3 lines
// Exclude(f.Zero). // Exclude lines that are empty or contain only whitespaces
// Dedup(). // Remove consecutive duplicate lines
// Map(g.String.Upper). // Convert each line to uppercase
// ForEach( // For each line, print it
// func(func(s Result[String]) {
// s.Ok().Print()
// })
//
// // Output:
// // UPPERCASED_LINE4
// // UPPERCASED_LINE5
// // UPPERCASED_LINE6
func (f *File) Lines() SeqResult[String] {
return func(yield func(Result[String]) bool) {
if f.file == nil {
if r := f.Open(); r.IsErr() {
yield(Err[String](r.err))
return
}
}
defer f.Close()
scanner := bufio.NewScanner(f.file)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
if !yield(Ok(String(scanner.Text()))) {
return
}
}
if err := scanner.Err(); err != nil {
yield(Err[String](err))
return
}
}
}
// LinesRaw returns a new iterator instance that reads the file line by line,
// yielding each line as a Bytes slice (raw []byte).
//
// This version avoids intermediate string allocations by working directly with byte slices.
// The returned Bytes are copies of the scanner buffer and are safe to retain.
//
// Returns:
//
// - SeqResult[Bytes]: An iterator over raw byte lines from the file.
//
// Example usage:
//
// g.NewFile("text.txt").
// LinesRaw(). // Read raw byte lines
// Filter(func(b g.Bytes) bool {
// return len(b) > 0
// }).
// ForEach(func(line g.Result[g.Bytes]) {
// line.Ok().Print()
// })
//
// Output:
// LINE_1
// LINE_2
// ...
//
// Note: Each line is copied before yielding to avoid scanner buffer reuse issues.
func (f *File) LinesRaw() SeqResult[Bytes] {
return func(yield func(Result[Bytes]) bool) {
if f.file == nil {
if r := f.Open(); r.IsErr() {
yield(Err[Bytes](r.err))
return
}
}
defer f.Close()
scanner := bufio.NewScanner(f.file)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
line := make(Bytes, len(scanner.Bytes()))
copy(line, scanner.Bytes())
if !yield(Ok(line)) {
return
}
}
if err := scanner.Err(); err != nil {
yield(Err[Bytes](err))
return
}
}
}
// Chunks returns a new iterator instance that can be used to read the file
// in fixed-size chunks of the specified size in bytes.
//
// Parameters:
//
// - size (int): The size of each chunk in bytes.
//
// Example usage:
//
// // Open a new file with the specified name "text.txt"
// g.NewFile("text.txt").
// Chunks(100). // Read the file in chunks of 100 bytes
// Map(g.String.Upper). // Convert each chunk to uppercase
// ForEach( // For each line, print it
// func(func(s Result[String]) {
// s.Ok().Print()
// })
//
// // Output:
// // UPPERCASED_CHUNK1
// // UPPERCASED_CHUNK2
// // UPPERCASED_CHUNK3
func (f *File) Chunks(size Int) SeqResult[String] {
return func(yield func(Result[String]) bool) {
if size.Lte(0) {
yield(Err[String](errors.New("chunk size must be > 0")))
return
}
if f.file == nil {
if r := f.Open(); r.IsErr() {
yield(Err[String](r.err))
return
}
}
defer f.Close()
buffer := make([]byte, size)
for {
n, err := f.file.Read(buffer)
if err != nil && err != io.EOF {
yield(Err[String](err))
return
}
if n == 0 {
break
}
if !yield(Ok(String(buffer[:n]))) {
return
}
}
}
}
// ChunksRaw returns a new iterator instance that reads the file in fixed-size
// chunks of bytes, yielding each chunk as a Bytes slice.
//
// This method avoids intermediate string allocations and operates directly on byte slices.
// Each chunk is copied from the underlying buffer to make it safe for downstream use.
//
// Parameters:
//
// - size (Int): The size of each chunk in bytes. Must be > 0.
//
// Returns:
//
// - SeqResult[Bytes]: An iterator over raw byte chunks from the file.
//
// Example usage:
//
// g.NewFile("text.txt").
// ChunksRaw(128). // Read raw 128-byte chunks
// ForEach(func(chunk g.Result[g.Bytes]) {
// chunk.Ok().Print()
// })
//
// Output:
// RAW_CHUNK_1
// RAW_CHUNK_2
// ...
//
// Note: Each chunk is copied from the buffer to ensure memory safety.
func (f *File) ChunksRaw(size Int) SeqResult[Bytes] {
return func(yield func(Result[Bytes]) bool) {
if size.Lte(0) {
yield(Err[Bytes](errors.New("chunk size must be > 0")))
return
}
if f.file == nil {
if r := f.Open(); r.IsErr() {
yield(Err[Bytes](r.err))
return
}
}
defer f.Close()
buf := make([]byte, size)
for {
n, err := f.file.Read(buf)
if err != nil && err != io.EOF {
yield(Err[Bytes](err))
return
}
if n == 0 {
break
}
chunk := make(Bytes, n)
copy(chunk, buf[:n])
if !yield(Ok(chunk)) {
return
}
}
}
}
// Append appends the given content to the file, with the specified mode (optional).
// If no FileMode is provided, the default FileMode (0644) is used.
// Don't forget to close the file!
func (f *File) Append(content String, mode ...os.FileMode) Result[*File] {
if f.file == nil {
if r := f.createAll(); r.IsErr() {
return r
}
fmode := Slice[os.FileMode](mode).Get(0).UnwrapOr(FileDefault)
if r := f.OpenFile(os.O_APPEND|os.O_CREATE|os.O_WRONLY, fmode); r.IsErr() {
return r
}
}
if _, err := f.file.WriteString(content.Std()); err != nil {
return Err[*File](err)
}
return Ok(f)
}
// Chmod changes the mode of the file.
func (f *File) Chmod(mode os.FileMode) Result[*File] {
var err error
if f.file != nil {
err = f.file.Chmod(mode)
} else {
err = os.Chmod(f.name.Std(), mode)
}
if err != nil {
return Err[*File](err)
}
return Ok(f)
}
// Chown changes the owner of the file.
func (f *File) Chown(uid, gid int) Result[*File] {
var err error
if f.file != nil {
err = f.file.Chown(uid, gid)
} else {
err = os.Chown(f.name.Std(), uid, gid)
}
if err != nil {
return Err[*File](err)
}
return Ok(f)
}
// Seek sets the file offset for the next Read or Write operation. The offset
// is specified by the 'offset' parameter, and the 'whence' parameter determines
// the reference point for the offset.
//
// The 'offset' parameter specifies the new offset in bytes relative to the
// reference point determined by 'whence'. If 'whence' is set to io.SeekStart,
// io.SeekCurrent, or io.SeekEnd, the offset is relative to the start of the file,
// the current offset, or the end of the file, respectively.
//
// If the file is not open, this method will attempt to open it. If the open
// operation fails, an error is returned.
//
// If the Seek operation fails, the file is closed, and an error is returned.
//
// Example:
//
// file := g.NewFile("example.txt")
// result := file.Seek(100, io.SeekStart)
// if result.Err() != nil {
// log.Fatal(result.Err())
// }
//
// Parameters:
// - offset: The new offset in bytes.
// - whence: The reference point for the offset (io.SeekStart, io.SeekCurrent, or io.SeekEnd).
//
// Don't forget to close the file!
func (f *File) Seek(offset int64, whence int) Result[*File] {
if f.file == nil {
if r := f.Open(); r.IsErr() {
return r
}
}
if _, err := f.file.Seek(offset, whence); err != nil {
f.Close()
return Err[*File](err)
}
return Ok(f)
}
// Close closes the File and unlocks its underlying file, if it is not already closed.
func (f *File) Close() error {
if f.file == nil {
return &ErrFileClosed{f.name.Std()}
}
var err error
if f.guard {
err = filelock.Unlock(f.file)
}
if closeErr := f.file.Close(); closeErr != nil {
err = closeErr
}
f.file = nil
return err
}
// Copy copies the file to the specified destination, with the specified mode (optional).
// If no mode is provided, the default FileMode (0644) is used.
func (f *File) Copy(dest String, mode ...os.FileMode) Result[*File] {
if r := f.Open(); r.IsErr() {
return r
}
defer f.Close()
nf := NewFile(dest)
if f.guard {
nf.guard = true
}
return nf.WriteFromReader(f.file, mode...)
}
// Create is similar to os.Create, but it returns a write-locked file.
// Don't forget to close the file!
func (f *File) Create() Result[*File] {
return f.OpenFile(os.O_RDWR|os.O_CREATE|os.O_TRUNC, FileCreate)
}
// Dir returns the directory the file is in as an Dir instance.
func (f *File) Dir() Result[*Dir] {
dirPath := f.dirPath()
if dirPath.IsErr() {
return Err[*Dir](dirPath.err)
}
return Ok(NewDir(dirPath.v))
}
// Exist checks if the file exists.
func (f *File) Exist() bool {
if f.dirPath().IsOk() {
filePath := f.filePath()
if filePath.IsOk() {
_, err := os.Stat(filePath.v.Std())
return !os.IsNotExist(err)
}
}
return false
}
// Ext returns the file extension.
func (f *File) Ext() String { return String(filepath.Ext(f.name.Std())) }
// Guard sets a lock on the file to protect it from concurrent access.
// It returns the File instance with the guard enabled.
func (f *File) Guard() *File {
f.guard = true
return f
}
// MimeType returns the MIME type of the file as Result[String].
func (f *File) MimeType() Result[String] {
if r := f.Open(); r.IsErr() {
return Err[String](r.err)
}
defer f.Close()
const bufferSize = 512
buff := make([]byte, bufferSize)
bytesRead, err := f.file.ReadAt(buff, 0)
if err != nil && err != io.EOF {
return Err[String](err)
}
buff = buff[:bytesRead]
return Ok(String(http.DetectContentType(buff)))
}
// Move function simply calls [File.Rename]
func (f *File) Move(newpath String) Result[*File] { return f.Rename(newpath) }
// Name returns the name of the file.
func (f *File) Name() String {
if f.file != nil {
return String(filepath.Base(f.file.Name()))
}
return String(filepath.Base(f.name.Std()))
}
// Open is like os.Open, but returns a read-locked file.
// Don't forget to close the file!
func (f *File) Open() Result[*File] { return f.OpenFile(os.O_RDONLY, 0) }
// OpenFile is like os.OpenFile, but returns a locked file.
// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked
// otherwise, it is read-locked.
// Don't forget to close the file!
func (f *File) OpenFile(flag int, perm fs.FileMode) Result[*File] {
file, err := os.OpenFile(f.name.Std(), flag&^os.O_TRUNC, perm)
if err != nil {
return Err[*File](err)
}
if f.guard {
switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) {
case os.O_WRONLY, os.O_RDWR:
err = filelock.Lock(file)
default:
err = filelock.RLock(file)
}
if err != nil {
file.Close()
return Err[*File](err)
}
}
if flag&os.O_TRUNC == os.O_TRUNC {
if err := file.Truncate(0); err != nil {
if fi, statErr := file.Stat(); statErr != nil || fi.Mode().IsRegular() {
if f.guard {
filelock.Unlock(file)
}
file.Close()
return Err[*File](err)
}
}
}
f.file = file
return Ok(f)
}
// Path returns the absolute path of the file.
func (f *File) Path() Result[String] { return f.filePath() }
// Print writes the content of the File to the standard output (console)
// and returns the File unchanged.
func (f *File) Print() *File { fmt.Print(f); return f }
// Println writes the content of the File to the standard output (console) with a newline
// and returns the File unchanged.
func (f *File) Println() *File { fmt.Println(f); return f }
// Read opens the named file with a read-lock and returns its contents.
func (f *File) Read() Result[String] {
if r := f.Open(); r.IsErr() {
return Err[String](r.err)
}
defer f.Close()
content, err := io.ReadAll(f.file)
if err != nil {
return Err[String](err)
}
return Ok(String(content))
}
// Remove removes the file.
func (f *File) Remove() Result[*File] {
if err := os.Remove(f.name.Std()); err != nil {
return Err[*File](err)
}
return Ok(f)
}
// Rename renames the file to the specified new path.
func (f *File) Rename(newpath String) Result[*File] {
if !f.Exist() {
return Err[*File](&ErrFileNotExist{f.name.Std()})
}
nf := NewFile(newpath)
if f.guard {
nf.guard = true
}
if r := nf.createAll(); r.IsErr() {
return r
}
if err := os.Rename(f.name.Std(), newpath.Std()); err != nil {
return Err[*File](err)
}
return Ok(nf)
}
// Split splits the file path into its directory and file components.
func (f *File) Split() (*Dir, *File) {
path := f.Path()
if path.IsErr() {
return nil, nil
}
dir, file := filepath.Split(path.v.Std())
return NewDir(String(dir)), NewFile(String(file))
}
// Stat returns the fs.FileInfo of the file.
// It calls the file's Stat method if the file is open, or os.Stat otherwise.
func (f *File) Stat() Result[fs.FileInfo] {
if f.file != nil {
return ResultOf(f.file.Stat())
}
return ResultOf(os.Stat(f.name.Std()))
}
// Lstat retrieves information about the symbolic link represented by the *File instance.
// It returns a Result[fs.FileInfo] containing details about the symbolic link's metadata.
// Unlike Stat, Lstat does not follow the link and provides information about the link itself.
func (f *File) Lstat() Result[fs.FileInfo] {
return ResultOf(os.Lstat(f.name.Std()))
}
// IsDir checks if the file is a directory.
func (f *File) IsDir() bool {
stat := f.Stat()
return stat.IsOk() && stat.v.IsDir()
}
// IsLink checks if the file is a symbolic link.
func (f *File) IsLink() bool {
stat := f.Lstat()
return stat.IsOk() && stat.v.Mode()&os.ModeSymlink != 0
}
// Std returns the underlying *os.File instance.
// Don't forget to close the file with g.File().Close()!
func (f *File) Std() *os.File { return f.file }
// CreateTemp creates a new temporary file in the specified directory with the
// specified name pattern and returns a Result, which contains a pointer to the File
// or an error if the operation fails.
// If no directory is specified, the default directory for temporary files is used.
// If no name pattern is specified, the default pattern "*" is used.
//
// Parameters:
//
// - args ...String: A variadic parameter specifying the directory and/or name
// pattern for the temporary file.
//
// Returns:
//
// - *File: A pointer to the File representing the temporary file.
//
// Example usage:
//
// f := g.NewFile("")
// tmpfile := f.CreateTemp() // Creates a temporary file with default settings
// tmpfileWithDir := f.CreateTemp("mydir") // Creates a temporary file in "mydir" directory
// tmpfileWithPattern := f.CreateTemp("", "tmp") // Creates a temporary file with "tmp" pattern
func (f *File) CreateTemp(args ...String) Result[*File] {
dir := ""
pattern := "*"
if len(args) != 0 {
if len(args) > 1 {
pattern = args[1].Std()
}
dir = args[0].Std()
}
tmpfile, err := os.CreateTemp(dir, pattern)
if err != nil {
return Err[*File](err)
}
ntmpfile := NewFile(String(tmpfile.Name()))
ntmpfile.file = tmpfile
if f.guard {
ntmpfile.guard = true
}
defer ntmpfile.Close()
return Ok(ntmpfile)
}
// Write opens the named file (creating it with the given permissions if needed),
// then write-locks it and overwrites it with the given content.
func (f *File) Write(content String, mode ...os.FileMode) Result[*File] {
return f.WriteFromReader(content.Reader(), mode...)
}
// WriteFromReader takes an io.Reader (scr) as input and writes the data from the reader into the file.
// If no FileMode is provided, the default FileMode (0644) is used.
func (f *File) WriteFromReader(scr io.Reader, mode ...os.FileMode) Result[*File] {
if f.file == nil {
if r := f.createAll(); r.IsErr() {
return r
}
}
filePath := f.filePath()
if filePath.IsErr() {
return Err[*File](filePath.err)
}
fmode := Slice[os.FileMode](mode).Get(0).UnwrapOr(FileDefault)
if r := f.OpenFile(os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fmode); r.IsErr() {
return Err[*File](r.err)
}
defer f.Close()
_, err := io.Copy(f.file, scr)
if err != nil {
return Err[*File](err)
}
err = f.file.Sync()
if err != nil {
return Err[*File](err)
}
return Ok(f)
}
// dirPath returns the absolute path of the directory containing the file.
func (f *File) dirPath() Result[String] {
var (
path string
err error
)
if f.IsDir() {
path, err = filepath.Abs(f.name.Std())
} else {
path, err = filepath.Abs(filepath.Dir(f.name.Std()))
}
if err != nil {
return Err[String](err)
}
return Ok(String(path))
}
// filePath returns the full file path, including the directory and file name.
func (f *File) filePath() Result[String] {
dirPath := f.dirPath()
if dirPath.IsErr() {
return Err[String](dirPath.err)
}
if f.IsDir() {
return dirPath
}
return Ok(String(filepath.Join(dirPath.v.Std(), filepath.Base(f.name.Std()))))
}
func (f *File) createAll() Result[*File] {
dirPath := f.dirPath()
if dirPath.IsErr() {
return Err[*File](dirPath.err)
}
if !f.Exist() {
if err := os.MkdirAll(dirPath.v.Std(), DirDefault); err != nil {
return Err[*File](err)
}
}
return Ok(f)
}