github.com/adrianjagielak/goofys@v0.24.1-0.20230810095418-94919a5d2254/internal/flags.go (about) 1 // Copyright 2015 - 2017 Ka-Hing Cheung 2 // Copyright 2015 - 2017 Google Inc. All Rights Reserved. 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 package internal 17 18 import ( 19 . "github.com/kahing/goofys/api/common" 20 21 "fmt" 22 "io" 23 "io/ioutil" 24 "os" 25 "os/exec" 26 "strings" 27 "text/tabwriter" 28 "text/template" 29 "time" 30 31 "github.com/urfave/cli" 32 ) 33 34 var flagCategories map[string]string 35 36 // Set up custom help text for goofys; in particular the usage section. 37 func filterCategory(flags []cli.Flag, category string) (ret []cli.Flag) { 38 for _, f := range flags { 39 if flagCategories[f.GetName()] == category { 40 ret = append(ret, f) 41 } 42 } 43 return 44 } 45 46 func init() { 47 cli.AppHelpTemplate = `NAME: 48 {{.Name}} - {{.Usage}} 49 50 USAGE: 51 {{.Name}} {{if .Flags}}[global options]{{end}} bucket[:prefix] mountpoint 52 {{if .Version}} 53 VERSION: 54 {{.Version}} 55 {{end}}{{if len .Authors}} 56 AUTHOR(S): 57 {{range .Authors}}{{ . }}{{end}} 58 {{end}}{{if .Commands}} 59 COMMANDS: 60 {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} 61 {{end}}{{end}}{{if .Flags}} 62 GLOBAL OPTIONS: 63 {{range category .Flags ""}}{{.}} 64 {{end}} 65 TUNING OPTIONS: 66 {{range category .Flags "tuning"}}{{.}} 67 {{end}} 68 AWS S3 OPTIONS: 69 {{range category .Flags "aws"}}{{.}} 70 {{end}} 71 MISC OPTIONS: 72 {{range category .Flags "misc"}}{{.}} 73 {{end}}{{end}}{{if .Copyright }} 74 COPYRIGHT: 75 {{.Copyright}} 76 {{end}} 77 ` 78 } 79 80 var VersionNumber string 81 var VersionHash string 82 83 func NewApp() (app *cli.App) { 84 uid, gid := MyUserAndGroup() 85 86 s3Default := (&S3Config{}).Init() 87 88 app = &cli.App{ 89 Name: "goofys", 90 Version: VersionNumber + "-" + VersionHash, 91 Usage: "Mount an S3 bucket locally", 92 HideHelp: true, 93 Writer: os.Stderr, 94 Flags: []cli.Flag{ 95 96 cli.BoolFlag{ 97 Name: "help, h", 98 Usage: "Print this help text and exit successfully.", 99 }, 100 101 ///////////////////////// 102 // File system 103 ///////////////////////// 104 105 cli.StringSliceFlag{ 106 Name: "o", 107 Usage: "Additional system-specific mount options. Be careful!", 108 }, 109 110 cli.StringFlag{ 111 Name: "cache", 112 Usage: "Directory to use for data cache. " + 113 "Requires catfs and `-o allow_other'. " + 114 "Can also pass in other catfs options " + 115 "(ex: --cache \"--free:10%:$HOME/cache\") (default: off)", 116 }, 117 118 cli.IntFlag{ 119 Name: "dir-mode", 120 Value: 0755, 121 Usage: "Permission bits for directories. (default: 0755)", 122 }, 123 124 cli.IntFlag{ 125 Name: "file-mode", 126 Value: 0644, 127 Usage: "Permission bits for files. (default: 0644)", 128 }, 129 130 cli.IntFlag{ 131 Name: "uid", 132 Value: uid, 133 Usage: "UID owner of all inodes.", 134 }, 135 136 cli.IntFlag{ 137 Name: "gid", 138 Value: gid, 139 Usage: "GID owner of all inodes.", 140 }, 141 142 ///////////////////////// 143 // S3 144 ///////////////////////// 145 146 cli.StringFlag{ 147 Name: "endpoint", 148 Value: "", 149 Usage: "The non-AWS endpoint to connect to." + 150 " Possible values: http://127.0.0.1:8081/", 151 }, 152 153 cli.StringFlag{ 154 Name: "region", 155 Value: s3Default.Region, 156 Usage: "The region to connect to. Usually this is auto-detected." + 157 " Possible values: us-east-1, us-west-1, us-west-2, eu-west-1, " + 158 "eu-central-1, ap-southeast-1, ap-southeast-2, ap-northeast-1, " + 159 "sa-east-1, cn-north-1", 160 }, 161 162 cli.BoolFlag{ 163 Name: "requester-pays", 164 Usage: "Whether to allow access to requester-pays buckets (default: off)", 165 }, 166 167 cli.StringFlag{ 168 Name: "storage-class", 169 Value: s3Default.StorageClass, 170 Usage: "The type of storage to use when writing objects." + 171 " Possible values: REDUCED_REDUNDANCY, STANDARD, STANDARD_IA.", 172 }, 173 174 cli.StringFlag{ 175 Name: "profile", 176 Usage: "Use a named profile from $HOME/.aws/credentials instead of \"default\"", 177 }, 178 179 cli.BoolFlag{ 180 Name: "use-content-type", 181 Usage: "Set Content-Type according to file extension and /etc/mime.types (default: off)", 182 }, 183 184 /// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html 185 /// See http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html 186 cli.BoolFlag{ 187 Name: "sse", 188 Usage: "Enable basic server-side encryption at rest (SSE-S3) in S3 for all writes (default: off)", 189 }, 190 191 cli.StringFlag{ 192 Name: "sse-kms", 193 Usage: "Enable KMS encryption (SSE-KMS) for all writes using this particular KMS `key-id`. Leave blank to Use the account's CMK - customer master key (default: off)", 194 Value: "", 195 }, 196 197 cli.StringFlag{ 198 Name: "sse-c", 199 Usage: "Enable server-side encryption using this base64-encoded key (default: off)", 200 Value: "", 201 }, 202 203 /// http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl 204 cli.StringFlag{ 205 Name: "acl", 206 Usage: "The canned ACL to apply to the object. Possible values: private, public-read, public-read-write, authenticated-read, aws-exec-read, bucket-owner-read, bucket-owner-full-control (default: off)", 207 Value: "", 208 }, 209 210 cli.BoolFlag{ 211 Name: "subdomain", 212 Usage: "Enable subdomain mode of S3", 213 }, 214 215 ///////////////////////// 216 // Tuning 217 ///////////////////////// 218 219 cli.BoolFlag{ 220 Name: "cheap", 221 Usage: "Reduce S3 operation costs at the expense of some performance (default: off)", 222 }, 223 224 cli.BoolFlag{ 225 Name: "no-implicit-dir", 226 Usage: "Assume all directory objects (\"dir/\") exist (default: off)", 227 }, 228 229 cli.DurationFlag{ 230 Name: "stat-cache-ttl", 231 Value: time.Minute, 232 Usage: "How long to cache StatObject results and inode attributes.", 233 }, 234 235 cli.DurationFlag{ 236 Name: "type-cache-ttl", 237 Value: time.Minute, 238 Usage: "How long to cache name -> file/dir mappings in directory " + 239 "inodes.", 240 }, 241 cli.DurationFlag{ 242 Name: "http-timeout", 243 Value: 30 * time.Second, 244 Usage: "Set the timeout on HTTP requests to S3", 245 }, 246 247 ///////////////////////// 248 // Debugging 249 ///////////////////////// 250 251 cli.BoolFlag{ 252 Name: "debug_fuse", 253 Usage: "Enable fuse-related debugging output.", 254 }, 255 256 cli.BoolFlag{ 257 Name: "debug_s3", 258 Usage: "Enable S3-related debugging output.", 259 }, 260 261 cli.BoolFlag{ 262 Name: "f", 263 Usage: "Run goofys in foreground.", 264 }, 265 }, 266 } 267 268 var funcMap = template.FuncMap{ 269 "category": filterCategory, 270 "join": strings.Join, 271 } 272 273 flagCategories = map[string]string{} 274 275 for _, f := range []string{"region", "sse", "sse-kms", "sse-c", "storage-class", "acl", "requester-pays"} { 276 flagCategories[f] = "aws" 277 } 278 279 for _, f := range []string{"cheap", "no-implicit-dir", "stat-cache-ttl", "type-cache-ttl", "http-timeout"} { 280 flagCategories[f] = "tuning" 281 } 282 283 for _, f := range []string{"help, h", "debug_fuse", "debug_s3", "version, v", "f"} { 284 flagCategories[f] = "misc" 285 } 286 287 cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { 288 w = tabwriter.NewWriter(w, 1, 8, 2, ' ', 0) 289 var tmplGet = template.Must(template.New("help").Funcs(funcMap).Parse(templ)) 290 tmplGet.Execute(w, app) 291 } 292 293 return 294 } 295 296 func parseOptions(m map[string]string, s string) { 297 // NOTE(jacobsa): The man pages don't define how escaping works, and as far 298 // as I can tell there is no way to properly escape or quote a comma in the 299 // options list for an fstab entry. So put our fingers in our ears and hope 300 // that nobody needs a comma. 301 for _, p := range strings.Split(s, ",") { 302 var name string 303 var value string 304 305 // Split on the first equals sign. 306 if equalsIndex := strings.IndexByte(p, '='); equalsIndex != -1 { 307 name = p[:equalsIndex] 308 value = p[equalsIndex+1:] 309 } else { 310 name = p 311 } 312 313 m[name] = value 314 } 315 316 return 317 } 318 319 // PopulateFlags adds the flags accepted by run to the supplied flag set, returning the 320 // variables into which the flags will parse. 321 func PopulateFlags(c *cli.Context) (ret *FlagStorage) { 322 flags := &FlagStorage{ 323 // File system 324 MountOptions: make(map[string]string), 325 DirMode: os.FileMode(c.Int("dir-mode")), 326 FileMode: os.FileMode(c.Int("file-mode")), 327 Uid: uint32(c.Int("uid")), 328 Gid: uint32(c.Int("gid")), 329 330 // Tuning, 331 Cheap: c.Bool("cheap"), 332 ExplicitDir: c.Bool("no-implicit-dir"), 333 StatCacheTTL: c.Duration("stat-cache-ttl"), 334 TypeCacheTTL: c.Duration("type-cache-ttl"), 335 HTTPTimeout: c.Duration("http-timeout"), 336 337 // Common Backend Config 338 Endpoint: c.String("endpoint"), 339 UseContentType: c.Bool("use-content-type"), 340 341 // Debugging, 342 DebugFuse: c.Bool("debug_fuse"), 343 DebugS3: c.Bool("debug_s3"), 344 Foreground: c.Bool("f"), 345 } 346 347 // S3 348 if c.IsSet("region") || c.IsSet("requester-pays") || c.IsSet("storage-class") || 349 c.IsSet("profile") || c.IsSet("sse") || c.IsSet("sse-kms") || 350 c.IsSet("sse-c") || c.IsSet("acl") || c.IsSet("subdomain") { 351 352 if flags.Backend == nil { 353 flags.Backend = (&S3Config{}).Init() 354 } 355 config, _ := flags.Backend.(*S3Config) 356 357 config.Region = c.String("region") 358 config.RegionSet = c.IsSet("region") 359 config.RequesterPays = c.Bool("requester-pays") 360 config.StorageClass = c.String("storage-class") 361 config.Profile = c.String("profile") 362 config.UseSSE = c.Bool("sse") 363 config.UseKMS = c.IsSet("sse-kms") 364 config.KMSKeyID = c.String("sse-kms") 365 config.SseC = c.String("sse-c") 366 config.ACL = c.String("acl") 367 config.Subdomain = c.Bool("subdomain") 368 369 // KMS implies SSE 370 if config.UseKMS { 371 config.UseSSE = true 372 } 373 } 374 375 // Handle the repeated "-o" flag. 376 for _, o := range c.StringSlice("o") { 377 parseOptions(flags.MountOptions, o) 378 } 379 380 flags.MountPointArg = c.Args()[1] 381 flags.MountPoint = flags.MountPointArg 382 var err error 383 384 defer func() { 385 if err != nil { 386 flags.Cleanup() 387 } 388 }() 389 390 if c.IsSet("cache") { 391 cache := c.String("cache") 392 cacheArgs := strings.Split(c.String("cache"), ":") 393 cacheDir := cacheArgs[len(cacheArgs)-1] 394 cacheArgs = cacheArgs[:len(cacheArgs)-1] 395 396 fi, err := os.Stat(cacheDir) 397 if err != nil || !fi.IsDir() { 398 io.WriteString(cli.ErrWriter, 399 fmt.Sprintf("Invalid value \"%v\" for --cache: not a directory\n\n", 400 cacheDir)) 401 return nil 402 } 403 404 if _, ok := flags.MountOptions["allow_other"]; !ok { 405 flags.MountPointCreated, err = ioutil.TempDir("", ".goofys-mnt") 406 if err != nil { 407 io.WriteString(cli.ErrWriter, 408 fmt.Sprintf("Unable to create temp dir: %v", err)) 409 return nil 410 } 411 flags.MountPoint = flags.MountPointCreated 412 } 413 414 cacheArgs = append([]string{"--test", "-f"}, cacheArgs...) 415 416 if flags.MountPointArg == flags.MountPoint { 417 cacheArgs = append(cacheArgs, "-ononempty") 418 } 419 420 cacheArgs = append(cacheArgs, "--") 421 cacheArgs = append(cacheArgs, flags.MountPoint) 422 cacheArgs = append(cacheArgs, cacheDir) 423 cacheArgs = append(cacheArgs, flags.MountPointArg) 424 425 fuseLog.Debugf("catfs %v", cacheArgs) 426 catfs := exec.Command("catfs", cacheArgs...) 427 _, err = catfs.Output() 428 if err != nil { 429 if ee, ok := err.(*exec.Error); ok { 430 io.WriteString(cli.ErrWriter, 431 fmt.Sprintf("--cache requires catfs (%v) but %v\n\n", 432 "http://github.com/kahing/catfs", 433 ee.Error())) 434 } else if ee, ok := err.(*exec.ExitError); ok { 435 io.WriteString(cli.ErrWriter, 436 fmt.Sprintf("Invalid value \"%v\" for --cache: %v\n\n", 437 cache, string(ee.Stderr))) 438 } 439 return nil 440 } 441 442 flags.Cache = cacheArgs[1:] 443 } 444 445 return flags 446 } 447 448 func MassageMountFlags(args []string) (ret []string) { 449 if len(args) == 5 && args[3] == "-o" { 450 // looks like it's coming from fstab! 451 mountOptions := "" 452 ret = append(ret, args[0]) 453 454 for _, p := range strings.Split(args[4], ",") { 455 if strings.HasPrefix(p, "-") { 456 ret = append(ret, p) 457 } else { 458 mountOptions += p 459 mountOptions += "," 460 } 461 } 462 463 if len(mountOptions) != 0 { 464 // remove trailing , 465 mountOptions = mountOptions[:len(mountOptions)-1] 466 ret = append(ret, "-o") 467 ret = append(ret, mountOptions) 468 } 469 470 ret = append(ret, args[1]) 471 ret = append(ret, args[2]) 472 } else { 473 return args 474 } 475 476 return 477 }