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  }