Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+200
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package vcstime provides a file system wrapper that gives embedded
|
||||
// files a meaningful modification time.
|
||||
//
|
||||
// embed.FS stores and serves no ModTime: files report a zero
|
||||
// [fs.FileInfo.ModTime], which disables HTTP caching semantics when
|
||||
// served with [net/http]: no Last-Modified header is sent and
|
||||
// If-Modified-Since requests are never answered with a 304. vcstime
|
||||
// derives a modification time from the vcs.time build setting recorded
|
||||
// by the go command (see "go version -m" or [debug.BuildInfo]),
|
||||
// enabling [http.FS] to provide working cache headers for clean builds.
|
||||
//
|
||||
// FS wraps an [fs.FS] (typically an [embed.FS]) and reports that time as
|
||||
// the modification time of every file that does not have a real
|
||||
// timestamp of its own. Usage:
|
||||
//
|
||||
// //go:embed internal/whatever/*.*
|
||||
// var content embed.FS
|
||||
//
|
||||
// mux.Handle("/", http.FileServer(http.FS(vcstime.FS(content))))
|
||||
//
|
||||
// vcs.time is the commit time of the revision that was built, not the
|
||||
// wall-clock build time: the two coincide for clean CI builds, but a
|
||||
// tree with local modifications yields an older commit time. Binaries
|
||||
// built without VCS stamping (for example with -buildvcs=false) have no
|
||||
// vcs.time, and FS then behaves exactly like the file system it wraps.
|
||||
package vcstime
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
vcsTimeOnce sync.Once
|
||||
vcsTimeVal time.Time
|
||||
)
|
||||
|
||||
// ModTime returns the modification time reported for timestamp-less files
|
||||
// by file systems created with FS: the vcs.time commit time of the
|
||||
// running binary's source revision, or the zero time if the binary
|
||||
// carries no VCS build stamp.
|
||||
func ModTime() time.Time {
|
||||
vcsTimeOnce.Do(func() {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if ok {
|
||||
vcsTimeVal = vcsTime(bi)
|
||||
}
|
||||
})
|
||||
return vcsTimeVal
|
||||
}
|
||||
|
||||
// vcsTime returns the vcs.time build setting from bi as a time, or the
|
||||
// zero time if it is absent or unparseable.
|
||||
func vcsTime(bi *debug.BuildInfo) time.Time {
|
||||
for _, s := range bi.Settings {
|
||||
if s.Key != "vcs.time" {
|
||||
continue
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, s.Value)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// FS returns an [fs.FS] that serves files from fsys, reporting [ModTime]
|
||||
// as the modification time of any file that has no real timestamp of its
|
||||
// own.
|
||||
//
|
||||
// It is intended for wrapping an [embed.FS], whose files always report a
|
||||
// zero ModTime. Files that already have a real timestamp (for example in
|
||||
// an [fstest.MapFS] or os-backed file system) are passed through
|
||||
// unchanged, so it is always safe to wrap.
|
||||
//
|
||||
// If the binary carries no vcs.time stamp, fsys itself is returned
|
||||
// unchanged.
|
||||
func FS(fsys fs.FS) fs.FS {
|
||||
return New(fsys, ModTime())
|
||||
}
|
||||
|
||||
// New is like FS, but with an explicit modification time to report for
|
||||
// files that have none of their own. If mod is the zero time, fsys is
|
||||
// returned unchanged.
|
||||
//
|
||||
// This exists mostly for tests; prefer FS in production code.
|
||||
func New(fsys fs.FS, mod time.Time) fs.FS {
|
||||
if fsys == nil || mod.IsZero() {
|
||||
return fsys
|
||||
}
|
||||
return &modFS{fsys: fsys, mod: mod}
|
||||
}
|
||||
|
||||
// modFS is an fs.FS wrapper that stamps timestamp-less files with a fixed
|
||||
// modification time.
|
||||
type modFS struct {
|
||||
fsys fs.FS
|
||||
mod time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
_ fs.ReadDirFS = (*modFS)(nil)
|
||||
_ fs.ReadFileFS = (*modFS)(nil)
|
||||
)
|
||||
|
||||
func (m *modFS) Open(name string) (fs.File, error) {
|
||||
f, err := m.fsys.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &modFile{File: f, name: name, mod: m.mod}, nil
|
||||
}
|
||||
|
||||
func (m *modFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
des, err := fs.ReadDir(m.fsys, name)
|
||||
return stampDirEntries(des, m.mod), err
|
||||
}
|
||||
|
||||
func (m *modFS) ReadFile(name string) ([]byte, error) {
|
||||
return fs.ReadFile(m.fsys, name)
|
||||
}
|
||||
|
||||
// modFile is an fs.File wrapper that reports a stamped modification time
|
||||
// from Stat. Read, Close, Seek and ReadDir are forwarded to the
|
||||
// underlying file when it supports them, so that http.FS and other
|
||||
// consumers keep working (http.FS type-asserts files for io.Seeker and
|
||||
// fs.ReadDirFile).
|
||||
type modFile struct {
|
||||
fs.File
|
||||
name string
|
||||
mod time.Time
|
||||
}
|
||||
|
||||
var _ fs.ReadDirFile = (*modFile)(nil)
|
||||
|
||||
func (f *modFile) Stat() (fs.FileInfo, error) {
|
||||
info, err := f.File.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stampFileInfo(info, f.mod), nil
|
||||
}
|
||||
|
||||
func (f *modFile) Seek(offset int64, whence int) (int64, error) {
|
||||
if s, ok := f.File.(io.Seeker); ok {
|
||||
return s.Seek(offset, whence)
|
||||
}
|
||||
return 0, &fs.PathError{Op: "seek", Path: f.name, Err: fs.ErrInvalid}
|
||||
}
|
||||
|
||||
func (f *modFile) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
rdf, ok := f.File.(fs.ReadDirFile)
|
||||
if !ok {
|
||||
return nil, &fs.PathError{Op: "readdir", Path: f.name, Err: fs.ErrInvalid}
|
||||
}
|
||||
des, err := rdf.ReadDir(n)
|
||||
return stampDirEntries(des, f.mod), err
|
||||
}
|
||||
|
||||
// stampFileInfo returns info unchanged if it already has a modification
|
||||
// time or there is nothing to stamp it with, and otherwise a FileInfo
|
||||
// that reports mod instead.
|
||||
func stampFileInfo(info fs.FileInfo, mod time.Time) fs.FileInfo {
|
||||
if mod.IsZero() || !info.ModTime().IsZero() {
|
||||
return info
|
||||
}
|
||||
return modFileInfo{FileInfo: info, mod: mod}
|
||||
}
|
||||
|
||||
func stampDirEntries(des []fs.DirEntry, mod time.Time) []fs.DirEntry {
|
||||
if mod.IsZero() {
|
||||
return des
|
||||
}
|
||||
for i, de := range des {
|
||||
info, err := de.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().IsZero() {
|
||||
des[i] = fs.FileInfoToDirEntry(modFileInfo{FileInfo: info, mod: mod})
|
||||
}
|
||||
}
|
||||
return des
|
||||
}
|
||||
|
||||
// modFileInfo is an fs.FileInfo that reports mod as its modification
|
||||
// time, delegating everything else.
|
||||
type modFileInfo struct {
|
||||
fs.FileInfo
|
||||
mod time.Time
|
||||
}
|
||||
|
||||
func (i modFileInfo) ModTime() time.Time { return i.mod }
|
||||
Reference in new issue
Block a user