github.com/djmaze/goofys@v0.24.2/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/djmaze/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 ///////////////////////// 247 // Debugging 248 ///////////////////////// 249 250 cli.BoolFlag{ 251 Name: "debug_fuse", 252 Usage: "Enable fuse-related debugging output.", 253 }, 254 255 cli.BoolFlag{ 256 Name: "debug_s3", 257 Usage: "Enable S3-related debugging output.", 258 }, 259 260 cli.BoolFlag{ 261 Name: "f", 262 Usage: "Run goofys in foreground.", 263 }, 264 }, 265 } 266 267 var funcMap = template.FuncMap{ 268 "category": filterCategory, 269 "join": strings.Join, 270 } 271 272 flagCategories = map[string]string{} 273 274 for _, f := range []string{"region", "sse", "sse-kms", "sse-c", "storage-class", "acl", "requester-pays"} { 275 flagCategories[f] = "aws" 276 } 277 278 for _, f := range []string{"cheap", "no-implicit-dir", "stat-cache-ttl", "type-cache-ttl", "http-timeout"} { 279 flagCategories[f] = "tuning" 280 } 281 282 for _, f := range []string{"help, h", "debug_fuse", "debug_s3", "version, v", "f"} { 283 flagCategories[f] = "misc" 284 } 285 286 cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { 287 w = tabwriter.NewWriter(w, 1, 8, 2, ' ', 0) 288 var tmplGet = template.Must(template.New("help").Funcs(funcMap).Parse(templ)) 289 tmplGet.Execute(w, app) 290 } 291 292 return 293 } 294 295 func parseOptions(m map[string]string, s string) { 296 // NOTE(jacobsa): The man pages don't define how escaping works, and as far 297 // as I can tell there is no way to properly escape or quote a comma in the 298 // options list for an fstab entry. So put our fingers in our ears and hope 299 // that nobody needs a comma. 300 for _, p := range strings.Split(s, ",") { 301 var name string 302 var value string 303 304 // Split on the first equals sign. 305 if equalsIndex := strings.IndexByte(p, '='); equalsIndex != -1 { 306 name = p[:equalsIndex] 307 value = p[equalsIndex+1:] 308 } else { 309 name = p 310 } 311 312 m[name] = value 313 } 314 315 return 316 } 317 318 // PopulateFlags adds the flags accepted by run to the supplied flag set, returning the 319 // variables into which the flags will parse. 320 func PopulateFlags(c *cli.Context) (ret *FlagStorage) { 321 flags := &FlagStorage{ 322 // File system 323 MountOptions: make(map[string]string), 324 DirMode: os.FileMode(c.Int("dir-mode")), 325 FileMode: os.FileMode(c.Int("file-mode")), 326 Uid: uint32(c.Int("uid")), 327 Gid: uint32(c.Int("gid")), 328 329 // Tuning, 330 Cheap: c.Bool("cheap"), 331 ExplicitDir: c.Bool("no-implicit-dir"), 332 StatCacheTTL: c.Duration("stat-cache-ttl"), 333 TypeCacheTTL: c.Duration("type-cache-ttl"), 334 HTTPTimeout: c.Duration("http-timeout"), 335 336 // Common Backend Config 337 Endpoint: c.String("endpoint"), 338 UseContentType: c.Bool("use-content-type"), 339 340 // Debugging, 341 DebugFuse: c.Bool("debug_fuse"), 342 DebugS3: c.Bool("debug_s3"), 343 Foreground: c.Bool("f"), 344 } 345 346 // S3 347 if c.IsSet("region") || c.IsSet("requester-pays") || c.IsSet("storage-class") || 348 c.IsSet("profile") || c.IsSet("sse") || c.IsSet("sse-kms") || 349 c.IsSet("sse-c") || c.IsSet("acl") || c.IsSet("subdomain") { 350 351 if flags.Backend == nil { 352 flags.Backend = (&S3Config{}).Init() 353 } 354 config, _ := flags.Backend.(*S3Config) 355 356 config.Region = c.String("region") 357 config.RegionSet = c.IsSet("region") 358 config.RequesterPays = c.Bool("requester-pays") 359 config.StorageClass = c.String("storage-class") 360 config.Profile = c.String("profile") 361 config.UseSSE = c.Bool("sse") 362 config.UseKMS = c.IsSet("sse-kms") 363 config.KMSKeyID = c.String("sse-kms") 364 config.SseC = c.String("sse-c") 365 config.ACL = c.String("acl") 366 config.Subdomain = c.Bool("subdomain") 367 368 // KMS implies SSE 369 if config.UseKMS { 370 config.UseSSE = true 371 } 372 } 373 374 // Handle the repeated "-o" flag. 375 for _, o := range c.StringSlice("o") { 376 parseOptions(flags.MountOptions, o) 377 } 378 379 flags.MountPointArg = c.Args()[1] 380 flags.MountPoint = flags.MountPointArg 381 var err error 382 383 defer func() { 384 if err != nil { 385 flags.Cleanup() 386 } 387 }() 388 389 if c.IsSet("cache") { 390 cache := c.String("cache") 391 cacheArgs := strings.Split(c.String("cache"), ":") 392 cacheDir := cacheArgs[len(cacheArgs)-1] 393 cacheArgs = cacheArgs[:len(cacheArgs)-1] 394 395 fi, err := os.Stat(cacheDir) 396 if err != nil || !fi.IsDir() { 397 io.WriteString(cli.ErrWriter, 398 fmt.Sprintf("Invalid value \"%v\" for --cache: not a directory\n\n", 399 cacheDir)) 400 return nil 401 } 402 403 if _, ok := flags.MountOptions["allow_other"]; !ok { 404 flags.MountPointCreated, err = ioutil.TempDir("", ".goofys-mnt") 405 if err != nil { 406 io.WriteString(cli.ErrWriter, 407 fmt.Sprintf("Unable to create temp dir: %v", err)) 408 return nil 409 } 410 flags.MountPoint = flags.MountPointCreated 411 } 412 413 cacheArgs = append([]string{"--test", "-f"}, cacheArgs...) 414 415 if flags.MountPointArg == flags.MountPoint { 416 cacheArgs = append(cacheArgs, "-ononempty") 417 } 418 419 cacheArgs = append(cacheArgs, "--") 420 cacheArgs = append(cacheArgs, flags.MountPoint) 421 cacheArgs = append(cacheArgs, cacheDir) 422 cacheArgs = append(cacheArgs, flags.MountPointArg) 423 424 fuseLog.Debugf("catfs %v", cacheArgs) 425 catfs := exec.Command("catfs", cacheArgs...) 426 _, err = catfs.Output() 427 if err != nil { 428 if ee, ok := err.(*exec.Error); ok { 429 io.WriteString(cli.ErrWriter, 430 fmt.Sprintf("--cache requires catfs (%v) but %v\n\n", 431 "http://github.com/kahing/catfs", 432 ee.Error())) 433 } else if ee, ok := err.(*exec.ExitError); ok { 434 io.WriteString(cli.ErrWriter, 435 fmt.Sprintf("Invalid value \"%v\" for --cache: %v\n\n", 436 cache, string(ee.Stderr))) 437 } 438 return nil 439 } 440 441 flags.Cache = cacheArgs[1:] 442 } 443 444 return flags 445 } 446 447 func MassageMountFlags(args []string) (ret []string) { 448 if len(args) == 5 && args[3] == "-o" { 449 // looks like it's coming from fstab! 450 mountOptions := "" 451 ret = append(ret, args[0]) 452 453 for _, p := range strings.Split(args[4], ",") { 454 if strings.HasPrefix(p, "-") { 455 ret = append(ret, p) 456 } else { 457 mountOptions += p 458 mountOptions += "," 459 } 460 } 461 462 if len(mountOptions) != 0 { 463 // remove trailing , 464 mountOptions = mountOptions[:len(mountOptions)-1] 465 ret = append(ret, "-o") 466 ret = append(ret, mountOptions) 467 } 468 469 ret = append(ret, args[1]) 470 ret = append(ret, args[2]) 471 } else { 472 return args 473 } 474 475 return 476 }