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