github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/storage/driver/azure/randomwriter.go (about)

     1  package azure
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  
     8  	azure "github.com/Azure/azure-sdk-for-go/storage"
     9  )
    10  
    11  // blockStorage is the interface required from a block storage service
    12  // client implementation
    13  type blockStorage interface {
    14  	CreateBlockBlob(container, blob string) error
    15  	GetBlob(container, blob string) (io.ReadCloser, error)
    16  	GetSectionReader(container, blob string, start, length int64) (io.ReadCloser, error)
    17  	PutBlock(container, blob, blockID string, chunk []byte) error
    18  	GetBlockList(container, blob string, blockType azure.BlockListType) (azure.BlockListResponse, error)
    19  	PutBlockList(container, blob string, blocks []azure.Block) error
    20  }
    21  
    22  // randomBlobWriter enables random access semantics on Azure block blobs
    23  // by enabling writing arbitrary length of chunks to arbitrary write offsets
    24  // within the blob. Normally, Azure Blob Storage does not support random
    25  // access semantics on block blobs; however, this writer can download, split and
    26  // reupload the overlapping blocks and discards those being overwritten entirely.
    27  type randomBlobWriter struct {
    28  	bs        blockStorage
    29  	blockSize int
    30  }
    31  
    32  func newRandomBlobWriter(bs blockStorage, blockSize int) randomBlobWriter {
    33  	return randomBlobWriter{bs: bs, blockSize: blockSize}
    34  }
    35  
    36  // WriteBlobAt writes the given chunk to the specified position of an existing blob.
    37  // The offset must be equals to size of the blob or smaller than it.
    38  func (r *randomBlobWriter) WriteBlobAt(container, blob string, offset int64, chunk io.Reader) (int64, error) {
    39  	rand := newBlockIDGenerator()
    40  
    41  	blocks, err := r.bs.GetBlockList(container, blob, azure.BlockListTypeCommitted)
    42  	if err != nil {
    43  		return 0, err
    44  	}
    45  	rand.Feed(blocks) // load existing block IDs
    46  
    47  	// Check for write offset for existing blob
    48  	size := getBlobSize(blocks)
    49  	if offset < 0 || offset > size {
    50  		return 0, fmt.Errorf("wrong offset for Write: %v", offset)
    51  	}
    52  
    53  	// Upload the new chunk as blocks
    54  	blockList, nn, err := r.writeChunkToBlocks(container, blob, chunk, rand)
    55  	if err != nil {
    56  		return 0, err
    57  	}
    58  
    59  	// For non-append operations, existing blocks may need to be splitted
    60  	if offset != size {
    61  		// Split the block on the left end (if any)
    62  		leftBlocks, err := r.blocksLeftSide(container, blob, offset, rand)
    63  		if err != nil {
    64  			return 0, err
    65  		}
    66  		blockList = append(leftBlocks, blockList...)
    67  
    68  		// Split the block on the right end (if any)
    69  		rightBlocks, err := r.blocksRightSide(container, blob, offset, nn, rand)
    70  		if err != nil {
    71  			return 0, err
    72  		}
    73  		blockList = append(blockList, rightBlocks...)
    74  	} else {
    75  		// Use existing block list
    76  		var existingBlocks []azure.Block
    77  		for _, v := range blocks.CommittedBlocks {
    78  			existingBlocks = append(existingBlocks, azure.Block{ID: v.Name, Status: azure.BlockStatusCommitted})
    79  		}
    80  		blockList = append(existingBlocks, blockList...)
    81  	}
    82  	// Put block list
    83  	return nn, r.bs.PutBlockList(container, blob, blockList)
    84  }
    85  
    86  func (r *randomBlobWriter) GetSize(container, blob string) (int64, error) {
    87  	blocks, err := r.bs.GetBlockList(container, blob, azure.BlockListTypeCommitted)
    88  	if err != nil {
    89  		return 0, err
    90  	}
    91  	return getBlobSize(blocks), nil
    92  }
    93  
    94  // writeChunkToBlocks writes given chunk to one or multiple blocks within specified
    95  // blob and returns their block representations. Those blocks are not committed, yet
    96  func (r *randomBlobWriter) writeChunkToBlocks(container, blob string, chunk io.Reader, rand *blockIDGenerator) ([]azure.Block, int64, error) {
    97  	var newBlocks []azure.Block
    98  	var nn int64
    99  
   100  	// Read chunks of at most size N except the last chunk to
   101  	// maximize block size and minimize block count.
   102  	buf := make([]byte, r.blockSize)
   103  	for {
   104  		n, err := io.ReadFull(chunk, buf)
   105  		if err == io.EOF {
   106  			break
   107  		}
   108  		nn += int64(n)
   109  		data := buf[:n]
   110  		blockID := rand.Generate()
   111  		if err := r.bs.PutBlock(container, blob, blockID, data); err != nil {
   112  			return newBlocks, nn, err
   113  		}
   114  		newBlocks = append(newBlocks, azure.Block{ID: blockID, Status: azure.BlockStatusUncommitted})
   115  	}
   116  	return newBlocks, nn, nil
   117  }
   118  
   119  // blocksLeftSide returns the blocks that are going to be at the left side of
   120  // the writeOffset: [0, writeOffset) by identifying blocks that will remain
   121  // the same and splitting blocks and reuploading them as needed.
   122  func (r *randomBlobWriter) blocksLeftSide(container, blob string, writeOffset int64, rand *blockIDGenerator) ([]azure.Block, error) {
   123  	var left []azure.Block
   124  	bx, err := r.bs.GetBlockList(container, blob, azure.BlockListTypeAll)
   125  	if err != nil {
   126  		return left, err
   127  	}
   128  
   129  	o := writeOffset
   130  	elapsed := int64(0)
   131  	for _, v := range bx.CommittedBlocks {
   132  		blkSize := int64(v.Size)
   133  		if o >= blkSize { // use existing block
   134  			left = append(left, azure.Block{ID: v.Name, Status: azure.BlockStatusCommitted})
   135  			o -= blkSize
   136  			elapsed += blkSize
   137  		} else if o > 0 { // current block needs to be splitted
   138  			start := elapsed
   139  			size := o
   140  			part, err := r.bs.GetSectionReader(container, blob, start, size)
   141  			if err != nil {
   142  				return left, err
   143  			}
   144  			newBlockID := rand.Generate()
   145  
   146  			data, err := ioutil.ReadAll(part)
   147  			if err != nil {
   148  				return left, err
   149  			}
   150  			if err = r.bs.PutBlock(container, blob, newBlockID, data); err != nil {
   151  				return left, err
   152  			}
   153  			left = append(left, azure.Block{ID: newBlockID, Status: azure.BlockStatusUncommitted})
   154  			break
   155  		}
   156  	}
   157  	return left, nil
   158  }
   159  
   160  // blocksRightSide returns the blocks that are going to be at the right side of
   161  // the written chunk: [writeOffset+size, +inf) by identifying blocks that will remain
   162  // the same and splitting blocks and reuploading them as needed.
   163  func (r *randomBlobWriter) blocksRightSide(container, blob string, writeOffset int64, chunkSize int64, rand *blockIDGenerator) ([]azure.Block, error) {
   164  	var right []azure.Block
   165  
   166  	bx, err := r.bs.GetBlockList(container, blob, azure.BlockListTypeAll)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	re := writeOffset + chunkSize - 1 // right end of written chunk
   172  	var elapsed int64
   173  	for _, v := range bx.CommittedBlocks {
   174  		var (
   175  			bs = elapsed                     // left end of current block
   176  			be = elapsed + int64(v.Size) - 1 // right end of current block
   177  		)
   178  
   179  		if bs > re { // take the block as is
   180  			right = append(right, azure.Block{ID: v.Name, Status: azure.BlockStatusCommitted})
   181  		} else if be > re { // current block needs to be splitted
   182  			part, err := r.bs.GetSectionReader(container, blob, re+1, be-(re+1)+1)
   183  			if err != nil {
   184  				return right, err
   185  			}
   186  			newBlockID := rand.Generate()
   187  
   188  			data, err := ioutil.ReadAll(part)
   189  			if err != nil {
   190  				return right, err
   191  			}
   192  			if err = r.bs.PutBlock(container, blob, newBlockID, data); err != nil {
   193  				return right, err
   194  			}
   195  			right = append(right, azure.Block{ID: newBlockID, Status: azure.BlockStatusUncommitted})
   196  		}
   197  		elapsed += int64(v.Size)
   198  	}
   199  	return right, nil
   200  }
   201  
   202  func getBlobSize(blocks azure.BlockListResponse) int64 {
   203  	var n int64
   204  	for _, v := range blocks.CommittedBlocks {
   205  		n += int64(v.Size)
   206  	}
   207  	return n
   208  }