github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/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  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"path"
    14  	"regexp"
    15  	"sort"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  	"time"
    20  
    21  	"github.com/pkg/errors"
    22  	"github.com/rclone/rclone/cmd"
    23  	"github.com/rclone/rclone/cmd/info/internal"
    24  	"github.com/rclone/rclone/fs"
    25  	"github.com/rclone/rclone/fs/config/flags"
    26  	"github.com/rclone/rclone/fs/hash"
    27  	"github.com/rclone/rclone/fs/object"
    28  	"github.com/rclone/rclone/lib/random"
    29  	"github.com/spf13/cobra"
    30  )
    31  
    32  var (
    33  	writeJSON          string
    34  	checkNormalization bool
    35  	checkControl       bool
    36  	checkLength        bool
    37  	checkStreaming     bool
    38  	uploadWait         time.Duration
    39  	positionLeftRe     = regexp.MustCompile(`(?s)^(.*)-position-left-([[:xdigit:]]+)$`)
    40  	positionMiddleRe   = regexp.MustCompile(`(?s)^position-middle-([[:xdigit:]]+)-(.*)-$`)
    41  	positionRightRe    = regexp.MustCompile(`(?s)^position-right-([[:xdigit:]]+)-(.*)$`)
    42  )
    43  
    44  func init() {
    45  	cmd.Root.AddCommand(commandDefinition)
    46  	cmdFlags := commandDefinition.Flags()
    47  	flags.StringVarP(cmdFlags, &writeJSON, "write-json", "", "", "Write results to file.")
    48  	flags.BoolVarP(cmdFlags, &checkNormalization, "check-normalization", "", true, "Check UTF-8 Normalization.")
    49  	flags.BoolVarP(cmdFlags, &checkControl, "check-control", "", true, "Check control characters.")
    50  	flags.DurationVarP(cmdFlags, &uploadWait, "upload-wait", "", 0, "Wait after writing a file.")
    51  	flags.BoolVarP(cmdFlags, &checkLength, "check-length", "", true, "Check max filename length.")
    52  	flags.BoolVarP(cmdFlags, &checkStreaming, "check-streaming", "", true, "Check uploadxs with indeterminate file size.")
    53  }
    54  
    55  var commandDefinition = &cobra.Command{
    56  	Use:   "info [remote:path]+",
    57  	Short: `Discovers file name or other limitations for paths.`,
    58  	Long: `rclone info discovers what filenames and upload methods are possible
    59  to write to the paths passed in and how long they can be.  It can take some
    60  time.  It will write test files into the remote:path passed in.  It outputs
    61  a bit of go code for each one.
    62  `,
    63  	Hidden: true,
    64  	Run: func(command *cobra.Command, args []string) {
    65  		cmd.CheckArgs(1, 1e6, command, args)
    66  		for i := range args {
    67  			f := cmd.NewFsDir(args[i : i+1])
    68  			cmd.Run(false, false, command, func() error {
    69  				return readInfo(context.Background(), f)
    70  			})
    71  		}
    72  	},
    73  }
    74  
    75  type results struct {
    76  	ctx                  context.Context
    77  	f                    fs.Fs
    78  	mu                   sync.Mutex
    79  	stringNeedsEscaping  map[string]internal.Position
    80  	controlResults       map[string]internal.ControlResult
    81  	maxFileLength        int
    82  	canWriteUnnormalized bool
    83  	canReadUnnormalized  bool
    84  	canReadRenormalized  bool
    85  	canStream            bool
    86  }
    87  
    88  func newResults(ctx context.Context, f fs.Fs) *results {
    89  	return &results{
    90  		ctx:                 ctx,
    91  		f:                   f,
    92  		stringNeedsEscaping: make(map[string]internal.Position),
    93  		controlResults:      make(map[string]internal.ControlResult),
    94  	}
    95  }
    96  
    97  // Print the results to stdout
    98  func (r *results) Print() {
    99  	fmt.Printf("// %s\n", r.f.Name())
   100  	if checkControl {
   101  		escape := []string{}
   102  		for c, needsEscape := range r.stringNeedsEscaping {
   103  			if needsEscape != internal.PositionNone {
   104  				k := strconv.Quote(c)
   105  				k = k[1 : len(k)-1]
   106  				escape = append(escape, fmt.Sprintf("'%s'", k))
   107  			}
   108  		}
   109  		sort.Strings(escape)
   110  		fmt.Printf("stringNeedsEscaping = []rune{\n")
   111  		fmt.Printf("\t%s\n", strings.Join(escape, ", "))
   112  		fmt.Printf("}\n")
   113  	}
   114  	if checkLength {
   115  		fmt.Printf("maxFileLength = %d\n", r.maxFileLength)
   116  	}
   117  	if checkNormalization {
   118  		fmt.Printf("canWriteUnnormalized = %v\n", r.canWriteUnnormalized)
   119  		fmt.Printf("canReadUnnormalized   = %v\n", r.canReadUnnormalized)
   120  		fmt.Printf("canReadRenormalized   = %v\n", r.canReadRenormalized)
   121  	}
   122  	if checkStreaming {
   123  		fmt.Printf("canStream = %v\n", r.canStream)
   124  	}
   125  }
   126  
   127  // WriteJSON writes the results to a JSON file when requested
   128  func (r *results) WriteJSON() {
   129  	if writeJSON == "" {
   130  		return
   131  	}
   132  
   133  	report := internal.InfoReport{
   134  		Remote: r.f.Name(),
   135  	}
   136  	if checkControl {
   137  		report.ControlCharacters = &r.controlResults
   138  	}
   139  	if checkLength {
   140  		report.MaxFileLength = &r.maxFileLength
   141  	}
   142  	if checkNormalization {
   143  		report.CanWriteUnnormalized = &r.canWriteUnnormalized
   144  		report.CanReadUnnormalized = &r.canReadUnnormalized
   145  		report.CanReadRenormalized = &r.canReadRenormalized
   146  	}
   147  	if checkStreaming {
   148  		report.CanStream = &r.canStream
   149  	}
   150  
   151  	if f, err := os.Create(writeJSON); err != nil {
   152  		fs.Errorf(r.f, "Creating JSON file failed: %s", err)
   153  	} else {
   154  		defer fs.CheckClose(f, &err)
   155  		enc := json.NewEncoder(f)
   156  		enc.SetIndent("", "  ")
   157  		err := enc.Encode(report)
   158  		if err != nil {
   159  			fs.Errorf(r.f, "Writing JSON file failed: %s", err)
   160  		}
   161  	}
   162  	fs.Infof(r.f, "Wrote JSON file: %s", writeJSON)
   163  }
   164  
   165  // writeFile writes a file with some random contents
   166  func (r *results) writeFile(path string) (fs.Object, error) {
   167  	contents := random.String(50)
   168  	src := object.NewStaticObjectInfo(path, time.Now(), int64(len(contents)), true, nil, r.f)
   169  	obj, err := r.f.Put(r.ctx, bytes.NewBufferString(contents), src)
   170  	if uploadWait > 0 {
   171  		time.Sleep(uploadWait)
   172  	}
   173  	return obj, err
   174  }
   175  
   176  // check whether normalization is enforced and check whether it is
   177  // done on the files anyway
   178  func (r *results) checkUTF8Normalization() {
   179  	unnormalized := "Héroique"
   180  	normalized := "Héroique"
   181  	_, err := r.writeFile(unnormalized)
   182  	if err != nil {
   183  		r.canWriteUnnormalized = false
   184  		return
   185  	}
   186  	r.canWriteUnnormalized = true
   187  	_, err = r.f.NewObject(r.ctx, unnormalized)
   188  	if err == nil {
   189  		r.canReadUnnormalized = true
   190  	}
   191  	_, err = r.f.NewObject(r.ctx, normalized)
   192  	if err == nil {
   193  		r.canReadRenormalized = true
   194  	}
   195  }
   196  
   197  func (r *results) checkStringPositions(k, s string) {
   198  	fs.Infof(r.f, "Writing position file 0x%0X", s)
   199  	positionError := internal.PositionNone
   200  	res := internal.ControlResult{
   201  		Text:       s,
   202  		WriteError: make(map[internal.Position]string, 3),
   203  		GetError:   make(map[internal.Position]string, 3),
   204  		InList:     make(map[internal.Position]internal.Presence, 3),
   205  	}
   206  
   207  	for _, pos := range internal.PositionList {
   208  		path := ""
   209  		switch pos {
   210  		case internal.PositionMiddle:
   211  			path = fmt.Sprintf("position-middle-%0X-%s-", s, s)
   212  		case internal.PositionLeft:
   213  			path = fmt.Sprintf("%s-position-left-%0X", s, s)
   214  		case internal.PositionRight:
   215  			path = fmt.Sprintf("position-right-%0X-%s", s, s)
   216  		default:
   217  			panic("invalid position: " + pos.String())
   218  		}
   219  		_, writeError := r.writeFile(path)
   220  		if writeError != nil {
   221  			res.WriteError[pos] = writeError.Error()
   222  			fs.Infof(r.f, "Writing %s position file 0x%0X Error: %s", pos.String(), s, writeError)
   223  		} else {
   224  			fs.Infof(r.f, "Writing %s position file 0x%0X OK", pos.String(), s)
   225  		}
   226  		obj, getErr := r.f.NewObject(r.ctx, path)
   227  		if getErr != nil {
   228  			res.GetError[pos] = getErr.Error()
   229  			fs.Infof(r.f, "Getting %s position file 0x%0X Error: %s", pos.String(), s, getErr)
   230  		} else {
   231  			if obj.Size() != 50 {
   232  				res.GetError[pos] = fmt.Sprintf("invalid size %d", obj.Size())
   233  				fs.Infof(r.f, "Getting %s position file 0x%0X Invalid Size: %d", pos.String(), s, obj.Size())
   234  			} else {
   235  				fs.Infof(r.f, "Getting %s position file 0x%0X OK", pos.String(), s)
   236  			}
   237  		}
   238  		if writeError != nil || getErr != nil {
   239  			positionError += pos
   240  		}
   241  	}
   242  
   243  	r.mu.Lock()
   244  	r.stringNeedsEscaping[k] = positionError
   245  	r.controlResults[k] = res
   246  	r.mu.Unlock()
   247  }
   248  
   249  // check we can write a file with the control chars
   250  func (r *results) checkControls() {
   251  	fs.Infof(r.f, "Trying to create control character file names")
   252  	// Concurrency control
   253  	tokens := make(chan struct{}, fs.Config.Checkers)
   254  	for i := 0; i < fs.Config.Checkers; i++ {
   255  		tokens <- struct{}{}
   256  	}
   257  	var wg sync.WaitGroup
   258  	for i := rune(0); i < 128; i++ {
   259  		s := string(i)
   260  		if i == 0 || i == '/' {
   261  			// We're not even going to check NULL or /
   262  			r.stringNeedsEscaping[s] = internal.PositionAll
   263  			continue
   264  		}
   265  		wg.Add(1)
   266  		go func(s string) {
   267  			defer wg.Done()
   268  			token := <-tokens
   269  			k := s
   270  			r.checkStringPositions(k, s)
   271  			tokens <- token
   272  		}(s)
   273  	}
   274  	for _, s := range []string{"\", "\u00A0", "\xBF", "\xFE"} {
   275  		wg.Add(1)
   276  		go func(s string) {
   277  			defer wg.Done()
   278  			token := <-tokens
   279  			k := s
   280  			r.checkStringPositions(k, s)
   281  			tokens <- token
   282  		}(s)
   283  	}
   284  	wg.Wait()
   285  	r.checkControlsList()
   286  	fs.Infof(r.f, "Done trying to create control character file names")
   287  }
   288  
   289  func (r *results) checkControlsList() {
   290  	l, err := r.f.List(context.TODO(), "")
   291  	if err != nil {
   292  		fs.Errorf(r.f, "Listing control character file names failed: %s", err)
   293  		return
   294  	}
   295  
   296  	namesMap := make(map[string]struct{}, len(l))
   297  	for _, s := range l {
   298  		namesMap[path.Base(s.Remote())] = struct{}{}
   299  	}
   300  
   301  	for path := range namesMap {
   302  		var pos internal.Position
   303  		var hex, value string
   304  		if g := positionLeftRe.FindStringSubmatch(path); g != nil {
   305  			pos, hex, value = internal.PositionLeft, g[2], g[1]
   306  		} else if g := positionMiddleRe.FindStringSubmatch(path); g != nil {
   307  			pos, hex, value = internal.PositionMiddle, g[1], g[2]
   308  		} else if g := positionRightRe.FindStringSubmatch(path); g != nil {
   309  			pos, hex, value = internal.PositionRight, g[1], g[2]
   310  		} else {
   311  			fs.Infof(r.f, "Unknown path %q", path)
   312  			continue
   313  		}
   314  		var hexValue []byte
   315  		for ; len(hex) >= 2; hex = hex[2:] {
   316  			if b, err := strconv.ParseUint(hex[:2], 16, 8); err != nil {
   317  				fs.Infof(r.f, "Invalid path %q: %s", path, err)
   318  				continue
   319  			} else {
   320  				hexValue = append(hexValue, byte(b))
   321  			}
   322  		}
   323  		if hex != "" {
   324  			fs.Infof(r.f, "Invalid path %q", path)
   325  			continue
   326  		}
   327  
   328  		hexStr := string(hexValue)
   329  		k := hexStr
   330  		switch r.controlResults[k].InList[pos] {
   331  		case internal.Absent:
   332  			if hexStr == value {
   333  				r.controlResults[k].InList[pos] = internal.Present
   334  			} else {
   335  				r.controlResults[k].InList[pos] = internal.Renamed
   336  			}
   337  		case internal.Present:
   338  			r.controlResults[k].InList[pos] = internal.Multiple
   339  		case internal.Renamed:
   340  			r.controlResults[k].InList[pos] = internal.Multiple
   341  		}
   342  		delete(namesMap, path)
   343  	}
   344  
   345  	if len(namesMap) > 0 {
   346  		fs.Infof(r.f, "Found additional control character file names:")
   347  		for name := range namesMap {
   348  			fs.Infof(r.f, "%q", name)
   349  		}
   350  	}
   351  }
   352  
   353  // find the max file name size we can use
   354  func (r *results) findMaxLength() {
   355  	const maxLen = 16 * 1024
   356  	name := make([]byte, maxLen)
   357  	for i := range name {
   358  		name[i] = 'a'
   359  	}
   360  	// Find the first size of filename we can't write
   361  	i := sort.Search(len(name), func(i int) (fail bool) {
   362  		defer func() {
   363  			if err := recover(); err != nil {
   364  				fs.Infof(r.f, "Couldn't write file with name length %d: %v", i, err)
   365  				fail = true
   366  			}
   367  		}()
   368  
   369  		path := string(name[:i])
   370  		_, err := r.writeFile(path)
   371  		if err != nil {
   372  			fs.Infof(r.f, "Couldn't write file with name length %d: %v", i, err)
   373  			return true
   374  		}
   375  		fs.Infof(r.f, "Wrote file with name length %d", i)
   376  		return false
   377  	})
   378  	r.maxFileLength = i - 1
   379  	fs.Infof(r.f, "Max file length is %d", r.maxFileLength)
   380  }
   381  
   382  func (r *results) checkStreaming() {
   383  	putter := r.f.Put
   384  	if r.f.Features().PutStream != nil {
   385  		fs.Infof(r.f, "Given remote has specialized streaming function. Using that to test streaming.")
   386  		putter = r.f.Features().PutStream
   387  	}
   388  
   389  	contents := "thinking of test strings is hard"
   390  	buf := bytes.NewBufferString(contents)
   391  	hashIn := hash.NewMultiHasher()
   392  	in := io.TeeReader(buf, hashIn)
   393  
   394  	objIn := object.NewStaticObjectInfo("checkStreamingTest", time.Now(), -1, true, nil, r.f)
   395  	objR, err := putter(r.ctx, in, objIn)
   396  	if err != nil {
   397  		fs.Infof(r.f, "Streamed file failed to upload (%v)", err)
   398  		r.canStream = false
   399  		return
   400  	}
   401  
   402  	hashes := hashIn.Sums()
   403  	types := objR.Fs().Hashes().Array()
   404  	for _, Hash := range types {
   405  		sum, err := objR.Hash(r.ctx, Hash)
   406  		if err != nil {
   407  			fs.Infof(r.f, "Streamed file failed when getting hash %v (%v)", Hash, err)
   408  			r.canStream = false
   409  			return
   410  		}
   411  		if !hash.Equals(hashes[Hash], sum) {
   412  			fs.Infof(r.f, "Streamed file has incorrect hash %v: expecting %q got %q", Hash, hashes[Hash], sum)
   413  			r.canStream = false
   414  			return
   415  		}
   416  	}
   417  	if int64(len(contents)) != objR.Size() {
   418  		fs.Infof(r.f, "Streamed file has incorrect file size: expecting %d got %d", len(contents), objR.Size())
   419  		r.canStream = false
   420  		return
   421  	}
   422  	r.canStream = true
   423  }
   424  
   425  func readInfo(ctx context.Context, f fs.Fs) error {
   426  	err := f.Mkdir(ctx, "")
   427  	if err != nil {
   428  		return errors.Wrap(err, "couldn't mkdir")
   429  	}
   430  	r := newResults(ctx, f)
   431  	if checkControl {
   432  		r.checkControls()
   433  	}
   434  	if checkLength {
   435  		r.findMaxLength()
   436  	}
   437  	if checkNormalization {
   438  		r.checkUTF8Normalization()
   439  	}
   440  	if checkStreaming {
   441  		r.checkStreaming()
   442  	}
   443  	r.Print()
   444  	r.WriteJSON()
   445  	return nil
   446  }