mirror of
https://github.com/golang/go
synced 2024-09-29 08:24:36 -06:00
embed: implement FS
embed.FS is the implementation of embedded file trees, providing an fs.FS for each embed.FS variable. Tests are in a follow-up CL, in the package embed/internal/embedtest. (They can only be written once the toolchain can initialize one of these, which requires changes to cmd/compile and cmd/go.) For #41191. Change-Id: Ieb0ead1d305cdac3d5d4e11772dca75740a72730 Reviewed-on: https://go-review.googlesource.com/c/go/+/243942 Trust: Russ Cox <rsc@golang.org> Trust: Jay Conrod <jayconrod@google.com> Run-TryBot: Russ Cox <rsc@golang.org> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Rob Pike <r@golang.org> Reviewed-by: Jay Conrod <jayconrod@google.com>
This commit is contained in:
parent
67bce7c1cf
commit
400581b8b0
403
src/embed/embed.go
Normal file
403
src/embed/embed.go
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
// Copyright 2020 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package embed provides access to files embedded in the running Go program.
|
||||||
|
//
|
||||||
|
// Go source files that import "embed" can use the //go:embed directive
|
||||||
|
// to initialize a variable of type string, []byte, or FS with the contents of
|
||||||
|
// files read from the package directory or subdirectories at compile time.
|
||||||
|
//
|
||||||
|
// For example, here are three ways to embed a file named hello.txt
|
||||||
|
// and then print its contents at run time:
|
||||||
|
//
|
||||||
|
// import "embed"
|
||||||
|
//
|
||||||
|
// //go:embed hello.txt
|
||||||
|
// var s string
|
||||||
|
// print(s)
|
||||||
|
//
|
||||||
|
// //go:embed hello.txt
|
||||||
|
// var b []byte
|
||||||
|
// print(string(b))
|
||||||
|
//
|
||||||
|
// //go:embed hello.txt
|
||||||
|
// var f embed.FS
|
||||||
|
// data, _ := f.ReadFile("hello.txt")
|
||||||
|
// print(string(data))
|
||||||
|
//
|
||||||
|
// Directives
|
||||||
|
//
|
||||||
|
// A //go:embed directive above a variable declaration specifies which files to embed,
|
||||||
|
// using one or more path.Match patterns.
|
||||||
|
//
|
||||||
|
// The directive must immediately precede a line containing the declaration of a single variable.
|
||||||
|
// Only blank lines and ‘//’ line comments are permitted between the directive and the declaration.
|
||||||
|
//
|
||||||
|
// The variable must be of type string, []byte, or FS exactly. Named types or type aliases
|
||||||
|
// derived from those types are not allowed.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
//
|
||||||
|
// package server
|
||||||
|
//
|
||||||
|
// import "embed"
|
||||||
|
//
|
||||||
|
// // content holds our static web server content.
|
||||||
|
// //go:embed image/* template/*
|
||||||
|
// //go:embed html/index.html
|
||||||
|
// var content embed.FS
|
||||||
|
//
|
||||||
|
// The Go build system will recognize the directives and arrange for the declared variable
|
||||||
|
// (in the example above, content) to be populated with the matching files from the file system.
|
||||||
|
//
|
||||||
|
// The //go:embed directive accepts multiple space-separated patterns for brevity,
|
||||||
|
// but it can also be repeated, to avoid very long lines when there are many patterns.
|
||||||
|
// The patterns are interpreted relative to the package directory containing the source file.
|
||||||
|
// The path separator is a forward slash, even on Windows systems.
|
||||||
|
// To allow for naming files with spaces in their names, patterns can be written
|
||||||
|
// as Go double-quoted or back-quoted string literals.
|
||||||
|
//
|
||||||
|
// If a pattern names a directory, all files in the subtree rooted at that directory are
|
||||||
|
// embedded (recursively), so the variable in the above example is equivalent to:
|
||||||
|
//
|
||||||
|
// // content is our static web server content.
|
||||||
|
// //go:embed image template html/index.html
|
||||||
|
// var content embed.FS
|
||||||
|
//
|
||||||
|
// The //go:embed directive can be used with both exported and unexported variables,
|
||||||
|
// depending on whether the package wants to make the data available to other packages.
|
||||||
|
// Similarly, it can be used with both global and function-local variables,
|
||||||
|
// depending on what is more convenient in context.
|
||||||
|
//
|
||||||
|
// Patterns must not match files outside the package's module, such as ‘.git/*’ or symbolic links.
|
||||||
|
// Matches for empty directories are ignored. After that, each pattern in a //go:embed line
|
||||||
|
// must match at least one file or non-empty directory.
|
||||||
|
//
|
||||||
|
// Patterns must not contain ‘.’ or ‘..’ path elements nor begin with a leading slash.
|
||||||
|
// To match everything in the current directory, use ‘*’ instead of ‘.’.
|
||||||
|
//
|
||||||
|
// If any patterns are invalid or have invalid matches, the build will fail.
|
||||||
|
//
|
||||||
|
// Strings and Bytes
|
||||||
|
//
|
||||||
|
// The //go:embed line for a variable of type string or []byte can have only a single pattern,
|
||||||
|
// and that pattern can match only a single file. The string or []byte is initialized with
|
||||||
|
// the contents of that file.
|
||||||
|
//
|
||||||
|
// The //go:embed directive requires importing "embed", even when using a string or []byte.
|
||||||
|
// In source files that don't refer to embed.FS, use a blank import (import _ "embed").
|
||||||
|
//
|
||||||
|
// File Systems
|
||||||
|
//
|
||||||
|
// For embedding a single file, a variable of type string or []byte is often best.
|
||||||
|
// The FS type enables embedding a tree of files, such as a directory of static
|
||||||
|
// web server content, as in the example above.
|
||||||
|
//
|
||||||
|
// FS implements the io/fs package's FS interface, so it can be used with any package that
|
||||||
|
// understands file systems, including net/http, text/template, and html/template.
|
||||||
|
//
|
||||||
|
// For example, given the content variable in the example above, we can write:
|
||||||
|
//
|
||||||
|
// http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(content))))
|
||||||
|
//
|
||||||
|
// template.ParseFS(content, "*.tmpl")
|
||||||
|
//
|
||||||
|
// Tools
|
||||||
|
//
|
||||||
|
// To support tools that analyze Go packages, the patterns found in //go:embed lines
|
||||||
|
// are available in “go list” output. See the EmbedPatterns, TestEmbedPatterns,
|
||||||
|
// and XTestEmbedPatterns fields in the “go help list” output.
|
||||||
|
//
|
||||||
|
package embed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An FS is a read-only collection of files, usually initialized with a //go:embed directive.
|
||||||
|
// When declared without a //go:embed directive, an FS is an empty file system.
|
||||||
|
//
|
||||||
|
// An FS is a read-only value, so it is safe to use from multiple goroutines
|
||||||
|
// simultaneously and also safe to assign values of type FS to each other.
|
||||||
|
//
|
||||||
|
// FS implements fs.FS, so it can be used with any package that understands
|
||||||
|
// file system interfaces, including net/http, text/template, and html/template.
|
||||||
|
//
|
||||||
|
// See the package documentation for more details about initializing an FS.
|
||||||
|
type FS struct {
|
||||||
|
// The compiler knows the layout of this struct.
|
||||||
|
// See cmd/compile/internal/gc's initEmbed.
|
||||||
|
//
|
||||||
|
// The files list is sorted by name but not by simple string comparison.
|
||||||
|
// Instead, each file's name takes the form "dir/elem" or "dir/elem/".
|
||||||
|
// The optional trailing slash indicates that the file is itself a directory.
|
||||||
|
// The files list is sorted first by dir (if dir is missing, it is taken to be ".")
|
||||||
|
// and then by base, so this list of files:
|
||||||
|
//
|
||||||
|
// p
|
||||||
|
// q/
|
||||||
|
// q/r
|
||||||
|
// q/s/
|
||||||
|
// q/s/t
|
||||||
|
// q/s/u
|
||||||
|
// q/v
|
||||||
|
// w
|
||||||
|
//
|
||||||
|
// is actually sorted as:
|
||||||
|
//
|
||||||
|
// p # dir=. elem=p
|
||||||
|
// q/ # dir=. elem=q
|
||||||
|
// w/ # dir=. elem=w
|
||||||
|
// q/r # dir=q elem=r
|
||||||
|
// q/s/ # dir=q elem=s
|
||||||
|
// q/v # dir=q elem=v
|
||||||
|
// q/s/t # dir=q/s elem=t
|
||||||
|
// q/s/u # dir=q/s elem=u
|
||||||
|
//
|
||||||
|
// This order brings directory contents together in contiguous sections
|
||||||
|
// of the list, allowing a directory read to use binary search to find
|
||||||
|
// the relevant sequence of entries.
|
||||||
|
files *[]file
|
||||||
|
}
|
||||||
|
|
||||||
|
// split splits the name into dir and elem as described in the
|
||||||
|
// comment in the FS struct above. isDir reports whether the
|
||||||
|
// final trailing slash was present, indicating that name is a directory.
|
||||||
|
func split(name string) (dir, elem string, isDir bool) {
|
||||||
|
if name[len(name)-1] == '/' {
|
||||||
|
isDir = true
|
||||||
|
name = name[:len(name)-1]
|
||||||
|
}
|
||||||
|
i := len(name) - 1
|
||||||
|
for i >= 0 && name[i] != '/' {
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
if i < 0 {
|
||||||
|
return ".", name, isDir
|
||||||
|
}
|
||||||
|
return name[:i], name[i+1:], isDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// trimSlash trims a trailing slash from name, if present,
|
||||||
|
// returning the possibly shortened name.
|
||||||
|
func trimSlash(name string) string {
|
||||||
|
if len(name) > 0 && name[len(name)-1] == '/' {
|
||||||
|
return name[:len(name)-1]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ fs.ReadDirFS = FS{}
|
||||||
|
_ fs.ReadFileFS = FS{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// A file is a single file in the FS.
|
||||||
|
// It implements fs.FileInfo and fs.DirEntry.
|
||||||
|
type file struct {
|
||||||
|
// The compiler knows the layout of this struct.
|
||||||
|
// See cmd/compile/internal/gc's initEmbed.
|
||||||
|
name string
|
||||||
|
data string
|
||||||
|
hash [16]byte // truncated SHA256 hash
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ fs.FileInfo = (*file)(nil)
|
||||||
|
_ fs.DirEntry = (*file)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *file) Name() string { _, elem, _ := split(f.name); return elem }
|
||||||
|
func (f *file) Size() int64 { return int64(len(f.data)) }
|
||||||
|
func (f *file) ModTime() time.Time { return time.Time{} }
|
||||||
|
func (f *file) IsDir() bool { _, _, isDir := split(f.name); return isDir }
|
||||||
|
func (f *file) Sys() interface{} { return nil }
|
||||||
|
func (f *file) Type() fs.FileMode { return f.Mode().Type() }
|
||||||
|
func (f *file) Info() (fs.FileInfo, error) { return f, nil }
|
||||||
|
|
||||||
|
func (f *file) Mode() fs.FileMode {
|
||||||
|
if f.IsDir() {
|
||||||
|
return fs.ModeDir | 0555
|
||||||
|
}
|
||||||
|
return 0444
|
||||||
|
}
|
||||||
|
|
||||||
|
// dotFile is a file for the root directory,
|
||||||
|
// which is omitted from the files list in a FS.
|
||||||
|
var dotFile = &file{name: "./"}
|
||||||
|
|
||||||
|
// lookup returns the named file, or nil if it is not present.
|
||||||
|
func (f FS) lookup(name string) *file {
|
||||||
|
if !fs.ValidPath(name) {
|
||||||
|
// The compiler should never emit a file with an invalid name,
|
||||||
|
// so this check is not strictly necessary (if name is invalid,
|
||||||
|
// we shouldn't find a match below), but it's a good backstop anyway.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if name == "." {
|
||||||
|
return dotFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary search to find where name would be in the list,
|
||||||
|
// and then check if name is at that position.
|
||||||
|
dir, elem, _ := split(name)
|
||||||
|
files := *f.files
|
||||||
|
i := sortSearch(len(files), func(i int) bool {
|
||||||
|
idir, ielem, _ := split(files[i].name)
|
||||||
|
return idir > dir || idir == dir && ielem >= elem
|
||||||
|
})
|
||||||
|
if i < len(files) && trimSlash(files[i].name) == name {
|
||||||
|
return &files[i]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readDir returns the list of files corresponding to the directory dir.
|
||||||
|
func (f FS) readDir(dir string) []file {
|
||||||
|
// Binary search to find where dir starts and ends in the list
|
||||||
|
// and then return that slice of the list.
|
||||||
|
files := *f.files
|
||||||
|
i := sortSearch(len(files), func(i int) bool {
|
||||||
|
idir, _, _ := split(files[i].name)
|
||||||
|
return idir >= dir
|
||||||
|
})
|
||||||
|
j := sortSearch(len(files), func(j int) bool {
|
||||||
|
jdir, _, _ := split(files[j].name)
|
||||||
|
return jdir > dir
|
||||||
|
})
|
||||||
|
return files[i:j]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens the named file for reading and returns it as an fs.File.
|
||||||
|
func (f FS) Open(name string) (fs.File, error) {
|
||||||
|
file := f.lookup(name)
|
||||||
|
if file == nil {
|
||||||
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||||
|
}
|
||||||
|
if file.IsDir() {
|
||||||
|
return &openDir{file, f.readDir(name), 0}, nil
|
||||||
|
}
|
||||||
|
return &openFile{file, 0}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDir reads and returns the entire named directory.
|
||||||
|
func (f FS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||||
|
file, err := f.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dir, ok := file.(*openDir)
|
||||||
|
if !ok {
|
||||||
|
return nil, &fs.PathError{Op: "read", Path: name, Err: errors.New("not a directory")}
|
||||||
|
}
|
||||||
|
list := make([]fs.DirEntry, len(dir.files))
|
||||||
|
for i := range list {
|
||||||
|
list[i] = &dir.files[i]
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFile reads and returns the content of the named file.
|
||||||
|
func (f FS) ReadFile(name string) ([]byte, error) {
|
||||||
|
file, err := f.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ofile, ok := file.(*openFile)
|
||||||
|
if !ok {
|
||||||
|
return nil, &fs.PathError{Op: "read", Path: name, Err: errors.New("is a directory")}
|
||||||
|
}
|
||||||
|
return []byte(ofile.f.data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// An openFile is a regular file open for reading.
|
||||||
|
type openFile struct {
|
||||||
|
f *file // the file itself
|
||||||
|
offset int64 // current read offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *openFile) Close() error { return nil }
|
||||||
|
func (f *openFile) Stat() (fs.FileInfo, error) { return f.f, nil }
|
||||||
|
|
||||||
|
func (f *openFile) Read(b []byte) (int, error) {
|
||||||
|
if f.offset >= int64(len(f.f.data)) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
if f.offset < 0 {
|
||||||
|
return 0, &fs.PathError{Op: "read", Path: f.f.name, Err: fs.ErrInvalid}
|
||||||
|
}
|
||||||
|
n := copy(b, f.f.data[f.offset:])
|
||||||
|
f.offset += int64(n)
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *openFile) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
switch whence {
|
||||||
|
case 0:
|
||||||
|
// offset += 0
|
||||||
|
case 1:
|
||||||
|
offset += f.offset
|
||||||
|
case 2:
|
||||||
|
offset += int64(len(f.f.data))
|
||||||
|
}
|
||||||
|
if offset < 0 || offset > int64(len(f.f.data)) {
|
||||||
|
return 0, &fs.PathError{Op: "seek", Path: f.f.name, Err: fs.ErrInvalid}
|
||||||
|
}
|
||||||
|
f.offset = offset
|
||||||
|
return offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// An openDir is a directory open for reading.
|
||||||
|
type openDir struct {
|
||||||
|
f *file // the directory file itself
|
||||||
|
files []file // the directory contents
|
||||||
|
offset int // the read offset, an index into the files slice
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *openDir) Close() error { return nil }
|
||||||
|
func (d *openDir) Stat() (fs.FileInfo, error) { return d.f, nil }
|
||||||
|
|
||||||
|
func (d *openDir) Read([]byte) (int, error) {
|
||||||
|
return 0, &fs.PathError{Op: "read", Path: d.f.name, Err: errors.New("is a directory")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) {
|
||||||
|
n := len(d.files) - d.offset
|
||||||
|
if count > 0 && n > count {
|
||||||
|
n = count
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
if count <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
list := make([]fs.DirEntry, n)
|
||||||
|
for i := range list {
|
||||||
|
list[i] = &d.files[d.offset+i]
|
||||||
|
}
|
||||||
|
d.offset += n
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortSearch is like sort.Search, avoiding an import.
|
||||||
|
func sortSearch(n int, f func(int) bool) int {
|
||||||
|
// Define f(-1) == false and f(n) == true.
|
||||||
|
// Invariant: f(i-1) == false, f(j) == true.
|
||||||
|
i, j := 0, n
|
||||||
|
for i < j {
|
||||||
|
h := int(uint(i+j) >> 1) // avoid overflow when computing h
|
||||||
|
// i ≤ h < j
|
||||||
|
if !f(h) {
|
||||||
|
i = h + 1 // preserves f(i-1) == false
|
||||||
|
} else {
|
||||||
|
j = h // preserves f(j) == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// i == j, f(i-1) == false, and f(j) (= f(i)) == true => answer is i.
|
||||||
|
return i
|
||||||
|
}
|
@ -236,23 +236,23 @@ var readEmbedTests = []struct {
|
|||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package p\nimport \"embed\"\nvar i int\n//go:embed x y z\nvar files embed.Files",
|
"package p\nimport \"embed\"\nvar i int\n//go:embed x y z\nvar files embed.FS",
|
||||||
[]string{"x", "y", "z"},
|
[]string{"x", "y", "z"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package p\nimport \"embed\"\nvar i int\n//go:embed x \"\\x79\" `z`\nvar files embed.Files",
|
"package p\nimport \"embed\"\nvar i int\n//go:embed x \"\\x79\" `z`\nvar files embed.FS",
|
||||||
[]string{"x", "y", "z"},
|
[]string{"x", "y", "z"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package p\nimport \"embed\"\nvar i int\n//go:embed x y\n//go:embed z\nvar files embed.Files",
|
"package p\nimport \"embed\"\nvar i int\n//go:embed x y\n//go:embed z\nvar files embed.FS",
|
||||||
[]string{"x", "y", "z"},
|
[]string{"x", "y", "z"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package p\nimport \"embed\"\nvar i int\n\t //go:embed x y\n\t //go:embed z\n\t var files embed.Files",
|
"package p\nimport \"embed\"\nvar i int\n\t //go:embed x y\n\t //go:embed z\n\t var files embed.FS",
|
||||||
[]string{"x", "y", "z"},
|
[]string{"x", "y", "z"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package p\nimport \"embed\"\n//go:embed x y z\nvar files embed.Files",
|
"package p\nimport \"embed\"\n//go:embed x y z\nvar files embed.FS",
|
||||||
[]string{"x", "y", "z"},
|
[]string{"x", "y", "z"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -260,7 +260,7 @@ var readEmbedTests = []struct {
|
|||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package p\n//go:embed x y z\nvar files embed.Files", // no import, no scan
|
"package p\n//go:embed x y z\nvar files embed.FS", // no import, no scan
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user