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