github.com/Files-com/files-sdk-go/v2@v2.1.2/file/remotefs.go (about) 1 package file 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 goFs "io/fs" 11 "math" 12 "math/rand" 13 "net/http" 14 "path/filepath" 15 "strconv" 16 "strings" 17 "sync" 18 "time" 19 20 files_sdk "github.com/Files-com/files-sdk-go/v2" 21 "github.com/Files-com/files-sdk-go/v2/folder" 22 "github.com/Files-com/files-sdk-go/v2/lib" 23 "github.com/samber/lo" 24 ) 25 26 type SizeTrust int 27 28 const ( 29 NullSizeTrust SizeTrust = iota 30 UntrustedSizeValue 31 TrustedSizeValue 32 ) 33 34 type FS struct { 35 files_sdk.Config 36 context.Context 37 Root string 38 cache *sync.Map 39 cacheDir *sync.Map 40 useCache bool 41 cacheMutex *lib.KeyedMutex 42 } 43 44 func (f *FS) Init(config files_sdk.Config, cache bool) *FS { 45 f.Config = config 46 f.ClearCache() 47 f.useCache = cache 48 return f 49 } 50 51 func (f *FS) WithContext(ctx context.Context) interface{} { 52 return &FS{Context: ctx, Config: f.Config, cache: f.cache, useCache: f.useCache, cacheDir: f.cacheDir, cacheMutex: f.cacheMutex} 53 } 54 55 func (f *FS) ClearCache() { 56 f.cache = &sync.Map{} 57 f.cacheDir = &sync.Map{} 58 m := lib.NewKeyedMutex() 59 f.cacheMutex = &m 60 } 61 62 type File struct { 63 *files_sdk.File 64 *FS 65 io.ReadCloser 66 downloadRequestId string 67 MaxConnections int 68 stat bool 69 fileMutex *sync.Mutex 70 SizeTrust 71 serverBytesSent int64 72 } 73 74 type ReadDirFile struct { 75 *File 76 count int 77 } 78 79 func (f *File) safeFile() files_sdk.File { 80 f.fileMutex.Lock() 81 defer f.fileMutex.Unlock() 82 return *f.File 83 } 84 85 func (f *File) Init() *File { 86 f.fileMutex = &sync.Mutex{} 87 f.SizeTrust = NullSizeTrust 88 return f 89 } 90 91 func (f *File) Name() string { 92 return f.safeFile().DisplayName 93 } 94 95 func (f *File) IsDir() bool { 96 return f.safeFile().Type == "directory" 97 } 98 99 func (f *File) Type() goFs.FileMode { 100 return goFs.ModePerm 101 } 102 103 func (f *File) Info() (goFs.FileInfo, error) { 104 return f.Stat() 105 } 106 107 type Info struct { 108 files_sdk.File 109 sizeTrust SizeTrust 110 } 111 112 func (i Info) Name() string { 113 return i.File.DisplayName 114 } 115 116 func (i Info) Size() int64 { 117 return i.File.Size 118 } 119 120 type UntrustedSize interface { 121 UntrustedSize() bool 122 SizeTrust() SizeTrust 123 goFs.FileInfo 124 } 125 126 func (i Info) UntrustedSize() bool { 127 return i.sizeTrust == UntrustedSizeValue || i.sizeTrust == NullSizeTrust 128 } 129 130 func (i Info) SizeTrust() SizeTrust { 131 return i.sizeTrust 132 } 133 134 type PossibleSize interface { 135 PossibleSize() int64 136 } 137 138 func (i Info) PossibleSize() int64 { 139 return i.File.Size 140 } 141 142 func (i Info) Mode() goFs.FileMode { 143 return goFs.ModePerm 144 } 145 146 func (i Info) ModTime() time.Time { 147 return *i.File.Mtime 148 } 149 150 func (i Info) IsDir() bool { 151 return i.File.Type == "directory" 152 } 153 154 func (i Info) Sys() interface{} { 155 return i.File 156 } 157 158 func (i Info) RemoteMount() bool { 159 if i.Crc32 != "" { // Detect if is Files.com native file. 160 return false 161 } 162 163 return true 164 } 165 166 func (f *File) Stat() (goFs.FileInfo, error) { 167 f.fileMutex.Lock() 168 defer f.fileMutex.Unlock() 169 return Info{File: *f.File, sizeTrust: f.SizeTrust}, nil 170 } 171 172 func (f *File) Read(b []byte) (n int, err error) { 173 f.fileMutex.Lock() 174 defer f.fileMutex.Unlock() 175 176 if f.ReadCloser == nil { 177 err = f.readCloserInit() 178 if downloadRequestExpired(err) { 179 f.Config.LogPath(f.File.Path, map[string]interface{}{"message": "downloadRequestExpired", "error": err}) 180 f.File.DownloadUri = "" // force a new query 181 *f.File, err = (&Client{Config: f.Config}).DownloadUri(files_sdk.FileDownloadParams{File: *f.File}, files_sdk.WithContext(f.Context)) 182 if err == nil { 183 err = f.readCloserInit() 184 } 185 } 186 187 if err != nil { 188 status, statusErr := (&Client{Config: f.Config}).DownloadRequestStatus(f.File.DownloadUri, f.downloadRequestId, files_sdk.WithContext(f.Context)) 189 if statusErr != nil { 190 return n, err 191 } 192 if !status.IsNil() { 193 return n, status 194 } 195 196 return 197 } 198 } 199 200 return f.ReadCloser.Read(b) 201 } 202 203 func parseSize(response *http.Response) (size int64, sizeTrust SizeTrust) { 204 var err error 205 206 if response.StatusCode == http.StatusPartialContent { 207 if contentRange := response.Header.Get("Content-Range"); contentRange != "" { 208 rangeParts := strings.SplitN(contentRange, "/", 2) 209 if len(rangeParts) == 2 { 210 size, err = strconv.ParseInt(rangeParts[1], 10, 64) 211 if err == nil { 212 sizeTrust = TrustedSizeValue 213 return 214 } 215 } 216 } 217 } else if response.ContentLength > -1 { 218 sizeTrust = TrustedSizeValue 219 size = response.ContentLength 220 221 return 222 } 223 224 // For some remote mounts file size information cannot be trusted and will not be returned. 225 // In order to ensure the total file was received after a download `Client{}.DownloadRequestStatus` should be called. 226 sizeTrust = UntrustedSizeValue 227 228 return 229 } 230 231 func parseMaxConnections(response *http.Response) int { 232 maxConnections, _ := strconv.Atoi(response.Header.Get("X-Files-Max-Connections")) 233 return maxConnections 234 } 235 236 func (f *File) readCloserInit() (err error) { 237 *f.File, err = (&Client{Config: f.Config}).Download( 238 files_sdk.FileDownloadParams{File: *f.File}, 239 files_sdk.WithContext(f.Context), 240 files_sdk.ResponseOption(func(response *http.Response) error { 241 f.MaxConnections = parseMaxConnections(response) 242 f.downloadRequestId = response.Header.Get("X-Files-Download-Request-Id") 243 f.Size, f.SizeTrust = parseSize(response) 244 if err := lib.ResponseErrors(response, files_sdk.APIError(), lib.NotStatus(http.StatusOK)); err != nil { 245 return &goFs.PathError{Path: f.File.Path, Err: err, Op: "read"} 246 } 247 248 f.ReadCloser = &ReadWrapper{ReadCloser: response.Body} 249 return nil 250 }), 251 ) 252 return err 253 } 254 255 type ReaderRange interface { 256 ReaderRange(off int64, end int64) (io.ReadCloser, error) 257 goFs.File 258 } 259 260 type ReadAtLeastWrapper struct { 261 io.ReadCloser 262 io.Reader 263 } 264 265 func (r ReadAtLeastWrapper) Close() error { 266 return r.ReadCloser.Close() 267 } 268 269 func (f ReadAtLeastWrapper) Read(b []byte) (n int, err error) { 270 return f.Reader.Read(b) 271 } 272 273 func (f *File) ReaderRange(off int64, end int64) (r io.ReadCloser, err error) { 274 if err = f.downloadURI(); err != nil { 275 return 276 } 277 f.fileMutex.Lock() 278 rangerReaderCloser := ReaderCloserDownloadStatus{file: f, expectedSize: (end + 1) - off, rangeRequest: true, ReadWrapper: &ReadWrapper{}} 279 280 headers := &http.Header{} 281 headers.Set("Range", fmt.Sprintf("bytes=%v-%v", off, end)) 282 _, err = (&Client{Config: f.Config}).Download( 283 files_sdk.FileDownloadParams{File: *f.File}, 284 files_sdk.WithContext(f.Context), 285 files_sdk.RequestHeadersOption(headers), 286 files_sdk.ResponseOption(func(response *http.Response) error { 287 f.downloadRequestId = response.Header.Get("X-Files-Download-Request-Id") 288 rangerReaderCloser.file.downloadRequestId = response.Header.Get("X-Files-Download-Request-Id") 289 f.MaxConnections = parseMaxConnections(response) 290 f.Size, f.SizeTrust = parseSize(response) 291 if err := lib.ResponseErrors(response, lib.IsStatus(http.StatusForbidden), files_sdk.APIError(), lib.NotStatus(http.StatusPartialContent)); err != nil { 292 return &goFs.PathError{Path: f.File.Path, Err: err, Op: "ReaderRange"} 293 } 294 rangerReaderCloser.ReadCloser = &ReadWrapper{ReadCloser: response.Body} 295 return nil 296 }), 297 ) 298 f.fileMutex.Unlock() 299 if downloadRequestExpired(err) { 300 f.Config.LogPath(f.File.Path, map[string]interface{}{"message": "downloadRequestExpired", "error": err}) 301 f.File.DownloadUri = "" // force a new query 302 err = f.downloadURI() 303 if err != nil { 304 return r, err 305 } 306 307 return f.ReaderRange(off, end) 308 } 309 return rangerReaderCloser, err 310 } 311 312 type ReadWrapper struct { 313 io.ReadCloser 314 read int 315 } 316 317 func (r *ReadWrapper) Read(p []byte) (n int, err error) { 318 n, err = r.ReadCloser.Read(p) 319 r.read += n 320 return 321 } 322 323 type ReaderCloserDownloadStatus struct { 324 *ReadWrapper 325 file *File 326 expectedSize int64 327 rangeRequest bool 328 UntrustedSizeRangeRequestSize 329 } 330 331 type UntrustedSizeRangeRequestSize struct { 332 ExpectedSize int64 333 SentSize int64 334 ReceivedSize int64 335 Status string 336 } 337 338 func (u UntrustedSizeRangeRequestSize) VerifyReceived() error { 339 if u.Status == "started" { // Race condition where server does not record download status. Trust what we asked for and got is correct. 340 if u.ReceivedSize != u.ExpectedSize { 341 errors.Join(UntrustedSizeRangeRequestSizeExpectedReceived, fmt.Errorf("expected %v bytes %v received", u.ExpectedSize, u.ReceivedSize)) 342 } 343 } else if u.ReceivedSize != u.SentSize { 344 return errors.Join(UntrustedSizeRangeRequestSizeSentReceived, fmt.Errorf("expected %v bytes sent %v received", u.SentSize, u.ReceivedSize)) 345 } 346 return nil 347 } 348 349 func (u UntrustedSizeRangeRequestSize) Log() map[string]interface{} { 350 return map[string]interface{}{ 351 "expected_size": u.ExpectedSize, 352 "sent_size": u.SentSize, 353 "received_size": u.ReceivedSize, 354 "VerifyReceived": u.VerifyReceived(), 355 "Mismatch": u.Mismatch(), 356 "Status": u.Status, 357 } 358 } 359 360 var UntrustedSizeRangeRequestSizeExpectedReceived = fmt.Errorf("received size did not match server expected size") 361 var UntrustedSizeRangeRequestSizeSentReceived = fmt.Errorf("received size did not match server send size") 362 363 func (u UntrustedSizeRangeRequestSize) Mismatch() error { 364 if u.Status == "started" { 365 return nil 366 } 367 if u.ExpectedSize > u.SentSize { 368 return UntrustedSizeRangeRequestSizeSentLessThanExpected 369 } 370 if u.ExpectedSize < u.SentSize { 371 return UntrustedSizeRangeRequestSizeSentMoreThanExpected 372 } 373 return nil 374 } 375 376 var UntrustedSizeRangeRequestSizeSentMoreThanExpected = fmt.Errorf("server send more than expected") 377 378 var UntrustedSizeRangeRequestSizeSentLessThanExpected = fmt.Errorf("server send less than expected") 379 380 func (r ReaderCloserDownloadStatus) Close() error { 381 if r.ReadCloser == nil { 382 return nil 383 } 384 err := r.ReadCloser.Close() 385 defer func() { r.ReadCloser = nil }() 386 if err != nil { 387 return err 388 } 389 390 if r.file.downloadRequestId == "" { 391 return nil 392 } 393 394 info, err := r.file.Info() 395 if err != nil { 396 return err 397 } 398 399 if untrustedInfo, ok := info.(UntrustedSize); ok && (untrustedInfo.UntrustedSize() || untrustedInfo.SizeTrust() == NullSizeTrust) { 400 r.file.fileMutex.Lock() 401 402 status, err := (&Client{Config: r.file.Config}).DownloadRequestStatus(r.file.DownloadUri, r.file.downloadRequestId, files_sdk.WithContext(r.file.Context)) 403 r.file.fileMutex.Unlock() 404 if err != nil { 405 return err 406 } 407 if !status.IsNil() && (status.Data.Status == "failed" || status.Data.Status == "error") { 408 return status 409 } 410 r.UntrustedSizeRangeRequestSize = UntrustedSizeRangeRequestSize{ 411 r.expectedSize, 412 status.Data.BytesTransferred, 413 int64(r.ReadWrapper.read), 414 status.Data.Status, 415 } 416 417 if err := r.UntrustedSizeRangeRequestSize.VerifyReceived(); err != nil { 418 r.file.Config.LogPath(info.Name(), r.UntrustedSizeRangeRequestSize.Log()) 419 return err 420 } 421 422 // The true size can only be known after the server determines that the full file has been sent without any errors. 423 if r.rangeRequest { 424 if err := r.UntrustedSizeRangeRequestSize.Mismatch(); err != nil { 425 r.file.Config.LogPath(info.Name(), r.UntrustedSizeRangeRequestSize.Log()) 426 return err 427 } 428 429 if r.file.SizeTrust == UntrustedSizeValue { 430 r.file.serverBytesSent += status.Data.BytesTransferred 431 } 432 } else { 433 r.file.SizeTrust = TrustedSizeValue 434 r.file.Size = status.Data.BytesTransferred 435 } 436 437 if dataBytes, err := json.Marshal(status.Data); err == nil { 438 dataMap := make(map[string]interface{}) 439 if err = json.Unmarshal(dataBytes, &dataMap); err == nil { 440 r.file.Config.LogPath(info.Name(), lo.Assign(dataMap, map[string]interface{}{"message": "download request server status"})) 441 } 442 } 443 } 444 return nil 445 } 446 447 func (f *File) ReadAt(p []byte, off int64) (n int, err error) { 448 err = f.downloadURI() 449 if err != nil { 450 return 451 } 452 headers := &http.Header{} 453 headers.Set("Range", fmt.Sprintf("bytes=%v-%v", off, int64(len(p))+off-1)) 454 _, err = (&Client{Config: f.Config}).Download( 455 files_sdk.FileDownloadParams{ 456 File: *f.File, 457 }, 458 files_sdk.WithContext(f.Context), 459 files_sdk.RequestHeadersOption(headers), 460 files_sdk.ResponseOption(func(response *http.Response) error { 461 if err := lib.ResponseErrors(response, lib.IsStatus(http.StatusForbidden), lib.NotStatus(http.StatusPartialContent), files_sdk.APIError()); err != nil { 462 return &goFs.PathError{Path: f.File.Path, Err: err, Op: "ReadAt"} 463 } 464 n, err = io.ReadFull(response.Body, p) 465 if err != nil && err != io.EOF { 466 return err 467 } 468 if int64(len(p)) >= response.ContentLength && int64(n) != response.ContentLength { 469 return &goFs.PathError{Path: f.File.Path, Err: fmt.Errorf("content-length did not match body"), Op: "ReadAt"} 470 } 471 return nil 472 }), 473 ) 474 475 if downloadRequestExpired(err) { 476 f.Config.LogPath(f.File.Path, map[string]interface{}{"message": "downloadRequestExpired", "error": err}) 477 f.File.DownloadUri = "" // force a new query 478 err = f.downloadURI() 479 if err != nil { 480 return n, err 481 } 482 483 return f.ReadAt(p, off) 484 } 485 486 return n, err 487 } 488 489 func downloadRequestExpired(err error) bool { 490 if err == nil { 491 return false 492 } 493 responseErr, ok := errors.Unwrap(err).(lib.ResponseError) 494 return ok && responseErr.StatusCode == http.StatusForbidden 495 } 496 497 func (f *File) downloadURI() (err error) { 498 f.fileMutex.Lock() 499 *f.File, err = (&Client{Config: f.Config}).DownloadUri(files_sdk.FileDownloadParams{File: *f.File}, files_sdk.WithContext(f.Context)) 500 f.fileMutex.Unlock() 501 return 502 } 503 504 func (f *File) Close() error { 505 f.fileMutex.Lock() 506 f.fileMutex.Unlock() 507 defer func() { f.ReadCloser = nil }() 508 switch f.ReadCloser.(type) { 509 case *ReadWrapper: 510 return ReaderCloserDownloadStatus{ReadWrapper: f.ReadCloser.(*ReadWrapper), file: f}.Close() 511 default: 512 return ReaderCloserDownloadStatus{ReadWrapper: &ReadWrapper{ReadCloser: f.ReadCloser}, file: f}.Close() 513 } 514 } 515 516 func (f *File) WithContext(ctx context.Context) goFs.File { 517 newF := *f 518 fs := *newF.FS 519 newF.FS = fs.WithContext(ctx).(*FS) 520 return &newF 521 } 522 523 func (f *FS) Open(name string) (goFs.File, error) { 524 if name == "." { 525 name = "" 526 } 527 result, ok := f.cache.Load(lib.NormalizeForComparison(name)) 528 if ok { 529 file := result.(*File) 530 if file.IsDir() { 531 return &ReadDirFile{File: file}, nil 532 } 533 return file, nil 534 } 535 path := lib.UrlJoinNoEscape(f.Root, name) 536 var err error 537 var fileInfo files_sdk.File 538 if path == "" { // skip call on root path 539 fileInfo = files_sdk.File{Type: "directory"} 540 } else { 541 fileInfo, err = (&Client{Config: f.Config}).Find(files_sdk.FileFindParams{Path: path}, files_sdk.WithContext(f.Context)) 542 if err != nil { 543 return &File{}, &goFs.PathError{Path: path, Err: err, Op: "open"} 544 } 545 } 546 547 file := (&File{File: &fileInfo, FS: f}).Init() 548 if f.useCache { 549 f.cache.Store(lib.NormalizeForComparison(path), file) 550 } 551 if fileInfo.Type == "directory" { 552 return &ReadDirFile{File: file}, nil 553 } else { 554 return file, nil 555 } 556 } 557 558 type DirEntryError struct { 559 DirEntries []goFs.DirEntry 560 error 561 } 562 563 func (f *FS) ReadDir(name string) ([]goFs.DirEntry, error) { 564 if name == "." { 565 name = "" 566 } 567 cacheName := lib.NormalizeForComparison(name) 568 if f.useCache { 569 f.cacheMutex.Lock(cacheName) 570 defer f.cacheMutex.Unlock(cacheName) 571 572 dirs, ok := f.cacheDir.Load(cacheName) 573 if ok { 574 dirEntryError := dirs.(DirEntryError) 575 return dirEntryError.DirEntries, dirEntryError.error 576 } 577 } 578 579 dirs, err := ReadDirFile{File: (&File{File: &files_sdk.File{Path: name}, FS: f}).Init()}.ReadDir(0) 580 if f.useCache && errors.Is(err, files_sdk.ResponseError{}) { 581 f.cacheDir.Store(cacheName, DirEntryError{dirs, err}) 582 } 583 return dirs, err 584 } 585 586 func (f ReadDirFile) ReadDir(n int) ([]goFs.DirEntry, error) { 587 var files []goFs.DirEntry 588 if f.Context != nil && f.Context.Err() != nil { 589 return files, &goFs.PathError{Path: f.Path, Err: f.Context.Err(), Op: "readdir"} 590 } 591 folderClient := folder.Client{Config: f.Config} 592 it, err := folderClient.ListFor(files_sdk.FolderListForParams{Path: f.Path}, files_sdk.WithContext(f.Context)) 593 if err != nil { 594 return files, &goFs.PathError{Path: f.Path, Err: err, Op: "readdir"} 595 } 596 if f.count > 0 { 597 return files, io.EOF 598 } 599 for it.Next() && (n <= 0 || n > 0 && n >= f.count) { 600 fi := it.File() 601 if err != nil { 602 return files, &goFs.PathError{Path: f.Path, Err: err, Op: "readdir"} 603 } 604 parts := strings.Split(fi.Path, "/") 605 dir := strings.Join(parts[0:len(parts)-1], "/") 606 if lib.NormalizeForComparison(dir) == lib.NormalizeForComparison(f.Path) { 607 // There is a bug in the API that it could return a nested file not in the current directory. 608 file := (&File{File: &fi, FS: f.FS}).Init() 609 if f.useCache { 610 f.cache.Store(lib.NormalizeForComparison(fi.Path), file) 611 } 612 files = append(files, file) 613 } 614 615 f.count += 1 616 } 617 618 if it.Err() != nil { 619 return files, &goFs.PathError{Path: f.Path, Err: it.Err(), Op: "readdir"} 620 } 621 return files, nil 622 } 623 624 func (f *FS) MkdirAll(dir string, _ goFs.FileMode) error { 625 var parentPath string 626 for _, dirPath := range strings.Split(dir, "/") { 627 if dirPath == "" { 628 break 629 } 630 folderClient := folder.Client{Config: f.Config} 631 _, err := folderClient.Create(files_sdk.FolderCreateParams{Path: lib.UrlJoinNoEscape(parentPath, dirPath)}, files_sdk.WithContext(f.Context)) 632 rErr, ok := err.(files_sdk.ResponseError) 633 if err != nil && ok && rErr.Type != "processing-failure/destination-exists" { 634 return err 635 } 636 637 parentPath = lib.UrlJoinNoEscape(parentPath, dirPath) 638 } 639 return nil 640 } 641 642 func (f *FS) PathSeparator() string { 643 return "/" 644 } 645 646 var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 647 648 func randSeq(n int) string { 649 b := make([]rune, n) 650 for i := range b { 651 b[i] = letters[rand.Intn(len(letters))] 652 } 653 return string(b) 654 } 655 656 func (f *FS) MkdirTemp(dir, pattern string) (string, error) { 657 if dir == "" { 658 dir = filepath.Join(f.TempDir(), randSeq(10)) 659 } 660 path := f.PathJoin(dir, pattern) 661 return path, f.MkdirAll(path, 0750) 662 } 663 664 type WritableFile struct { 665 *Client 666 *FS 667 path string 668 *bytes.Buffer 669 } 670 671 func (w WritableFile) init() WritableFile { 672 w.Buffer = bytes.NewBuffer([]byte{}) 673 return w 674 } 675 676 func (w WritableFile) Write(p []byte) (int, error) { 677 return w.Buffer.Write(p) 678 } 679 680 func (w WritableFile) Close() (err error) { 681 return w.Client.Upload( 682 UploadWithContext(w.Context), 683 UploadWithReader(bytes.NewReader(w.Buffer.Bytes())), 684 UploadWithDestinationPath(w.path), 685 UploadWithSize(int64(w.Buffer.Len())), 686 ) 687 } 688 689 // Create Not for performant use cases. 690 func (f *FS) Create(path string) (io.WriteCloser, error) { 691 return WritableFile{FS: f, Client: &Client{Config: f.Config}, path: path}.init(), nil 692 } 693 694 func (f *FS) RemoveAll(path string) error { 695 return (&Client{Config: f.Config}).Delete(files_sdk.FileDeleteParams{Path: path, Recursive: lib.Bool(true)}, files_sdk.WithContext(f.Context)) 696 } 697 698 func (f *FS) Remove(path string) error { 699 return (&Client{Config: f.Config}).Delete(files_sdk.FileDeleteParams{Path: path}, files_sdk.WithContext(f.Context)) 700 } 701 702 func (f *FS) PathJoin(s ...string) string { 703 return lib.UrlJoinNoEscape(s...) 704 } 705 706 func (f *FS) RelPath(parent, child string) (string, error) { 707 path := strings.Replace(child, parent, "", 1) 708 if path == "" { 709 return ".", nil 710 } 711 path = strings.TrimSuffix(path, f.PathSeparator()) 712 path = strings.TrimPrefix(path, f.PathSeparator()) 713 return path, nil 714 } 715 716 func (f *FS) SplitPath(path string) (string, string) { 717 if path == "" { 718 return "", "" 719 } 720 721 parts := strings.Split(path, f.PathSeparator()) 722 723 return f.PathJoin(parts[:int(math.Min(float64(len(parts)-2), float64(len(parts))))]...), parts[len(parts)-1] 724 } 725 726 func (f *FS) TempDir() string { 727 return "tmp" 728 }