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 }