github.com/t2y/goofys@v0.19.1-0.20190123053037-27053313e616/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 "fmt" 20 "io" 21 "io/ioutil" 22 "os" 23 "os/exec" 24 "strings" 25 "text/tabwriter" 26 "text/template" 27 "time" 28 29 "github.com/urfave/cli" 30 ) 31 32 var flagCategories map[string]string 33 34 // Set up custom help text for goofys; in particular the usage section. 35 func filterCategory(flags []cli.Flag, category string) (ret []cli.Flag) { 36 for _, f := range flags { 37 if flagCategories[f.GetName()] == category { 38 ret = append(ret, f) 39 } 40 } 41 return 42 } 43 44 func init() { 45 cli.AppHelpTemplate = `NAME: 46 {{.Name}} - {{.Usage}} 47 48 USAGE: 49 {{.Name}} {{if .Flags}}[global options]{{end}} bucket[:prefix] mountpoint 50 {{if .Version}} 51 VERSION: 52 {{.Version}} 53 {{end}}{{if len .Authors}} 54 AUTHOR(S): 55 {{range .Authors}}{{ . }}{{end}} 56 {{end}}{{if .Commands}} 57 COMMANDS: 58 {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} 59 {{end}}{{end}}{{if .Flags}} 60 GLOBAL OPTIONS: 61 {{range category .Flags ""}}{{.}} 62 {{end}} 63 TUNING OPTIONS: 64 {{range category .Flags "tuning"}}{{.}} 65 {{end}} 66 AWS S3 OPTIONS: 67 {{range category .Flags "aws"}}{{.}} 68 {{end}} 69 MISC OPTIONS: 70 {{range category .Flags "misc"}}{{.}} 71 {{end}}{{end}}{{if .Copyright }} 72 COPYRIGHT: 73 {{.Copyright}} 74 {{end}} 75 ` 76 } 77 78 var VersionHash string 79 80 func NewApp() (app *cli.App) { 81 uid, gid := MyUserAndGroup() 82 83 app = &cli.App{ 84 Name: "goofys", 85 Version: "0.19.0-" + VersionHash, 86 Usage: "Mount an S3 bucket locally", 87 HideHelp: true, 88 Writer: os.Stderr, 89 Flags: []cli.Flag{ 90 91 cli.BoolFlag{ 92 Name: "help, h", 93 Usage: "Print this help text and exit successfully.", 94 }, 95 96 ///////////////////////// 97 // File system 98 ///////////////////////// 99 100 cli.StringSliceFlag{ 101 Name: "o", 102 Usage: "Additional system-specific mount options. Be careful!", 103 }, 104 105 cli.StringFlag{ 106 Name: "cache", 107 Usage: "Directory to use for data cache. " + 108 "Requires catfs and `-o allow_other'. " + 109 "Can also pass in other catfs options " + 110 "(ex: --cache \"--free:10%:$HOME/cache\") (default: off)", 111 }, 112 113 cli.IntFlag{ 114 Name: "dir-mode", 115 Value: 0755, 116 Usage: "Permission bits for directories. (default: 0755)", 117 }, 118 119 cli.IntFlag{ 120 Name: "file-mode", 121 Value: 0644, 122 Usage: "Permission bits for files. (default: 0644)", 123 }, 124 125 cli.IntFlag{ 126 Name: "uid", 127 Value: uid, 128 Usage: "UID owner of all inodes.", 129 }, 130 131 cli.IntFlag{ 132 Name: "gid", 133 Value: gid, 134 Usage: "GID owner of all inodes.", 135 }, 136 137 ///////////////////////// 138 // S3 139 ///////////////////////// 140 141 cli.StringFlag{ 142 Name: "endpoint", 143 Value: "", 144 Usage: "The non-AWS endpoint to connect to." + 145 " Possible values: http://127.0.0.1:8081/", 146 }, 147 148 cli.StringFlag{ 149 Name: "region", 150 Value: "us-east-1", 151 Usage: "The region to connect to. Usually this is auto-detected." + 152 " Possible values: us-east-1, us-west-1, us-west-2, eu-west-1, " + 153 "eu-central-1, ap-southeast-1, ap-southeast-2, ap-northeast-1, " + 154 "sa-east-1, cn-north-1", 155 }, 156 157 cli.StringFlag{ 158 Name: "storage-class", 159 Value: "STANDARD", 160 Usage: "The type of storage to use when writing objects." + 161 " Possible values: REDUCED_REDUNDANCY, STANDARD, STANDARD_IA.", 162 }, 163 164 cli.StringFlag{ 165 Name: "profile", 166 Usage: "Use a named profile from $HOME/.aws/credentials instead of \"default\"", 167 }, 168 169 cli.BoolFlag{ 170 Name: "use-content-type", 171 Usage: "Set Content-Type according to file extension and /etc/mime.types (default: off)", 172 }, 173 174 /// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html 175 /// See http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html 176 cli.BoolFlag{ 177 Name: "sse", 178 Usage: "Enable basic server-side encryption at rest (SSE-S3) in S3 for all writes (default: off)", 179 }, 180 181 cli.StringFlag{ 182 Name: "sse-kms", 183 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)", 184 Value: "", 185 }, 186 187 /// http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl 188 cli.StringFlag{ 189 Name: "acl", 190 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)", 191 Value: "", 192 }, 193 194 ///////////////////////// 195 // Tuning 196 ///////////////////////// 197 198 cli.BoolFlag{ 199 Name: "cheap", 200 Usage: "Reduce S3 operation costs at the expense of some performance (default: off)", 201 }, 202 203 cli.BoolFlag{ 204 Name: "no-implicit-dir", 205 Usage: "Assume all directory objects (\"dir/\") exist (default: off)", 206 }, 207 208 cli.DurationFlag{ 209 Name: "stat-cache-ttl", 210 Value: time.Minute, 211 Usage: "How long to cache StatObject results and inode attributes.", 212 }, 213 214 cli.DurationFlag{ 215 Name: "type-cache-ttl", 216 Value: time.Minute, 217 Usage: "How long to cache name -> file/dir mappings in directory " + 218 "inodes.", 219 }, 220 cli.DurationFlag{ 221 Name: "http-timeout", 222 Value: 30 * time.Second, 223 Usage: "Set the timeout on HTTP requests to S3", 224 }, 225 226 ///////////////////////// 227 // Debugging 228 ///////////////////////// 229 230 cli.BoolFlag{ 231 Name: "debug_fuse", 232 Usage: "Enable fuse-related debugging output.", 233 }, 234 235 cli.BoolFlag{ 236 Name: "debug_s3", 237 Usage: "Enable S3-related debugging output.", 238 }, 239 240 cli.BoolFlag{ 241 Name: "f", 242 Usage: "Run goofys in foreground.", 243 }, 244 }, 245 } 246 247 var funcMap = template.FuncMap{ 248 "category": filterCategory, 249 "join": strings.Join, 250 } 251 252 flagCategories = map[string]string{} 253 254 for _, f := range []string{"region", "sse", "sse-kms", "storage-class", "acl"} { 255 flagCategories[f] = "aws" 256 } 257 258 for _, f := range []string{"cheap", "no-implicit-dir", "stat-cache-ttl", "type-cache-ttl", "http-timeout"} { 259 flagCategories[f] = "tuning" 260 } 261 262 for _, f := range []string{"help, h", "debug_fuse", "debug_s3", "version, v", "f"} { 263 flagCategories[f] = "misc" 264 } 265 266 cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { 267 w = tabwriter.NewWriter(w, 1, 8, 2, ' ', 0) 268 var tmplGet = template.Must(template.New("help").Funcs(funcMap).Parse(templ)) 269 tmplGet.Execute(w, app) 270 } 271 272 return 273 } 274 275 type FlagStorage struct { 276 // File system 277 MountOptions map[string]string 278 MountPoint string 279 MountPointArg string 280 MountPointCreated string 281 282 Cache []string 283 DirMode os.FileMode 284 FileMode os.FileMode 285 Uid uint32 286 Gid uint32 287 288 // S3 289 Endpoint string 290 Region string 291 RegionSet bool 292 StorageClass string 293 Profile string 294 UseContentType bool 295 UseSSE bool 296 UseKMS bool 297 KMSKeyID string 298 ACL string 299 300 // Tuning 301 Cheap bool 302 ExplicitDir bool 303 StatCacheTTL time.Duration 304 TypeCacheTTL time.Duration 305 HTTPTimeout time.Duration 306 307 // Debugging 308 DebugFuse bool 309 DebugS3 bool 310 Foreground bool 311 } 312 313 func parseOptions(m map[string]string, s string) { 314 // NOTE(jacobsa): The man pages don't define how escaping works, and as far 315 // as I can tell there is no way to properly escape or quote a comma in the 316 // options list for an fstab entry. So put our fingers in our ears and hope 317 // that nobody needs a comma. 318 for _, p := range strings.Split(s, ",") { 319 var name string 320 var value string 321 322 // Split on the first equals sign. 323 if equalsIndex := strings.IndexByte(p, '='); equalsIndex != -1 { 324 name = p[:equalsIndex] 325 value = p[equalsIndex+1:] 326 } else { 327 name = p 328 } 329 330 m[name] = value 331 } 332 333 return 334 } 335 336 func (flags *FlagStorage) Cleanup() { 337 if flags.MountPointCreated != "" && flags.MountPointCreated != flags.MountPointArg { 338 err := os.Remove(flags.MountPointCreated) 339 if err != nil { 340 log.Errorf("rmdir %v = %v", flags.MountPointCreated, err) 341 } 342 } 343 } 344 345 // Add the flags accepted by run to the supplied flag set, returning the 346 // variables into which the flags will parse. 347 func PopulateFlags(c *cli.Context) (ret *FlagStorage) { 348 flags := &FlagStorage{ 349 // File system 350 MountOptions: make(map[string]string), 351 DirMode: os.FileMode(c.Int("dir-mode")), 352 FileMode: os.FileMode(c.Int("file-mode")), 353 Uid: uint32(c.Int("uid")), 354 Gid: uint32(c.Int("gid")), 355 356 // Tuning, 357 Cheap: c.Bool("cheap"), 358 ExplicitDir: c.Bool("no-implicit-dir"), 359 StatCacheTTL: c.Duration("stat-cache-ttl"), 360 TypeCacheTTL: c.Duration("type-cache-ttl"), 361 HTTPTimeout: c.Duration("http-timeout"), 362 363 // S3 364 Endpoint: c.String("endpoint"), 365 Region: c.String("region"), 366 RegionSet: c.IsSet("region"), 367 StorageClass: c.String("storage-class"), 368 Profile: c.String("profile"), 369 UseContentType: c.Bool("use-content-type"), 370 UseSSE: c.Bool("sse"), 371 UseKMS: c.IsSet("sse-kms"), 372 KMSKeyID: c.String("sse-kms"), 373 ACL: c.String("acl"), 374 375 // Debugging, 376 DebugFuse: c.Bool("debug_fuse"), 377 DebugS3: c.Bool("debug_s3"), 378 Foreground: c.Bool("f"), 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 // KMS implies SSE 452 if flags.UseKMS { 453 flags.UseSSE = true 454 } 455 456 return flags 457 } 458 459 func MassageMountFlags(args []string) (ret []string) { 460 if len(args) == 5 && args[3] == "-o" { 461 // looks like it's coming from fstab! 462 mountOptions := "" 463 ret = append(ret, args[0]) 464 465 for _, p := range strings.Split(args[4], ",") { 466 if strings.HasPrefix(p, "-") { 467 ret = append(ret, p) 468 } else { 469 mountOptions += p 470 mountOptions += "," 471 } 472 } 473 474 if len(mountOptions) != 0 { 475 // remove trailing , 476 mountOptions = mountOptions[:len(mountOptions)-1] 477 ret = append(ret, "-o") 478 ret = append(ret, mountOptions) 479 } 480 481 ret = append(ret, args[1]) 482 ret = append(ret, args[2]) 483 } else { 484 return args 485 } 486 487 return 488 }