/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/kbfs/data/file_data.go
1 349 строк
43 KB
chrisnojima-zoom
Version 670 - clean2 (#29122)
08 июн 2026, 19:31
Не верифицирован
08 июн 2026, 19:31
1943197
Код
Авторство
О чём код?
// Copyright 2016 Keybase Inc. All rights reserved. // Use of this source code is governed by a BSD // license that can be found in the LICENSE file. package data import ( "context" "fmt" "math" "time" "github.com/keybase/client/go/kbfs/kbfsblock" "github.com/keybase/client/go/kbfs/libkey" "github.com/keybase/client/go/kbfs/tlf" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/logger" "github.com/keybase/client/go/protocol/keybase1" ) // FileBlockGetter is a function that gets a block suitable for // reading or writing, and also returns whether the block was already // dirty. It may be called from new goroutines, and must handle any // required locks accordingly. type FileBlockGetter func(context.Context, libkey.KeyMetadata, BlockPointer, Path, BlockReqType) (fblock *FileBlock, wasDirty bool, err error) // FileData is a helper struct for accessing and manipulating data // within a file. It's meant for use within a single scope, not for // long-term storage. The caller must ensure goroutine-safety. type FileData struct { getter FileBlockGetter tree *blockTree } // NewFileData makes a new file data object for the given `file` // within the given `kmd`. func NewFileData( file Path, chargedTo keybase1.UserOrTeamID, bsplit BlockSplitter, kmd libkey.KeyMetadata, getter FileBlockGetter, cacher dirtyBlockCacher, log logger.Logger, vlog *libkb.VDebugLog, ) *FileData { fd := &FileData{ getter: getter, } fd.tree = &blockTree{ file: file, chargedTo: chargedTo, kmd: kmd, bsplit: bsplit, getter: fd.blockGetter, cacher: cacher, log: log, vlog: vlog, } return fd } func (fd *FileData) rootBlockPointer() BlockPointer { return fd.tree.file.TailPointer() } func (fd *FileData) blockGetter( ctx context.Context, kmd libkey.KeyMetadata, ptr BlockPointer, file Path, rtype BlockReqType) ( block BlockWithPtrs, wasDirty bool, err error, ) { return fd.getter(ctx, kmd, ptr, file, rtype) } func (fd *FileData) getLeafBlocksForOffsetRange(ctx context.Context, ptr BlockPointer, pblock *FileBlock, startOff, endOff Int64Offset, prefixOk bool) (pathsFromRoot [][]ParentBlockAndChildIndex, blocks map[BlockPointer]Block, nextBlockOffset Int64Offset, err error, ) { var eo Offset if endOff >= 0 { eo = endOff } pathsFromRoot, blocks, nbo, err := fd.tree.getBlocksForOffsetRange( ctx, ptr, pblock, startOff, eo, prefixOk, true) if err != nil { return nil, nil, 0, err } if nbo != nil { nextBlockOffset = nbo.(Int64Offset) } else { nextBlockOffset = -1 } return pathsFromRoot, blocks, nextBlockOffset, nil } func childFileIptr(p ParentBlockAndChildIndex) IndirectFilePtr { fb := p.pblock.(*FileBlock) return fb.IPtrs[p.childIndex] } // getByteSlicesInOffsetRange returns an ordered, continuous slice of // byte ranges for the data described by the half-inclusive offset // range `[startOff, endOff)`. If `endOff` == -1, it returns data to // the end of the file. The caller is responsible for concatenating // the data into a single buffer if desired. If `prefixOk` is true, // the function will ignore context deadline errors and return // whatever prefix of the data it could fetch within the deadine. func (fd *FileData) getByteSlicesInOffsetRange(ctx context.Context, startOff, endOff Int64Offset, prefixOk bool, ) ([][]byte, error) { if startOff < 0 || endOff < -1 { return nil, fmt.Errorf("Bad offset range [%d, %d)", startOff, endOff) } else if endOff != -1 && endOff <= startOff { return nil, nil } topBlock, _, err := fd.getter(ctx, fd.tree.kmd, fd.rootBlockPointer(), fd.tree.file, BlockRead) if err != nil { return nil, err } // Find all the indirect pointers to leaf blocks in the offset range. var iptrs []IndirectFilePtr endBlockOff := Int64Offset(-1) nextBlockOff := Int64Offset(-1) var blockMap map[BlockPointer]Block if topBlock.IsInd { var pfr [][]ParentBlockAndChildIndex pfr, blockMap, nextBlockOff, err = fd.getLeafBlocksForOffsetRange( ctx, fd.rootBlockPointer(), topBlock, startOff, endOff, prefixOk) if err != nil { return nil, err } for i, p := range pfr { if len(p) == 0 { return nil, fmt.Errorf("Unexpected empty path to child for "+ "file %v", fd.rootBlockPointer()) } lowestAncestor := p[len(p)-1] iptr := childFileIptr(lowestAncestor) iptrs = append(iptrs, iptr) if i == len(pfr)-1 { leafBlock := blockMap[iptr.BlockPointer].(*FileBlock) endBlockOff = iptr.Off + Int64Offset(len(leafBlock.Contents)) } } } else { iptrs = []IndirectFilePtr{{ BlockInfo: BlockInfo{BlockPointer: fd.rootBlockPointer()}, Off: 0, }} endBlockOff = Int64Offset(len(topBlock.Contents)) blockMap = map[BlockPointer]Block{fd.rootBlockPointer(): topBlock} } if len(iptrs) == 0 { return nil, nil } nRead := int64(0) n := int64(endOff - startOff) if endOff == -1 { n = int64(endBlockOff - startOff) } // Grab the relevant byte slices from each block described by the // indirect pointer, filling in holes as needed. var bytes [][]byte for i, iptr := range iptrs { block := blockMap[iptr.BlockPointer].(*FileBlock) blockLen := int64(len(block.Contents)) nextByte := nRead + int64(startOff) toRead := n - nRead blockOff := iptr.Off lastByteInBlock := int64(blockOff) + blockLen nextIPtrOff := nextBlockOff if i < len(iptrs)-1 { nextIPtrOff = iptrs[i+1].Off } if nextByte >= lastByteInBlock { if nextIPtrOff > 0 { fill := min(int64(nextIPtrOff)-nextByte, toRead) fd.tree.vlog.CLogf( ctx, libkb.VLog1, "Read from hole: nextByte=%d "+ "lastByteInBlock=%d fill=%d", nextByte, lastByteInBlock, fill) if fill <= 0 { fd.tree.log.CErrorf(ctx, "Read invalid file fill <= 0 while reading hole") return nil, BadSplitError{} } bytes = append(bytes, make([]byte, fill)) nRead += fill continue } return bytes, nil } else if toRead > lastByteInBlock-nextByte { toRead = lastByteInBlock - nextByte } // Check for holes in the middle of a file. if nextByte < int64(blockOff) { fill := int64(blockOff) - nextByte bytes = append(bytes, make([]byte, fill)) nRead += fill nextByte += fill toRead -= fill } firstByteToRead := nextByte - int64(blockOff) bytes = append(bytes, block.Contents[firstByteToRead:toRead+firstByteToRead]) nRead += toRead } // If we didn't complete the read and there's another block, then // we've hit another hole and need to add a fill. if nRead < n && nextBlockOff > 0 { toRead := n - nRead nextByte := nRead + int64(startOff) fill := min(int64(nextBlockOff)-nextByte, toRead) fd.tree.vlog.CLogf( ctx, libkb.VLog1, "Read from hole at end of file: nextByte=%d "+ "fill=%d", nextByte, fill) if fill <= 0 { fd.tree.log.CErrorf(ctx, "Read invalid file fill <= 0 while reading hole") return nil, BadSplitError{} } bytes = append(bytes, make([]byte, fill)) } return bytes, nil } // The amount that the read timeout is smaller than the global one. const readTimeoutSmallerBy = 2 * time.Second // read fills the `dest` buffer with data from the file, starting at // `startOff`. Returns the number of bytes copied. If the read // operation nears the deadline set in `ctx`, it returns as big a // prefix as possible before reaching the deadline. func (fd *FileData) Read(ctx context.Context, dest []byte, startOff Int64Offset, ) (int64, error) { if len(dest) == 0 { return 0, nil } // If we have a large enough timeout add a temporary timeout that is // readTimeoutSmallerBy. Use that for reading so short reads get returned // upstream without triggering the global timeout. now := time.Now() deadline, haveTimeout := ctx.Deadline() if haveTimeout { rem := deadline.Sub(now) - readTimeoutSmallerBy if rem > 0 { var cancel func() ctx, cancel = context.WithTimeout(ctx, rem) defer cancel() } } bytes, err := fd.getByteSlicesInOffsetRange(ctx, startOff, startOff+Int64Offset(len(dest)), true) if err != nil { return 0, err } currLen := int64(0) for _, b := range bytes { bLen := int64(len(b)) copy(dest[currLen:currLen+bLen], b) currLen += bLen } return currLen, nil } // GetBytes returns a buffer containing data from the file, in the // half-inclusive range `[startOff, endOff)`. If `endOff` == -1, it // returns data until the end of the file. func (fd *FileData) GetBytes(ctx context.Context, startOff, endOff Int64Offset, ) (data []byte, err error) { bytes, err := fd.getByteSlicesInOffsetRange(ctx, startOff, endOff, false) if err != nil { return nil, err } bufSize := 0 for _, b := range bytes { bufSize += len(b) } data = make([]byte, bufSize) currLen := 0 for _, b := range bytes { copy(data[currLen:currLen+len(b)], b) currLen += len(b) } return data, nil } // createIndirectBlock creates a new indirect block and pick a new id // for the existing block, and use the existing block's ID for the new // indirect block that becomes the parent. func (fd *FileData) createIndirectBlock( ctx context.Context, df *DirtyFile, dver Ver, ) (*FileBlock, error) { newID, err := kbfsblock.MakeTemporaryID() if err != nil { return nil, err } fblock := &FileBlock{ CommonBlock: CommonBlock{ IsInd: true, }, IPtrs: []IndirectFilePtr{ { BlockInfo: BlockInfo{ BlockPointer: BlockPointer{ ID: newID, KeyGen: fd.tree.kmd.LatestKeyGeneration(), DataVer: dver, Context: kbfsblock.MakeFirstContext( fd.tree.chargedTo, fd.rootBlockPointer().GetBlockType()), DirectType: fd.rootBlockPointer().DirectType, }, EncodedSize: 0, }, Off: 0, }, }, } fd.tree.vlog.CLogf( ctx, libkb.VLog1, "Creating new level of indirection for file %v, "+ "new block id for old top level is %v", fd.rootBlockPointer(), newID) // Mark the old block ID as not dirty, so that we will treat the // old block ID as newly dirtied in cacheBlockIfNotYetDirtyLocked. df.setBlockNotDirty(fd.rootBlockPointer()) err = fd.tree.cacher(ctx, fd.rootBlockPointer(), fblock) if err != nil { return nil, err } return fblock, nil } // GetFileBlockAtOffset returns the leaf file block responsible for // the given offset. func (fd *FileData) GetFileBlockAtOffset(ctx context.Context, topBlock *FileBlock, off Int64Offset, rtype BlockReqType) ( ptr BlockPointer, parentBlocks []ParentBlockAndChildIndex, block *FileBlock, nextBlockStartOff, startOff Int64Offset, wasDirty bool, err error, ) { ptr, parentBlocks, b, nbso, so, wasDirty, err := fd.tree.getBlockAtOffset( ctx, topBlock, off, rtype) if err != nil { return ZeroPtr, nil, nil, 0, 0, false, err } if b != nil { block = b.(*FileBlock) } if nbso != nil { nextBlockStartOff = nbso.(Int64Offset) } else { nextBlockStartOff = -1 } if so != nil { startOff = so.(Int64Offset) } return ptr, parentBlocks, block, nextBlockStartOff, startOff, wasDirty, nil } func (fd *FileData) fileTopBlocker(df *DirtyFile) createTopBlockFn { return func(ctx context.Context, dv Ver) (BlockWithPtrs, error) { return fd.createIndirectBlock(ctx, df, dv) } } // Write sets the given data and the given offset within the file, // making new blocks and new levels of indirection as needed. Return // params: // - newDe: a new directory entry with the EncodedSize cleared if the file // was extended. // - dirtyPtrs: a slice of the BlockPointers that have been dirtied during // the write. This includes any interior indirect blocks that may not // have been changed yet, but which will need to change as part of the // sync process because of leaf node changes below it. // - unrefs: a slice of BlockInfos that must be unreferenced as part of an // eventual sync of this write. May be non-nil even if err != nil. // - newlyDirtiedChildBytes is the total amount of block data dirtied by this // write, including the entire size of blocks that have had at least one // byte dirtied. As above, it may be non-zero even if err != nil. // - bytesExtended is the number of bytes the length of the file has been // extended as part of this write. func (fd *FileData) Write(ctx context.Context, data []byte, off Int64Offset, topBlock *FileBlock, oldDe DirEntry, df *DirtyFile) ( newDe DirEntry, dirtyPtrs []BlockPointer, unrefs []BlockInfo, newlyDirtiedChildBytes int64, bytesExtended int64, err error, ) { n := int64(len(data)) nCopied := int64(0) oldSizeWithoutHoles := oldDe.Size newDe = oldDe fd.tree.vlog.CLogf(ctx, libkb.VLog1, "Writing %d bytes at off %d", n, off) dirtyMap := make(map[BlockPointer]bool) for nCopied < n { ptr, parentBlocks, block, nextBlockOff, startOff, wasDirty, err := fd.GetFileBlockAtOffset( ctx, topBlock, off+Int64Offset(nCopied), BlockWrite) if err != nil { return newDe, nil, unrefs, newlyDirtiedChildBytes, 0, err } oldLen := len(block.Contents) // Take care not to write past the beginning of the next block // by using maxWrite. maxWrite := Int64Offset(len(data)) if nextBlockOff > 0 { if room := nextBlockOff - off; room < maxWrite { maxWrite = room } } oldNCopied := nCopied nCopied += fd.tree.bsplit.CopyUntilSplit( block, nextBlockOff < 0, data[nCopied:maxWrite], int64(off+Int64Offset(nCopied)-startOff)) // If we need another block but there are no more, then make one. switchToIndirect := false if nCopied < n { needExtendFile := nextBlockOff < 0 needFillHole := off+Int64Offset(nCopied) < nextBlockOff newBlockOff := startOff + Int64Offset(len(block.Contents)) if nCopied == 0 { if newBlockOff < off { // We are writing past the end of a file, or // somewhere inside a hole, not right at the start // of it; all we have done so far it reached the // end of an existing block (possibly zero-filling // it out to its capacity). Make sure the next // block starts right at the offset we care about. newBlockOff = off } } else if newBlockOff != off+Int64Offset(nCopied) { return newDe, nil, unrefs, newlyDirtiedChildBytes, 0, fmt.Errorf("Copied %d bytes, but newBlockOff=%d does not "+ "match off=%d plus new bytes", nCopied, newBlockOff, off) } var rightParents []ParentBlockAndChildIndex if needExtendFile || needFillHole { // Make a new right block and update the parent's // indirect block list, adding a level of indirection // if needed. If we're just filling a hole, the block // will end up all the way to the right of the range, // and its offset will be smaller than the block to // its left -- we'll fix that up below. var newDirtyPtrs []BlockPointer fd.tree.vlog.CLogf( ctx, libkb.VLog1, "Making new right block at "+ "nCopied=%d, newBlockOff=%d", nCopied, newBlockOff) wasIndirect := topBlock.IsInd rightParents, newDirtyPtrs, err = fd.tree.newRightBlock( ctx, parentBlocks, newBlockOff, DefaultNewBlockDataVersion(false), NewFileBlockWithPtrs, fd.fileTopBlocker(df), ) if err != nil { return newDe, nil, unrefs, newlyDirtiedChildBytes, 0, err } topBlock = rightParents[0].pblock.(*FileBlock) for _, p := range newDirtyPtrs { dirtyMap[p] = true } if topBlock.IsInd != wasIndirect { // The whole direct data block needs to be // re-uploaded as a child block with a new block // pointer, so below we'll need to track the dirty // bytes of the direct block and cache the block // as dirty. (Note that currently we don't track // dirty bytes for indirect blocks.) switchToIndirect = true ptr = topBlock.IPtrs[0].BlockPointer } } // If we're filling a hole, swap the new right block into // the hole and shift everything else over. if needFillHole { newDirtyPtrs, newUnrefs, bytes, err := fd.tree.shiftBlocksToFillHole(ctx, rightParents) if err != nil { return newDe, nil, unrefs, newlyDirtiedChildBytes, 0, err } for _, p := range newDirtyPtrs { dirtyMap[p] = true } unrefs = append(unrefs, newUnrefs...) newlyDirtiedChildBytes += bytes if oldSizeWithoutHoles == oldDe.Size { // For the purposes of calculating the newly-dirtied // bytes for the deferral calculation, disregard the // existing "hole" in the file. oldSizeWithoutHoles = uint64(newBlockOff) //nolint:gosec // G115: File offsets are bounded by filesystem limits } } } // Nothing was copied, no need to dirty anything. This can // happen when trying to append to the contents of the file // (i.e., either to the end of the file or right before the // "hole"), and the last block is already full. if nCopied == oldNCopied && oldLen == len(block.Contents) && !switchToIndirect { continue } // Only in the last block does the file size grow. if oldLen != len(block.Contents) && nextBlockOff < 0 { newDe.EncodedSize = 0 // Since this is the last block, the end of this block // marks the file size. newDe.Size = uint64(startOff + Int64Offset(len(block.Contents))) //nolint:gosec // G115: File sizes are bounded by filesystem limits } // Calculate the amount of bytes we've newly-dirtied as part // of this write. newlyDirtiedChildBytes += int64(len(block.Contents)) if wasDirty { newlyDirtiedChildBytes -= int64(oldLen) } newDirtyPtrs, newUnrefs, err := fd.tree.markParentsDirty( ctx, parentBlocks) unrefs = append(unrefs, newUnrefs...) if err != nil { return newDe, nil, unrefs, newlyDirtiedChildBytes, 0, err } for _, p := range newDirtyPtrs { dirtyMap[p] = true } // keep the old block ID while it's dirty if err = fd.tree.cacher(ctx, ptr, block); err != nil { return newDe, nil, unrefs, newlyDirtiedChildBytes, 0, err } dirtyMap[ptr] = true } // Always make the top block dirty, so we will sync any indirect // blocks. This has the added benefit of ensuring that any write // to a file while it's being sync'd will be deferred, even if // it's to a block that's not currently being sync'd, since this // top-most block will always be in the dirtyFiles map. We do // this even for 0-byte writes, which indicate a forced sync. if err = fd.tree.cacher(ctx, fd.rootBlockPointer(), topBlock); err != nil { return newDe, nil, unrefs, newlyDirtiedChildBytes, 0, err } dirtyMap[fd.rootBlockPointer()] = true lastByteWritten := int64(off) + int64(len(data)) // not counting holes bytesExtended = 0 if lastByteWritten > int64(oldSizeWithoutHoles) { //nolint:gosec // G115: File sizes are bounded by filesystem limits bytesExtended = lastByteWritten - int64(oldSizeWithoutHoles) //nolint:gosec // G115: File sizes are bounded by filesystem limits } dirtyPtrs = make([]BlockPointer, 0, len(dirtyMap)) for p := range dirtyMap { dirtyPtrs = append(dirtyPtrs, p) } return newDe, dirtyPtrs, unrefs, newlyDirtiedChildBytes, bytesExtended, nil } // TruncateExtend increases file size to the given size by appending // a "hole" to the file. Return params: // - newDe: a new directory entry with the EncodedSize cleared. // - dirtyPtrs: a slice of the BlockPointers that have been dirtied during // the truncate. func (fd *FileData) TruncateExtend(ctx context.Context, size uint64, topBlock *FileBlock, parentBlocks []ParentBlockAndChildIndex, oldDe DirEntry, df *DirtyFile) ( newDe DirEntry, dirtyPtrs []BlockPointer, err error, ) { fd.tree.vlog.CLogf( ctx, libkb.VLog1, "truncateExtend: extending file %v to size %d", fd.rootBlockPointer(), size) switchToIndirect := !topBlock.IsInd oldTopBlock := topBlock if switchToIndirect { fd.tree.vlog.CLogf( ctx, libkb.VLog1, "truncateExtend: making block indirect %v", fd.rootBlockPointer()) } rightParents, newDirtyPtrs, err := fd.tree.newRightBlock( ctx, parentBlocks, Int64Offset(size), //nolint:gosec // G115: File sizes are bounded by filesystem limits DefaultNewBlockDataVersion(true), NewFileBlockWithPtrs, fd.fileTopBlocker(df)) if err != nil { return DirEntry{}, nil, err } topBlock = rightParents[0].pblock.(*FileBlock) if switchToIndirect { topBlock.IPtrs[0].Holes = true err = fd.tree.cacher(ctx, topBlock.IPtrs[0].BlockPointer, oldTopBlock) if err != nil { return DirEntry{}, nil, err } dirtyPtrs = append(dirtyPtrs, topBlock.IPtrs[0].BlockPointer) fd.tree.vlog.CLogf( ctx, libkb.VLog1, "truncateExtend: new zero data block %v", topBlock.IPtrs[0].BlockPointer) } dirtyPtrs = append(dirtyPtrs, newDirtyPtrs...) newDe = oldDe newDe.EncodedSize = 0 // update the file info newDe.Size = size // Mark all for presence of holes, one would be enough, // but this is more robust and easy. for i := range topBlock.IPtrs { topBlock.IPtrs[i].Holes = true } // Always make the top block dirty, so we will sync its // indirect blocks. This has the added benefit of ensuring // that any write to a file while it's being sync'd will be // deferred, even if it's to a block that's not currently // being sync'd, since this top-most block will always be in // the fileBlockStates map. err = fd.tree.cacher(ctx, fd.rootBlockPointer(), topBlock) if err != nil { return DirEntry{}, nil, err } dirtyPtrs = append(dirtyPtrs, fd.rootBlockPointer()) return newDe, dirtyPtrs, nil } // TruncateShrink shrinks the file to the given size. Return params: // - newDe: a new directory entry with the EncodedSize cleared if the file // shrunk. // - dirtyPtrs: a slice of the BlockPointers that have been dirtied during // the truncate. This includes any interior indirect blocks that may not // have been changed yet, but which will need to change as part of the // sync process because of leaf node changes below it. // - unrefs: a slice of BlockInfos that must be unreferenced as part of an // eventual sync of this write. May be non-nil even if err != nil. // - newlyDirtiedChildBytes is the total amount of block data dirtied by this // truncate, including the entire size of blocks that have had at least one // byte dirtied. As above, it may be non-zero even if err != nil. func (fd *FileData) TruncateShrink(ctx context.Context, size uint64, topBlock *FileBlock, oldDe DirEntry) ( newDe DirEntry, dirtyPtrs []BlockPointer, unrefs []BlockInfo, newlyDirtiedChildBytes int64, err error, ) { if size > math.MaxInt64 { return DirEntry{}, nil, nil, 0, fmt.Errorf("file size %d exceeds maximum offset", size) } iSize := Int64Offset(size) ptr, parentBlocks, block, nextBlockOff, startOff, wasDirty, err := fd.GetFileBlockAtOffset(ctx, topBlock, iSize, BlockWrite) if err != nil { return DirEntry{}, nil, nil, 0, err } oldLen := len(block.Contents) // We need to delete some data (and possibly entire blocks). Note // we make a new slice and copy data in order to make sure the // data being truncated can be fully garbage-collected. block.Contents = append([]byte(nil), block.Contents[:iSize-startOff]...) newlyDirtiedChildBytes = int64(len(block.Contents)) if wasDirty { newlyDirtiedChildBytes -= int64(oldLen) // negative } // Need to mark the parents dirty before calling // `getIndirectBlocksForOffsetRange`, so that function will see // the new copies when fetching the blocks. newDirtyPtrs, newUnrefs, err := fd.tree.markParentsDirty(ctx, parentBlocks) unrefs = append(unrefs, newUnrefs...) if err != nil { return DirEntry{}, nil, unrefs, newlyDirtiedChildBytes, err } dirtyMap := make(map[BlockPointer]bool) for _, p := range newDirtyPtrs { dirtyMap[p] = true } if nextBlockOff > 0 { // TODO: remove any unnecessary levels of indirection if the // number of leaf nodes shrinks significantly (KBFS-1824). // Get all paths to any leaf nodes following the new // right-most block, since those blocks need to be // unreferenced, and their parents need to be modified or // unreferenced. pfr, err := fd.tree.getIndirectBlocksForOffsetRange( ctx, topBlock, nextBlockOff, nil) if err != nil { return DirEntry{}, nil, nil, 0, err } // A map from a pointer to an indirect block -> that block's // original set of block pointers, before they are truncated // in the loop below. It also tracks which pointed-to blocks // have already been processed. savedChildPtrs := make(map[BlockPointer][]IndirectFilePtr) for _, Path := range pfr { // parentInfo points to pb.pblock in the loop below. The // initial case is the top block, for which we need to // fake a block info using just the root block pointer. parentInfo := BlockInfo{BlockPointer: fd.rootBlockPointer()} leftMost := true for i, pb := range Path { ptrs := savedChildPtrs[parentInfo.BlockPointer] if ptrs == nil { // Process each block exactly once, removing all // now-unnecessary indirect pointers (but caching // that list so we can still walk the tree on the // next iterations). pblock := pb.pblock.(*FileBlock) ptrs = pblock.IPtrs savedChildPtrs[parentInfo.BlockPointer] = ptrs // Remove the first child iptr and everything // following it if all the child indices below // this level are 0. removeStartingFromIndex := pb.childIndex for j := i + 1; j < len(Path); j++ { if Path[j].childIndex > 0 { removeStartingFromIndex++ break } } // If we remove iptr 0, this block can be // unreferenced (unless it's on the left-most edge // of the tree, in which case we keep it around // for now -- see above TODO). if pb.childIndex == 0 && !leftMost { if parentInfo.EncodedSize != 0 { unrefs = append(unrefs, parentInfo) } } else if removeStartingFromIndex < len(pblock.IPtrs) { // Make sure we're modifying a copy of the // block by fetching it again with blockWrite. // We do this instead of calling DeepCopy in // case the a copy of the block has already // been made and put into the dirty // cache. (e.g., in a previous iteration of // this loop). pblock, _, err = fd.getter( ctx, fd.tree.kmd, parentInfo.BlockPointer, fd.tree.file, BlockWrite) if err != nil { return DirEntry{}, nil, nil, newlyDirtiedChildBytes, err } pblock.IPtrs = pblock.IPtrs[:removeStartingFromIndex] err = fd.tree.cacher( ctx, parentInfo.BlockPointer, pblock) if err != nil { return DirEntry{}, nil, nil, newlyDirtiedChildBytes, err } dirtyMap[parentInfo.BlockPointer] = true } } // Down to the next level. If we've hit the leaf // level, unreference the block. parentInfo = ptrs[pb.childIndex].BlockInfo if i == len(Path)-1 && parentInfo.EncodedSize != 0 { unrefs = append(unrefs, parentInfo) } else if pb.childIndex > 0 { leftMost = false } } } } if topBlock.IsInd { // Always make the top block dirty, so we will sync its // indirect blocks. This has the added benefit of ensuring // that any truncate to a file while it's being sync'd will be // deferred, even if it's to a block that's not currently // being sync'd, since this top-most block will always be in // the dirtyFiles map. err = fd.tree.cacher(ctx, fd.rootBlockPointer(), topBlock) if err != nil { return DirEntry{}, nil, nil, newlyDirtiedChildBytes, err } dirtyMap[fd.rootBlockPointer()] = true } newDe = oldDe newDe.EncodedSize = 0 newDe.Size = size // Keep the old block ID while it's dirty. if err = fd.tree.cacher(ctx, ptr, block); err != nil { return DirEntry{}, nil, nil, newlyDirtiedChildBytes, err } dirtyMap[ptr] = true dirtyPtrs = make([]BlockPointer, 0, len(dirtyMap)) for p := range dirtyMap { dirtyPtrs = append(dirtyPtrs, p) } return newDe, dirtyPtrs, unrefs, newlyDirtiedChildBytes, nil } func (fd *FileData) getNextDirtyFileBlockAtOffset(ctx context.Context, topBlock *FileBlock, off Int64Offset, rtype BlockReqType, dirtyBcache DirtyBlockCache) ( ptr BlockPointer, parentBlocks []ParentBlockAndChildIndex, block *FileBlock, nextBlockStartOff, startOff Int64Offset, err error, ) { ptr, parentBlocks, b, nbso, so, err := fd.tree.getNextDirtyBlockAtOffset( ctx, topBlock, off, rtype, dirtyBcache) if err != nil { return ZeroPtr, nil, nil, 0, 0, err } if b != nil { block = b.(*FileBlock) } if nbso != nil { nextBlockStartOff = nbso.(Int64Offset) } else { nextBlockStartOff = -1 } if so != nil { startOff = so.(Int64Offset) } return ptr, parentBlocks, block, nextBlockStartOff, startOff, nil } // Split checks, if given an indirect top block of a file, whether any // of the dirty leaf blocks in that file need to be split up // differently (i.e., if the BlockSplitter is using // fingerprinting-based boundaries). It returns the set of blocks // that now need to be unreferenced. func (fd *FileData) Split(ctx context.Context, id tlf.ID, dirtyBcache DirtyBlockCache, topBlock *FileBlock, df *DirtyFile) ( unrefs []BlockInfo, err error, ) { if !topBlock.IsInd { return nil, nil } // For an indirect file: // 1) check if each dirty block is split at the right place. // 2) if it needs fewer bytes, prepend the extra bytes to the next // block (making a new one if it doesn't exist), and the next block // gets marked dirty // 3) if it needs more bytes, then use copyUntilSplit() to fetch bytes // from the next block (if there is one), remove the copied bytes // from the next block and mark it dirty // 4) Then go through once more, and ready and finalize each // dirty block, updating its ID in the indirect pointer list off := Int64Offset(0) for off >= 0 { _, parentBlocks, block, nextBlockOff, startOff, err := fd.getNextDirtyFileBlockAtOffset( ctx, topBlock, off, BlockWrite, dirtyBcache) if err != nil { return unrefs, err } if block == nil { // No more dirty blocks. break } off = nextBlockOff // Will be -1 if there are no more blocks. splitAt := fd.tree.bsplit.CheckSplit(block) switch { case splitAt == 0: continue case splitAt > 0: endOfBlock := startOff + Int64Offset(len(block.Contents)) extraBytes := block.Contents[splitAt:] block.Contents = block.Contents[:splitAt] // put the extra bytes in front of the next block if nextBlockOff < 0 { // Need to make a new block. if _, _, err := fd.tree.newRightBlock( ctx, parentBlocks, endOfBlock, DefaultNewBlockDataVersion(false), NewFileBlockWithPtrs, fd.fileTopBlocker(df)); err != nil { return unrefs, err } } rPtr, rParentBlocks, rblock, _, _, _, err := fd.GetFileBlockAtOffset( ctx, topBlock, endOfBlock, BlockWrite) if err != nil { return unrefs, err } rblock.Contents = append(extraBytes, rblock.Contents...) if err = fd.tree.cacher(ctx, rPtr, rblock); err != nil { return unrefs, err } endOfBlock = startOff + Int64Offset(len(block.Contents)) // Mark the old rblock as unref'd. pb := rParentBlocks[len(rParentBlocks)-1] childInfo, _ := pb.childIPtr() unrefs = append(unrefs, childInfo) pb.clearEncodedSize() // Update parent pointer offsets as needed. for i := len(rParentBlocks) - 1; i >= 0; i-- { pb := rParentBlocks[i] pb.pblock.(*FileBlock).IPtrs[pb.childIndex].Off = endOfBlock // If this isn't the leftmost child at this level, // there's no need to update the parent. if pb.childIndex > 0 { break } } _, newUnrefs, err := fd.tree.markParentsDirty(ctx, rParentBlocks) unrefs = append(unrefs, newUnrefs...) if err != nil { return unrefs, err } off = endOfBlock case splitAt < 0: if nextBlockOff < 0 { // End of the line. continue } endOfBlock := startOff + Int64Offset(len(block.Contents)) rPtr, rParentBlocks, rblock, _, _, _, err := fd.GetFileBlockAtOffset( ctx, topBlock, endOfBlock, BlockWrite) if err != nil { return unrefs, err } // Copy some of that block's data into this block. nCopied := fd.tree.bsplit.CopyUntilSplit(block, false, rblock.Contents, int64(len(block.Contents))) rblock.Contents = rblock.Contents[nCopied:] endOfBlock = startOff + Int64Offset(len(block.Contents)) // Mark the old right block as unref'd. pb := rParentBlocks[len(rParentBlocks)-1] pblock := pb.pblock.(*FileBlock) childInfo, _ := pb.childIPtr() unrefs = append(unrefs, childInfo) pb.clearEncodedSize() // For the right block, adjust offset or delete as needed. if len(rblock.Contents) > 0 { if err = fd.tree.cacher(ctx, rPtr, rblock); err != nil { return unrefs, err } // Update parent pointer offsets as needed. for i := len(rParentBlocks) - 1; i >= 0; i-- { pb := rParentBlocks[i] pb.pblock.(*FileBlock).IPtrs[pb.childIndex].Off = endOfBlock // If this isn't the leftmost child at this level, // there's no need to update the parent. if pb.childIndex > 0 { break } } } else { // TODO: If we're down to just one leaf block at this // level, remove the layer of indirection (KBFS-1824). iptrs := pblock.IPtrs pblock.IPtrs = make([]IndirectFilePtr, len(iptrs)-1) copy(pblock.IPtrs, iptrs[:pb.childIndex]) copy(pblock.IPtrs[pb.childIndex:], iptrs[pb.childIndex+1:]) } // Mark all parents as dirty. _, newUnrefs, err := fd.tree.markParentsDirty(ctx, rParentBlocks) unrefs = append(unrefs, newUnrefs...) if err != nil { return unrefs, err } off = endOfBlock } } return unrefs, nil } // Ready readies, if given an indirect top-block, all the dirty child // blocks, and updates their block IDs in their parent block's list of // indirect pointers. It returns a map pointing from the new block // info from any readied block to its corresponding old block pointer. func (fd *FileData) Ready(ctx context.Context, id tlf.ID, bcache BlockCache, dirtyBcache IsDirtyProvider, rp ReadyProvider, bps BlockPutState, topBlock *FileBlock, df *DirtyFile, hashBehavior BlockCacheHashBehavior) ( map[BlockInfo]BlockPointer, error, ) { return fd.tree.ready( ctx, id, bcache, dirtyBcache, rp, bps, topBlock, func(ptr BlockPointer) func() error { if df != nil { return func() error { return df.setBlockSynced(ptr) } } return nil }, hashBehavior) } // GetIndirectFileBlockInfosWithTopBlock returns the block infos // contained in all the indirect blocks in this file tree, given an // already-fetched top block. func (fd *FileData) GetIndirectFileBlockInfosWithTopBlock( ctx context.Context, topBlock *FileBlock, ) ([]BlockInfo, error) { return fd.tree.getIndirectBlockInfosWithTopBlock(ctx, topBlock) } // GetIndirectFileBlockInfos returns the block infos contained in all // the indirect blocks in this file tree. func (fd *FileData) GetIndirectFileBlockInfos(ctx context.Context) ( []BlockInfo, error, ) { return fd.tree.getIndirectBlockInfos(ctx) } // FindIPtrsAndClearSize looks for the given set of indirect pointers, // and returns whether they could be found. As a side effect, it also // clears the encoded size for those indirect pointers. func (fd *FileData) FindIPtrsAndClearSize( ctx context.Context, topBlock *FileBlock, ptrs map[BlockPointer]bool) ( found map[BlockPointer]bool, err error, ) { if !topBlock.IsInd || len(ptrs) == 0 { return nil, nil } pfr, err := fd.tree.getIndirectBlocksForOffsetRange( ctx, topBlock, Int64Offset(0), nil) if err != nil { return nil, err } found = make(map[BlockPointer]bool) // Search all paths for the given block pointer, clear its encoded // size, and dirty all its parents up to the root. infoSeen := make(map[BlockPointer]bool) for _, Path := range pfr { parentPtr := fd.rootBlockPointer() for level, pb := range Path { if infoSeen[parentPtr] { parentPtr = pb.childBlockPtr() continue } infoSeen[parentPtr] = true for i, iptr := range pb.pblock.(*FileBlock).IPtrs { if ptrs[iptr.BlockPointer] { // Mark this pointer, and all parent blocks, as dirty. parentPtr := fd.rootBlockPointer() for i := 0; i <= level; i++ { // Get a writeable copy for each block. pblock, _, err := fd.getter( ctx, fd.tree.kmd, parentPtr, fd.tree.file, BlockWrite) if err != nil { return nil, err } Path[i].pblock = pblock parentPtr = Path[i].childBlockPtr() } // Because we only check each parent once, the // `path` we're using here will be the one with a // childIndex of 0. But, that's not necessarily // the one that matches the pointer that needs to // be dirty. So make a new path and set the // childIndex to the correct pointer instead. newPath := make([]ParentBlockAndChildIndex, level+1) copy(newPath, Path[:level+1]) newPath[level].childIndex = i _, _, err = fd.tree.markParentsDirty(ctx, newPath) if err != nil { return nil, err } found[iptr.BlockPointer] = true if len(found) == len(ptrs) { return found, nil } } } parentPtr = pb.childBlockPtr() } } return found, nil } // DeepCopy makes a complete copy of this file, deduping leaf blocks // and making new random BlockPointers for all indirect blocks. It // returns the new top pointer of the copy, and all the new child // pointers in the copy. func (fd *FileData) DeepCopy(ctx context.Context, dataVer Ver) ( newTopPtr BlockPointer, allChildPtrs []BlockPointer, err error, ) { topBlock, _, err := fd.getter(ctx, fd.tree.kmd, fd.rootBlockPointer(), fd.tree.file, BlockRead) if err != nil { return ZeroPtr, nil, err } // Handle the single-level case first. if !topBlock.IsInd { newTopBlock := topBlock.DeepCopy() newTopPtr = fd.rootBlockPointer() newTopPtr.RefNonce, err = kbfsblock.MakeRefNonce() if err != nil { return ZeroPtr, nil, err } newTopPtr.SetWriter(fd.tree.chargedTo) if err = fd.tree.cacher(ctx, newTopPtr, newTopBlock); err != nil { return ZeroPtr, nil, err } fd.tree.vlog.CLogf( ctx, libkb.VLog1, "Deep copied file %s: %v -> %v", fd.tree.file.TailName(), fd.rootBlockPointer(), newTopPtr) return newTopPtr, nil, nil } // For indirect files, get all the paths to leaf blocks. pfr, err := fd.tree.getIndirectBlocksForOffsetRange( ctx, topBlock, Int64Offset(0), nil) if err != nil { return ZeroPtr, nil, err } if len(pfr) == 0 { return ZeroPtr, nil, fmt.Errorf("Indirect file %v had no indirect blocks", fd.rootBlockPointer()) } // Make a new reference for all leaf blocks first. copiedBlocks := make(map[BlockPointer]*FileBlock) leafLevel := len(pfr[0]) - 1 for level := leafLevel; level >= 0; level-- { for _, Path := range pfr { // What is the current ptr for this pblock? ptr := fd.rootBlockPointer() if level > 0 { ptr = Path[level-1].childBlockPtr() } if _, ok := copiedBlocks[ptr]; ok { continue } // Copy the parent block and save it for later (it will be // cached below). pblock := Path[level].pblock.(*FileBlock).DeepCopy() if err != nil { return ZeroPtr, nil, err } copiedBlocks[ptr] = pblock for i, iptr := range pblock.IPtrs { if level == leafLevel { // Generate a new nonce for each indirect pointer // to a leaf. iptr.RefNonce, err = kbfsblock.MakeRefNonce() if err != nil { return ZeroPtr, nil, err } iptr.SetWriter(fd.tree.chargedTo) pblock.IPtrs[i] = iptr allChildPtrs = append(allChildPtrs, iptr.BlockPointer) } else { // Generate a new random ID for each indirect // pointer to an indirect block. newID, err := kbfsblock.MakeTemporaryID() if err != nil { return ZeroPtr, nil, err } // No need for a new refnonce here, since indirect // blocks are guaranteed to get a new block ID // when readied, since the child block pointers // will have changed. newPtr := BlockPointer{ ID: newID, KeyGen: fd.tree.kmd.LatestKeyGeneration(), DataVer: dataVer, Context: kbfsblock.MakeFirstContext( fd.tree.chargedTo, fd.rootBlockPointer().GetBlockType()), DirectType: IndirectBlock, } pblock.IPtrs[i].BlockPointer = newPtr allChildPtrs = append(allChildPtrs, newPtr) childBlock, ok := copiedBlocks[iptr.BlockPointer] if !ok { return ZeroPtr, nil, fmt.Errorf( "No copied child block found for ptr %v", iptr.BlockPointer) } err = fd.tree.cacher(ctx, newPtr, childBlock) if err != nil { return ZeroPtr, nil, err } } } } } // Finally, make a new ID for the top block and cache it. newID, err := kbfsblock.MakeTemporaryID() if err != nil { return ZeroPtr, nil, err } newTopPtr = BlockPointer{ ID: newID, KeyGen: fd.tree.kmd.LatestKeyGeneration(), DataVer: dataVer, Context: kbfsblock.MakeFirstContext( fd.tree.chargedTo, fd.rootBlockPointer().GetBlockType()), DirectType: IndirectBlock, } fd.tree.vlog.CLogf( ctx, libkb.VLog1, "Deep copied indirect file %s: %v -> %v", fd.tree.file.TailName(), fd.rootBlockPointer(), newTopPtr) newTopBlock, ok := copiedBlocks[fd.rootBlockPointer()] if !ok { return ZeroPtr, nil, fmt.Errorf( "No copied root block found for ptr %v", fd.rootBlockPointer()) } if err = fd.tree.cacher(ctx, newTopPtr, newTopBlock); err != nil { return ZeroPtr, nil, err } return newTopPtr, allChildPtrs, nil } // UndupChildrenInCopy takes a top block that's been copied via // deepCopy(), and un-deduplicates all leaf children of the block. It // adds all child blocks to the provided `bps`, including both the // ones that were deduplicated and the ones that weren't. It returns // the BlockInfos for all children. func (fd *FileData) UndupChildrenInCopy(ctx context.Context, bcache BlockCache, rp ReadyProvider, bps BlockPutState, topBlock *FileBlock, hashBehavior BlockCacheHashBehavior) ( []BlockInfo, error, ) { if !topBlock.IsInd { return nil, nil } // For indirect files, get all the paths to leaf blocks. Note // that because topBlock is a result of `deepCopy`, all of the // indirect blocks that will make up the paths are also deep // copies, and thus are modifiable. pfr, err := fd.tree.getIndirectBlocksForOffsetRange( ctx, topBlock, Int64Offset(0), nil) if err != nil { return nil, err } if len(pfr) == 0 { return nil, fmt.Errorf( "Indirect file %v had no indirect blocks", fd.rootBlockPointer()) } // Append the leaf block to each path, since readyHelper expects it. // TODO: parallelize these fetches. for i, Path := range pfr { leafPtr := Path[len(Path)-1].childBlockPtr() leafBlock, _, err := fd.getter( ctx, fd.tree.kmd, leafPtr, fd.tree.file, BlockWrite) if err != nil { return nil, err } pfr[i] = append(pfr[i], ParentBlockAndChildIndex{leafBlock, -1}) } newInfos, err := fd.tree.readyHelper( ctx, fd.tree.file.Tlf, bcache, rp, bps, pfr, nil, hashBehavior) if err != nil { return nil, err } blockInfos := make([]BlockInfo, 0, len(newInfos)) for newInfo := range newInfos { blockInfos = append(blockInfos, newInfo) } return blockInfos, nil } // ReadyNonLeafBlocksInCopy takes a top block that's been copied via // deepCopy(), and readies all the non-leaf children of the top block. // It adds all readied blocks to the provided `bps`. It returns the // BlockInfos for all non-leaf children. func (fd *FileData) ReadyNonLeafBlocksInCopy(ctx context.Context, bcache BlockCache, rp ReadyProvider, bps BlockPutState, topBlock *FileBlock, hashBehavior BlockCacheHashBehavior) ( []BlockInfo, error, ) { if !topBlock.IsInd { return nil, nil } // For indirect files, get all the paths to leaf blocks. Note // that because topBlock is a deepCopy, all of the blocks are also // deepCopys and thus are modifiable. pfr, err := fd.tree.getIndirectBlocksForOffsetRange( ctx, topBlock, Int64Offset(0), nil) if err != nil { return nil, err } if len(pfr) == 0 { return nil, fmt.Errorf( "Indirect file %v had no indirect blocks", fd.rootBlockPointer()) } newInfos, err := fd.tree.readyHelper( ctx, fd.tree.file.Tlf, bcache, rp, bps, pfr, nil, hashBehavior) if err != nil { return nil, err } blockInfos := make([]BlockInfo, 0, len(newInfos)) for newInfo := range newInfos { blockInfos = append(blockInfos, newInfo) } return blockInfos, nil }