goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/util/fsutil/fsutil.go (about)

     1  package fsutil
     2  
     3  import (
     4  	"io"
     5  	"io/fs"
     6  	"net/http"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	"goyave.dev/goyave/v5/util/errors"
    12  )
    13  
    14  var contentTypeByExtension = map[string]string{
    15  	".jsonld": "application/ld+json",
    16  	".json":   "application/json",
    17  	".js":     "text/javascript",
    18  	".mjs":    "text/javascript",
    19  	".css":    "text/css",
    20  }
    21  
    22  // GetFileExtension returns the last part of a file name.
    23  // If the file doesn't have an extension, returns an empty string.
    24  func GetFileExtension(filename string) string {
    25  	index := strings.LastIndex(filename, ".")
    26  	if index == -1 {
    27  		return ""
    28  	}
    29  	return filename[index+1:]
    30  }
    31  
    32  // GetMIMEType get the mime type and size of the given file.
    33  // This function calls `http.DetectContentType`. If the detected content type
    34  // could not be determined or if it's a text file, `GetMIMEType` will attempt to
    35  // detect the MIME type based on the file extension. The following extensions are
    36  // supported:
    37  //   - `.jsonld`: "application/ld+json"
    38  //   - `.json`: "application/json"
    39  //   - `.js` / `.mjs`: "text/javascript"
    40  //   - `.css`: "text/css"
    41  //
    42  // If a specific MIME type cannot be determined, returns "application/octet-stream" as a fallback.
    43  func GetMIMEType(filesystem fs.FS, file string) (contentType string, size int64, err error) {
    44  	var f fs.File
    45  	f, err = filesystem.Open(file)
    46  	if err != nil {
    47  		err = errors.New(err)
    48  		return
    49  	}
    50  	defer func() {
    51  		errClose := f.Close()
    52  		if err == nil && errClose != nil {
    53  			err = errors.New(errClose)
    54  		}
    55  	}()
    56  
    57  	var stat fs.FileInfo
    58  	stat, err = f.Stat()
    59  	if err != nil {
    60  		err = errors.New(err)
    61  		return
    62  	}
    63  
    64  	size = stat.Size()
    65  
    66  	buffer := make([]byte, 512)
    67  	contentType = "application/octet-stream"
    68  
    69  	if size != 0 {
    70  		_, err = f.Read(buffer)
    71  		if err != nil {
    72  			err = errors.New(err)
    73  			return
    74  		}
    75  
    76  		contentType = http.DetectContentType(buffer)
    77  	}
    78  
    79  	if strings.HasPrefix(contentType, "application/octet-stream") || strings.HasPrefix(contentType, "text/plain") {
    80  		for ext, t := range contentTypeByExtension {
    81  			if strings.HasSuffix(file, ext) {
    82  				tmp := t
    83  				if i := strings.Index(contentType, ";"); i != -1 {
    84  					tmp = t + contentType[i:]
    85  				}
    86  				contentType = tmp
    87  				break
    88  			}
    89  		}
    90  	}
    91  
    92  	return
    93  }
    94  
    95  // FileExists returns true if the file at the given path exists and is readable.
    96  // Returns false if the given file is a directory.
    97  func FileExists(fs fs.StatFS, file string) bool {
    98  	if stats, err := fs.Stat(file); err == nil {
    99  		return !stats.IsDir()
   100  	}
   101  	return false
   102  }
   103  
   104  // IsDirectory returns true if the file at the given path exists, is a directory and is readable.
   105  func IsDirectory(fs fs.StatFS, path string) bool {
   106  	if stats, err := fs.Stat(path); err == nil {
   107  		return stats.IsDir()
   108  	}
   109  	return false
   110  }
   111  
   112  func timestampFileName(name string) string {
   113  	var prefix string
   114  	var extension string
   115  	index := strings.LastIndex(name, ".")
   116  	if index == -1 {
   117  		prefix = name
   118  		extension = ""
   119  	} else {
   120  		prefix = name[:index]
   121  		extension = name[index:]
   122  	}
   123  	return prefix + "-" + strconv.FormatInt(time.Now().UnixNano()/int64(time.Microsecond), 10) + extension
   124  }
   125  
   126  // An FS provides access to a hierarchical file system
   127  // and implements `io/fs`'s `FS`, `ReadDirFS` and `StatFS` interfaces.
   128  type FS interface {
   129  	fs.ReadDirFS
   130  	fs.StatFS
   131  }
   132  
   133  // A WorkingDirFS is a file system with a `Getwd()` method.
   134  type WorkingDirFS interface {
   135  	// Getwd returns a rooted path name corresponding to the
   136  	// current directory. If the current directory can be
   137  	// reached via multiple paths (due to symbolic links),
   138  	// Getwd may return any one of them.
   139  	Getwd() (dir string, err error)
   140  }
   141  
   142  // A MkdirFS is a file system with a `Mkdir()` and a `MkdirAll()` methods.
   143  type MkdirFS interface {
   144  	// MkdirAll creates a directory named path,
   145  	// along with any necessary parents, and returns `nil`,
   146  	// or else returns an error.
   147  	// The permission bits perm (before umask) are used for all
   148  	// directories that `MkdirAll` creates.
   149  	// If path is already a directory, `MkdirAll` does nothing
   150  	// and returns `nil`.
   151  	MkdirAll(path string, perm fs.FileMode) error
   152  
   153  	// Mkdir creates a new directory with the specified name and permission
   154  	// bits (before umask).
   155  	// If there is an error, it will be of type `*PathError`.
   156  	Mkdir(path string, perm fs.FileMode) error
   157  }
   158  
   159  // A WritableFS is a file system with a `OpenFile()` method.
   160  type WritableFS interface {
   161  	// OpenFile is the generalized open call. It opens the named file with specified flag
   162  	// (`O_RDONLY` etc.). If the file does not exist, and the `O_CREATE` flag
   163  	// is passed, it is created with mode perm (before umask). If successful,
   164  	// methods on the returned file can be used for I/O.
   165  	// If there is an error, it will be of type `*PathError`.
   166  	OpenFile(path string, flag int, perm fs.FileMode) (io.ReadWriteCloser, error)
   167  }
   168  
   169  // A RemoveFS is a file system with a `Remove()` and a `RemoveAll()` methods.
   170  type RemoveFS interface {
   171  	// Remove removes the named file or (empty) directory.
   172  	// If there is an error, it will be of type `*PathError`.
   173  	Remove(path string) error
   174  
   175  	// RemoveAll removes path and any children it contains.
   176  	// It removes everything it can but returns the first error
   177  	// it encounters. If the path does not exist, `RemoveAll`
   178  	// returns `nil` (no error).
   179  	// If there is an error, it will be of type `*PathError`.
   180  	RemoveAll(path string) error
   181  }
   182  
   183  // Embed is an extension of aimed at improving `embed.FS` by
   184  // implementing `fs.StatFS` and a `Sub()` function.
   185  type Embed struct {
   186  	FS fs.ReadDirFS
   187  }
   188  
   189  // NewEmbed returns a new Embed with the given FS.
   190  func NewEmbed(fs fs.ReadDirFS) Embed {
   191  	return Embed{
   192  		FS: fs,
   193  	}
   194  }
   195  
   196  // Open opens the named file.
   197  //
   198  // When Open returns an error, it should be of type *PathError
   199  // with the Op field set to "open", the Path field set to name,
   200  // and the Err field describing the problem.
   201  //
   202  // Open should reject attempts to open names that do not satisfy
   203  // ValidPath(name), returning a *PathError with Err set to
   204  // ErrInvalid or ErrNotExist.
   205  func (e Embed) Open(name string) (fs.File, error) {
   206  	f, err := e.FS.Open(name)
   207  	return f, errors.NewSkip(err, 3)
   208  }
   209  
   210  // ReadDir reads the named directory
   211  // and returns a list of directory entries sorted by filename.
   212  func (e Embed) ReadDir(name string) ([]fs.DirEntry, error) {
   213  	entries, err := e.FS.ReadDir(name)
   214  	return entries, errors.NewSkip(err, 3)
   215  }
   216  
   217  // Stat returns a FileInfo describing the file.
   218  func (e Embed) Stat(name string) (fileinfo fs.FileInfo, err error) {
   219  	if statsFS, ok := e.FS.(fs.StatFS); ok {
   220  		return statsFS.Stat(name)
   221  	}
   222  	f, err := e.FS.Open(name)
   223  	if err != nil {
   224  		return nil, errors.New(err)
   225  	}
   226  	defer func() {
   227  		e := f.Close()
   228  		if err == nil && e != nil {
   229  			err = errors.New(&fs.PathError{Op: "close", Path: name, Err: e})
   230  		}
   231  	}()
   232  
   233  	fileinfo, err = f.Stat()
   234  	if err != nil {
   235  		err = errors.New(err)
   236  	}
   237  	return
   238  }
   239  
   240  // Sub returns an Embed FS corresponding to the subtree rooted at dir.
   241  // Returns and error if the underlying sub FS doesn't implement `fs.ReadDirFS`.
   242  func (e Embed) Sub(dir string) (Embed, error) {
   243  	sub, err := fs.Sub(e.FS, dir)
   244  	if err != nil {
   245  		return Embed{}, errors.NewSkip(err, 3)
   246  	}
   247  	subFS, ok := sub.(fs.ReadDirFS)
   248  	if !ok {
   249  		return Embed{}, errors.NewSkip("fsutil.Embed: cannot Sub, underlying sub FS doesn't implement fsutil.FS", 3)
   250  	}
   251  	return Embed{FS: subFS}, nil
   252  }