summaryrefslogtreecommitdiffstats
path: root/modules/assetfs/layered.go
blob: 9678d23ad675fc3a03bb5cea6799a11de85f57a7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package assetfs

import (
	"context"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"time"

	"code.gitea.io/gitea/modules/container"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/process"
	"code.gitea.io/gitea/modules/util"

	"github.com/fsnotify/fsnotify"
)

// Layer represents a layer in a layered asset file-system. It has a name and works like http.FileSystem
type Layer struct {
	name      string
	fs        http.FileSystem
	localPath string
}

func (l *Layer) Name() string {
	return l.name
}

// Open opens the named file. The caller is responsible for closing the file.
func (l *Layer) Open(name string) (http.File, error) {
	return l.fs.Open(name)
}

// Local returns a new Layer with the given name, it serves files from the given local path.
func Local(name, base string, sub ...string) *Layer {
	// TODO: the old behavior (StaticRootPath might not be absolute), not ideal, just keep the same as before
	// Ideally, the caller should guarantee the base is absolute, guessing a relative path based on the current working directory is unreliable.
	base, err := filepath.Abs(base)
	if err != nil {
		// This should never happen in a real system. If it happens, the user must have already been in trouble: the system is not able to resolve its own paths.
		panic(fmt.Sprintf("Unable to get absolute path for %q: %v", base, err))
	}
	root := util.FilePathJoinAbs(base, sub...)
	return &Layer{name: name, fs: http.Dir(root), localPath: root}
}

// Bindata returns a new Layer with the given name, it serves files from the given bindata asset.
func Bindata(name string, fs http.FileSystem) *Layer {
	return &Layer{name: name, fs: fs}
}

// LayeredFS is a layered asset file-system. It works like http.FileSystem, but it can have multiple layers.
// The first layer is the top layer, and it will be used first.
// If the file is not found in the top layer, it will be searched in the next layer.
type LayeredFS struct {
	layers []*Layer
}

// Layered returns a new LayeredFS with the given layers. The first layer is the top layer.
func Layered(layers ...*Layer) *LayeredFS {
	return &LayeredFS{layers: layers}
}

// Open opens the named file. The caller is responsible for closing the file.
func (l *LayeredFS) Open(name string) (http.File, error) {
	for _, layer := range l.layers {
		f, err := layer.Open(name)
		if err == nil || !os.IsNotExist(err) {
			return f, err
		}
	}
	return nil, fs.ErrNotExist
}

// ReadFile reads the named file.
func (l *LayeredFS) ReadFile(elems ...string) ([]byte, error) {
	bs, _, err := l.ReadLayeredFile(elems...)
	return bs, err
}

// ReadLayeredFile reads the named file, and returns the layer name.
func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) {
	name := util.PathJoinRel(elems...)
	for _, layer := range l.layers {
		f, err := layer.Open(name)
		if os.IsNotExist(err) {
			continue
		} else if err != nil {
			return nil, layer.name, err
		}
		bs, err := io.ReadAll(f)
		_ = f.Close()
		return bs, layer.name, err
	}
	return nil, "", fs.ErrNotExist
}

func shouldInclude(info fs.FileInfo, fileMode ...bool) bool {
	if util.CommonSkip(info.Name()) {
		return false
	}
	if len(fileMode) == 0 {
		return true
	} else if len(fileMode) == 1 {
		return fileMode[0] == !info.Mode().IsDir()
	}
	panic("too many arguments for fileMode in shouldInclude")
}

func readDir(layer *Layer, name string) ([]fs.FileInfo, error) {
	f, err := layer.Open(name)
	if os.IsNotExist(err) {
		return nil, nil
	} else if err != nil {
		return nil, err
	}
	defer f.Close()
	return f.Readdir(-1)
}

// ListFiles lists files/directories in the given directory. The fileMode controls the returned files.
// * omitted: all files and directories will be returned.
// * true: only files will be returned.
// * false: only directories will be returned.
// The returned files are sorted by name.
func (l *LayeredFS) ListFiles(name string, fileMode ...bool) ([]string, error) {
	fileSet := make(container.Set[string])
	for _, layer := range l.layers {
		infos, err := readDir(layer, name)
		if err != nil {
			return nil, err
		}
		for _, info := range infos {
			if shouldInclude(info, fileMode...) {
				fileSet.Add(info.Name())
			}
		}
	}
	files := fileSet.Values()
	sort.Strings(files)
	return files, nil
}

// ListAllFiles returns files/directories in the given directory, including subdirectories, recursively.
// The fileMode controls the returned files:
// * omitted: all files and directories will be returned.
// * true: only files will be returned.
// * false: only directories will be returned.
// The returned files are sorted by name.
func (l *LayeredFS) ListAllFiles(name string, fileMode ...bool) ([]string, error) {
	return listAllFiles(l.layers, name, fileMode...)
}

func listAllFiles(layers []*Layer, name string, fileMode ...bool) ([]string, error) {
	fileSet := make(container.Set[string])
	var list func(dir string) error
	list = func(dir string) error {
		for _, layer := range layers {
			infos, err := readDir(layer, dir)
			if err != nil {
				return err
			}
			for _, info := range infos {
				path := util.PathJoinRelX(dir, info.Name())
				if shouldInclude(info, fileMode...) {
					fileSet.Add(path)
				}
				if info.IsDir() {
					if err = list(path); err != nil {
						return err
					}
				}
			}
		}
		return nil
	}
	if err := list(name); err != nil {
		return nil, err
	}
	files := fileSet.Values()
	sort.Strings(files)
	return files, nil
}

// WatchLocalChanges watches local changes in the file-system. It's used to help to reload assets when the local file-system changes.
func (l *LayeredFS) WatchLocalChanges(ctx context.Context, callback func()) {
	ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Asset Local FileSystem Watcher", process.SystemProcessType, true)
	defer finished()

	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Error("Unable to create watcher for asset local file-system: %v", err)
		return
	}
	defer watcher.Close()

	for _, layer := range l.layers {
		if layer.localPath == "" {
			continue
		}
		layerDirs, err := listAllFiles([]*Layer{layer}, ".", false)
		if err != nil {
			log.Error("Unable to list directories for asset local file-system %q: %v", layer.localPath, err)
			continue
		}
		layerDirs = append(layerDirs, ".")
		for _, dir := range layerDirs {
			if err = watcher.Add(util.FilePathJoinAbs(layer.localPath, dir)); err != nil && !os.IsNotExist(err) {
				log.Error("Unable to watch directory %s: %v", dir, err)
			}
		}
	}

	debounce := util.Debounce(100 * time.Millisecond)

	for {
		select {
		case <-ctx.Done():
			return
		case event, ok := <-watcher.Events:
			if !ok {
				return
			}
			log.Trace("Watched asset local file-system had event: %v", event)
			debounce(callback)
		case err, ok := <-watcher.Errors:
			if !ok {
				return
			}
			log.Error("Watched asset local file-system had error: %v", err)
		}
	}
}

// GetFileLayerName returns the name of the first-seen layer that contains the given file.
func (l *LayeredFS) GetFileLayerName(elems ...string) string {
	name := util.PathJoinRel(elems...)
	for _, layer := range l.layers {
		f, err := layer.Open(name)
		if os.IsNotExist(err) {
			continue
		} else if err != nil {
			return ""
		}
		_ = f.Close()
		return layer.name
	}
	return ""
}