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 }