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