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 }