github.com/sagansystems/goofys-app@v0.19.1-0.20180410053237-b2302fdf5af9/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  
   221  			/////////////////////////
   222  			// Debugging
   223  			/////////////////////////
   224  
   225  			cli.BoolFlag{
   226  				Name:  "debug_fuse",
   227  				Usage: "Enable fuse-related debugging output.",
   228  			},
   229  
   230  			cli.BoolFlag{
   231  				Name:  "debug_s3",
   232  				Usage: "Enable S3-related debugging output.",
   233  			},
   234  
   235  			cli.BoolFlag{
   236  				Name:  "f",
   237  				Usage: "Run goofys in foreground.",
   238  			},
   239  		},
   240  	}
   241  
   242  	var funcMap = template.FuncMap{
   243  		"category": filterCategory,
   244  		"join":     strings.Join,
   245  	}
   246  
   247  	flagCategories = map[string]string{}
   248  
   249  	for _, f := range []string{"region", "sse", "sse-kms", "storage-class", "acl"} {
   250  		flagCategories[f] = "aws"
   251  	}
   252  
   253  	for _, f := range []string{"cheap", "no-implicit-dir", "stat-cache-ttl", "type-cache-ttl"} {
   254  		flagCategories[f] = "tuning"
   255  	}
   256  
   257  	for _, f := range []string{"help, h", "debug_fuse", "debug_s3", "version, v", "f"} {
   258  		flagCategories[f] = "misc"
   259  	}
   260  
   261  	cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) {
   262  		w = tabwriter.NewWriter(w, 1, 8, 2, ' ', 0)
   263  		var tmplGet = template.Must(template.New("help").Funcs(funcMap).Parse(templ))
   264  		tmplGet.Execute(w, app)
   265  	}
   266  
   267  	return
   268  }
   269  
   270  type FlagStorage struct {
   271  	// File system
   272  	MountOptions      map[string]string
   273  	MountPoint        string
   274  	MountPointArg     string
   275  	MountPointCreated string
   276  
   277  	Cache    []string
   278  	DirMode  os.FileMode
   279  	FileMode os.FileMode
   280  	Uid      uint32
   281  	Gid      uint32
   282  
   283  	// S3
   284  	Endpoint       string
   285  	Region         string
   286  	RegionSet      bool
   287  	StorageClass   string
   288  	Profile        string
   289  	UseContentType bool
   290  	UseSSE         bool
   291  	UseKMS         bool
   292  	KMSKeyID       string
   293  	ACL            string
   294  
   295  	// Tuning
   296  	Cheap        bool
   297  	ExplicitDir  bool
   298  	StatCacheTTL time.Duration
   299  	TypeCacheTTL time.Duration
   300  
   301  	// Debugging
   302  	DebugFuse  bool
   303  	DebugS3    bool
   304  	Foreground bool
   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  func (flags *FlagStorage) Cleanup() {
   331  	if flags.MountPointCreated != "" && flags.MountPointCreated != flags.MountPointArg {
   332  		err := os.Remove(flags.MountPointCreated)
   333  		if err != nil {
   334  			log.Errorf("rmdir %v = %v", flags.MountPointCreated, err)
   335  		}
   336  	}
   337  }
   338  
   339  // Add the flags accepted by run to the supplied flag set, returning the
   340  // variables into which the flags will parse.
   341  func PopulateFlags(c *cli.Context) (ret *FlagStorage) {
   342  	flags := &FlagStorage{
   343  		// File system
   344  		MountOptions: make(map[string]string),
   345  		DirMode:      os.FileMode(c.Int("dir-mode")),
   346  		FileMode:     os.FileMode(c.Int("file-mode")),
   347  		Uid:          uint32(c.Int("uid")),
   348  		Gid:          uint32(c.Int("gid")),
   349  
   350  		// Tuning,
   351  		Cheap:        c.Bool("cheap"),
   352  		ExplicitDir:  c.Bool("no-implicit-dir"),
   353  		StatCacheTTL: c.Duration("stat-cache-ttl"),
   354  		TypeCacheTTL: c.Duration("type-cache-ttl"),
   355  
   356  		// S3
   357  		Endpoint:       c.String("endpoint"),
   358  		Region:         c.String("region"),
   359  		RegionSet:      c.IsSet("region"),
   360  		StorageClass:   c.String("storage-class"),
   361  		Profile:        c.String("profile"),
   362  		UseContentType: c.Bool("use-content-type"),
   363  		UseSSE:         c.Bool("sse"),
   364  		UseKMS:         c.IsSet("sse-kms"),
   365  		KMSKeyID:       c.String("sse-kms"),
   366  		ACL:            c.String("acl"),
   367  
   368  		// Debugging,
   369  		DebugFuse:  c.Bool("debug_fuse"),
   370  		DebugS3:    c.Bool("debug_s3"),
   371  		Foreground: c.Bool("f"),
   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  	// KMS implies SSE
   445  	if flags.UseKMS {
   446  		flags.UseSSE = true
   447  	}
   448  
   449  	return flags
   450  }
   451  
   452  func MassageMountFlags(args []string) (ret []string) {
   453  	if len(args) == 5 && args[3] == "-o" {
   454  		// looks like it's coming from fstab!
   455  		mountOptions := ""
   456  		ret = append(ret, args[0])
   457  
   458  		for _, p := range strings.Split(args[4], ",") {
   459  			if strings.HasPrefix(p, "-") {
   460  				ret = append(ret, p)
   461  			} else {
   462  				mountOptions += p
   463  				mountOptions += ","
   464  			}
   465  		}
   466  
   467  		if len(mountOptions) != 0 {
   468  			// remove trailing ,
   469  			mountOptions = mountOptions[:len(mountOptions)-1]
   470  			ret = append(ret, "-o")
   471  			ret = append(ret, mountOptions)
   472  		}
   473  
   474  		ret = append(ret, args[1])
   475  		ret = append(ret, args[2])
   476  	} else {
   477  		return args
   478  	}
   479  
   480  	return
   481  }