|
| 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"] = §ion{Path: "public", Names: public.AssetNames, IsDir: public.AssetIsDir, Asset: public.Asset} |
| 125 | + sections["options"] = §ion{Path: "options", Names: options.AssetNames, IsDir: options.AssetIsDir, Asset: options.Asset} |
| 126 | + sections["templates"] = §ion{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 | +} |
0 commit comments