github.com/wayscript/goofys@v0.24.0/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  			/////////////////////////
   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  }