fs.go 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942
  1. package fasthttp
  2. import (
  3. "bytes"
  4. "errors"
  5. "fmt"
  6. "html"
  7. "io"
  8. "io/fs"
  9. "maps"
  10. "mime"
  11. "net/http"
  12. "os"
  13. "path"
  14. "path/filepath"
  15. "sort"
  16. "strings"
  17. "sync"
  18. "time"
  19. "github.com/andybalholm/brotli"
  20. "github.com/klauspost/compress/gzip"
  21. "github.com/klauspost/compress/zstd"
  22. "github.com/valyala/bytebufferpool"
  23. )
  24. // ServeFileBytesUncompressed returns HTTP response containing file contents
  25. // from the given path.
  26. //
  27. // Directory contents is returned if path points to directory.
  28. //
  29. // ServeFileBytes may be used for saving network traffic when serving files
  30. // with good compression ratio.
  31. //
  32. // See also RequestCtx.SendFileBytes.
  33. //
  34. // WARNING: do not pass any user supplied paths to this function!
  35. // WARNING: if path is based on user input users will be able to request
  36. // any file on your filesystem! Use fasthttp.FS with a sane Root instead.
  37. func ServeFileBytesUncompressed(ctx *RequestCtx, path []byte) {
  38. ServeFileUncompressed(ctx, b2s(path))
  39. }
  40. // ServeFileUncompressed returns HTTP response containing file contents
  41. // from the given path.
  42. //
  43. // Directory contents is returned if path points to directory.
  44. //
  45. // ServeFile may be used for saving network traffic when serving files
  46. // with good compression ratio.
  47. //
  48. // See also RequestCtx.SendFile.
  49. //
  50. // WARNING: do not pass any user supplied paths to this function!
  51. // WARNING: if path is based on user input users will be able to request
  52. // any file on your filesystem! Use fasthttp.FS with a sane Root instead.
  53. func ServeFileUncompressed(ctx *RequestCtx, path string) {
  54. ctx.Request.Header.DelBytes(strAcceptEncoding)
  55. ServeFile(ctx, path)
  56. }
  57. // ServeFileBytes returns HTTP response containing compressed file contents
  58. // from the given path.
  59. //
  60. // HTTP response may contain uncompressed file contents in the following cases:
  61. //
  62. // - Missing 'Accept-Encoding: gzip' request header.
  63. // - No write access to directory containing the file.
  64. //
  65. // Directory contents is returned if path points to directory.
  66. //
  67. // Use ServeFileBytesUncompressed is you don't need serving compressed
  68. // file contents.
  69. //
  70. // See also RequestCtx.SendFileBytes, ServeFileLiteral.
  71. //
  72. // WARNING: do not pass any user supplied paths to this function!
  73. // WARNING: if path is based on user input users will be able to request
  74. // any file on your filesystem! Use fasthttp.FS with a sane Root instead.
  75. func ServeFileBytes(ctx *RequestCtx, path []byte) {
  76. ServeFile(ctx, b2s(path))
  77. }
  78. // ServeFile returns HTTP response containing compressed file contents
  79. // from the given path.
  80. //
  81. // HTTP response may contain uncompressed file contents in the following cases:
  82. //
  83. // - Missing 'Accept-Encoding: gzip' request header.
  84. // - No write access to directory containing the file.
  85. //
  86. // Directory contents is returned if path points to directory.
  87. //
  88. // Use ServeFileUncompressed is you don't need serving compressed file contents.
  89. //
  90. // ServeFile interprets path as a URI path internally. Percent-encoded
  91. // sequences may be decoded, and '?' or '#' may be treated as URI delimiters.
  92. // Use ServeFileLiteral if you need literal path semantics.
  93. //
  94. // See also RequestCtx.SendFile, ServeFileLiteral.
  95. //
  96. // WARNING: do not pass any user supplied paths to this function!
  97. // WARNING: if path is based on user input users will be able to request
  98. // any file on your filesystem! Use fasthttp.FS with a sane Root instead.
  99. func ServeFile(ctx *RequestCtx, path string) {
  100. rootFSOnce.Do(func() {
  101. rootFSHandler = rootFS.NewRequestHandler()
  102. })
  103. path, ok := normalizeServeFilePath(ctx, path)
  104. if !ok {
  105. return
  106. }
  107. ctx.Request.SetRequestURI(path)
  108. rootFSHandler(ctx)
  109. }
  110. // ServeFileLiteral returns HTTP response containing compressed file contents
  111. // from the given path using literal path semantics.
  112. //
  113. // Reserved URI characters in path such as '%', '?' and '#' are preserved
  114. // instead of being interpreted during internal request URI processing.
  115. //
  116. // HTTP response may contain uncompressed file contents in the following cases:
  117. //
  118. // - Missing 'Accept-Encoding: gzip' request header.
  119. // - No write access to directory containing the file.
  120. //
  121. // Directory contents is returned if path points to directory.
  122. //
  123. // Use ServeFileUncompressed if you don't need serving compressed file contents.
  124. //
  125. // See also RequestCtx.SendFileLiteral, ServeFile.
  126. //
  127. // WARNING: do not pass any user supplied paths to this function!
  128. // WARNING: if path is based on user input users will be able to request
  129. // any file on your filesystem! Use fasthttp.FS with a sane Root instead.
  130. func ServeFileLiteral(ctx *RequestCtx, path string) {
  131. rootFSOnce.Do(func() {
  132. rootFSHandler = rootFS.NewRequestHandler()
  133. })
  134. path, ok := normalizeServeFilePath(ctx, path)
  135. if !ok {
  136. return
  137. }
  138. ctx.Request.SetRequestURIBytes(appendQuotedPath(nil, s2b(path)))
  139. rootFSHandler(ctx)
  140. }
  141. var (
  142. rootFSOnce sync.Once
  143. rootFS = &FS{
  144. Root: "",
  145. AllowEmptyRoot: true,
  146. GenerateIndexPages: true,
  147. Compress: true,
  148. CompressBrotli: true,
  149. CompressZstd: true,
  150. AcceptByteRange: true,
  151. }
  152. rootFSHandler RequestHandler
  153. )
  154. // ServeFS returns HTTP response containing compressed file contents from the given fs.FS's path.
  155. //
  156. // HTTP response may contain uncompressed file contents in the following cases:
  157. //
  158. // - Missing 'Accept-Encoding: gzip' request header.
  159. // - No write access to directory containing the file.
  160. //
  161. // Directory contents is returned if path points to directory.
  162. //
  163. // ServeFS interprets path as a URI path internally. Percent-encoded
  164. // sequences may be decoded, and '?' or '#' may be treated as URI delimiters.
  165. // Use ServeFSLiteral if you need literal path semantics.
  166. //
  167. // See also ServeFile, ServeFSLiteral.
  168. func ServeFS(ctx *RequestCtx, filesystem fs.FS, path string) {
  169. serveFS(ctx, filesystem, path, false)
  170. }
  171. // ServeFSLiteral returns HTTP response containing compressed file contents
  172. // from the given fs.FS's path using literal path semantics.
  173. //
  174. // Reserved URI characters in path such as '%', '?' and '#' are preserved
  175. // instead of being interpreted during internal request URI processing.
  176. //
  177. // HTTP response may contain uncompressed file contents in the following cases:
  178. //
  179. // - Missing 'Accept-Encoding: gzip' request header.
  180. // - No write access to directory containing the file.
  181. //
  182. // Directory contents is returned if path points to directory.
  183. //
  184. // See also ServeFS, ServeFileLiteral.
  185. func ServeFSLiteral(ctx *RequestCtx, filesystem fs.FS, path string) {
  186. serveFS(ctx, filesystem, path, true)
  187. }
  188. func serveFS(ctx *RequestCtx, filesystem fs.FS, path string, literal bool) {
  189. f := &FS{
  190. FS: filesystem,
  191. Root: "",
  192. AllowEmptyRoot: true,
  193. GenerateIndexPages: true,
  194. Compress: true,
  195. CompressBrotli: true,
  196. CompressZstd: true,
  197. AcceptByteRange: true,
  198. }
  199. handler := f.NewRequestHandler()
  200. if literal {
  201. ctx.Request.SetRequestURIBytes(appendQuotedPath(nil, s2b(path)))
  202. } else {
  203. ctx.Request.SetRequestURI(path)
  204. }
  205. handler(ctx)
  206. }
  207. func normalizeServeFilePath(ctx *RequestCtx, path string) (string, bool) {
  208. if path == "" || !filepath.IsAbs(path) {
  209. // extend relative path to absolute path
  210. hasTrailingSlash := path != "" && (path[len(path)-1] == '/' || path[len(path)-1] == '\\')
  211. var err error
  212. path = filepath.FromSlash(path)
  213. if path, err = filepath.Abs(path); err != nil {
  214. ctx.Logger().Printf("cannot resolve path %q to absolute file path: %v", path, err)
  215. ctx.Error("Internal Server Error", StatusInternalServerError)
  216. return "", false
  217. }
  218. if hasTrailingSlash {
  219. path += "/"
  220. }
  221. }
  222. // convert the path to forward slashes regardless the OS in order to set the URI properly
  223. // the handler will convert back to OS path separator before opening the file
  224. return filepath.ToSlash(path), true
  225. }
  226. // PathRewriteFunc must return new request path based on arbitrary ctx
  227. // info such as ctx.Path().
  228. //
  229. // Path rewriter is used in FS for translating the current request
  230. // to the local filesystem path relative to FS.Root.
  231. //
  232. // The returned path must not contain '..' path segments due to security reasons,
  233. // since such paths may refer files outside FS.Root.
  234. //
  235. // The returned path may refer to ctx members. For example, ctx.Path().
  236. type PathRewriteFunc func(ctx *RequestCtx) []byte
  237. // NewVHostPathRewriter returns path rewriter, which strips slashesCount
  238. // leading slashes from the path and prepends the path with request's host,
  239. // thus simplifying virtual hosting for static files.
  240. //
  241. // Examples:
  242. //
  243. // - host=foobar.com, slashesCount=0, original path="/foo/bar".
  244. // Resulting path: "/foobar.com/foo/bar"
  245. //
  246. // - host=img.aaa.com, slashesCount=1, original path="/images/123/456.jpg"
  247. // Resulting path: "/img.aaa.com/123/456.jpg"
  248. func NewVHostPathRewriter(slashesCount int) PathRewriteFunc {
  249. return func(ctx *RequestCtx) []byte {
  250. path := stripLeadingSlashes(ctx.Path(), slashesCount)
  251. host := ctx.Host()
  252. if n := bytes.IndexByte(host, '/'); n >= 0 {
  253. host = nil
  254. }
  255. if len(host) == 0 {
  256. host = strInvalidHost
  257. }
  258. b := bytebufferpool.Get()
  259. b.B = append(b.B, '/')
  260. b.B = append(b.B, host...)
  261. b.B = append(b.B, path...)
  262. ctx.URI().SetPathBytes(b.B)
  263. bytebufferpool.Put(b)
  264. return ctx.Path()
  265. }
  266. }
  267. var strInvalidHost = []byte("invalid-host")
  268. // NewPathSlashesStripper returns path rewriter, which strips slashesCount
  269. // leading slashes from the path.
  270. //
  271. // Examples:
  272. //
  273. // - slashesCount = 0, original path: "/foo/bar", result: "/foo/bar"
  274. // - slashesCount = 1, original path: "/foo/bar", result: "/bar"
  275. // - slashesCount = 2, original path: "/foo/bar", result: ""
  276. //
  277. // The returned path rewriter may be used as FS.PathRewrite .
  278. func NewPathSlashesStripper(slashesCount int) PathRewriteFunc {
  279. return func(ctx *RequestCtx) []byte {
  280. return stripLeadingSlashes(ctx.Path(), slashesCount)
  281. }
  282. }
  283. // NewPathPrefixStripper returns path rewriter, which removes prefixSize bytes
  284. // from the path prefix.
  285. //
  286. // Examples:
  287. //
  288. // - prefixSize = 0, original path: "/foo/bar", result: "/foo/bar"
  289. // - prefixSize = 3, original path: "/foo/bar", result: "o/bar"
  290. // - prefixSize = 7, original path: "/foo/bar", result: "r"
  291. //
  292. // The returned path rewriter may be used as FS.PathRewrite .
  293. func NewPathPrefixStripper(prefixSize int) PathRewriteFunc {
  294. return func(ctx *RequestCtx) []byte {
  295. path := ctx.Path()
  296. if len(path) >= prefixSize {
  297. path = path[prefixSize:]
  298. }
  299. return path
  300. }
  301. }
  302. // FS represents settings for request handler serving static files
  303. // from the local filesystem.
  304. //
  305. // It is prohibited copying FS values. Create new values instead.
  306. type FS struct {
  307. noCopy noCopy
  308. // FS is filesystem to serve files from. eg: embed.FS os.DirFS
  309. FS fs.FS
  310. // Path rewriting function.
  311. //
  312. // By default request path is not modified.
  313. PathRewrite PathRewriteFunc
  314. // PathNotFound fires when file is not found in filesystem
  315. // this functions tries to replace "Cannot open requested path"
  316. // server response giving to the programmer the control of server flow.
  317. //
  318. // By default PathNotFound returns
  319. // "Cannot open requested path"
  320. PathNotFound RequestHandler
  321. // Suffixes list to add to compressedFileSuffix depending on encoding
  322. //
  323. // This value has sense only if Compress is set.
  324. //
  325. // FSCompressedFileSuffixes is used by default.
  326. CompressedFileSuffixes map[string]string
  327. // If CleanStop is set, the channel can be closed to stop the cleanup handlers
  328. // for the FS RequestHandlers created with NewRequestHandler.
  329. // NEVER close this channel while the handler is still being used!
  330. CleanStop chan struct{}
  331. h RequestHandler
  332. // Path to the root directory to serve files from.
  333. Root string
  334. // Path to the compressed root directory to serve files from. If this value
  335. // is empty, Root is used.
  336. CompressRoot string
  337. // Suffix to add to the name of cached compressed file.
  338. //
  339. // This value has sense only if Compress is set.
  340. //
  341. // FSCompressedFileSuffix is used by default.
  342. CompressedFileSuffix string
  343. // List of index file names to try opening during directory access.
  344. //
  345. // For example:
  346. //
  347. // * index.html
  348. // * index.htm
  349. // * my-super-index.xml
  350. //
  351. // By default the list is empty.
  352. IndexNames []string
  353. // Expiration duration for inactive file handlers.
  354. //
  355. // FSHandlerCacheDuration is used by default.
  356. CacheDuration time.Duration
  357. once sync.Once
  358. // AllowEmptyRoot controls what happens when Root is empty. When false (default) it will default to the
  359. // current working directory. An empty root is mostly useful when you want to use absolute paths
  360. // on windows that are on different filesystems. On linux setting your Root to "/" already allows you to use
  361. // absolute paths on any filesystem.
  362. AllowEmptyRoot bool
  363. // Uses brotli encoding and fallbacks to zstd or gzip in responses if set to true, uses zstd or gzip if set to false.
  364. //
  365. // This value has sense only if Compress is set.
  366. //
  367. // Brotli encoding is disabled by default.
  368. CompressBrotli bool
  369. // Uses zstd encoding and fallbacks to gzip in responses if set to true, uses gzip if set to false.
  370. //
  371. // This value has sense only if Compress is set.
  372. //
  373. // zstd encoding is disabled by default.
  374. CompressZstd bool
  375. // Index pages for directories without files matching IndexNames
  376. // are automatically generated if set.
  377. //
  378. // Directory index generation may be quite slow for directories
  379. // with many files (more than 1K), so it is discouraged enabling
  380. // index pages' generation for such directories.
  381. //
  382. // By default index pages aren't generated.
  383. GenerateIndexPages bool
  384. // Transparently compresses responses if set to true.
  385. //
  386. // The server tries minimizing CPU usage by caching compressed files.
  387. // It adds CompressedFileSuffix suffix to the original file name and
  388. // tries saving the resulting compressed file under the new file name.
  389. // So it is advisable to give the server write access to Root
  390. // and to all inner folders in order to minimize CPU usage when serving
  391. // compressed responses.
  392. //
  393. // Transparent compression is disabled by default.
  394. Compress bool
  395. // Enables byte range requests if set to true.
  396. //
  397. // Byte range requests are disabled by default.
  398. AcceptByteRange bool
  399. // SkipCache if true, will cache no file handler.
  400. //
  401. // By default is false.
  402. SkipCache bool
  403. }
  404. // FSCompressedFileSuffix is the suffix FS adds to the original file names
  405. // when trying to store compressed file under the new file name.
  406. // See FS.Compress for details.
  407. const FSCompressedFileSuffix = ".fasthttp.gz"
  408. // FSCompressedFileSuffixes is the suffixes FS adds to the original file names depending on encoding
  409. // when trying to store compressed file under the new file name.
  410. // See FS.Compress for details.
  411. var FSCompressedFileSuffixes = map[string]string{
  412. "gzip": ".fasthttp.gz",
  413. "br": ".fasthttp.br",
  414. "zstd": ".fasthttp.zst",
  415. }
  416. // FSHandlerCacheDuration is the default expiration duration for inactive
  417. // file handlers opened by FS.
  418. const FSHandlerCacheDuration = 10 * time.Second
  419. // FSHandler returns request handler serving static files from
  420. // the given root folder.
  421. //
  422. // stripSlashes indicates how many leading slashes must be stripped
  423. // from requested path before searching requested file in the root folder.
  424. // Examples:
  425. //
  426. // - stripSlashes = 0, original path: "/foo/bar", result: "/foo/bar"
  427. // - stripSlashes = 1, original path: "/foo/bar", result: "/bar"
  428. // - stripSlashes = 2, original path: "/foo/bar", result: ""
  429. //
  430. // The returned request handler automatically generates index pages
  431. // for directories without index.html.
  432. //
  433. // The returned handler caches requested file handles
  434. // for FSHandlerCacheDuration.
  435. // Make sure your program has enough 'max open files' limit aka
  436. // 'ulimit -n' if root folder contains many files.
  437. //
  438. // Do not create multiple request handler instances for the same
  439. // (root, stripSlashes) arguments - just reuse a single instance.
  440. // Otherwise goroutine leak will occur.
  441. func FSHandler(root string, stripSlashes int) RequestHandler {
  442. fs := &FS{
  443. Root: root,
  444. IndexNames: []string{"index.html"},
  445. GenerateIndexPages: true,
  446. AcceptByteRange: true,
  447. }
  448. if stripSlashes > 0 {
  449. fs.PathRewrite = NewPathSlashesStripper(stripSlashes)
  450. }
  451. return fs.NewRequestHandler()
  452. }
  453. // NewRequestHandler returns new request handler with the given FS settings.
  454. //
  455. // The returned handler caches requested file handles
  456. // for FS.CacheDuration.
  457. // Make sure your program has enough 'max open files' limit aka
  458. // 'ulimit -n' if FS.Root folder contains many files.
  459. //
  460. // Do not create multiple request handlers from a single FS instance -
  461. // just reuse a single request handler.
  462. func (fs *FS) NewRequestHandler() RequestHandler {
  463. fs.once.Do(fs.initRequestHandler)
  464. return fs.h
  465. }
  466. func (fs *FS) normalizeRoot(root string) string {
  467. // fs.FS uses relative paths, that paths are slash-separated on all systems, even Windows.
  468. if fs.FS == nil {
  469. // Serve files from the current working directory if Root is empty or if Root is a relative path.
  470. if (!fs.AllowEmptyRoot && root == "") || (root != "" && !filepath.IsAbs(root)) {
  471. path, err := os.Getwd()
  472. if err != nil {
  473. path = "."
  474. }
  475. root = path + "/" + root
  476. }
  477. // convert the root directory slashes to the native format
  478. root = filepath.FromSlash(root)
  479. } else {
  480. if root == "" {
  481. return root
  482. }
  483. // Normalize fs.FS roots to slash-separated relative paths.
  484. root = strings.ReplaceAll(root, "\\", "/")
  485. root = strings.TrimLeft(root, "/")
  486. if root == "" {
  487. return root
  488. }
  489. root = path.Clean(root)
  490. if root == "." {
  491. return "."
  492. }
  493. return root
  494. }
  495. // strip trailing slashes from the root path
  496. for root != "" && root[len(root)-1] == os.PathSeparator {
  497. root = root[:len(root)-1]
  498. }
  499. return root
  500. }
  501. func (fs *FS) initRequestHandler() {
  502. root := fs.normalizeRoot(fs.Root)
  503. compressRoot := fs.CompressRoot
  504. if compressRoot == "" {
  505. compressRoot = root
  506. } else {
  507. compressRoot = fs.normalizeRoot(compressRoot)
  508. }
  509. compressedFileSuffixes := fs.CompressedFileSuffixes
  510. if compressedFileSuffixes["br"] == "" || compressedFileSuffixes["gzip"] == "" ||
  511. compressedFileSuffixes["zstd"] == "" || compressedFileSuffixes["br"] == compressedFileSuffixes["gzip"] ||
  512. compressedFileSuffixes["br"] == compressedFileSuffixes["zstd"] ||
  513. compressedFileSuffixes["gzip"] == compressedFileSuffixes["zstd"] {
  514. // Copy global map
  515. compressedFileSuffixes = make(map[string]string, len(FSCompressedFileSuffixes))
  516. maps.Copy(compressedFileSuffixes, FSCompressedFileSuffixes)
  517. }
  518. if fs.CompressedFileSuffix != "" {
  519. compressedFileSuffixes["gzip"] = fs.CompressedFileSuffix
  520. compressedFileSuffixes["br"] = FSCompressedFileSuffixes["br"]
  521. compressedFileSuffixes["zstd"] = FSCompressedFileSuffixes["zstd"]
  522. }
  523. h := &fsHandler{
  524. filesystem: fs.FS,
  525. root: root,
  526. indexNames: fs.IndexNames,
  527. pathRewrite: fs.PathRewrite,
  528. generateIndexPages: fs.GenerateIndexPages,
  529. compress: fs.Compress,
  530. compressBrotli: fs.CompressBrotli,
  531. compressZstd: fs.CompressZstd,
  532. compressRoot: compressRoot,
  533. pathNotFound: fs.PathNotFound,
  534. acceptByteRange: fs.AcceptByteRange,
  535. compressedFileSuffixes: compressedFileSuffixes,
  536. }
  537. h.cacheManager = newCacheManager(fs)
  538. if h.filesystem == nil {
  539. h.filesystem = &osFS{} // It provides os.Open and os.Stat
  540. }
  541. fs.h = h.handleRequest
  542. }
  543. type fsHandler struct {
  544. smallFileReaderPool sync.Pool
  545. filesystem fs.FS
  546. cacheManager cacheManager
  547. pathRewrite PathRewriteFunc
  548. pathNotFound RequestHandler
  549. compressedFileSuffixes map[string]string
  550. root string
  551. compressRoot string
  552. indexNames []string
  553. generateIndexPages bool
  554. compress bool
  555. compressBrotli bool
  556. compressZstd bool
  557. acceptByteRange bool
  558. }
  559. type fsFile struct {
  560. lastModified time.Time
  561. t time.Time
  562. f fs.File
  563. h *fsHandler
  564. filename string // fs.FileInfo.Name() return filename, isn't filepath.
  565. contentType string
  566. dirIndex []byte
  567. lastModifiedStr []byte
  568. bigFiles []*bigFileReader
  569. contentLength int
  570. readersCount int
  571. bigFilesLock sync.Mutex
  572. compressed bool
  573. }
  574. func (ff *fsFile) NewReader() (io.Reader, error) {
  575. if ff.isBig() {
  576. return ff.bigFileReader()
  577. }
  578. return ff.smallFileReader(), nil
  579. }
  580. func (ff *fsFile) smallFileReader() io.Reader {
  581. v := ff.h.smallFileReaderPool.Get()
  582. if v == nil {
  583. v = &fsSmallFileReader{}
  584. }
  585. r := v.(*fsSmallFileReader)
  586. r.ff = ff
  587. r.endPos = ff.contentLength
  588. if r.startPos > 0 {
  589. panic("bug: fsSmallFileReader with non-nil startPos found in the pool")
  590. }
  591. return r
  592. }
  593. // Files bigger than this size are sent with sendfile.
  594. const maxSmallFileSize = 2 * 4096
  595. func (ff *fsFile) isBig() bool {
  596. if _, ok := ff.h.filesystem.(*osFS); !ok { // fs.FS only uses bigFileReader, memory cache uses fsSmallFileReader
  597. return ff.f != nil
  598. }
  599. return ff.contentLength > maxSmallFileSize && len(ff.dirIndex) == 0
  600. }
  601. func (ff *fsFile) bigFileReader() (io.Reader, error) {
  602. if ff.f == nil {
  603. return nil, errors.New("bug: ff.f must be non-nil in bigFileReader")
  604. }
  605. var r io.Reader
  606. ff.bigFilesLock.Lock()
  607. n := len(ff.bigFiles)
  608. if n > 0 {
  609. r = ff.bigFiles[n-1]
  610. ff.bigFiles = ff.bigFiles[:n-1]
  611. }
  612. ff.bigFilesLock.Unlock()
  613. if r != nil {
  614. return r, nil
  615. }
  616. f, err := ff.h.filesystem.Open(ff.filename)
  617. if err != nil {
  618. return nil, fmt.Errorf("cannot open already opened file: %w", err)
  619. }
  620. return &bigFileReader{
  621. f: f,
  622. ff: ff,
  623. r: f,
  624. }, nil
  625. }
  626. func (ff *fsFile) Release() {
  627. if ff.f != nil {
  628. _ = ff.f.Close()
  629. if ff.isBig() {
  630. ff.bigFilesLock.Lock()
  631. for _, r := range ff.bigFiles {
  632. _ = r.f.Close()
  633. }
  634. ff.bigFilesLock.Unlock()
  635. }
  636. }
  637. }
  638. func (ff *fsFile) decReadersCount() {
  639. ff.h.cacheManager.Lock()
  640. ff.readersCount--
  641. if ff.readersCount < 0 {
  642. panic("bug: fsFile.readersCount < 0")
  643. }
  644. ff.h.cacheManager.Unlock()
  645. }
  646. // bigFileReader attempts to trigger sendfile
  647. // for sending big files over the wire.
  648. type bigFileReader struct {
  649. f fs.File
  650. ff *fsFile
  651. r io.Reader
  652. lr io.LimitedReader
  653. }
  654. func (r *bigFileReader) UpdateByteRange(startPos, endPos int) error {
  655. seeker, ok := r.f.(io.Seeker)
  656. if !ok {
  657. return errors.New("must implement io.Seeker")
  658. }
  659. if _, err := seeker.Seek(int64(startPos), io.SeekStart); err != nil {
  660. return err
  661. }
  662. r.r = &r.lr
  663. r.lr.R = r.f
  664. r.lr.N = int64(endPos - startPos + 1)
  665. return nil
  666. }
  667. func (r *bigFileReader) Read(p []byte) (int, error) {
  668. return r.r.Read(p)
  669. }
  670. func (r *bigFileReader) WriteTo(w io.Writer) (int64, error) {
  671. if rf, ok := w.(io.ReaderFrom); ok {
  672. // fast path. Send file must be triggered
  673. return rf.ReadFrom(r.r)
  674. }
  675. // slow path
  676. return copyZeroAlloc(w, r.r)
  677. }
  678. func (r *bigFileReader) Close() error {
  679. r.r = r.f
  680. seeker, ok := r.f.(io.Seeker)
  681. if !ok {
  682. _ = r.f.Close()
  683. return errors.New("must implement io.Seeker")
  684. }
  685. n, err := seeker.Seek(0, io.SeekStart)
  686. if err == nil {
  687. if n == 0 {
  688. ff := r.ff
  689. ff.bigFilesLock.Lock()
  690. ff.bigFiles = append(ff.bigFiles, r)
  691. ff.bigFilesLock.Unlock()
  692. } else {
  693. _ = r.f.Close()
  694. err = errors.New("bug: File.Seek(0, io.SeekStart) returned (non-zero, nil)")
  695. }
  696. } else {
  697. _ = r.f.Close()
  698. }
  699. r.ff.decReadersCount()
  700. return err
  701. }
  702. type fsSmallFileReader struct {
  703. ff *fsFile
  704. startPos int
  705. endPos int
  706. }
  707. func (r *fsSmallFileReader) Close() error {
  708. ff := r.ff
  709. ff.decReadersCount()
  710. r.ff = nil
  711. r.startPos = 0
  712. r.endPos = 0
  713. ff.h.smallFileReaderPool.Put(r)
  714. return nil
  715. }
  716. func (r *fsSmallFileReader) UpdateByteRange(startPos, endPos int) error {
  717. r.startPos = startPos
  718. r.endPos = endPos + 1
  719. return nil
  720. }
  721. func (r *fsSmallFileReader) Read(p []byte) (int, error) {
  722. tailLen := r.endPos - r.startPos
  723. if tailLen <= 0 {
  724. return 0, io.EOF
  725. }
  726. if len(p) > tailLen {
  727. p = p[:tailLen]
  728. }
  729. ff := r.ff
  730. if ff.f != nil {
  731. ra, ok := ff.f.(io.ReaderAt)
  732. if !ok {
  733. return 0, errors.New("must implement io.ReaderAt")
  734. }
  735. n, err := ra.ReadAt(p, int64(r.startPos))
  736. r.startPos += n
  737. return n, err
  738. }
  739. n := copy(p, ff.dirIndex[r.startPos:])
  740. r.startPos += n
  741. return n, nil
  742. }
  743. func (r *fsSmallFileReader) WriteTo(w io.Writer) (int64, error) {
  744. ff := r.ff
  745. var n int
  746. var err error
  747. if ff.f == nil {
  748. n, err = w.Write(ff.dirIndex[r.startPos:r.endPos])
  749. return int64(n), err
  750. }
  751. if rf, ok := w.(io.ReaderFrom); ok {
  752. return rf.ReadFrom(r)
  753. }
  754. curPos := r.startPos
  755. bufv := copyBufPool.Get()
  756. buf := bufv.([]byte)
  757. for err == nil {
  758. tailLen := r.endPos - curPos
  759. if tailLen <= 0 {
  760. break
  761. }
  762. if len(buf) > tailLen {
  763. buf = buf[:tailLen]
  764. }
  765. ra, ok := ff.f.(io.ReaderAt)
  766. if !ok {
  767. return 0, errors.New("must implement io.ReaderAt")
  768. }
  769. n, err = ra.ReadAt(buf, int64(curPos))
  770. nw, errw := w.Write(buf[:n])
  771. curPos += nw
  772. if errw == nil && nw != n {
  773. errw = errors.New("bug: Write(p) returned (n, nil), where n != len(p)")
  774. }
  775. if err == nil {
  776. err = errw
  777. }
  778. }
  779. copyBufPool.Put(bufv)
  780. if err == io.EOF {
  781. err = nil
  782. }
  783. return int64(curPos - r.startPos), err
  784. }
  785. type cacheManager interface {
  786. Lock()
  787. Unlock()
  788. GetFileFromCache(cacheKind CacheKind, path []byte) (*fsFile, bool)
  789. SetFileToCache(cacheKind CacheKind, path []byte, ff *fsFile) *fsFile
  790. }
  791. var (
  792. _ cacheManager = (*inMemoryCacheManager)(nil)
  793. _ cacheManager = (*noopCacheManager)(nil)
  794. )
  795. type CacheKind uint8
  796. const (
  797. defaultCacheKind CacheKind = iota
  798. brotliCacheKind
  799. gzipCacheKind
  800. zstdCacheKind
  801. )
  802. func newCacheManager(fs *FS) cacheManager {
  803. if fs.SkipCache {
  804. return &noopCacheManager{}
  805. }
  806. cacheDuration := fs.CacheDuration
  807. if cacheDuration <= 0 {
  808. cacheDuration = FSHandlerCacheDuration
  809. }
  810. instance := &inMemoryCacheManager{
  811. cacheDuration: cacheDuration,
  812. cache: make(map[string]*fsFile),
  813. cacheBrotli: make(map[string]*fsFile),
  814. cacheGzip: make(map[string]*fsFile),
  815. cacheZstd: make(map[string]*fsFile),
  816. }
  817. go instance.handleCleanCache(fs.CleanStop)
  818. return instance
  819. }
  820. type noopCacheManager struct {
  821. cacheLock sync.Mutex
  822. }
  823. func (n *noopCacheManager) Lock() {
  824. n.cacheLock.Lock()
  825. }
  826. func (n *noopCacheManager) Unlock() {
  827. n.cacheLock.Unlock()
  828. }
  829. func (*noopCacheManager) GetFileFromCache(cacheKind CacheKind, path []byte) (*fsFile, bool) {
  830. return nil, false
  831. }
  832. func (n *noopCacheManager) SetFileToCache(cacheKind CacheKind, path []byte, ff *fsFile) *fsFile {
  833. n.cacheLock.Lock()
  834. ff.readersCount++
  835. n.cacheLock.Unlock()
  836. return ff
  837. }
  838. type inMemoryCacheManager struct {
  839. cache map[string]*fsFile
  840. cacheBrotli map[string]*fsFile
  841. cacheGzip map[string]*fsFile
  842. cacheZstd map[string]*fsFile
  843. cacheDuration time.Duration
  844. cacheLock sync.Mutex
  845. }
  846. func (cm *inMemoryCacheManager) Lock() {
  847. cm.cacheLock.Lock()
  848. }
  849. func (cm *inMemoryCacheManager) Unlock() {
  850. cm.cacheLock.Unlock()
  851. }
  852. func (cm *inMemoryCacheManager) getFsCache(cacheKind CacheKind) map[string]*fsFile {
  853. fileCache := cm.cache
  854. switch cacheKind {
  855. case brotliCacheKind:
  856. fileCache = cm.cacheBrotli
  857. case gzipCacheKind:
  858. fileCache = cm.cacheGzip
  859. case zstdCacheKind:
  860. fileCache = cm.cacheZstd
  861. }
  862. return fileCache
  863. }
  864. func (cm *inMemoryCacheManager) GetFileFromCache(cacheKind CacheKind, path []byte) (*fsFile, bool) {
  865. fileCache := cm.getFsCache(cacheKind)
  866. cm.cacheLock.Lock()
  867. ff, ok := fileCache[string(path)]
  868. if ok {
  869. ff.readersCount++
  870. }
  871. cm.cacheLock.Unlock()
  872. return ff, ok
  873. }
  874. func (cm *inMemoryCacheManager) SetFileToCache(cacheKind CacheKind, path []byte, ff *fsFile) *fsFile {
  875. fileCache := cm.getFsCache(cacheKind)
  876. cm.cacheLock.Lock()
  877. ff1, ok := fileCache[string(path)]
  878. if !ok {
  879. fileCache[string(path)] = ff
  880. ff.readersCount++
  881. } else {
  882. ff1.readersCount++
  883. }
  884. cm.cacheLock.Unlock()
  885. if ok {
  886. // The file has been already opened by another
  887. // goroutine, so close the current file and use
  888. // the file opened by another goroutine instead.
  889. ff.Release()
  890. ff = ff1
  891. }
  892. return ff
  893. }
  894. func (cm *inMemoryCacheManager) handleCleanCache(cleanStop chan struct{}) {
  895. var pendingFiles []*fsFile
  896. clean := func() {
  897. pendingFiles = cm.cleanCache(pendingFiles)
  898. }
  899. if cleanStop != nil {
  900. t := time.NewTicker(cm.cacheDuration / 2)
  901. for {
  902. select {
  903. case <-t.C:
  904. clean()
  905. case _, stillOpen := <-cleanStop:
  906. // Ignore values send on the channel, only stop when it is closed.
  907. if !stillOpen {
  908. t.Stop()
  909. return
  910. }
  911. }
  912. }
  913. }
  914. for {
  915. time.Sleep(cm.cacheDuration / 2)
  916. clean()
  917. }
  918. }
  919. func (cm *inMemoryCacheManager) cleanCache(pendingFiles []*fsFile) []*fsFile {
  920. var filesToRelease []*fsFile
  921. cm.cacheLock.Lock()
  922. // Close files which couldn't be closed before due to non-zero
  923. // readers count on the previous run.
  924. var remainingFiles []*fsFile
  925. for _, ff := range pendingFiles {
  926. if ff.readersCount > 0 {
  927. remainingFiles = append(remainingFiles, ff)
  928. } else {
  929. filesToRelease = append(filesToRelease, ff)
  930. }
  931. }
  932. pendingFiles = remainingFiles
  933. pendingFiles, filesToRelease = cleanCacheNolock(cm.cache, pendingFiles, filesToRelease, cm.cacheDuration)
  934. pendingFiles, filesToRelease = cleanCacheNolock(cm.cacheBrotli, pendingFiles, filesToRelease, cm.cacheDuration)
  935. pendingFiles, filesToRelease = cleanCacheNolock(cm.cacheGzip, pendingFiles, filesToRelease, cm.cacheDuration)
  936. pendingFiles, filesToRelease = cleanCacheNolock(cm.cacheZstd, pendingFiles, filesToRelease, cm.cacheDuration)
  937. cm.cacheLock.Unlock()
  938. for _, ff := range filesToRelease {
  939. ff.Release()
  940. }
  941. return pendingFiles
  942. }
  943. func cleanCacheNolock(
  944. cache map[string]*fsFile, pendingFiles, filesToRelease []*fsFile, cacheDuration time.Duration,
  945. ) ([]*fsFile, []*fsFile) {
  946. t := time.Now()
  947. for k, ff := range cache {
  948. if t.Sub(ff.t) > cacheDuration {
  949. if ff.readersCount > 0 {
  950. // There are pending readers on stale file handle,
  951. // so we cannot close it. Put it into pendingFiles
  952. // so it will be closed later.
  953. pendingFiles = append(pendingFiles, ff)
  954. } else {
  955. filesToRelease = append(filesToRelease, ff)
  956. }
  957. delete(cache, k)
  958. }
  959. }
  960. return pendingFiles, filesToRelease
  961. }
  962. func (h *fsHandler) pathToFilePath(path []byte, hasTrailingSlash bool) string {
  963. if hasTrailingSlash {
  964. path = path[:len(path)-1]
  965. }
  966. hasLeadingSlash := len(path) > 0 && path[0] == '/'
  967. if _, ok := h.filesystem.(*osFS); !ok {
  968. root := h.root
  969. if root == "." {
  970. root = ""
  971. }
  972. if len(path) < 1 || (hasLeadingSlash && len(path) == 1) {
  973. if h.root == "." {
  974. return "."
  975. }
  976. return root
  977. }
  978. if root == "" {
  979. if hasLeadingSlash {
  980. return string(path[1:])
  981. }
  982. return string(path)
  983. }
  984. // Use byte buffer pool to avoid string concatenation allocations.
  985. b := bytebufferpool.Get()
  986. defer bytebufferpool.Put(b)
  987. b.B = append(b.B, root...)
  988. b.B = append(b.B, '/')
  989. if hasLeadingSlash {
  990. b.B = append(b.B, path[1:]...)
  991. } else {
  992. b.B = append(b.B, path...)
  993. }
  994. return string(b.B)
  995. }
  996. // Use byte buffer pool to avoid string concatenation allocations
  997. b := bytebufferpool.Get()
  998. defer bytebufferpool.Put(b)
  999. b.B = append(b.B, h.root...)
  1000. if hasLeadingSlash {
  1001. b.B = append(b.B, path...)
  1002. } else {
  1003. if h.root != "" && len(path) > 0 {
  1004. b.B = append(b.B, '/')
  1005. }
  1006. b.B = append(b.B, path...)
  1007. }
  1008. return filepath.FromSlash(string(b.B))
  1009. }
  1010. func (h *fsHandler) filePathToCompressed(filePath string) string {
  1011. if h.root == h.compressRoot {
  1012. return filePath
  1013. }
  1014. if !strings.HasPrefix(filePath, h.root) {
  1015. return filePath
  1016. }
  1017. return filepath.FromSlash(h.compressRoot + filePath[len(h.root):])
  1018. }
  1019. func (h *fsHandler) handleRequest(ctx *RequestCtx) {
  1020. var path []byte
  1021. if h.pathRewrite != nil {
  1022. path = h.pathRewrite(ctx)
  1023. } else {
  1024. path = ctx.Path()
  1025. }
  1026. hasTrailingSlash := len(path) > 0 && path[len(path)-1] == '/'
  1027. if n := bytes.IndexByte(path, 0); n >= 0 {
  1028. ctx.Logger().Printf("cannot serve path with nil byte at position %d: %q", n, path)
  1029. ctx.Error("Are you a hacker?", StatusBadRequest)
  1030. return
  1031. }
  1032. if h.pathRewrite != nil {
  1033. // There is no need to check rewritten paths if path = ctx.Path(),
  1034. // since ctx.Path must normalize and sanitize the path.
  1035. if hasDotDotPathSegment(path) {
  1036. ctx.Logger().Printf("cannot serve rewritten path with '..' path segment due to security reasons: %q", path)
  1037. ctx.Error("Internal Server Error", StatusInternalServerError)
  1038. return
  1039. }
  1040. }
  1041. mustCompress := false
  1042. fileCacheKind := defaultCacheKind
  1043. fileEncoding := ""
  1044. byteRange := ctx.Request.Header.peek(strRange)
  1045. if len(byteRange) == 0 && h.compress {
  1046. switch {
  1047. case h.compressBrotli && ctx.Request.Header.HasAcceptEncodingBytes(strBr):
  1048. mustCompress = true
  1049. fileCacheKind = brotliCacheKind
  1050. fileEncoding = "br"
  1051. case h.compressZstd && ctx.Request.Header.HasAcceptEncodingBytes(strZstd):
  1052. mustCompress = true
  1053. fileCacheKind = zstdCacheKind
  1054. fileEncoding = "zstd"
  1055. case ctx.Request.Header.HasAcceptEncodingBytes(strGzip):
  1056. mustCompress = true
  1057. fileCacheKind = gzipCacheKind
  1058. fileEncoding = "gzip"
  1059. }
  1060. }
  1061. ff, ok := h.cacheManager.GetFileFromCache(fileCacheKind, path)
  1062. if !ok {
  1063. filePath := h.pathToFilePath(path, hasTrailingSlash)
  1064. var err error
  1065. ff, err = h.openFSFile(filePath, mustCompress, fileEncoding)
  1066. if mustCompress && err == errNoCreatePermission {
  1067. ctx.Logger().Printf("insufficient permissions for saving compressed file for %q. Serving uncompressed file. "+
  1068. "Allow write access to the directory with this file in order to improve fasthttp performance", filePath)
  1069. mustCompress = false
  1070. ff, err = h.openFSFile(filePath, mustCompress, fileEncoding)
  1071. }
  1072. if errors.Is(err, errDirIndexRequired) {
  1073. if !hasTrailingSlash {
  1074. ctx.RedirectBytes(append(path, '/'), StatusFound)
  1075. return
  1076. }
  1077. ff, err = h.openIndexFile(ctx, filePath, mustCompress, fileEncoding)
  1078. if err != nil {
  1079. ctx.Logger().Printf("cannot open dir index %q: %v", filePath, err)
  1080. ctx.Error("Directory index is forbidden", StatusForbidden)
  1081. return
  1082. }
  1083. } else if err != nil {
  1084. ctx.Logger().Printf("cannot open file %q: %v", filePath, err)
  1085. if h.pathNotFound == nil {
  1086. ctx.Error("Cannot open requested path", StatusNotFound)
  1087. } else {
  1088. ctx.SetStatusCode(StatusNotFound)
  1089. h.pathNotFound(ctx)
  1090. }
  1091. return
  1092. }
  1093. ff = h.cacheManager.SetFileToCache(fileCacheKind, path, ff)
  1094. }
  1095. if !ctx.IfModifiedSince(ff.lastModified) {
  1096. ff.decReadersCount()
  1097. ctx.NotModified()
  1098. return
  1099. }
  1100. r, err := ff.NewReader()
  1101. if err != nil {
  1102. ff.decReadersCount()
  1103. ctx.Logger().Printf("cannot obtain file reader for path=%q: %v", path, err)
  1104. ctx.Error("Internal Server Error", StatusInternalServerError)
  1105. return
  1106. }
  1107. hdr := &ctx.Response.Header
  1108. if ff.compressed {
  1109. switch fileEncoding {
  1110. case "br":
  1111. hdr.SetContentEncodingBytes(strBr)
  1112. hdr.addVaryBytes(strAcceptEncoding)
  1113. case "gzip":
  1114. hdr.SetContentEncodingBytes(strGzip)
  1115. hdr.addVaryBytes(strAcceptEncoding)
  1116. case "zstd":
  1117. hdr.SetContentEncodingBytes(strZstd)
  1118. hdr.addVaryBytes(strAcceptEncoding)
  1119. }
  1120. }
  1121. statusCode := StatusOK
  1122. contentLength := ff.contentLength
  1123. if h.acceptByteRange {
  1124. hdr.setNonSpecial(strAcceptRanges, strBytes)
  1125. if len(byteRange) > 0 {
  1126. startPos, endPos, err := ParseByteRange(byteRange, contentLength)
  1127. if err != nil {
  1128. _ = r.(io.Closer).Close()
  1129. ctx.Logger().Printf("cannot parse byte range %q for path=%q: %v", byteRange, path, err)
  1130. ctx.Error("Range Not Satisfiable", StatusRequestedRangeNotSatisfiable)
  1131. return
  1132. }
  1133. if err = r.(byteRangeUpdater).UpdateByteRange(startPos, endPos); err != nil {
  1134. _ = r.(io.Closer).Close()
  1135. ctx.Logger().Printf("cannot seek byte range %q for path=%q: %v", byteRange, path, err)
  1136. ctx.Error("Internal Server Error", StatusInternalServerError)
  1137. return
  1138. }
  1139. hdr.SetContentRange(startPos, endPos, contentLength)
  1140. contentLength = endPos - startPos + 1
  1141. statusCode = StatusPartialContent
  1142. }
  1143. }
  1144. hdr.setNonSpecial(strLastModified, ff.lastModifiedStr)
  1145. if !ctx.IsHead() {
  1146. ctx.SetBodyStream(r, contentLength)
  1147. } else {
  1148. ctx.Response.ResetBody()
  1149. ctx.Response.SkipBody = true
  1150. ctx.Response.Header.SetContentLength(contentLength)
  1151. if rc, ok := r.(io.Closer); ok {
  1152. if err := rc.Close(); err != nil {
  1153. ctx.Logger().Printf("cannot close file reader: %v", err)
  1154. ctx.Error("Internal Server Error", StatusInternalServerError)
  1155. return
  1156. }
  1157. }
  1158. }
  1159. hdr.noDefaultContentType = true
  1160. if len(hdr.ContentType()) == 0 {
  1161. ctx.SetContentType(ff.contentType)
  1162. }
  1163. ctx.SetStatusCode(statusCode)
  1164. }
  1165. type byteRangeUpdater interface {
  1166. UpdateByteRange(startPos, endPos int) error
  1167. }
  1168. // ParseByteRange parses 'Range: bytes=...' header value.
  1169. //
  1170. // It follows https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35 .
  1171. func ParseByteRange(byteRange []byte, contentLength int) (startPos, endPos int, err error) {
  1172. b := byteRange
  1173. if !bytes.HasPrefix(b, strBytes) {
  1174. return 0, 0, fmt.Errorf("unsupported range units: %q. Expecting %q", byteRange, strBytes)
  1175. }
  1176. b = b[len(strBytes):]
  1177. if len(b) == 0 || b[0] != '=' {
  1178. return 0, 0, fmt.Errorf("missing byte range in %q", byteRange)
  1179. }
  1180. b = b[1:]
  1181. n := bytes.IndexByte(b, '-')
  1182. if n < 0 {
  1183. return 0, 0, fmt.Errorf("missing the end position of byte range in %q", byteRange)
  1184. }
  1185. if n == 0 {
  1186. v, err := ParseUint(b[n+1:])
  1187. if err != nil {
  1188. return 0, 0, err
  1189. }
  1190. startPos := max(contentLength-v, 0)
  1191. return startPos, contentLength - 1, nil
  1192. }
  1193. if startPos, err = ParseUint(b[:n]); err != nil {
  1194. return 0, 0, err
  1195. }
  1196. if startPos >= contentLength {
  1197. return 0, 0, fmt.Errorf("the start position of byte range cannot exceed %d. byte range %q", contentLength-1, byteRange)
  1198. }
  1199. b = b[n+1:]
  1200. if len(b) == 0 {
  1201. return startPos, contentLength - 1, nil
  1202. }
  1203. if endPos, err = ParseUint(b); err != nil {
  1204. return 0, 0, err
  1205. }
  1206. if endPos >= contentLength {
  1207. endPos = contentLength - 1
  1208. }
  1209. if endPos < startPos {
  1210. return 0, 0, fmt.Errorf("the start position of byte range cannot exceed the end position. byte range %q", byteRange)
  1211. }
  1212. return startPos, endPos, nil
  1213. }
  1214. func (h *fsHandler) openIndexFile(ctx *RequestCtx, dirPath string, mustCompress bool, fileEncoding string) (*fsFile, error) {
  1215. for _, indexName := range h.indexNames {
  1216. indexFilePath := indexName
  1217. if dirPath != "" {
  1218. indexFilePath = dirPath + "/" + indexName
  1219. }
  1220. ff, err := h.openFSFile(indexFilePath, mustCompress, fileEncoding)
  1221. if err == nil {
  1222. return ff, nil
  1223. }
  1224. if mustCompress && err == errNoCreatePermission {
  1225. ctx.Logger().Printf("insufficient permissions for saving compressed file for %q. Serving uncompressed file. "+
  1226. "Allow write access to the directory with this file in order to improve fasthttp performance", indexFilePath)
  1227. mustCompress = false
  1228. return h.openFSFile(indexFilePath, mustCompress, fileEncoding)
  1229. }
  1230. if !errors.Is(err, fs.ErrNotExist) {
  1231. return nil, fmt.Errorf("cannot open file %q: %w", indexFilePath, err)
  1232. }
  1233. }
  1234. if !h.generateIndexPages {
  1235. return nil, fmt.Errorf("cannot access directory without index page. Directory %q", dirPath)
  1236. }
  1237. return h.createDirIndex(ctx, dirPath, mustCompress, fileEncoding)
  1238. }
  1239. var (
  1240. errDirIndexRequired = errors.New("directory index required")
  1241. errNoCreatePermission = errors.New("no 'create file' permissions")
  1242. )
  1243. func (h *fsHandler) createDirIndex(ctx *RequestCtx, dirPath string, mustCompress bool, fileEncoding string) (*fsFile, error) {
  1244. w := &bytebufferpool.ByteBuffer{}
  1245. base := ctx.URI()
  1246. // io/fs doesn't support ReadDir with empty path.
  1247. if dirPath == "" {
  1248. dirPath = "."
  1249. }
  1250. basePathEscaped := html.EscapeString(string(base.Path()))
  1251. _, _ = fmt.Fprintf(w, "<html><head><title>%s</title><style>.dir { font-weight: bold }</style></head><body>", basePathEscaped)
  1252. _, _ = fmt.Fprintf(w, "<h1>%s</h1>", basePathEscaped)
  1253. _, _ = fmt.Fprintf(w, "<ul>")
  1254. if len(basePathEscaped) > 1 {
  1255. var parentURI URI
  1256. base.CopyTo(&parentURI)
  1257. parentURI.Update(string(base.Path()) + "/..")
  1258. parentPathEscaped := html.EscapeString(string(parentURI.Path()))
  1259. _, _ = fmt.Fprintf(w, `<li><a href="%s" class="dir">..</a></li>`, parentPathEscaped)
  1260. }
  1261. dirEntries, err := fs.ReadDir(h.filesystem, dirPath)
  1262. if err != nil {
  1263. return nil, err
  1264. }
  1265. fm := make(map[string]fs.FileInfo, len(dirEntries))
  1266. filenames := make([]string, 0, len(dirEntries))
  1267. nestedContinue:
  1268. for _, de := range dirEntries {
  1269. name := de.Name()
  1270. for _, cfs := range h.compressedFileSuffixes {
  1271. if strings.HasSuffix(name, cfs) {
  1272. // Do not show compressed files on index page.
  1273. continue nestedContinue
  1274. }
  1275. }
  1276. fi, err := de.Info()
  1277. if err != nil {
  1278. ctx.Logger().Printf("cannot fetch information from dir entry %q: %v, skip", name, err)
  1279. continue nestedContinue
  1280. }
  1281. fm[name] = fi
  1282. filenames = append(filenames, name)
  1283. }
  1284. var u URI
  1285. base.CopyTo(&u)
  1286. u.Update(string(u.Path()) + "/")
  1287. sort.Strings(filenames)
  1288. for _, name := range filenames {
  1289. u.Update(name)
  1290. pathEscaped := html.EscapeString(string(u.Path()))
  1291. fi := fm[name]
  1292. auxStr := "dir"
  1293. className := "dir"
  1294. if !fi.IsDir() {
  1295. auxStr = fmt.Sprintf("file, %d bytes", fi.Size())
  1296. className = "file"
  1297. }
  1298. _, _ = fmt.Fprintf(w, `<li><a href="%s" class="%s">%s</a>, %s, last modified %s</li>`,
  1299. pathEscaped, className, html.EscapeString(name), auxStr, fsModTime(fi.ModTime()))
  1300. }
  1301. _, _ = fmt.Fprintf(w, "</ul></body></html>")
  1302. if mustCompress {
  1303. var zbuf bytebufferpool.ByteBuffer
  1304. switch fileEncoding {
  1305. case "br":
  1306. zbuf.B = AppendBrotliBytesLevel(zbuf.B, w.B, CompressDefaultCompression)
  1307. case "gzip":
  1308. zbuf.B = AppendGzipBytesLevel(zbuf.B, w.B, CompressDefaultCompression)
  1309. case "zstd":
  1310. zbuf.B = AppendZstdBytesLevel(zbuf.B, w.B, CompressZstdDefault)
  1311. }
  1312. w = &zbuf
  1313. }
  1314. dirIndex := w.B
  1315. lastModified := time.Now()
  1316. ff := &fsFile{
  1317. h: h,
  1318. dirIndex: dirIndex,
  1319. contentType: "text/html; charset=utf-8",
  1320. contentLength: len(dirIndex),
  1321. compressed: mustCompress,
  1322. lastModified: lastModified,
  1323. lastModifiedStr: AppendHTTPDate(nil, lastModified),
  1324. t: lastModified,
  1325. }
  1326. return ff, nil
  1327. }
  1328. const (
  1329. fsMinCompressRatio = 0.8
  1330. fsMaxCompressibleFileSize = 8 * 1024 * 1024
  1331. )
  1332. func (h *fsHandler) compressAndOpenFSFile(filePath, fileEncoding string) (*fsFile, error) {
  1333. f, err := h.filesystem.Open(filePath)
  1334. if err != nil {
  1335. return nil, err
  1336. }
  1337. fileInfo, err := f.Stat()
  1338. if err != nil {
  1339. _ = f.Close()
  1340. return nil, fmt.Errorf("cannot obtain info for file %q: %w", filePath, err)
  1341. }
  1342. if fileInfo.IsDir() {
  1343. _ = f.Close()
  1344. return nil, errDirIndexRequired
  1345. }
  1346. if strings.HasSuffix(filePath, h.compressedFileSuffixes[fileEncoding]) ||
  1347. fileInfo.Size() > fsMaxCompressibleFileSize ||
  1348. !isFileCompressible(f, fsMinCompressRatio) {
  1349. return h.newFSFile(f, fileInfo, false, filePath, "")
  1350. }
  1351. compressedFilePath := h.filePathToCompressed(filePath)
  1352. if _, ok := h.filesystem.(*osFS); !ok {
  1353. return h.newCompressedFSFileCache(f, fileInfo, compressedFilePath, fileEncoding)
  1354. }
  1355. if compressedFilePath != filePath {
  1356. if err := os.MkdirAll(filepath.Dir(compressedFilePath), 0o750); err != nil {
  1357. return nil, err
  1358. }
  1359. }
  1360. compressedFilePath += h.compressedFileSuffixes[fileEncoding]
  1361. absPath, err := filepath.Abs(compressedFilePath)
  1362. if err != nil {
  1363. _ = f.Close()
  1364. return nil, fmt.Errorf("cannot determine absolute path for %q: %v", compressedFilePath, err)
  1365. }
  1366. flock := getFileLock(absPath)
  1367. flock.Lock()
  1368. ff, err := h.compressFileNolock(f, fileInfo, filePath, compressedFilePath, fileEncoding)
  1369. flock.Unlock()
  1370. return ff, err
  1371. }
  1372. func (h *fsHandler) compressFileNolock(
  1373. f fs.File, fileInfo fs.FileInfo, filePath, compressedFilePath, fileEncoding string,
  1374. ) (*fsFile, error) {
  1375. // Attempt to open compressed file created by another concurrent
  1376. // goroutine.
  1377. // It is safe opening such a file, since the file creation
  1378. // is guarded by file mutex - see getFileLock call.
  1379. if _, err := os.Stat(compressedFilePath); err == nil {
  1380. _ = f.Close()
  1381. return h.newCompressedFSFile(compressedFilePath, fileEncoding)
  1382. }
  1383. // Create temporary file, so concurrent goroutines don't use
  1384. // it until it is created.
  1385. tmpFilePath := compressedFilePath + ".tmp"
  1386. zf, err := os.Create(tmpFilePath)
  1387. if err != nil {
  1388. _ = f.Close()
  1389. if !errors.Is(err, fs.ErrPermission) {
  1390. return nil, fmt.Errorf("cannot create temporary file %q: %w", tmpFilePath, err)
  1391. }
  1392. return nil, errNoCreatePermission
  1393. }
  1394. switch fileEncoding {
  1395. case "br":
  1396. zw := acquireStacklessBrotliWriter(zf, CompressDefaultCompression)
  1397. _, err = copyZeroAlloc(zw, f)
  1398. if errf := zw.Flush(); err == nil {
  1399. err = errf
  1400. }
  1401. releaseStacklessBrotliWriter(zw, CompressDefaultCompression)
  1402. case "gzip":
  1403. zw := acquireStacklessGzipWriter(zf, CompressDefaultCompression)
  1404. _, err = copyZeroAlloc(zw, f)
  1405. if errf := zw.Flush(); err == nil {
  1406. err = errf
  1407. }
  1408. releaseStacklessGzipWriter(zw, CompressDefaultCompression)
  1409. case "zstd":
  1410. zw := acquireStacklessZstdWriter(zf, CompressZstdDefault)
  1411. _, err = copyZeroAlloc(zw, f)
  1412. if errf := zw.Flush(); err == nil {
  1413. err = errf
  1414. }
  1415. releaseStacklessZstdWriter(zw, CompressZstdDefault)
  1416. }
  1417. _ = zf.Close()
  1418. _ = f.Close()
  1419. if err != nil {
  1420. return nil, fmt.Errorf("error when compressing file %q to %q: %w", filePath, tmpFilePath, err)
  1421. }
  1422. if err = os.Chtimes(tmpFilePath, time.Now(), fileInfo.ModTime()); err != nil {
  1423. return nil, fmt.Errorf("cannot change modification time to %v for tmp file %q: %v",
  1424. fileInfo.ModTime(), tmpFilePath, err)
  1425. }
  1426. if err = os.Rename(tmpFilePath, compressedFilePath); err != nil {
  1427. return nil, fmt.Errorf("cannot move compressed file from %q to %q: %w", tmpFilePath, compressedFilePath, err)
  1428. }
  1429. return h.newCompressedFSFile(compressedFilePath, fileEncoding)
  1430. }
  1431. // newCompressedFSFileCache use memory cache compressed files.
  1432. func (h *fsHandler) newCompressedFSFileCache(f fs.File, fileInfo fs.FileInfo, filePath, fileEncoding string) (*fsFile, error) {
  1433. var (
  1434. w = &bytebufferpool.ByteBuffer{}
  1435. err error
  1436. )
  1437. switch fileEncoding {
  1438. case "br":
  1439. zw := acquireStacklessBrotliWriter(w, CompressDefaultCompression)
  1440. _, err = copyZeroAlloc(zw, f)
  1441. if errf := zw.Flush(); err == nil {
  1442. err = errf
  1443. }
  1444. releaseStacklessBrotliWriter(zw, CompressDefaultCompression)
  1445. case "gzip":
  1446. zw := acquireStacklessGzipWriter(w, CompressDefaultCompression)
  1447. _, err = copyZeroAlloc(zw, f)
  1448. if errf := zw.Flush(); err == nil {
  1449. err = errf
  1450. }
  1451. releaseStacklessGzipWriter(zw, CompressDefaultCompression)
  1452. case "zstd":
  1453. zw := acquireStacklessZstdWriter(w, CompressZstdDefault)
  1454. _, err = copyZeroAlloc(zw, f)
  1455. if errf := zw.Flush(); err == nil {
  1456. err = errf
  1457. }
  1458. releaseStacklessZstdWriter(zw, CompressZstdDefault)
  1459. }
  1460. defer func() { _ = f.Close() }()
  1461. if err != nil {
  1462. return nil, fmt.Errorf("error when compressing file %q: %w", filePath, err)
  1463. }
  1464. seeker, ok := f.(io.Seeker)
  1465. if !ok {
  1466. return nil, errors.New("not implemented io.Seeker")
  1467. }
  1468. if _, err = seeker.Seek(0, io.SeekStart); err != nil {
  1469. return nil, err
  1470. }
  1471. ext := fileExtension(fileInfo.Name(), false, h.compressedFileSuffixes[fileEncoding])
  1472. contentType := mime.TypeByExtension(ext)
  1473. if contentType == "" {
  1474. data, err := readFileHeader(f, false, fileEncoding)
  1475. if err != nil {
  1476. return nil, fmt.Errorf("cannot read header of the file %q: %w", fileInfo.Name(), err)
  1477. }
  1478. contentType = http.DetectContentType(data)
  1479. }
  1480. dirIndex := w.B
  1481. lastModified := fileInfo.ModTime()
  1482. ff := &fsFile{
  1483. h: h,
  1484. dirIndex: dirIndex,
  1485. contentType: contentType,
  1486. contentLength: len(dirIndex),
  1487. compressed: true,
  1488. lastModified: lastModified,
  1489. lastModifiedStr: AppendHTTPDate(nil, lastModified),
  1490. t: time.Now(),
  1491. }
  1492. return ff, nil
  1493. }
  1494. func (h *fsHandler) newCompressedFSFile(filePath, fileEncoding string) (*fsFile, error) {
  1495. f, err := h.filesystem.Open(filePath)
  1496. if err != nil {
  1497. return nil, fmt.Errorf("cannot open compressed file %q: %w", filePath, err)
  1498. }
  1499. fileInfo, err := f.Stat()
  1500. if err != nil {
  1501. _ = f.Close()
  1502. return nil, fmt.Errorf("cannot obtain info for compressed file %q: %w", filePath, err)
  1503. }
  1504. return h.newFSFile(f, fileInfo, true, filePath, fileEncoding)
  1505. }
  1506. func (h *fsHandler) openFSFile(filePath string, mustCompress bool, fileEncoding string) (*fsFile, error) {
  1507. filePathOriginal := filePath
  1508. if mustCompress {
  1509. filePath += h.compressedFileSuffixes[fileEncoding]
  1510. }
  1511. f, err := h.filesystem.Open(filePath)
  1512. if err != nil {
  1513. if mustCompress && errors.Is(err, fs.ErrNotExist) {
  1514. return h.compressAndOpenFSFile(filePathOriginal, fileEncoding)
  1515. }
  1516. // If the file is not found and the path is empty, let's return errDirIndexRequired error.
  1517. if filePath == "" && (errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid)) {
  1518. return nil, errDirIndexRequired
  1519. }
  1520. return nil, err
  1521. }
  1522. fileInfo, err := f.Stat()
  1523. if err != nil {
  1524. _ = f.Close()
  1525. return nil, fmt.Errorf("cannot obtain info for file %q: %w", filePath, err)
  1526. }
  1527. if fileInfo.IsDir() {
  1528. _ = f.Close()
  1529. if mustCompress {
  1530. return nil, fmt.Errorf("directory with unexpected suffix found: %q. Suffix: %q",
  1531. filePath, h.compressedFileSuffixes[fileEncoding])
  1532. }
  1533. return nil, errDirIndexRequired
  1534. }
  1535. if mustCompress {
  1536. fileInfoOriginal, err := fs.Stat(h.filesystem, filePathOriginal)
  1537. if err != nil {
  1538. _ = f.Close()
  1539. return nil, fmt.Errorf("cannot obtain info for original file %q: %w", filePathOriginal, err)
  1540. }
  1541. // Only re-create the compressed file if there was more than a second between the mod times.
  1542. // On macOS the gzip seems to truncate the nanoseconds in the mod time causing the original file
  1543. // to look newer than the gzipped file.
  1544. if fileInfoOriginal.ModTime().Sub(fileInfo.ModTime()) >= time.Second {
  1545. // The compressed file became stale. Re-create it.
  1546. _ = f.Close()
  1547. _ = os.Remove(filePath)
  1548. return h.compressAndOpenFSFile(filePathOriginal, fileEncoding)
  1549. }
  1550. }
  1551. return h.newFSFile(f, fileInfo, mustCompress, filePath, fileEncoding)
  1552. }
  1553. func (h *fsHandler) newFSFile(f fs.File, fileInfo fs.FileInfo, compressed bool, filePath, fileEncoding string) (*fsFile, error) {
  1554. n := fileInfo.Size()
  1555. contentLength := int(n)
  1556. if n != int64(contentLength) {
  1557. _ = f.Close()
  1558. return nil, fmt.Errorf("too big file: %d bytes", n)
  1559. }
  1560. // detect content-type
  1561. ext := fileExtension(fileInfo.Name(), compressed, h.compressedFileSuffixes[fileEncoding])
  1562. contentType := mime.TypeByExtension(ext)
  1563. if contentType == "" {
  1564. data, err := readFileHeader(f, compressed, fileEncoding)
  1565. if err != nil {
  1566. return nil, fmt.Errorf("cannot read header of the file %q: %w", fileInfo.Name(), err)
  1567. }
  1568. contentType = http.DetectContentType(data)
  1569. }
  1570. lastModified := fileInfo.ModTime()
  1571. ff := &fsFile{
  1572. h: h,
  1573. f: f,
  1574. filename: filePath,
  1575. contentType: contentType,
  1576. contentLength: contentLength,
  1577. compressed: compressed,
  1578. lastModified: lastModified,
  1579. lastModifiedStr: AppendHTTPDate(nil, lastModified),
  1580. t: time.Now(),
  1581. }
  1582. return ff, nil
  1583. }
  1584. func readFileHeader(f io.Reader, compressed bool, fileEncoding string) ([]byte, error) {
  1585. r := f
  1586. var (
  1587. br *brotli.Reader
  1588. zr *gzip.Reader
  1589. zsr *zstd.Decoder
  1590. )
  1591. if compressed {
  1592. var err error
  1593. switch fileEncoding {
  1594. case "br":
  1595. if br, err = acquireBrotliReader(f); err != nil {
  1596. return nil, err
  1597. }
  1598. r = br
  1599. case "gzip":
  1600. if zr, err = acquireGzipReader(f); err != nil {
  1601. return nil, err
  1602. }
  1603. r = zr
  1604. case "zstd":
  1605. if zsr, err = acquireZstdReader(f); err != nil {
  1606. return nil, err
  1607. }
  1608. r = zsr
  1609. }
  1610. }
  1611. lr := &io.LimitedReader{
  1612. R: r,
  1613. N: 512,
  1614. }
  1615. data, err := io.ReadAll(lr)
  1616. seeker, ok := f.(io.Seeker)
  1617. if !ok {
  1618. return nil, errors.New("must implement io.Seeker")
  1619. }
  1620. if _, err := seeker.Seek(0, io.SeekStart); err != nil {
  1621. return nil, err
  1622. }
  1623. if br != nil {
  1624. releaseBrotliReader(br)
  1625. }
  1626. if zr != nil {
  1627. releaseGzipReader(zr)
  1628. }
  1629. if zsr != nil {
  1630. releaseZstdReader(zsr)
  1631. }
  1632. return data, err
  1633. }
  1634. func stripLeadingSlashes(path []byte, stripSlashes int) []byte {
  1635. for stripSlashes > 0 && len(path) > 0 {
  1636. if path[0] != '/' {
  1637. // developer sanity-check
  1638. panic("BUG: path must start with slash")
  1639. }
  1640. n := bytes.IndexByte(path[1:], '/')
  1641. if n < 0 {
  1642. path = path[:0]
  1643. break
  1644. }
  1645. path = path[n+1:]
  1646. stripSlashes--
  1647. }
  1648. return path
  1649. }
  1650. func hasDotDotPathSegment(path []byte) bool {
  1651. segmentStart := 0
  1652. for i := 0; i <= len(path); i++ {
  1653. isSeparator := i == len(path)
  1654. if i < len(path) {
  1655. isSeparator = path[i] == '/'
  1656. if filepath.Separator == '\\' && path[i] == '\\' {
  1657. isSeparator = true
  1658. }
  1659. }
  1660. if !isSeparator {
  1661. continue
  1662. }
  1663. if i-segmentStart == 2 && path[segmentStart] == '.' && path[segmentStart+1] == '.' {
  1664. return true
  1665. }
  1666. segmentStart = i + 1
  1667. }
  1668. return false
  1669. }
  1670. func fileExtension(path string, compressed bool, compressedFileSuffix string) string {
  1671. if compressed && strings.HasSuffix(path, compressedFileSuffix) {
  1672. path = path[:len(path)-len(compressedFileSuffix)]
  1673. }
  1674. n := strings.LastIndexByte(path, '.')
  1675. if n < 0 {
  1676. return ""
  1677. }
  1678. return path[n:]
  1679. }
  1680. // FileLastModified returns last modified time for the file.
  1681. func FileLastModified(path string) (time.Time, error) {
  1682. f, err := os.Open(path)
  1683. if err != nil {
  1684. return zeroTime, err
  1685. }
  1686. fileInfo, err := f.Stat()
  1687. _ = f.Close()
  1688. if err != nil {
  1689. return zeroTime, err
  1690. }
  1691. return fsModTime(fileInfo.ModTime()), nil
  1692. }
  1693. func fsModTime(t time.Time) time.Time {
  1694. return t.In(time.UTC).Truncate(time.Second)
  1695. }
  1696. var filesLockMap sync.Map
  1697. func getFileLock(absPath string) *sync.Mutex {
  1698. v, _ := filesLockMap.LoadOrStore(absPath, &sync.Mutex{})
  1699. filelock := v.(*sync.Mutex)
  1700. return filelock
  1701. }
  1702. var _ fs.FS = (*osFS)(nil)
  1703. type osFS struct{}
  1704. func (o *osFS) Open(name string) (fs.File, error) { return os.Open(name) }
  1705. func (o *osFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }