forked from TrueCloudLab/restic
448 lines
13 KiB
Go
448 lines
13 KiB
Go
package swift
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
gopath "path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// NotLargeObject is returned if an operation is performed on an object which isn't large.
|
|
var NotLargeObject = errors.New("Not a large object")
|
|
|
|
// readAfterWriteTimeout defines the time we wait before an object appears after having been uploaded
|
|
var readAfterWriteTimeout = 15 * time.Second
|
|
|
|
// readAfterWriteWait defines the time to sleep between two retries
|
|
var readAfterWriteWait = 200 * time.Millisecond
|
|
|
|
// largeObjectCreateFile represents an open static or dynamic large object
|
|
type largeObjectCreateFile struct {
|
|
conn *Connection
|
|
container string
|
|
objectName string
|
|
currentLength int64
|
|
filePos int64
|
|
chunkSize int64
|
|
segmentContainer string
|
|
prefix string
|
|
contentType string
|
|
checkHash bool
|
|
segments []Object
|
|
headers Headers
|
|
minChunkSize int64
|
|
}
|
|
|
|
func swiftSegmentPath(path string) (string, error) {
|
|
checksum := sha1.New()
|
|
random := make([]byte, 32)
|
|
if _, err := rand.Read(random); err != nil {
|
|
return "", err
|
|
}
|
|
path = hex.EncodeToString(checksum.Sum(append([]byte(path), random...)))
|
|
return strings.TrimLeft(strings.TrimRight("segments/"+path[0:3]+"/"+path[3:], "/"), "/"), nil
|
|
}
|
|
|
|
func getSegment(segmentPath string, partNumber int) string {
|
|
return fmt.Sprintf("%s/%016d", segmentPath, partNumber)
|
|
}
|
|
|
|
func parseFullPath(manifest string) (container string, prefix string) {
|
|
components := strings.SplitN(manifest, "/", 2)
|
|
container = components[0]
|
|
if len(components) > 1 {
|
|
prefix = components[1]
|
|
}
|
|
return container, prefix
|
|
}
|
|
|
|
func (headers Headers) IsLargeObjectDLO() bool {
|
|
_, isDLO := headers["X-Object-Manifest"]
|
|
return isDLO
|
|
}
|
|
|
|
func (headers Headers) IsLargeObjectSLO() bool {
|
|
_, isSLO := headers["X-Static-Large-Object"]
|
|
return isSLO
|
|
}
|
|
|
|
func (headers Headers) IsLargeObject() bool {
|
|
return headers.IsLargeObjectSLO() || headers.IsLargeObjectDLO()
|
|
}
|
|
|
|
func (c *Connection) getAllSegments(container string, path string, headers Headers) (string, []Object, error) {
|
|
if manifest, isDLO := headers["X-Object-Manifest"]; isDLO {
|
|
segmentContainer, segmentPath := parseFullPath(manifest)
|
|
segments, err := c.getAllDLOSegments(segmentContainer, segmentPath)
|
|
return segmentContainer, segments, err
|
|
}
|
|
if headers.IsLargeObjectSLO() {
|
|
return c.getAllSLOSegments(container, path)
|
|
}
|
|
return "", nil, NotLargeObject
|
|
}
|
|
|
|
// LargeObjectOpts describes how a large object should be created
|
|
type LargeObjectOpts struct {
|
|
Container string // Name of container to place object
|
|
ObjectName string // Name of object
|
|
Flags int // Creation flags
|
|
CheckHash bool // If set Check the hash
|
|
Hash string // If set use this hash to check
|
|
ContentType string // Content-Type of the object
|
|
Headers Headers // Additional headers to upload the object with
|
|
ChunkSize int64 // Size of chunks of the object, defaults to 10MB if not set
|
|
MinChunkSize int64 // Minimum chunk size, automatically set for SLO's based on info
|
|
SegmentContainer string // Name of the container to place segments
|
|
SegmentPrefix string // Prefix to use for the segments
|
|
NoBuffer bool // Prevents using a bufio.Writer to write segments
|
|
}
|
|
|
|
type LargeObjectFile interface {
|
|
io.Writer
|
|
io.Seeker
|
|
io.Closer
|
|
Size() int64
|
|
Flush() error
|
|
}
|
|
|
|
// largeObjectCreate creates a large object at opts.Container, opts.ObjectName.
|
|
//
|
|
// opts.Flags can have the following bits set
|
|
// os.TRUNC - remove the contents of the large object if it exists
|
|
// os.APPEND - write at the end of the large object
|
|
func (c *Connection) largeObjectCreate(opts *LargeObjectOpts) (*largeObjectCreateFile, error) {
|
|
var (
|
|
segmentPath string
|
|
segmentContainer string
|
|
segments []Object
|
|
currentLength int64
|
|
err error
|
|
)
|
|
|
|
if opts.SegmentPrefix != "" {
|
|
segmentPath = opts.SegmentPrefix
|
|
} else if segmentPath, err = swiftSegmentPath(opts.ObjectName); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if info, headers, err := c.Object(opts.Container, opts.ObjectName); err == nil {
|
|
if opts.Flags&os.O_TRUNC != 0 {
|
|
c.LargeObjectDelete(opts.Container, opts.ObjectName)
|
|
} else {
|
|
currentLength = info.Bytes
|
|
if headers.IsLargeObject() {
|
|
segmentContainer, segments, err = c.getAllSegments(opts.Container, opts.ObjectName, headers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(segments) > 0 {
|
|
segmentPath = gopath.Dir(segments[0].Name)
|
|
}
|
|
} else {
|
|
if err = c.ObjectMove(opts.Container, opts.ObjectName, opts.Container, getSegment(segmentPath, 1)); err != nil {
|
|
return nil, err
|
|
}
|
|
segments = append(segments, info)
|
|
}
|
|
}
|
|
} else if err != ObjectNotFound {
|
|
return nil, err
|
|
}
|
|
|
|
// segmentContainer is not empty when the manifest already existed
|
|
if segmentContainer == "" {
|
|
if opts.SegmentContainer != "" {
|
|
segmentContainer = opts.SegmentContainer
|
|
} else {
|
|
segmentContainer = opts.Container + "_segments"
|
|
}
|
|
}
|
|
|
|
file := &largeObjectCreateFile{
|
|
conn: c,
|
|
checkHash: opts.CheckHash,
|
|
container: opts.Container,
|
|
objectName: opts.ObjectName,
|
|
chunkSize: opts.ChunkSize,
|
|
minChunkSize: opts.MinChunkSize,
|
|
headers: opts.Headers,
|
|
segmentContainer: segmentContainer,
|
|
prefix: segmentPath,
|
|
segments: segments,
|
|
currentLength: currentLength,
|
|
}
|
|
|
|
if file.chunkSize == 0 {
|
|
file.chunkSize = 10 * 1024 * 1024
|
|
}
|
|
|
|
if file.minChunkSize > file.chunkSize {
|
|
file.chunkSize = file.minChunkSize
|
|
}
|
|
|
|
if opts.Flags&os.O_APPEND != 0 {
|
|
file.filePos = currentLength
|
|
}
|
|
|
|
return file, nil
|
|
}
|
|
|
|
// LargeObjectDelete deletes the large object named by container, path
|
|
func (c *Connection) LargeObjectDelete(container string, objectName string) error {
|
|
_, headers, err := c.Object(container, objectName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var objects [][]string
|
|
if headers.IsLargeObject() {
|
|
segmentContainer, segments, err := c.getAllSegments(container, objectName, headers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, obj := range segments {
|
|
objects = append(objects, []string{segmentContainer, obj.Name})
|
|
}
|
|
}
|
|
objects = append(objects, []string{container, objectName})
|
|
|
|
info, err := c.cachedQueryInfo()
|
|
if err == nil && info.SupportsBulkDelete() && len(objects) > 0 {
|
|
filenames := make([]string, len(objects))
|
|
for i, obj := range objects {
|
|
filenames[i] = obj[0] + "/" + obj[1]
|
|
}
|
|
_, err = c.doBulkDelete(filenames)
|
|
// Don't fail on ObjectNotFound because eventual consistency
|
|
// makes this situation normal.
|
|
if err != nil && err != Forbidden && err != ObjectNotFound {
|
|
return err
|
|
}
|
|
} else {
|
|
for _, obj := range objects {
|
|
if err := c.ObjectDelete(obj[0], obj[1]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LargeObjectGetSegments returns all the segments that compose an object
|
|
// If the object is a Dynamic Large Object (DLO), it just returns the objects
|
|
// that have the prefix as indicated by the manifest.
|
|
// If the object is a Static Large Object (SLO), it retrieves the JSON content
|
|
// of the manifest and return all the segments of it.
|
|
func (c *Connection) LargeObjectGetSegments(container string, path string) (string, []Object, error) {
|
|
_, headers, err := c.Object(container, path)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
return c.getAllSegments(container, path, headers)
|
|
}
|
|
|
|
// Seek sets the offset for the next write operation
|
|
func (file *largeObjectCreateFile) Seek(offset int64, whence int) (int64, error) {
|
|
switch whence {
|
|
case 0:
|
|
file.filePos = offset
|
|
case 1:
|
|
file.filePos += offset
|
|
case 2:
|
|
file.filePos = file.currentLength + offset
|
|
default:
|
|
return -1, fmt.Errorf("invalid value for whence")
|
|
}
|
|
if file.filePos < 0 {
|
|
return -1, fmt.Errorf("negative offset")
|
|
}
|
|
return file.filePos, nil
|
|
}
|
|
|
|
func (file *largeObjectCreateFile) Size() int64 {
|
|
return file.currentLength
|
|
}
|
|
|
|
func withLORetry(expectedSize int64, fn func() (Headers, int64, error)) (err error) {
|
|
endTimer := time.NewTimer(readAfterWriteTimeout)
|
|
defer endTimer.Stop()
|
|
waitingTime := readAfterWriteWait
|
|
for {
|
|
var headers Headers
|
|
var sz int64
|
|
if headers, sz, err = fn(); err == nil {
|
|
if !headers.IsLargeObjectDLO() || (expectedSize == 0 && sz > 0) || expectedSize == sz {
|
|
return
|
|
}
|
|
} else {
|
|
return
|
|
}
|
|
waitTimer := time.NewTimer(waitingTime)
|
|
select {
|
|
case <-endTimer.C:
|
|
waitTimer.Stop()
|
|
err = fmt.Errorf("Timeout expired while waiting for object to have size == %d, got: %d", expectedSize, sz)
|
|
return
|
|
case <-waitTimer.C:
|
|
waitingTime *= 2
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Connection) waitForSegmentsToShowUp(container, objectName string, expectedSize int64) (err error) {
|
|
err = withLORetry(expectedSize, func() (Headers, int64, error) {
|
|
var info Object
|
|
var headers Headers
|
|
info, headers, err = c.objectBase(container, objectName)
|
|
if err != nil {
|
|
return headers, 0, err
|
|
}
|
|
return headers, info.Bytes, nil
|
|
})
|
|
return
|
|
}
|
|
|
|
// Write satisfies the io.Writer interface
|
|
func (file *largeObjectCreateFile) Write(buf []byte) (int, error) {
|
|
var sz int64
|
|
var relativeFilePos int
|
|
writeSegmentIdx := 0
|
|
for i, obj := range file.segments {
|
|
if file.filePos < sz+obj.Bytes || (i == len(file.segments)-1 && file.filePos < sz+file.minChunkSize) {
|
|
relativeFilePos = int(file.filePos - sz)
|
|
break
|
|
}
|
|
writeSegmentIdx++
|
|
sz += obj.Bytes
|
|
}
|
|
sizeToWrite := len(buf)
|
|
for offset := 0; offset < sizeToWrite; {
|
|
newSegment, n, err := file.writeSegment(buf[offset:], writeSegmentIdx, relativeFilePos)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if writeSegmentIdx < len(file.segments) {
|
|
file.segments[writeSegmentIdx] = *newSegment
|
|
} else {
|
|
file.segments = append(file.segments, *newSegment)
|
|
}
|
|
offset += n
|
|
writeSegmentIdx++
|
|
relativeFilePos = 0
|
|
}
|
|
file.filePos += int64(sizeToWrite)
|
|
file.currentLength = 0
|
|
for _, obj := range file.segments {
|
|
file.currentLength += obj.Bytes
|
|
}
|
|
return sizeToWrite, nil
|
|
}
|
|
|
|
func (file *largeObjectCreateFile) writeSegment(buf []byte, writeSegmentIdx int, relativeFilePos int) (*Object, int, error) {
|
|
var (
|
|
readers []io.Reader
|
|
existingSegment *Object
|
|
segmentSize int
|
|
)
|
|
segmentName := getSegment(file.prefix, writeSegmentIdx+1)
|
|
sizeToRead := int(file.chunkSize)
|
|
if writeSegmentIdx < len(file.segments) {
|
|
existingSegment = &file.segments[writeSegmentIdx]
|
|
if writeSegmentIdx != len(file.segments)-1 {
|
|
sizeToRead = int(existingSegment.Bytes)
|
|
}
|
|
if relativeFilePos > 0 {
|
|
headers := make(Headers)
|
|
headers["Range"] = "bytes=0-" + strconv.FormatInt(int64(relativeFilePos-1), 10)
|
|
existingSegmentReader, _, err := file.conn.ObjectOpen(file.segmentContainer, segmentName, true, headers)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer existingSegmentReader.Close()
|
|
sizeToRead -= relativeFilePos
|
|
segmentSize += relativeFilePos
|
|
readers = []io.Reader{existingSegmentReader}
|
|
}
|
|
}
|
|
if sizeToRead > len(buf) {
|
|
sizeToRead = len(buf)
|
|
}
|
|
segmentSize += sizeToRead
|
|
readers = append(readers, bytes.NewReader(buf[:sizeToRead]))
|
|
if existingSegment != nil && segmentSize < int(existingSegment.Bytes) {
|
|
headers := make(Headers)
|
|
headers["Range"] = "bytes=" + strconv.FormatInt(int64(segmentSize), 10) + "-"
|
|
tailSegmentReader, _, err := file.conn.ObjectOpen(file.segmentContainer, segmentName, true, headers)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer tailSegmentReader.Close()
|
|
segmentSize = int(existingSegment.Bytes)
|
|
readers = append(readers, tailSegmentReader)
|
|
}
|
|
segmentReader := io.MultiReader(readers...)
|
|
headers, err := file.conn.ObjectPut(file.segmentContainer, segmentName, segmentReader, true, "", file.contentType, nil)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return &Object{Name: segmentName, Bytes: int64(segmentSize), Hash: headers["Etag"]}, sizeToRead, nil
|
|
}
|
|
|
|
func withBuffer(opts *LargeObjectOpts, lo LargeObjectFile) LargeObjectFile {
|
|
if !opts.NoBuffer {
|
|
return &bufferedLargeObjectFile{
|
|
LargeObjectFile: lo,
|
|
bw: bufio.NewWriterSize(lo, int(opts.ChunkSize)),
|
|
}
|
|
}
|
|
return lo
|
|
}
|
|
|
|
type bufferedLargeObjectFile struct {
|
|
LargeObjectFile
|
|
bw *bufio.Writer
|
|
}
|
|
|
|
func (blo *bufferedLargeObjectFile) Close() error {
|
|
err := blo.bw.Flush()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return blo.LargeObjectFile.Close()
|
|
}
|
|
|
|
func (blo *bufferedLargeObjectFile) Write(p []byte) (n int, err error) {
|
|
return blo.bw.Write(p)
|
|
}
|
|
|
|
func (blo *bufferedLargeObjectFile) Seek(offset int64, whence int) (int64, error) {
|
|
err := blo.bw.Flush()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return blo.LargeObjectFile.Seek(offset, whence)
|
|
}
|
|
|
|
func (blo *bufferedLargeObjectFile) Size() int64 {
|
|
return blo.LargeObjectFile.Size() + int64(blo.bw.Buffered())
|
|
}
|
|
|
|
func (blo *bufferedLargeObjectFile) Flush() error {
|
|
err := blo.bw.Flush()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return blo.LargeObjectFile.Flush()
|
|
}
|