| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- // Copyright 2025 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package assetfs
-
- import (
- "bytes"
- "compress/gzip"
- "io"
- "io/fs"
- "os"
- "path"
- "path/filepath"
- "strings"
- "sync"
- "time"
-
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/util"
- )
-
- type EmbeddedFile interface {
- io.ReadSeeker
- fs.ReadDirFile
- ReadDir(n int) ([]fs.DirEntry, error)
- }
-
- type EmbeddedFileInfo interface {
- fs.FileInfo
- fs.DirEntry
- GetGzipContent() ([]byte, bool)
- }
-
- type decompressor interface {
- io.Reader
- Close() error
- Reset(io.Reader) error
- }
-
- type embeddedFileInfo struct {
- fs *embeddedFS
- fullName string
- data []byte
-
- BaseName string `json:"n"`
- OriginSize int64 `json:"s,omitempty"`
- DataBegin int64 `json:"b,omitempty"`
- DataLen int64 `json:"l,omitempty"`
- Children []*embeddedFileInfo `json:"c,omitempty"`
- }
-
- func (fi *embeddedFileInfo) GetGzipContent() ([]byte, bool) {
- // when generating the bindata, if the compressed data equals or is larger than the original data, we store the original data
- if fi.DataLen == fi.OriginSize {
- return nil, false
- }
- return fi.data, true
- }
-
- type EmbeddedFileBase struct {
- info *embeddedFileInfo
- dataReader io.ReadSeeker
- seekPos int64
- }
-
- func (f *EmbeddedFileBase) ReadDir(n int) ([]fs.DirEntry, error) {
- // this method is used to satisfy the "func (f ioFile) ReadDir(...)" in httpfs
- l, err := f.info.fs.ReadDir(f.info.fullName)
- if err != nil {
- return nil, err
- }
- if n < 0 || n > len(l) {
- return l, nil
- }
- return l[:n], nil
- }
-
- type EmbeddedOriginFile struct {
- EmbeddedFileBase
- }
-
- type EmbeddedCompressedFile struct {
- EmbeddedFileBase
- decompressor decompressor
- decompressorPos int64
- }
-
- type embeddedFS struct {
- meta func() *EmbeddedMeta
-
- files map[string]*embeddedFileInfo
- filesMu sync.RWMutex
-
- data []byte
- }
-
- type EmbeddedMeta struct {
- Root *embeddedFileInfo
- }
-
- func NewEmbeddedFS(data []byte) fs.ReadDirFS {
- efs := &embeddedFS{data: data, files: make(map[string]*embeddedFileInfo)}
- efs.meta = sync.OnceValue(func() *EmbeddedMeta {
- var meta EmbeddedMeta
- p := bytes.LastIndexByte(data, '\n')
- if p < 0 {
- return &meta
- }
- if err := json.Unmarshal(data[p+1:], &meta); err != nil {
- panic("embedded file is not valid")
- }
- return &meta
- })
- return efs
- }
-
- var _ fs.ReadDirFS = (*embeddedFS)(nil)
-
- func (e *embeddedFS) ReadDir(name string) (l []fs.DirEntry, err error) {
- fi, err := e.getFileInfo(name)
- if err != nil {
- return nil, err
- }
- if !fi.IsDir() {
- return nil, fs.ErrNotExist
- }
- l = make([]fs.DirEntry, len(fi.Children))
- for i, child := range fi.Children {
- l[i], err = e.getFileInfo(name + "/" + child.BaseName)
- if err != nil {
- return nil, err
- }
- }
- return l, nil
- }
-
- func (e *embeddedFS) getFileInfo(fullName string) (*embeddedFileInfo, error) {
- // no need to do heavy "path.Clean()" because we don't want to support "foo/../bar" or absolute paths
- fullName = strings.TrimPrefix(fullName, "./")
- if fullName == "" {
- fullName = "."
- }
-
- e.filesMu.RLock()
- fi := e.files[fullName]
- e.filesMu.RUnlock()
- if fi != nil {
- return fi, nil
- }
-
- fields := strings.Split(fullName, "/")
- fi = e.meta().Root
- if fullName != "." {
- found := true
- for _, field := range fields {
- for _, child := range fi.Children {
- if found = child.BaseName == field; found {
- fi = child
- break
- }
- }
- if !found {
- return nil, fs.ErrNotExist
- }
- }
- }
-
- e.filesMu.Lock()
- defer e.filesMu.Unlock()
- if fi != nil {
- fi.fs = e
- fi.fullName = fullName
- fi.data = e.data[fi.DataBegin : fi.DataBegin+fi.DataLen]
- e.files[fullName] = fi // do not cache nil, otherwise keeping accessing random non-existing file will cause OOM
- return fi, nil
- }
- return nil, fs.ErrNotExist
- }
-
- func (e *embeddedFS) Open(name string) (fs.File, error) {
- info, err := e.getFileInfo(name)
- if err != nil {
- return nil, err
- }
- base := EmbeddedFileBase{info: info}
- base.dataReader = bytes.NewReader(base.info.data)
- if info.DataLen != info.OriginSize {
- decomp, err := gzip.NewReader(base.dataReader)
- if err != nil {
- return nil, err
- }
- return &EmbeddedCompressedFile{EmbeddedFileBase: base, decompressor: decomp}, nil
- }
- return &EmbeddedOriginFile{base}, nil
- }
-
- var (
- _ EmbeddedFileInfo = (*embeddedFileInfo)(nil)
- _ EmbeddedFile = (*EmbeddedOriginFile)(nil)
- _ EmbeddedFile = (*EmbeddedCompressedFile)(nil)
- )
-
- func (f *EmbeddedOriginFile) Read(p []byte) (n int, err error) {
- return f.dataReader.Read(p)
- }
-
- func (f *EmbeddedCompressedFile) Read(p []byte) (n int, err error) {
- if f.decompressorPos > f.seekPos {
- if err = f.decompressor.Reset(bytes.NewReader(f.info.data)); err != nil {
- return 0, err
- }
- f.decompressorPos = 0
- }
- if f.decompressorPos < f.seekPos {
- if _, err = io.CopyN(io.Discard, f.decompressor, f.seekPos-f.decompressorPos); err != nil {
- return 0, err
- }
- f.decompressorPos = f.seekPos
- }
- n, err = f.decompressor.Read(p)
- f.decompressorPos += int64(n)
- f.seekPos = f.decompressorPos
- return n, err
- }
-
- func (f *EmbeddedFileBase) Seek(offset int64, whence int) (int64, error) {
- switch whence {
- case io.SeekStart:
- f.seekPos = offset
- case io.SeekCurrent:
- f.seekPos += offset
- case io.SeekEnd:
- f.seekPos = f.info.OriginSize + offset
- }
- return f.seekPos, nil
- }
-
- func (f *EmbeddedFileBase) Stat() (fs.FileInfo, error) {
- return f.info, nil
- }
-
- func (f *EmbeddedOriginFile) Close() error {
- return nil
- }
-
- func (f *EmbeddedCompressedFile) Close() error {
- return f.decompressor.Close()
- }
-
- func (fi *embeddedFileInfo) Name() string {
- return fi.BaseName
- }
-
- func (fi *embeddedFileInfo) Size() int64 {
- return fi.OriginSize
- }
-
- func (fi *embeddedFileInfo) Mode() fs.FileMode {
- return util.Iif(fi.IsDir(), fs.ModeDir|0o555, 0o444)
- }
-
- func (fi *embeddedFileInfo) ModTime() time.Time {
- return getExecutableModTime()
- }
-
- func (fi *embeddedFileInfo) IsDir() bool {
- return fi.Children != nil
- }
-
- func (fi *embeddedFileInfo) Sys() any {
- return nil
- }
-
- func (fi *embeddedFileInfo) Type() fs.FileMode {
- return util.Iif(fi.IsDir(), fs.ModeDir, 0)
- }
-
- func (fi *embeddedFileInfo) Info() (fs.FileInfo, error) {
- return fi, nil
- }
-
- // getExecutableModTime returns the modification time of the executable file.
- // In bindata, we can't use the ModTime of the files because we need to make the build reproducible
- var getExecutableModTime = sync.OnceValue(func() (modTime time.Time) {
- exePath, err := os.Executable()
- if err != nil {
- return modTime
- }
- exePath, err = filepath.Abs(exePath)
- if err != nil {
- return modTime
- }
- exePath, err = filepath.EvalSymlinks(exePath)
- if err != nil {
- return modTime
- }
- st, err := os.Stat(exePath)
- if err != nil {
- return modTime
- }
- return st.ModTime()
- })
-
- func GenerateEmbedBindata(fsRootPath, outputFile string) error {
- output, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
- if err != nil {
- return err
- }
- defer output.Close()
-
- meta := &EmbeddedMeta{}
- meta.Root = &embeddedFileInfo{}
- var outputOffset int64
- var embedFiles func(parent *embeddedFileInfo, fsPath, embedPath string) error
- embedFiles = func(parent *embeddedFileInfo, fsPath, embedPath string) error {
- dirEntries, err := os.ReadDir(fsPath)
- if err != nil {
- return err
- }
- for _, dirEntry := range dirEntries {
- if err != nil {
- return err
- }
- if dirEntry.IsDir() {
- child := &embeddedFileInfo{
- BaseName: dirEntry.Name(),
- Children: []*embeddedFileInfo{}, // non-nil means it's a directory
- }
- parent.Children = append(parent.Children, child)
- if err = embedFiles(child, filepath.Join(fsPath, dirEntry.Name()), path.Join(embedPath, dirEntry.Name())); err != nil {
- return err
- }
- } else {
- data, err := os.ReadFile(filepath.Join(fsPath, dirEntry.Name()))
- if err != nil {
- return err
- }
- var compressed bytes.Buffer
- gz, _ := gzip.NewWriterLevel(&compressed, gzip.BestCompression)
- if _, err = gz.Write(data); err != nil {
- return err
- }
- if err = gz.Close(); err != nil {
- return err
- }
-
- // only use the compressed data if it is smaller than the original data
- outputBytes := util.Iif(len(compressed.Bytes()) < len(data), compressed.Bytes(), data)
- child := &embeddedFileInfo{
- BaseName: dirEntry.Name(),
- OriginSize: int64(len(data)),
- DataBegin: outputOffset,
- DataLen: int64(len(outputBytes)),
- }
- if _, err = output.Write(outputBytes); err != nil {
- return err
- }
- outputOffset += child.DataLen
- parent.Children = append(parent.Children, child)
- }
- }
- return nil
- }
-
- if err = embedFiles(meta.Root, fsRootPath, ""); err != nil {
- return err
- }
- jsonBuf, err := json.Marshal(meta) // can't use json.NewEncoder here because it writes extra EOL
- if err != nil {
- return err
- }
- _, _ = output.Write([]byte{'\n'})
- _, err = output.Write(jsonBuf)
- return err
- }
|