github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/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  }