github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libdokan/fs.go (about) 1 // Copyright 2016 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libdokan 6 7 import ( 8 "errors" 9 "os" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/keybase/client/go/kbfs/dokan" 16 "github.com/keybase/client/go/kbfs/dokan/winacl" 17 "github.com/keybase/client/go/kbfs/idutil" 18 "github.com/keybase/client/go/kbfs/libcontext" 19 "github.com/keybase/client/go/kbfs/libfs" 20 "github.com/keybase/client/go/kbfs/libkbfs" 21 "github.com/keybase/client/go/kbfs/tlf" 22 "github.com/keybase/client/go/kbfs/tlfhandle" 23 kbname "github.com/keybase/client/go/kbun" 24 "github.com/keybase/client/go/libkb" 25 "github.com/keybase/client/go/logger" 26 "github.com/keybase/client/go/protocol/keybase1" 27 "golang.org/x/net/context" 28 ) 29 30 // FS implements the newfuse FS interface for KBFS. 31 type FS struct { 32 config libkbfs.Config 33 log logger.Logger 34 vlog *libkb.VDebugLog 35 // renameAndDeletionLock should be held when doing renames or deletions. 36 renameAndDeletionLock sync.Mutex 37 38 notifications *libfs.FSNotifications 39 40 root *Root 41 42 // remoteStatus is the current status of remote connections. 43 remoteStatus libfs.RemoteStatus 44 } 45 46 // DefaultMountFlags are the default mount flags for libdokan. 47 const DefaultMountFlags = dokan.CurrentSession 48 49 // currentUserSID stores the Windows identity of the user running 50 // this process. This is the same process-wide. 51 var currentUserSID, currentUserSIDErr = winacl.CurrentProcessUserSid() 52 var currentGroupSID, _ = winacl.CurrentProcessPrimaryGroupSid() 53 54 // NewFS creates an FS 55 func NewFS(ctx context.Context, config libkbfs.Config, log logger.Logger) (*FS, error) { 56 if currentUserSIDErr != nil { 57 return nil, currentUserSIDErr 58 } 59 f := &FS{ 60 config: config, 61 log: log, 62 vlog: config.MakeVLogger(log), 63 notifications: libfs.NewFSNotifications(log), 64 } 65 66 f.root = &Root{ 67 private: &FolderList{ 68 fs: f, 69 tlfType: tlf.Private, 70 folders: make(map[string]fileOpener), 71 aliasCache: map[string]string{}, 72 }, 73 public: &FolderList{ 74 fs: f, 75 tlfType: tlf.Public, 76 folders: make(map[string]fileOpener), 77 aliasCache: map[string]string{}, 78 }, 79 team: &FolderList{ 80 fs: f, 81 tlfType: tlf.SingleTeam, 82 folders: make(map[string]fileOpener), 83 aliasCache: map[string]string{}, 84 }} 85 86 ctx = wrapContext(ctx, f) 87 88 f.remoteStatus.Init(ctx, f.log, f.config, f) 89 f.notifications.LaunchProcessor(ctx) 90 go clearFolderListCacheLoop(ctx, f.root) 91 92 return f, nil 93 } 94 95 // Adds log tags etc 96 func wrapContext(ctx context.Context, f *FS) context.Context { 97 ctx = context.WithValue(ctx, libfs.CtxAppIDKey, f) 98 logTags := make(logger.CtxLogTags) 99 logTags[CtxIDKey] = CtxOpID 100 ctx = logger.NewContextWithLogTags(ctx, logTags) 101 return ctx 102 } 103 104 // WithContext creates context for filesystem operations. 105 func (f *FS) WithContext(ctx context.Context) (context.Context, context.CancelFunc) { 106 id, err := libkbfs.MakeRandomRequestID() 107 if err != nil { 108 f.log.CErrorf(ctx, "Couldn't make request ID: %v", err) 109 return ctx, func() {} 110 } 111 112 ctx, cancel := context.WithCancel(ctx) 113 114 // context.WithDeadline uses clock from `time` package, so we are not using 115 // f.config.Clock() here 116 start := time.Now() 117 ctx, err = libcontext.NewContextWithCancellationDelayer( 118 libcontext.NewContextReplayable(ctx, func(ctx context.Context) context.Context { 119 ctx = wrapContext(context.WithValue(ctx, CtxIDKey, id), f) 120 ctx, _ = context.WithDeadline(ctx, start.Add(29*time.Second)) 121 return ctx 122 })) 123 if err != nil { 124 panic(err) 125 } 126 return ctx, cancel 127 } 128 129 var vinfo = dokan.VolumeInformation{ 130 VolumeName: "KBFS", 131 MaximumComponentLength: 0xFF, // This can be changed. 132 FileSystemFlags: dokan.FileCasePreservedNames | dokan.FileCaseSensitiveSearch | 133 dokan.FileUnicodeOnDisk | dokan.FileSupportsReparsePoints | 134 dokan.FileSupportsRemoteStorage, 135 FileSystemName: "KBFS", 136 } 137 138 // GetVolumeInformation returns information about the whole filesystem for dokan. 139 func (f *FS) GetVolumeInformation(ctx context.Context) (dokan.VolumeInformation, error) { 140 // TODO should this be explicitely refused to other users? 141 // As the mount is limited to current session there is little need. 142 return vinfo, nil 143 } 144 145 const dummyFreeSpace = 10 * 1024 * 1024 * 1024 146 147 // quotaUsageStaleTolerance is the lifespan of stale usage data that libdokan 148 // accepts in the Statfs handler. In other words, this causes libkbfs to issue 149 // a fresh RPC call if cached usage data is older than 10s. 150 const quotaUsageStaleTolerance = 10 * time.Second 151 152 // GetDiskFreeSpace returns information about free space on the volume for dokan. 153 func (f *FS) GetDiskFreeSpace(ctx context.Context) (freeSpace dokan.FreeSpace, err error) { 154 // TODO should this be refused to other users? 155 // As the mount is limited to current session there is little need. 156 f.logEnter(ctx, "FS GetDiskFreeSpace") 157 // Refuse private directories while we are in a error state. 158 if f.remoteStatus.ExtraFileName() != "" { 159 f.log.Warning("Dummy disk free space while errors are present!") 160 return dokan.FreeSpace{ 161 TotalNumberOfBytes: dummyFreeSpace, 162 TotalNumberOfFreeBytes: dummyFreeSpace, 163 FreeBytesAvailable: dummyFreeSpace, 164 }, nil 165 } 166 defer func() { 167 if err == nil { 168 f.vlog.CLogf(ctx, libkb.VLog1, "Request complete") 169 } else { 170 // Don't report the error (perhaps resulting in a user 171 // notification) since this method is mostly called by the 172 // OS and not because of a user action. 173 f.log.CDebugf(ctx, err.Error()) 174 } 175 }() 176 177 session, err := idutil.GetCurrentSessionIfPossible( 178 ctx, f.config.KBPKI(), true) 179 if err != nil { 180 return dokan.FreeSpace{}, err 181 } else if session == (idutil.SessionInfo{}) { 182 // If user is not logged in, don't bother getting quota info. Otherwise 183 // reading a public TLF while logged out can fail on macOS. 184 return dokan.FreeSpace{ 185 TotalNumberOfBytes: dummyFreeSpace, 186 TotalNumberOfFreeBytes: dummyFreeSpace, 187 FreeBytesAvailable: dummyFreeSpace, 188 }, nil 189 } 190 _, usageBytes, _, limitBytes, err := f.config.GetQuotaUsage( 191 session.UID.AsUserOrTeam()).Get( 192 ctx, quotaUsageStaleTolerance/2, quotaUsageStaleTolerance) 193 if err != nil { 194 return dokan.FreeSpace{}, errToDokan(err) 195 } 196 free := uint64(limitBytes - usageBytes) 197 return dokan.FreeSpace{ 198 TotalNumberOfBytes: uint64(limitBytes), 199 TotalNumberOfFreeBytes: free, 200 FreeBytesAvailable: free, 201 }, nil 202 } 203 204 // openContext is for opening files. 205 type openContext struct { 206 fi *dokan.FileInfo 207 *dokan.CreateData 208 redirectionsLeft int 209 // isUppercasePath marks a path containing only upper case letters, 210 // associated with e.g. resolving some reparse points. This has 211 // special case insensitive path resolving functionality. 212 isUppercasePath bool 213 } 214 215 // reduceRedictionsLeft reduces redirections and returns whether there are 216 // redirections left (true), or whether processing should be stopped (false). 217 func (oc *openContext) reduceRedirectionsLeft() bool { 218 oc.redirectionsLeft-- 219 return oc.redirectionsLeft > 0 220 } 221 222 // isCreation checks the flags whether a file creation is wanted. 223 func (oc *openContext) isCreateDirectory() bool { 224 return oc.isCreation() && oc.CreateOptions&fileDirectoryFile != 0 225 } 226 227 const fileDirectoryFile = 1 228 229 // isCreation checks the flags whether a file creation is wanted. 230 func (oc *openContext) isCreation() bool { 231 switch oc.CreateDisposition { 232 case dokan.FileSupersede, dokan.FileCreate, dokan.FileOpenIf, dokan.FileOverwriteIf: 233 return true 234 } 235 return false 236 } 237 func (oc *openContext) isExistingError() bool { 238 return oc.CreateDisposition == dokan.FileCreate 239 } 240 241 // isTruncate checks the flags whether a file truncation is wanted. 242 func (oc *openContext) isTruncate() bool { 243 switch oc.CreateDisposition { 244 case dokan.FileSupersede, dokan.FileOverwrite, dokan.FileOverwriteIf: 245 return true 246 } 247 return false 248 } 249 250 // isOpenReparsePoint checks the flags whether a reparse point open is wanted. 251 func (oc *openContext) isOpenReparsePoint() bool { 252 return oc.CreateOptions&dokan.FileOpenReparsePoint != 0 253 } 254 255 // returnDirNoCleanup returns a dir or nothing depending on the open 256 // flags and does not call .Cleanup on error. 257 func (oc *openContext) returnDirNoCleanup(f dokan.File) ( 258 dokan.File, dokan.CreateStatus, error) { 259 if err := oc.ReturningDirAllowed(); err != nil { 260 return nil, 0, err 261 } 262 return f, dokan.ExistingDir, nil 263 } 264 265 // returnFileNoCleanup returns a file or nothing depending on the open 266 // flags and does not call .Cleanup on error. 267 func (oc *openContext) returnFileNoCleanup(f dokan.File) ( 268 dokan.File, dokan.CreateStatus, error) { 269 if err := oc.ReturningFileAllowed(); err != nil { 270 return nil, 0, err 271 } 272 return f, dokan.ExistingFile, nil 273 } 274 275 func newSyntheticOpenContext() *openContext { 276 var oc openContext 277 oc.CreateData = &dokan.CreateData{} 278 oc.CreateDisposition = dokan.FileOpen 279 oc.redirectionsLeft = 30 280 return &oc 281 } 282 283 // CreateFile called from dokan, may be a file or directory. 284 func (f *FS) CreateFile(ctx context.Context, fi *dokan.FileInfo, cd *dokan.CreateData) (dokan.File, dokan.CreateStatus, error) { 285 // Only allow the current user access 286 if !fi.IsRequestorUserSidEqualTo(currentUserSID) { 287 f.log.CErrorf(ctx, "FS CreateFile - Refusing real access: SID match error") 288 return openFakeRoot(ctx, f, fi) 289 } 290 return f.openRaw(ctx, fi, cd) 291 } 292 293 // openRaw is a wrapper between CreateFile/CreateDirectory/OpenDirectory and open 294 func (f *FS) openRaw(ctx context.Context, fi *dokan.FileInfo, caf *dokan.CreateData) (dokan.File, dokan.CreateStatus, error) { 295 ps, err := windowsPathSplit(fi.Path()) 296 if err != nil { 297 f.log.CErrorf(ctx, "FS openRaw - path split error: %v", err) 298 return nil, 0, err 299 } 300 oc := openContext{fi: fi, CreateData: caf, redirectionsLeft: 30} 301 file, cst, err := f.open(ctx, &oc, ps) 302 if err != nil { 303 f.log.CDebugf(ctx, "FS Open failed %#v with: %v", *caf, err) 304 err = errToDokan(err) 305 } 306 return file, cst, err 307 } 308 309 // open tries to open a file deferring to more specific implementations. 310 func (f *FS) open(ctx context.Context, oc *openContext, ps []string) (dokan.File, dokan.CreateStatus, error) { 311 f.vlog.CLogf(ctx, libkb.VLog1, "FS Open: %q", ps) 312 psl := len(ps) 313 switch { 314 case psl < 1: 315 return nil, 0, dokan.ErrObjectNameNotFound 316 case psl == 1 && ps[0] == ``: 317 return oc.returnDirNoCleanup(f.root) 318 319 // This section is equivalent to 320 // handleCommonSpecialFile in libfuse. 321 case libfs.ErrorFileName == ps[psl-1]: 322 return oc.returnFileNoCleanup(NewErrorFile(f)) 323 case libfs.MetricsFileName == ps[psl-1]: 324 return oc.returnFileNoCleanup(NewMetricsFile(f)) 325 // TODO: Make the two cases below available from any 326 // directory. 327 case libfs.ProfileListDirName == ps[0]: 328 return (ProfileList{fs: f}).open(ctx, oc, ps[1:]) 329 case libfs.ResetCachesFileName == ps[0]: 330 return oc.returnFileNoCleanup(&ResetCachesFile{fs: f.root.private.fs}) 331 332 // This section is equivalent to 333 // handleNonTLFSpecialFile in libfuse. 334 // 335 // TODO: Make the two cases below available from any 336 // non-TLF directory. 337 case libfs.StatusFileName == ps[0]: 338 return oc.returnFileNoCleanup(NewNonTLFStatusFile(f.root.private.fs)) 339 case libfs.HumanErrorFileName == ps[0], libfs.HumanNoLoginFileName == ps[0]: 340 return oc.returnFileNoCleanup(&SpecialReadFile{ 341 read: f.remoteStatus.NewSpecialReadFunc, 342 fs: f}) 343 344 case libfs.EnableAutoJournalsFileName == ps[0]: 345 return oc.returnFileNoCleanup(&JournalControlFile{ 346 folder: &Folder{fs: f}, // fake Folder for logging, etc. 347 action: libfs.JournalEnableAuto, 348 }) 349 case libfs.DisableAutoJournalsFileName == ps[0]: 350 return oc.returnFileNoCleanup(&JournalControlFile{ 351 folder: &Folder{fs: f}, // fake Folder for logging, etc. 352 action: libfs.JournalDisableAuto, 353 }) 354 case libfs.EnableBlockPrefetchingFileName == ps[0]: 355 return oc.returnFileNoCleanup(&PrefetchFile{ 356 fs: f, 357 enable: true, 358 }) 359 case libfs.DisableBlockPrefetchingFileName == ps[0]: 360 return oc.returnFileNoCleanup(&PrefetchFile{ 361 fs: f, 362 enable: false, 363 }) 364 365 case libfs.EditHistoryName == ps[0]: 366 return oc.returnFileNoCleanup(NewUserEditHistoryFile(&Folder{fs: f})) 367 368 case ".kbfs_unmount" == ps[0]: 369 f.log.CInfof(ctx, "Exiting due to .kbfs_unmount") 370 logger.Shutdown() 371 os.Exit(0) 372 case ".kbfs_restart" == ps[0]: 373 f.log.CInfof(ctx, "Exiting due to .kbfs_restart, should get restarted by watchdog process") 374 logger.Shutdown() 375 os.Exit(int(keybase1.ExitCode_RESTART)) 376 case ".kbfs_number_of_handles" == ps[0]: 377 x := stringReadFile(strconv.Itoa(int(oc.fi.NumberOfFileHandles()))) 378 return oc.returnFileNoCleanup(x) 379 // TODO 380 // Unfortunately sometimes we end up in this case while using 381 // reparse points. 382 case strings.ToUpper(PublicName) == ps[0]: 383 oc.isUppercasePath = true 384 fallthrough 385 case PublicName == ps[0]: 386 return f.root.public.open(ctx, oc, ps[1:]) 387 case strings.ToUpper(PrivateName) == ps[0]: 388 oc.isUppercasePath = true 389 fallthrough 390 case PrivateName == ps[0]: 391 return f.root.private.open(ctx, oc, ps[1:]) 392 case strings.ToUpper(TeamName) == ps[0]: 393 oc.isUppercasePath = true 394 fallthrough 395 case TeamName == ps[0]: 396 return f.root.team.open(ctx, oc, ps[1:]) 397 } 398 return nil, 0, dokan.ErrObjectNameNotFound 399 } 400 401 // windowsPathSplit handles paths we get from Dokan. 402 // As a special case “ means `\`, it gets generated 403 // on special occasions. 404 func windowsPathSplit(raw string) ([]string, error) { 405 if raw == `` { 406 raw = `\` 407 } 408 if raw[0] != '\\' || raw[len(raw)-1] == '*' { 409 return nil, dokan.ErrObjectNameNotFound 410 } 411 return strings.Split(raw[1:], `\`), nil 412 } 413 414 // ErrorPrint prints errors from the Dokan library. 415 func (f *FS) ErrorPrint(err error) { 416 f.log.Errorf("Dokan error: %v", err) 417 } 418 419 // Printf prints information from the Dokan library. 420 func (f *FS) Printf(fmt string, args ...interface{}) { 421 f.log.Info("Dokan info: "+fmt, args...) 422 } 423 424 // MoveFile tries to move a file. 425 func (f *FS) MoveFile(ctx context.Context, src dokan.File, sourceFI *dokan.FileInfo, targetPath string, replaceExisting bool) (err error) { 426 // User checking was handled by original file open, this is no longer true. 427 // However we only allow fake files with names that are not potential rename 428 // paths. Filter those out here. 429 430 f.vlog.CLogf( 431 ctx, libkb.VLog1, "MoveFile %T %q -> %q", src, 432 sourceFI.Path(), targetPath) 433 // isPotentialRenamePath filters out some special paths 434 // for rename. Especially those provided by fakeroot.go. 435 if !isPotentialRenamePath(sourceFI.Path()) { 436 f.log.CErrorf(ctx, "Refusing MoveFile access: not potential rename path") 437 return dokan.ErrAccessDenied 438 } 439 switch src.(type) { 440 case *FolderList, *File, *Dir, *TLF, *EmptyFolder: 441 default: 442 f.log.CErrorf(ctx, "Refusing MoveFile access: wrong type source argument") 443 return dokan.ErrAccessDenied 444 } 445 446 f.logEnter(ctx, "FS MoveFile") 447 // No racing deletions or renames. 448 // Note that this calls Cleanup multiple times, however with nil 449 // FileInfo which means that Cleanup will not try to lock renameAndDeletionLock. 450 // renameAndDeletionLock should be the first lock to be grabbed in libdokan. 451 f.renameAndDeletionLock.Lock() 452 defer func() { 453 f.renameAndDeletionLock.Unlock() 454 f.reportErr(ctx, libkbfs.WriteMode, err) 455 }() 456 457 oc := newSyntheticOpenContext() 458 459 // Source directory 460 srcDirPath, err := windowsPathSplit(sourceFI.Path()) 461 if err != nil { 462 return err 463 } 464 if len(srcDirPath) < 1 { 465 return errors.New("Invalid source for move") 466 } 467 srcName := srcDirPath[len(srcDirPath)-1] 468 srcDirPath = srcDirPath[0 : len(srcDirPath)-1] 469 srcDir, _, err := f.open(ctx, oc, srcDirPath) 470 if err != nil { 471 return err 472 } 473 defer srcDir.Cleanup(ctx, nil) 474 475 // Destination directory, not the destination file 476 dstPath, err := windowsPathSplit(targetPath) 477 if err != nil { 478 return err 479 } 480 if len(dstPath) < 1 { 481 return errors.New("Invalid destination for move") 482 } 483 dstDirPath := dstPath[0 : len(dstPath)-1] 484 485 dstDir, dstCst, err := f.open(ctx, oc, dstDirPath) 486 f.vlog.CLogf( 487 ctx, libkb.VLog1, "FS MoveFile dstDir open %v -> %v,%v,%v dstType %T", 488 dstDirPath, dstDir, dstCst, err, dstDir) 489 if err != nil { 490 return err 491 } 492 defer dstDir.Cleanup(ctx, nil) 493 if !dstCst.IsDir() { 494 return errors.New("Tried to move to a non-directory path") 495 } 496 497 fl1, ok := srcDir.(*FolderList) 498 fl2, ok2 := dstDir.(*FolderList) 499 if ok && ok2 && fl1 == fl2 { 500 return f.folderListRename(ctx, fl1, oc, src, srcName, dstPath, replaceExisting) 501 } 502 503 srcDirD := asDir(ctx, srcDir) 504 if srcDirD == nil { 505 return errors.New("Parent of src not a Dir") 506 } 507 srcFolder := srcDirD.folder 508 srcParent := srcDirD.node 509 510 ddst := asDir(ctx, dstDir) 511 if ddst == nil { 512 return errors.New("Destination directory is not of type Dir") 513 } 514 515 switch src.(type) { 516 case *Dir: 517 case *File: 518 case *TLF: 519 default: 520 return dokan.ErrAccessDenied 521 } 522 523 // here we race... 524 if !replaceExisting { 525 x, _, err := f.open(ctx, oc, dstPath) 526 if err == nil { 527 defer x.Cleanup(ctx, nil) 528 } 529 if !isNoSuchNameError(err) { 530 f.vlog.CLogf( 531 ctx, libkb.VLog1, 532 "FS MoveFile required non-existent destination, got: %T %v", 533 err, err) 534 return dokan.ErrObjectNameCollision 535 } 536 537 } 538 539 if srcFolder != ddst.folder { 540 return dokan.ErrNotSameDevice 541 } 542 543 // overwritten node, if any, will be removed from Folder.nodes, if 544 // it is there in the first place, by its Forget 545 546 dstName := dstPath[len(dstPath)-1] 547 f.vlog.CLogf( 548 ctx, libkb.VLog1, "FS MoveFile KBFSOps().Rename(ctx,%v,%v,%v,%v)", 549 srcParent, srcName, ddst.node, dstName) 550 if err := srcFolder.fs.config.KBFSOps().Rename( 551 ctx, srcParent, srcParent.ChildName(srcName), ddst.node, 552 ddst.node.ChildName(dstName)); err != nil { 553 f.log.CDebugf(ctx, "FS MoveFile KBFSOps().Rename FAILED %v", err) 554 return err 555 } 556 557 switch x := src.(type) { 558 case *Dir: 559 x.parent = ddst.node 560 x.name = dstName 561 case *File: 562 x.parent = ddst.node 563 x.name = dstName 564 } 565 566 f.vlog.CLogf(ctx, libkb.VLog1, "FS MoveFile SUCCESS") 567 return nil 568 } 569 570 func isPotentialRenamePath(s string) bool { 571 if len(s) < 3 || s[0] != '\\' { 572 return false 573 } 574 s = s[1:] 575 return strings.HasPrefix(s, PrivateName) || 576 strings.HasPrefix(s, PublicName) || 577 strings.HasPrefix(s, TeamName) 578 } 579 580 func (f *FS) folderListRename(ctx context.Context, fl *FolderList, oc *openContext, src dokan.File, srcName string, dstPath []string, replaceExisting bool) error { 581 ef, ok := src.(*EmptyFolder) 582 f.vlog.CLogf(ctx, libkb.VLog1, "FS MoveFile folderlist %v", ef) 583 if !ok || !isNewFolderName(srcName) { 584 return dokan.ErrAccessDenied 585 } 586 dstName := dstPath[len(dstPath)-1] 587 // Yes, this is slow, but that is ok here. 588 if _, err := tlfhandle.ParseHandlePreferred( 589 ctx, f.config.KBPKI(), f.config.MDOps(), f.config, dstName, 590 fl.tlfType); err != nil { 591 return dokan.ErrObjectNameNotFound 592 } 593 fl.mu.Lock() 594 _, ok = fl.folders[dstName] 595 fl.mu.Unlock() 596 if !replaceExisting && ok { 597 f.vlog.CLogf( 598 ctx, libkb.VLog1, 599 "FS MoveFile folderlist refusing to replace target") 600 return dokan.ErrAccessDenied 601 } 602 // Perhaps create destination by opening it. 603 x, _, err := f.open(ctx, oc, dstPath) 604 if err == nil { 605 x.Cleanup(ctx, nil) 606 } 607 fl.mu.Lock() 608 defer fl.mu.Unlock() 609 _, ok = fl.folders[dstName] 610 delete(fl.folders, srcName) 611 if !ok { 612 f.vlog.CLogf(ctx, libkb.VLog1, "FS MoveFile folderlist adding target") 613 fl.folders[dstName] = ef 614 } 615 f.vlog.CLogf(ctx, libkb.VLog1, "FS MoveFile folderlist success") 616 return nil 617 } 618 619 func (f *FS) queueNotification(fn func()) { 620 f.notifications.QueueNotification(fn) 621 } 622 623 func (f *FS) reportErr(ctx context.Context, mode libkbfs.ErrorModeType, err error) { 624 if err == nil { 625 f.vlog.CLogf(ctx, libkb.VLog1, "Request complete") 626 return 627 } 628 629 f.config.Reporter().ReportErr(ctx, "", tlf.Private, mode, err) 630 // We just log the error as debug, rather than error, because it 631 // might just indicate an expected error such as an ENOENT. 632 // 633 // TODO: Classify errors and escalate the logging level of the 634 // important ones. 635 f.log.CDebugf(ctx, err.Error()) 636 } 637 638 // NotificationGroupWait waits till the local notification group is done. 639 func (f *FS) NotificationGroupWait() { 640 f.notifications.Wait() 641 } 642 643 func (f *FS) logEnter(ctx context.Context, s string) { 644 f.vlog.CLogf(ctx, libkb.VLog1, "=> %s", s) 645 } 646 647 func (f *FS) logEnterf(ctx context.Context, fmt string, args ...interface{}) { 648 f.vlog.CLogf(ctx, libkb.VLog1, "=> "+fmt, args...) 649 } 650 651 // UserChanged is called from libfs. 652 func (f *FS) UserChanged(ctx context.Context, oldName, newName kbname.NormalizedUsername) { 653 f.log.CDebugf(ctx, "User changed: %q -> %q", oldName, newName) 654 f.root.public.userChanged(ctx, oldName, newName) 655 f.root.private.userChanged(ctx, oldName, newName) 656 } 657 658 var _ libfs.RemoteStatusUpdater = (*FS)(nil) 659 660 // Root represents the root of the KBFS file system. 661 type Root struct { 662 emptyFile 663 private *FolderList 664 public *FolderList 665 team *FolderList 666 } 667 668 // GetFileInformation for dokan stats. 669 func (r *Root) GetFileInformation(ctx context.Context, fi *dokan.FileInfo) (*dokan.Stat, error) { 670 return defaultDirectoryInformation() 671 } 672 673 // FindFiles for dokan readdir. 674 func (r *Root) FindFiles(ctx context.Context, fi *dokan.FileInfo, ignored string, callback func(*dokan.NamedStat) error) error { 675 var ns dokan.NamedStat 676 var err error 677 ns.FileAttributes = dokan.FileAttributeDirectory 678 ns.Name = PrivateName 679 err = callback(&ns) 680 if err != nil { 681 return err 682 } 683 ns.Name = TeamName 684 err = callback(&ns) 685 if err != nil { 686 return err 687 } 688 ns.Name = PublicName 689 err = callback(&ns) 690 if err != nil { 691 return err 692 } 693 if ename, esize := r.private.fs.remoteStatus.ExtraFileNameAndSize(); ename != "" { 694 ns.Name = ename 695 ns.FileAttributes = dokan.FileAttributeNormal 696 ns.FileSize = esize 697 err = callback(&ns) 698 if err != nil { 699 return err 700 } 701 } 702 return nil 703 }