code.gitea.io/gitea@v1.21.7/cmd/embedded.go (about)

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