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