Skip to content

Commit bcb52ae

Browse files
guillep2kzeripathlunny
authored
Implement "embedded" command to extract static resources (#9982)
* draft * Implement extract command * Fix nits and force args on extract * Add !bindata stub, support Windows, fmt * fix vendored flag * Remove leading slash for matching * Add docs * Fix typos * Add embedded view command Co-authored-by: zeripath <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent 9b9dd19 commit bcb52ae

File tree

8 files changed

+568
-6
lines changed

8 files changed

+568
-6
lines changed

cmd/embedded.go

+332
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build bindata
6+
7+
package cmd
8+
9+
import (
10+
"errors"
11+
"fmt"
12+
"os"
13+
"path/filepath"
14+
"sort"
15+
"strings"
16+
17+
"code.gitea.io/gitea/modules/log"
18+
"code.gitea.io/gitea/modules/options"
19+
"code.gitea.io/gitea/modules/public"
20+
"code.gitea.io/gitea/modules/setting"
21+
"code.gitea.io/gitea/modules/templates"
22+
23+
"github.com/gobwas/glob"
24+
"github.com/urfave/cli"
25+
)
26+
27+
// Cmdembedded represents the available extract sub-command.
28+
var (
29+
Cmdembedded = cli.Command{
30+
Name: "embedded",
31+
Usage: "Extract embedded resources",
32+
Description: "A command for extracting embedded resources, like templates and images",
33+
Subcommands: []cli.Command{
34+
subcmdList,
35+
subcmdView,
36+
subcmdExtract,
37+
},
38+
}
39+
40+
subcmdList = cli.Command{
41+
Name: "list",
42+
Usage: "List files matching the given pattern",
43+
Action: runList,
44+
Flags: []cli.Flag{
45+
cli.BoolFlag{
46+
Name: "include-vendored,vendor",
47+
Usage: "Include files under public/vendor as well",
48+
},
49+
},
50+
}
51+
52+
subcmdView = cli.Command{
53+
Name: "view",
54+
Usage: "View a file matching the given pattern",
55+
Action: runView,
56+
Flags: []cli.Flag{
57+
cli.BoolFlag{
58+
Name: "include-vendored,vendor",
59+
Usage: "Include files under public/vendor as well",
60+
},
61+
},
62+
}
63+
64+
subcmdExtract = cli.Command{
65+
Name: "extract",
66+
Usage: "Extract resources",
67+
Action: runExtract,
68+
Flags: []cli.Flag{
69+
cli.BoolFlag{
70+
Name: "include-vendored,vendor",
71+
Usage: "Include files under public/vendor as well",
72+
},
73+
cli.BoolFlag{
74+
Name: "overwrite",
75+
Usage: "Overwrite files if they already exist",
76+
},
77+
cli.BoolFlag{
78+
Name: "rename",
79+
Usage: "Rename files as {name}.bak if they already exist (overwrites previous .bak)",
80+
},
81+
cli.BoolFlag{
82+
Name: "custom",
83+
Usage: "Extract to the 'custom' directory as per app.ini",
84+
},
85+
cli.StringFlag{
86+
Name: "destination,dest-dir",
87+
Usage: "Extract to the specified directory",
88+
},
89+
},
90+
}
91+
92+
sections map[string]*section
93+
assets []asset
94+
)
95+
96+
type section struct {
97+
Path string
98+
Names func() []string
99+
IsDir func(string) (bool, error)
100+
Asset func(string) ([]byte, error)
101+
}
102+
103+
type asset struct {
104+
Section *section
105+
Name string
106+
Path string
107+
}
108+
109+
func initEmbeddedExtractor(c *cli.Context) error {
110+
111+
// Silence the console logger
112+
log.DelNamedLogger("console")
113+
log.DelNamedLogger(log.DEFAULT)
114+
115+
// Read configuration file
116+
setting.NewContext()
117+
118+
pats, err := getPatterns(c.Args())
119+
if err != nil {
120+
return err
121+
}
122+
sections := make(map[string]*section, 3)
123+
124+
sections["public"] = &section{Path: "public", Names: public.AssetNames, IsDir: public.AssetIsDir, Asset: public.Asset}
125+
sections["options"] = &section{Path: "options", Names: options.AssetNames, IsDir: options.AssetIsDir, Asset: options.Asset}
126+
sections["templates"] = &section{Path: "templates", Names: templates.AssetNames, IsDir: templates.AssetIsDir, Asset: templates.Asset}
127+
128+
for _, sec := range sections {
129+
assets = append(assets, buildAssetList(sec, pats, c)...)
130+
}
131+
132+
// Sort assets
133+
sort.SliceStable(assets, func(i, j int) bool {
134+
return assets[i].Path < assets[j].Path
135+
})
136+
137+
return nil
138+
}
139+
140+
func runList(c *cli.Context) error {
141+
if err := runListDo(c); err != nil {
142+
fmt.Fprintf(os.Stderr, "%v\n", err)
143+
return err
144+
}
145+
return nil
146+
}
147+
148+
func runView(c *cli.Context) error {
149+
if err := runViewDo(c); err != nil {
150+
fmt.Fprintf(os.Stderr, "%v\n", err)
151+
return err
152+
}
153+
return nil
154+
}
155+
156+
func runExtract(c *cli.Context) error {
157+
if err := runExtractDo(c); err != nil {
158+
fmt.Fprintf(os.Stderr, "%v\n", err)
159+
return err
160+
}
161+
return nil
162+
}
163+
164+
func runListDo(c *cli.Context) error {
165+
if err := initEmbeddedExtractor(c); err != nil {
166+
return err
167+
}
168+
169+
for _, a := range assets {
170+
fmt.Println(a.Path)
171+
}
172+
173+
return nil
174+
}
175+
176+
func runViewDo(c *cli.Context) error {
177+
if err := initEmbeddedExtractor(c); err != nil {
178+
return err
179+
}
180+
181+
if len(assets) == 0 {
182+
return fmt.Errorf("No files matched the given pattern")
183+
} else if len(assets) > 1 {
184+
return fmt.Errorf("Too many files matched the given pattern; try to be more specific")
185+
}
186+
187+
data, err := assets[0].Section.Asset(assets[0].Name)
188+
if err != nil {
189+
return fmt.Errorf("%s: %v", assets[0].Path, err)
190+
}
191+
192+
if _, err = os.Stdout.Write(data); err != nil {
193+
return fmt.Errorf("%s: %v", assets[0].Path, err)
194+
}
195+
196+
return nil
197+
}
198+
199+
func runExtractDo(c *cli.Context) error {
200+
if err := initEmbeddedExtractor(c); err != nil {
201+
return err
202+
}
203+
204+
if len(c.Args()) == 0 {
205+
return fmt.Errorf("A list of pattern of files to extract is mandatory (e.g. '**' for all)")
206+
}
207+
208+
destdir := "."
209+
210+
if c.IsSet("destination") {
211+
destdir = c.String("destination")
212+
} else if c.Bool("custom") {
213+
destdir = setting.CustomPath
214+
fmt.Println("Using app.ini at", setting.CustomConf)
215+
}
216+
217+
fi, err := os.Stat(destdir)
218+
if errors.Is(err, os.ErrNotExist) {
219+
// In case Windows users attempt to provide a forward-slash path
220+
wdestdir := filepath.FromSlash(destdir)
221+
if wfi, werr := os.Stat(wdestdir); werr == nil {
222+
destdir = wdestdir
223+
fi = wfi
224+
err = nil
225+
}
226+
}
227+
if err != nil {
228+
return fmt.Errorf("%s: %s", destdir, err)
229+
} else if !fi.IsDir() {
230+
return fmt.Errorf("%s is not a directory.", destdir)
231+
}
232+
233+
fmt.Printf("Extracting to %s:\n", destdir)
234+
235+
overwrite := c.Bool("overwrite")
236+
rename := c.Bool("rename")
237+
238+
for _, a := range assets {
239+
if err := extractAsset(destdir, a, overwrite, rename); err != nil {
240+
// Non-fatal error
241+
fmt.Fprintf(os.Stderr, "%s: %v", a.Path, err)
242+
}
243+
}
244+
245+
return nil
246+
}
247+
248+
func extractAsset(d string, a asset, overwrite, rename bool) error {
249+
dest := filepath.Join(d, filepath.FromSlash(a.Path))
250+
dir := filepath.Dir(dest)
251+
252+
data, err := a.Section.Asset(a.Name)
253+
if err != nil {
254+
return fmt.Errorf("%s: %v", a.Path, err)
255+
}
256+
257+
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
258+
return fmt.Errorf("%s: %v", dir, err)
259+
}
260+
261+
perms := os.ModePerm & 0666
262+
263+
fi, err := os.Lstat(dest)
264+
if err != nil {
265+
if !errors.Is(err, os.ErrNotExist) {
266+
return fmt.Errorf("%s: %v", dest, err)
267+
}
268+
} else if !overwrite && !rename {
269+
fmt.Printf("%s already exists; skipped.\n", dest)
270+
return nil
271+
} else if !fi.Mode().IsRegular() {
272+
return fmt.Errorf("%s already exists, but it's not a regular file", dest)
273+
} else if rename {
274+
if err := os.Rename(dest, dest+".bak"); err != nil {
275+
return fmt.Errorf("Error creating backup for %s: %v", dest, err)
276+
}
277+
// Attempt to respect file permissions mask (even if user:group will be set anew)
278+
perms = fi.Mode()
279+
}
280+
281+
file, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms)
282+
if err != nil {
283+
return fmt.Errorf("%s: %v", dest, err)
284+
}
285+
defer file.Close()
286+
287+
if _, err = file.Write(data); err != nil {
288+
return fmt.Errorf("%s: %v", dest, err)
289+
}
290+
291+
fmt.Println(dest)
292+
293+
return nil
294+
}
295+
296+
func buildAssetList(sec *section, globs []glob.Glob, c *cli.Context) []asset {
297+
var results = make([]asset, 0, 64)
298+
for _, name := range sec.Names() {
299+
if isdir, err := sec.IsDir(name); !isdir && err == nil {
300+
if sec.Path == "public" &&
301+
strings.HasPrefix(name, "vendor/") &&
302+
!c.Bool("include-vendored") {
303+
continue
304+
}
305+
matchName := sec.Path + "/" + name
306+
for _, g := range globs {
307+
if g.Match(matchName) {
308+
results = append(results, asset{Section: sec,
309+
Name: name,
310+
Path: sec.Path + "/" + name})
311+
break
312+
}
313+
}
314+
}
315+
}
316+
return results
317+
}
318+
319+
func getPatterns(args []string) ([]glob.Glob, error) {
320+
if len(args) == 0 {
321+
args = []string{"**"}
322+
}
323+
pat := make([]glob.Glob, len(args))
324+
for i := range args {
325+
if g, err := glob.Compile(args[i], '/'); err != nil {
326+
return nil, fmt.Errorf("'%s': Invalid glob pattern: %v", args[i], err)
327+
} else {
328+
pat[i] = g
329+
}
330+
}
331+
return pat, nil
332+
}

cmd/embedded_stub.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build !bindata
6+
7+
package cmd
8+
9+
import (
10+
"fmt"
11+
"os"
12+
13+
"github.com/urfave/cli"
14+
)
15+
16+
// Cmdembedded represents the available extract sub-command.
17+
var (
18+
Cmdembedded = cli.Command{
19+
Name: "embedded",
20+
Usage: "Extract embedded resources",
21+
Description: "A command for extracting embedded resources, like templates and images",
22+
Action: extractorNotImplemented,
23+
}
24+
)
25+
26+
func extractorNotImplemented(c *cli.Context) error {
27+
err := fmt.Errorf("Sorry: the 'embedded' subcommand is not available in builds without bindata")
28+
fmt.Fprintf(os.Stderr, "%s\n", err)
29+
return err
30+
}

0 commit comments

Comments
 (0)