github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libkbfs/util.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 libkbfs
     6  
     7  import (
     8  	"context"
     9  	"encoding/base64"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/keybase/client/go/kbfs/data"
    17  	"github.com/keybase/client/go/kbfs/idutil"
    18  	"github.com/keybase/client/go/kbfs/kbfscrypto"
    19  	"github.com/keybase/client/go/kbfs/kbfsmd"
    20  	"github.com/keybase/client/go/kbfs/libcontext"
    21  	"github.com/keybase/client/go/kbfs/tlf"
    22  	"github.com/keybase/client/go/kbfs/tlfhandle"
    23  	"github.com/keybase/client/go/libkb"
    24  	"github.com/keybase/client/go/logger"
    25  	"github.com/keybase/client/go/protocol/keybase1"
    26  	"github.com/pkg/errors"
    27  )
    28  
    29  // Runs fn (which may block) in a separate goroutine and waits for it
    30  // to finish, unless ctx is cancelled. Returns nil only when fn was
    31  // run to completion and succeeded.  Any closed-over variables updated
    32  // in fn should be considered visible only if nil is returned.
    33  func runUnlessCanceled(ctx context.Context, fn func() error) error {
    34  	c := make(chan error, 1) // buffered, in case the request is canceled
    35  	go func() {
    36  		c <- fn()
    37  	}()
    38  
    39  	select {
    40  	case <-ctx.Done():
    41  		return ctx.Err()
    42  	case err := <-c:
    43  		return err
    44  	}
    45  }
    46  
    47  // MakeRandomRequestID generates a random ID suitable for tagging a
    48  // request in KBFS, and very likely to be universally unique.
    49  func MakeRandomRequestID() (string, error) {
    50  	// Use a random ID to tag each request.  We want this to be really
    51  	// universally unique, as these request IDs might need to be
    52  	// propagated all the way to the server.  Use a base64-encoded
    53  	// random 128-bit number.
    54  	buf := make([]byte, 128/8)
    55  	err := kbfscrypto.RandRead(buf)
    56  	if err != nil {
    57  		return "", err
    58  	}
    59  	return base64.RawURLEncoding.EncodeToString(buf), nil
    60  }
    61  
    62  // BoolForString returns false if trimmed string is "" (empty), "0", "false", or "no"
    63  func BoolForString(s string) bool {
    64  	s = strings.TrimSpace(s)
    65  	if s == "" || s == "0" || s == "false" || s == "no" {
    66  		return false
    67  	}
    68  	return true
    69  }
    70  
    71  // PrereleaseBuild is set at compile time for prerelease builds
    72  var PrereleaseBuild string
    73  
    74  // VersionString returns semantic version string
    75  func VersionString() string {
    76  	if PrereleaseBuild != "" {
    77  		return fmt.Sprintf("%s-%s", libkb.Version, PrereleaseBuild)
    78  	}
    79  	return libkb.Version
    80  }
    81  
    82  // CtxBackgroundSyncKeyType is the type for a context background sync key.
    83  type CtxBackgroundSyncKeyType int
    84  
    85  const (
    86  	// CtxBackgroundSyncKey is set in the context for any change
    87  	// notifications that are triggered from a background sync.
    88  	// Observers can ignore these if they want, since they will have
    89  	// already gotten the relevant notifications via LocalChanges.
    90  	CtxBackgroundSyncKey CtxBackgroundSyncKeyType = iota
    91  )
    92  
    93  // Warninger is an interface that only waprs the Warning method.
    94  type Warninger interface {
    95  	Warning(format string, args ...interface{})
    96  }
    97  
    98  // CtxWithRandomIDReplayable returns a replayable context with a
    99  // random id associated with the given log key.
   100  func CtxWithRandomIDReplayable(ctx context.Context, tagKey interface{},
   101  	tagName string, log Warninger) context.Context {
   102  	ctx = logger.ConvertRPCTagsToLogTags(ctx)
   103  
   104  	id, err := MakeRandomRequestID()
   105  	if err != nil && log != nil {
   106  		log.Warning("Couldn't generate a random request ID: %v", err)
   107  	}
   108  	return libcontext.NewContextReplayable(ctx, func(ctx context.Context) context.Context {
   109  		logTags := make(logger.CtxLogTags)
   110  		logTags[tagKey] = tagName
   111  		newCtx := logger.NewContextWithLogTags(ctx, logTags)
   112  		if err == nil {
   113  			newCtx = context.WithValue(newCtx, tagKey, id)
   114  		}
   115  		return newCtx
   116  	})
   117  }
   118  
   119  // checkDataVersion validates that the data version for a
   120  // block pointer is valid for the given version validator
   121  func checkDataVersion(
   122  	versioner data.Versioner, p data.Path, ptr data.BlockPointer) error {
   123  	if ptr.DataVer < data.FirstValidVer {
   124  		return errors.WithStack(InvalidDataVersionError{ptr.DataVer})
   125  	}
   126  	if versioner != nil && ptr.DataVer > versioner.DataVersion() {
   127  		return errors.WithStack(NewDataVersionError{p, ptr.DataVer})
   128  	}
   129  	return nil
   130  }
   131  
   132  func checkContext(ctx context.Context) error {
   133  	select {
   134  	case <-ctx.Done():
   135  		return errors.WithStack(ctx.Err())
   136  	default:
   137  		return nil
   138  	}
   139  }
   140  
   141  func chargedToForTLF(
   142  	ctx context.Context, sessionGetter idutil.CurrentSessionGetter,
   143  	rootIDGetter teamRootIDGetter, osg idutil.OfflineStatusGetter,
   144  	handle *tlfhandle.Handle) (keybase1.UserOrTeamID, error) {
   145  	if handle.Type() == tlf.SingleTeam {
   146  		chargedTo := handle.FirstResolvedWriter()
   147  		if tid := chargedTo.AsTeamOrBust(); tid.IsSubTeam() {
   148  			offline := keybase1.OfflineAvailability_NONE
   149  			if osg != nil {
   150  				offline = osg.OfflineAvailabilityForID(handle.TlfID())
   151  			}
   152  
   153  			// Subteam blocks should be charged to the root team ID.
   154  			rootID, err := rootIDGetter.GetTeamRootID(ctx, tid, offline)
   155  			if err != nil {
   156  				return keybase1.UserOrTeamID(""), err
   157  			}
   158  			return rootID.AsUserOrTeam(), nil
   159  		}
   160  		return chargedTo, nil
   161  	}
   162  
   163  	// For private and public folders, use the session user.
   164  	session, err := sessionGetter.GetCurrentSession(ctx)
   165  	if err != nil {
   166  		return keybase1.UserOrTeamID(""), err
   167  	}
   168  	return session.UID.AsUserOrTeam(), nil
   169  }
   170  
   171  // GetHandleFromFolderNameAndType returns a TLFHandle given a folder
   172  // name (e.g., "u1,u2#u3") and a TLF type.
   173  func GetHandleFromFolderNameAndType(
   174  	ctx context.Context, kbpki KBPKI, idGetter tlfhandle.IDGetter,
   175  	syncGetter syncedTlfGetterSetter, tlfName string,
   176  	t tlf.Type) (*tlfhandle.Handle, error) {
   177  	for {
   178  		tlfHandle, err := tlfhandle.ParseHandle(
   179  			ctx, kbpki, idGetter, syncGetter, tlfName, t)
   180  		switch e := errors.Cause(err).(type) {
   181  		case idutil.TlfNameNotCanonical:
   182  			tlfName = e.NameToTry
   183  		case nil:
   184  			return tlfHandle, nil
   185  		default:
   186  			return nil, err
   187  		}
   188  	}
   189  }
   190  
   191  // getHandleFromFolderName returns a TLFHandle given a folder
   192  // name (e.g., "u1,u2#u3") and a public/private bool.  DEPRECATED.
   193  func getHandleFromFolderName(
   194  	ctx context.Context, kbpki KBPKI, idGetter tlfhandle.IDGetter,
   195  	syncGetter syncedTlfGetterSetter, tlfName string,
   196  	public bool) (*tlfhandle.Handle, error) {
   197  	// TODO(KBFS-2185): update the protocol to support requests
   198  	// for single-team TLFs.
   199  	t := tlf.Private
   200  	if public {
   201  		t = tlf.Public
   202  	}
   203  	return GetHandleFromFolderNameAndType(
   204  		ctx, kbpki, idGetter, syncGetter, tlfName, t)
   205  }
   206  
   207  // IsWriterFromHandle checks whether the given UID is a writer for the
   208  // given handle.  It understands team-keyed handles as well as
   209  // classically-keyed handles.
   210  func IsWriterFromHandle(
   211  	ctx context.Context, h *tlfhandle.Handle, checker kbfsmd.TeamMembershipChecker,
   212  	osg idutil.OfflineStatusGetter, uid keybase1.UID,
   213  	verifyingKey kbfscrypto.VerifyingKey) (bool, error) {
   214  	if h.TypeForKeying() != tlf.TeamKeying {
   215  		return h.IsWriter(uid), nil
   216  	}
   217  
   218  	// Team membership needs to be checked with the service.  For a
   219  	// SingleTeam TLF, there is always only a single writer in the
   220  	// handle.
   221  	tid, err := h.FirstResolvedWriter().AsTeam()
   222  	if err != nil {
   223  		return false, err
   224  	}
   225  	offline := keybase1.OfflineAvailability_NONE
   226  	if osg != nil {
   227  		offline = osg.OfflineAvailabilityForID(h.TlfID())
   228  	}
   229  	return checker.IsTeamWriter(ctx, tid, uid, verifyingKey, offline)
   230  }
   231  
   232  func isReaderFromHandle(
   233  	ctx context.Context, h *tlfhandle.Handle, checker kbfsmd.TeamMembershipChecker,
   234  	osg idutil.OfflineStatusGetter, uid keybase1.UID) (bool, error) {
   235  	if h.TypeForKeying() != tlf.TeamKeying {
   236  		return h.IsReader(uid), nil
   237  	}
   238  
   239  	// Team membership needs to be checked with the service.  For a
   240  	// SingleTeam TLF, there is always only a single writer in the
   241  	// handle.
   242  	tid, err := h.FirstResolvedWriter().AsTeam()
   243  	if err != nil {
   244  		return false, err
   245  	}
   246  	offline := keybase1.OfflineAvailability_NONE
   247  	if osg != nil {
   248  		offline = osg.OfflineAvailabilityForID(h.TlfID())
   249  	}
   250  	return checker.IsTeamReader(ctx, tid, uid, offline)
   251  }
   252  
   253  func tlfToMerkleTreeID(id tlf.ID) keybase1.MerkleTreeID {
   254  	switch id.Type() {
   255  	case tlf.Private:
   256  		return keybase1.MerkleTreeID_KBFS_PRIVATE
   257  	case tlf.Public:
   258  		return keybase1.MerkleTreeID_KBFS_PUBLIC
   259  	case tlf.SingleTeam:
   260  		return keybase1.MerkleTreeID_KBFS_PRIVATETEAM
   261  	default:
   262  		panic(fmt.Sprintf("Unexpected TLF type: %d", id.Type()))
   263  	}
   264  }
   265  
   266  // IsOnlyWriterInNonTeamTlf returns true if and only if the TLF described by h
   267  // is a non-team TLF, and the currently logged-in user is the only writer for
   268  // the TLF.  In case of any error false is returned.
   269  func IsOnlyWriterInNonTeamTlf(ctx context.Context, kbpki KBPKI,
   270  	h *tlfhandle.Handle) bool {
   271  	session, err := idutil.GetCurrentSessionIfPossible(
   272  		ctx, kbpki, h.Type() == tlf.Public)
   273  	if err != nil {
   274  		return false
   275  	}
   276  	if h.TypeForKeying() == tlf.TeamKeying {
   277  		return false
   278  	}
   279  	return tlf.UserIsOnlyWriter(session.Name, h.GetCanonicalName())
   280  }
   281  
   282  const (
   283  	// GitStorageRootPrefix is the prefix of the temp storage root
   284  	// directory made for single-op git operations.
   285  	GitStorageRootPrefix = "kbfsgit"
   286  	// ConflictStorageRootPrefix is the prefix of the temp directory
   287  	// made for the conflict resolution disk cache.
   288  	ConflictStorageRootPrefix = "kbfs_conflict_disk_cache"
   289  
   290  	minAgeForStorageCleanup = 24 * time.Hour
   291  )
   292  
   293  func cleanOldTempStorageRoots(config Config) {
   294  	log := config.MakeLogger("")
   295  	ctx := CtxWithRandomIDReplayable(
   296  		context.Background(), CtxInitKey, CtxInitID, log)
   297  
   298  	storageRoot := config.StorageRoot()
   299  	d, err := os.Open(storageRoot)
   300  	if err != nil {
   301  		log.CDebugf(ctx, "Error opening storage root %s: %+v", storageRoot, err)
   302  		return
   303  	}
   304  	defer d.Close()
   305  
   306  	fis, err := d.Readdir(0)
   307  	if err != nil {
   308  		log.CDebugf(ctx, "Error reading storage root %s: %+v", storageRoot, err)
   309  		return
   310  	}
   311  
   312  	modTimeCutoff := config.Clock().Now().Add(-minAgeForStorageCleanup)
   313  	cleanedOne := false
   314  	for _, fi := range fis {
   315  		if fi.ModTime().After(modTimeCutoff) {
   316  			continue
   317  		}
   318  
   319  		if !strings.HasPrefix(fi.Name(), GitStorageRootPrefix) &&
   320  			!strings.HasPrefix(fi.Name(), ConflictStorageRootPrefix) {
   321  			continue
   322  		}
   323  
   324  		cleanedOne = true
   325  		dir := filepath.Join(storageRoot, fi.Name())
   326  		log.CDebugf(ctx, "Cleaning up old storage root %s, "+
   327  			"last modified at %s", dir, fi.ModTime())
   328  		err = os.RemoveAll(dir)
   329  		if err != nil {
   330  			log.CDebugf(ctx, "Error deleting %s: %+v", dir, err)
   331  			continue
   332  		}
   333  	}
   334  
   335  	if cleanedOne {
   336  		log.CDebugf(ctx, "Done cleaning old storage roots")
   337  	}
   338  }
   339  
   340  // GetLocalDiskStats returns the local disk stats, according to the
   341  // disk block cache.
   342  func GetLocalDiskStats(ctx context.Context, dbc DiskBlockCache) (
   343  	bytesAvail, bytesTotal int64) {
   344  	if dbc == nil {
   345  		return 0, 0
   346  	}
   347  
   348  	dbcStatus := dbc.Status(ctx)
   349  	if status, ok := dbcStatus["SyncBlockCache"]; ok {
   350  		return int64(status.LocalDiskBytesAvailable),
   351  			int64(status.LocalDiskBytesTotal)
   352  	}
   353  	return 0, 0
   354  }
   355  
   356  // FillInDiskSpaceStatus fills in the `OutOfSyncSpace`,
   357  // prefetchStatus, and local disk space fields of the given status.
   358  func FillInDiskSpaceStatus(
   359  	ctx context.Context, status *keybase1.FolderSyncStatus,
   360  	prefetchStatus keybase1.PrefetchStatus, dbc DiskBlockCache) {
   361  	status.PrefetchStatus = prefetchStatus
   362  	if dbc == nil {
   363  		return
   364  	}
   365  
   366  	status.LocalDiskBytesAvailable, status.LocalDiskBytesTotal =
   367  		GetLocalDiskStats(ctx, dbc)
   368  
   369  	if prefetchStatus == keybase1.PrefetchStatus_COMPLETE {
   370  		return
   371  	}
   372  
   373  	hasRoom, _, err := dbc.DoesCacheHaveSpace(
   374  		context.Background(), DiskBlockSyncCache)
   375  	if err != nil {
   376  		return
   377  	}
   378  	status.OutOfSyncSpace = !hasRoom
   379  }
   380  
   381  // KeybaseServicePassthrough is an implementation of
   382  // `KeybaseServiceCn` that just uses the existing services in a given,
   383  // existing Config object.
   384  type KeybaseServicePassthrough struct {
   385  	config Config
   386  }
   387  
   388  // NewKeybaseServicePassthrough returns a new service passthrough
   389  // using the given config.
   390  func NewKeybaseServicePassthrough(config Config) KeybaseServicePassthrough {
   391  	return KeybaseServicePassthrough{config: config}
   392  }
   393  
   394  var _ KeybaseServiceCn = KeybaseServicePassthrough{}
   395  
   396  // NewKeybaseService implements the KeybaseServiceCn for
   397  // KeybaseServicePassthrough.
   398  func (ksp KeybaseServicePassthrough) NewKeybaseService(
   399  	_ Config, _ InitParams, _ Context, _ logger.Logger) (
   400  	KeybaseService, error) {
   401  	return ksp.config.KeybaseService(), nil
   402  }
   403  
   404  // NewCrypto implements the KeybaseServiceCn for
   405  // KeybaseServicePassthrough.
   406  func (ksp KeybaseServicePassthrough) NewCrypto(
   407  	_ Config, _ InitParams, _ Context, _ logger.Logger) (Crypto, error) {
   408  	return ksp.config.Crypto(), nil
   409  }
   410  
   411  // NewChat implements the KeybaseServiceCn for
   412  // KeybaseServicePassthrough.
   413  func (ksp KeybaseServicePassthrough) NewChat(
   414  	_ Config, _ InitParams, _ Context, _ logger.Logger) (Chat, error) {
   415  	return ksp.config.Chat(), nil
   416  }
   417  
   418  // MakeDiskMDServer creates a disk-based local MD server.
   419  func MakeDiskMDServer(config Config, serverRootDir string) (MDServer, error) {
   420  	mdPath := filepath.Join(serverRootDir, "kbfs_md")
   421  	return NewMDServerDir(mdServerLocalConfigAdapter{config}, mdPath)
   422  }
   423  
   424  // MakeDiskBlockServer creates a disk-based local block server.
   425  func MakeDiskBlockServer(config Config, serverRootDir string) BlockServer {
   426  	blockPath := filepath.Join(serverRootDir, "kbfs_block")
   427  	bserverLog := config.MakeLogger("BSD")
   428  	return NewBlockServerDir(config.Codec(), bserverLog, blockPath)
   429  }
   430  
   431  func cacheHashBehavior(
   432  	bsGetter blockServerGetter, modeGetter initModeGetter,
   433  	id tlf.ID) data.BlockCacheHashBehavior {
   434  	if modeGetter.Mode().IsSingleOp() || TLFJournalEnabled(bsGetter, id) {
   435  		// If the journal is enabled, or single-op mode is enabled
   436  		// (which implies either local or journal writes), then skip
   437  		// any known-ptr block hash computations.
   438  		return data.SkipCacheHash
   439  	}
   440  	return data.DoCacheHash
   441  }