mirror of
https://github.com/go-gitea/gitea.git
synced 2025-06-17 12:58:45 +03:00

Benefits: 1. smaller binary size (reduces more than 1MB) 2. better control of the assets details 3. fewer unmaintained dependencies 4. faster startup if the assets are not needed 5. won't hang up editors when open "bindata.go" by accident
376 lines
8.7 KiB
Go
376 lines
8.7 KiB
Go
// 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
|
|
}
|