1
// Copyright 2014 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
4
package blobstore // import "gopkg.in/juju/charmstore.v5-unstable/internal/blobstore"
13
"github.com/juju/blobstore"
14
"github.com/juju/errors"
19
type ReadSeekCloser interface {
25
// ContentChallengeError holds a proof-of-content
26
// challenge produced by a blobstore.
27
type ContentChallengeError struct {
31
func (e *ContentChallengeError) Error() string {
32
return "cannot upload because proof of content ownership is required"
35
// ContentChallenge holds a proof-of-content challenge
36
// produced by a blobstore. A client can satisfy the request
37
// by producing a ContentChallengeResponse containing
38
// the same request id and a hash of RangeLength bytes
39
// of the content starting at RangeStart.
40
type ContentChallenge struct {
46
// ContentChallengeResponse holds a response to a ContentChallenge.
47
type ContentChallengeResponse struct {
52
// NewHash is used to calculate checksums for the blob store.
53
func NewHash() hash.Hash {
54
return sha512.New384()
57
// NewContentChallengeResponse can be used by a client to respond to a content
58
// challenge. The returned value should be passed to BlobStorage.Put
59
// when the client retries the request.
60
func NewContentChallengeResponse(chal *ContentChallenge, r io.ReadSeeker) (*ContentChallengeResponse, error) {
61
_, err := r.Seek(chal.RangeStart, 0)
63
return nil, errgo.Mask(err)
66
nw, err := io.CopyN(hash, r, chal.RangeLength)
68
return nil, errgo.Mask(err)
70
if nw != chal.RangeLength {
71
return nil, errgo.Newf("content is not long enough")
73
return &ContentChallengeResponse{
74
RequestId: chal.RequestId,
75
Hash: fmt.Sprintf("%x", hash.Sum(nil)),
79
// Store stores data blobs in mongodb, de-duplicating by
82
mstore blobstore.ManagedStorage
85
// New returns a new blob store that writes to the given database,
86
// prefixing its collections with the given prefix.
87
func New(db *mgo.Database, prefix string) *Store {
88
rs := blobstore.NewGridFS(db.Name, prefix, db.Session)
90
mstore: blobstore.NewManagedStorage(db, rs),
94
func (s *Store) challengeResponse(resp *ContentChallengeResponse) error {
95
id, err := strconv.ParseInt(resp.RequestId, 10, 64)
97
return errgo.Newf("invalid request id %q", id)
99
return s.mstore.ProofOfAccessResponse(blobstore.NewPutResponse(id, resp.Hash))
102
// Put tries to stream the content from the given reader into blob
103
// storage, with the provided name. The content should have the given
104
// size and hash. If the content is already in the store, a
105
// ContentChallengeError is returned containing a challenge that must be
106
// satisfied by a client to prove that they have access to the content.
107
// If the proof has already been acquired, it should be passed in as the
109
func (s *Store) Put(r io.Reader, name string, size int64, hash string, proof *ContentChallengeResponse) (*ContentChallenge, error) {
111
err := s.challengeResponse(proof)
115
if err != blobstore.ErrResourceDeleted {
116
return nil, errgo.Mask(err)
118
// The blob has been deleted since the challenge
119
// was created, so continue on with uploading
120
// the content as if there was no previous challenge.
122
resp, err := s.mstore.PutForEnvironmentRequest("", name, hash)
124
if errors.IsNotFound(err) {
125
if err := s.mstore.PutForEnvironmentAndCheckHash("", name, r, size, hash); err != nil {
126
return nil, errgo.Mask(err)
132
return &ContentChallenge{
133
RequestId: fmt.Sprint(resp.RequestId),
134
RangeStart: resp.RangeStart,
135
RangeLength: resp.RangeLength,
139
// PutUnchallenged stream the content from the given reader into blob
140
// storage, with the provided name. The content should have the given
141
// size and hash. In this case a challenge is never returned and a proof
143
func (s *Store) PutUnchallenged(r io.Reader, name string, size int64, hash string) error {
144
return s.mstore.PutForEnvironmentAndCheckHash("", name, r, size, hash)
147
// Open opens the entry with the given name.
148
func (s *Store) Open(name string) (ReadSeekCloser, int64, error) {
149
r, length, err := s.mstore.GetForEnvironment("", name)
151
return nil, 0, errgo.Mask(err)
153
return r.(ReadSeekCloser), length, nil
156
// Remove the given name from the Store.
157
func (s *Store) Remove(name string) error {
158
return s.mstore.RemoveForEnvironment("", name)