github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/cmd/info/info.go (about)

     1  package info
     2  
     3  // FIXME once translations are implemented will need a no-escape
     4  // option for Put so we can make these tests work agaig
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"sort"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/ncw/rclone/cmd"
    17  	"github.com/ncw/rclone/fs"
    18  	"github.com/ncw/rclone/fs/hash"
    19  	"github.com/ncw/rclone/fs/object"
    20  	"github.com/ncw/rclone/fstest"
    21  	"github.com/pkg/errors"
    22  	"github.com/spf13/cobra"
    23  )
    24  
    25  type position int
    26  
    27  const (
    28  	positionMiddle position = 1 << iota
    29  	positionLeft
    30  	positionRight
    31  	positionNone position = 0
    32  	positionAll  position = positionRight<<1 - 1
    33  )
    34  
    35  var (
    36  	checkNormalization bool
    37  	checkControl       bool
    38  	checkLength        bool
    39  	checkStreaming     bool
    40  	positionList       = []position{positionMiddle, positionLeft, positionRight}
    41  )
    42  
    43  func init() {
    44  	cmd.Root.AddCommand(commandDefintion)
    45  	commandDefintion.Flags().BoolVarP(&checkNormalization, "check-normalization", "", true, "Check UTF-8 Normalization.")
    46  	commandDefintion.Flags().BoolVarP(&checkControl, "check-control", "", true, "Check control characters.")
    47  	commandDefintion.Flags().BoolVarP(&checkLength, "check-length", "", true, "Check max filename length.")
    48  	commandDefintion.Flags().BoolVarP(&checkStreaming, "check-streaming", "", true, "Check uploads with indeterminate file size.")
    49  }
    50  
    51  var commandDefintion = &cobra.Command{
    52  	Use:   "info [remote:path]+",
    53  	Short: `Discovers file name or other limitations for paths.`,
    54  	Long: `rclone info discovers what filenames and upload methods are possible
    55  to write to the paths passed in and how long they can be.  It can take some
    56  time.  It will write test files into the remote:path passed in.  It outputs
    57  a bit of go code for each one.
    58  `,
    59  	Hidden: true,
    60  	Run: func(command *cobra.Command, args []string) {
    61  		cmd.CheckArgs(1, 1E6, command, args)
    62  		for i := range args {
    63  			f := cmd.NewFsDir(args[i : i+1])
    64  			cmd.Run(false, false, command, func() error {
    65  				return readInfo(context.Background(), f)
    66  			})
    67  		}
    68  	},
    69  }
    70  
    71  type results struct {
    72  	ctx                  context.Context
    73  	f                    fs.Fs
    74  	mu                   sync.Mutex
    75  	stringNeedsEscaping  map[string]position
    76  	maxFileLength        int
    77  	canWriteUnnormalized bool
    78  	canReadUnnormalized  bool
    79  	canReadRenormalized  bool
    80  	canStream            bool
    81  }
    82  
    83  func newResults(ctx context.Context, f fs.Fs) *results {
    84  	return &results{
    85  		ctx:                 ctx,
    86  		f:                   f,
    87  		stringNeedsEscaping: make(map[string]position),
    88  	}
    89  }
    90  
    91  // Print the results to stdout
    92  func (r *results) Print() {
    93  	fmt.Printf("// %s\n", r.f.Name())
    94  	if checkControl {
    95  		escape := []string{}
    96  		for c, needsEscape := range r.stringNeedsEscaping {
    97  			if needsEscape != positionNone {
    98  				escape = append(escape, fmt.Sprintf("0x%02X", c))
    99  			}
   100  		}
   101  		sort.Strings(escape)
   102  		fmt.Printf("stringNeedsEscaping = []byte{\n")
   103  		fmt.Printf("\t%s\n", strings.Join(escape, ", "))
   104  		fmt.Printf("}\n")
   105  	}
   106  	if checkLength {
   107  		fmt.Printf("maxFileLength = %d\n", r.maxFileLength)
   108  	}
   109  	if checkNormalization {
   110  		fmt.Printf("canWriteUnnormalized = %v\n", r.canWriteUnnormalized)
   111  		fmt.Printf("canReadUnnormalized   = %v\n", r.canReadUnnormalized)
   112  		fmt.Printf("canReadRenormalized   = %v\n", r.canReadRenormalized)
   113  	}
   114  	if checkStreaming {
   115  		fmt.Printf("canStream = %v\n", r.canStream)
   116  	}
   117  }
   118  
   119  // writeFile writes a file with some random contents
   120  func (r *results) writeFile(path string) (fs.Object, error) {
   121  	contents := fstest.RandomString(50)
   122  	src := object.NewStaticObjectInfo(path, time.Now(), int64(len(contents)), true, nil, r.f)
   123  	return r.f.Put(r.ctx, bytes.NewBufferString(contents), src)
   124  }
   125  
   126  // check whether normalization is enforced and check whether it is
   127  // done on the files anyway
   128  func (r *results) checkUTF8Normalization() {
   129  	unnormalized := "Héroique"
   130  	normalized := "Héroique"
   131  	_, err := r.writeFile(unnormalized)
   132  	if err != nil {
   133  		r.canWriteUnnormalized = false
   134  		return
   135  	}
   136  	r.canWriteUnnormalized = true
   137  	_, err = r.f.NewObject(r.ctx, unnormalized)
   138  	if err == nil {
   139  		r.canReadUnnormalized = true
   140  	}
   141  	_, err = r.f.NewObject(r.ctx, normalized)
   142  	if err == nil {
   143  		r.canReadRenormalized = true
   144  	}
   145  }
   146  
   147  func (r *results) checkStringPositions(s string) {
   148  	fs.Infof(r.f, "Writing position file 0x%0X", s)
   149  	positionError := positionNone
   150  
   151  	for _, pos := range positionList {
   152  		path := ""
   153  		switch pos {
   154  		case positionMiddle:
   155  			path = fmt.Sprintf("position-middle-%0X-%s-", s, s)
   156  		case positionLeft:
   157  			path = fmt.Sprintf("%s-position-left-%0X", s, s)
   158  		case positionRight:
   159  			path = fmt.Sprintf("position-right-%0X-%s", s, s)
   160  		default:
   161  			panic("invalid position: " + pos.String())
   162  		}
   163  		_, writeErr := r.writeFile(path)
   164  		if writeErr != nil {
   165  			fs.Infof(r.f, "Writing %s position file 0x%0X Error: %s", pos.String(), s, writeErr)
   166  		} else {
   167  			fs.Infof(r.f, "Writing %s position file 0x%0X OK", pos.String(), s)
   168  		}
   169  		obj, getErr := r.f.NewObject(r.ctx, path)
   170  		if getErr != nil {
   171  			fs.Infof(r.f, "Getting %s position file 0x%0X Error: %s", pos.String(), s, getErr)
   172  		} else {
   173  			if obj.Size() != 50 {
   174  				fs.Infof(r.f, "Getting %s position file 0x%0X Invalid Size: %d", pos.String(), s, obj.Size())
   175  			} else {
   176  				fs.Infof(r.f, "Getting %s position file 0x%0X OK", pos.String(), s)
   177  			}
   178  		}
   179  		if writeErr != nil || getErr != nil {
   180  			positionError += pos
   181  		}
   182  	}
   183  
   184  	r.mu.Lock()
   185  	r.stringNeedsEscaping[s] = positionError
   186  	r.mu.Unlock()
   187  }
   188  
   189  // check we can write a file with the control chars
   190  func (r *results) checkControls() {
   191  	fs.Infof(r.f, "Trying to create control character file names")
   192  	// Concurrency control
   193  	tokens := make(chan struct{}, fs.Config.Checkers)
   194  	for i := 0; i < fs.Config.Checkers; i++ {
   195  		tokens <- struct{}{}
   196  	}
   197  	var wg sync.WaitGroup
   198  	for i := rune(0); i < 128; i++ {
   199  		s := string(i)
   200  		if i == 0 || i == '/' {
   201  			// We're not even going to check NULL or /
   202  			r.stringNeedsEscaping[s] = positionAll
   203  			continue
   204  		}
   205  		wg.Add(1)
   206  		go func(s string) {
   207  			defer wg.Done()
   208  			token := <-tokens
   209  			r.checkStringPositions(s)
   210  			tokens <- token
   211  		}(s)
   212  	}
   213  	for _, s := range []string{"\", "\xBF", "\xFE"} {
   214  		wg.Add(1)
   215  		go func(s string) {
   216  			defer wg.Done()
   217  			token := <-tokens
   218  			r.checkStringPositions(s)
   219  			tokens <- token
   220  		}(s)
   221  	}
   222  	wg.Wait()
   223  	fs.Infof(r.f, "Done trying to create control character file names")
   224  }
   225  
   226  // find the max file name size we can use
   227  func (r *results) findMaxLength() {
   228  	const maxLen = 16 * 1024
   229  	name := make([]byte, maxLen)
   230  	for i := range name {
   231  		name[i] = 'a'
   232  	}
   233  	// Find the first size of filename we can't write
   234  	i := sort.Search(len(name), func(i int) (fail bool) {
   235  		defer func() {
   236  			if err := recover(); err != nil {
   237  				fs.Infof(r.f, "Couldn't write file with name length %d: %v", i, err)
   238  				fail = true
   239  			}
   240  		}()
   241  
   242  		path := string(name[:i])
   243  		_, err := r.writeFile(path)
   244  		if err != nil {
   245  			fs.Infof(r.f, "Couldn't write file with name length %d: %v", i, err)
   246  			return true
   247  		}
   248  		fs.Infof(r.f, "Wrote file with name length %d", i)
   249  		return false
   250  	})
   251  	r.maxFileLength = i - 1
   252  	fs.Infof(r.f, "Max file length is %d", r.maxFileLength)
   253  }
   254  
   255  func (r *results) checkStreaming() {
   256  	putter := r.f.Put
   257  	if r.f.Features().PutStream != nil {
   258  		fs.Infof(r.f, "Given remote has specialized streaming function. Using that to test streaming.")
   259  		putter = r.f.Features().PutStream
   260  	}
   261  
   262  	contents := "thinking of test strings is hard"
   263  	buf := bytes.NewBufferString(contents)
   264  	hashIn := hash.NewMultiHasher()
   265  	in := io.TeeReader(buf, hashIn)
   266  
   267  	objIn := object.NewStaticObjectInfo("checkStreamingTest", time.Now(), -1, true, nil, r.f)
   268  	objR, err := putter(r.ctx, in, objIn)
   269  	if err != nil {
   270  		fs.Infof(r.f, "Streamed file failed to upload (%v)", err)
   271  		r.canStream = false
   272  		return
   273  	}
   274  
   275  	hashes := hashIn.Sums()
   276  	types := objR.Fs().Hashes().Array()
   277  	for _, Hash := range types {
   278  		sum, err := objR.Hash(r.ctx, Hash)
   279  		if err != nil {
   280  			fs.Infof(r.f, "Streamed file failed when getting hash %v (%v)", Hash, err)
   281  			r.canStream = false
   282  			return
   283  		}
   284  		if !hash.Equals(hashes[Hash], sum) {
   285  			fs.Infof(r.f, "Streamed file has incorrect hash %v: expecting %q got %q", Hash, hashes[Hash], sum)
   286  			r.canStream = false
   287  			return
   288  		}
   289  	}
   290  	if int64(len(contents)) != objR.Size() {
   291  		fs.Infof(r.f, "Streamed file has incorrect file size: expecting %d got %d", len(contents), objR.Size())
   292  		r.canStream = false
   293  		return
   294  	}
   295  	r.canStream = true
   296  }
   297  
   298  func readInfo(ctx context.Context, f fs.Fs) error {
   299  	err := f.Mkdir(ctx, "")
   300  	if err != nil {
   301  		return errors.Wrap(err, "couldn't mkdir")
   302  	}
   303  	r := newResults(ctx, f)
   304  	if checkControl {
   305  		r.checkControls()
   306  	}
   307  	if checkLength {
   308  		r.findMaxLength()
   309  	}
   310  	if checkNormalization {
   311  		r.checkUTF8Normalization()
   312  	}
   313  	if checkStreaming {
   314  		r.checkStreaming()
   315  	}
   316  	r.Print()
   317  	return nil
   318  }
   319  
   320  func (e position) String() string {
   321  	switch e {
   322  	case positionNone:
   323  		return "none"
   324  	case positionAll:
   325  		return "all"
   326  	}
   327  	var buf bytes.Buffer
   328  	if e&positionMiddle != 0 {
   329  		buf.WriteString("middle")
   330  		e &= ^positionMiddle
   331  	}
   332  	if e&positionLeft != 0 {
   333  		if buf.Len() != 0 {
   334  			buf.WriteRune(',')
   335  		}
   336  		buf.WriteString("left")
   337  		e &= ^positionLeft
   338  	}
   339  	if e&positionRight != 0 {
   340  		if buf.Len() != 0 {
   341  			buf.WriteRune(',')
   342  		}
   343  		buf.WriteString("right")
   344  		e &= ^positionRight
   345  	}
   346  	if e != positionNone {
   347  		panic("invalid position")
   348  	}
   349  	return buf.String()
   350  }