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  }