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
5package libdokan
6
7import (
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.
31type 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.
47const DefaultMountFlags = dokan.CurrentSession
48
49// currentUserSID stores the Windows identity of the user running
50// this process. This is the same process-wide.
51var currentUserSID, currentUserSIDErr = winacl.CurrentProcessUserSid()
52var currentGroupSID, _ = winacl.CurrentProcessPrimaryGroupSid()
53
54// NewFS creates an FS
55func 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
96func 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.
105func (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
129var 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.
139func (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
145const 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.
150const quotaUsageStaleTolerance = 10 * time.Second
151
152// GetDiskFreeSpace returns information about free space on the volume for dokan.
153func (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.
205type 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).
217func (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.
223func (oc *openContext) isCreateDirectory() bool {
224	return oc.isCreation() && oc.CreateOptions&fileDirectoryFile != 0
225}
226
227const fileDirectoryFile = 1
228
229// isCreation checks the flags whether a file creation is wanted.
230func (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}
237func (oc *openContext) isExistingError() bool {
238	return oc.CreateDisposition == dokan.FileCreate
239}
240
241// isTruncate checks the flags whether a file truncation is wanted.
242func (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.
251func (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.
257func (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.
267func (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
275func 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.
284func (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
294func (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.
310func (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.
404func 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.
415func (f *FS) ErrorPrint(err error) {
416	f.log.Errorf("Dokan error: %v", err)
417}
418
419// Printf prints information from the Dokan library.
420func (f *FS) Printf(fmt string, args ...interface{}) {
421	f.log.Info("Dokan info: "+fmt, args...)
422}
423
424// MoveFile tries to move a file.
425func (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
570func 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
580func (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
619func (f *FS) queueNotification(fn func()) {
620	f.notifications.QueueNotification(fn)
621}
622
623func (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.
639func (f *FS) NotificationGroupWait() {
640	f.notifications.Wait()
641}
642
643func (f *FS) logEnter(ctx context.Context, s string) {
644	f.vlog.CLogf(ctx, libkb.VLog1, "=> %s", s)
645}
646
647func (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.
652func (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
658var _ libfs.RemoteStatusUpdater = (*FS)(nil)
659
660// Root represents the root of the KBFS file system.
661type Root struct {
662	emptyFile
663	private *FolderList
664	public  *FolderList
665	team    *FolderList
666}
667
668// GetFileInformation for dokan stats.
669func (r *Root) GetFileInformation(ctx context.Context, fi *dokan.FileInfo) (*dokan.Stat, error) {
670	return defaultDirectoryInformation()
671}
672
673// FindFiles for dokan readdir.
674func (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}
704