forked from TrueCloudLab/distribution
Merge pull request #55 from BrianBland/layerhandler
Adds support for content redirects for layer downloads
This commit is contained in:
commit
e8714b9977
14 changed files with 364 additions and 42 deletions
|
@ -24,6 +24,9 @@ type Configuration struct {
|
||||||
// used to gate requests.
|
// used to gate requests.
|
||||||
Auth Auth `yaml:"auth"`
|
Auth Auth `yaml:"auth"`
|
||||||
|
|
||||||
|
// LayerHandler specifies a middleware for serving image layers.
|
||||||
|
LayerHandler LayerHandler `yaml:"layerhandler"`
|
||||||
|
|
||||||
// Reporting is the configuration for error reporting
|
// Reporting is the configuration for error reporting
|
||||||
Reporting Reporting `yaml:"reporting"`
|
Reporting Reporting `yaml:"reporting"`
|
||||||
|
|
||||||
|
@ -240,6 +243,62 @@ type NewRelicReporting struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LayerHandler defines the configuration for middleware layer serving
|
||||||
|
type LayerHandler map[string]Parameters
|
||||||
|
|
||||||
|
// Type returns the layerhandler type
|
||||||
|
func (layerHandler LayerHandler) Type() string {
|
||||||
|
// Return only key in this map
|
||||||
|
for k := range layerHandler {
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters returns the Parameters map for a LayerHandler configuration
|
||||||
|
func (layerHandler LayerHandler) Parameters() Parameters {
|
||||||
|
return layerHandler[layerHandler.Type()]
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements the yaml.Unmarshaler interface
|
||||||
|
// Unmarshals a single item map into a Storage or a string into a Storage type with no parameters
|
||||||
|
func (layerHandler *LayerHandler) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var storageMap map[string]Parameters
|
||||||
|
err := unmarshal(&storageMap)
|
||||||
|
if err == nil {
|
||||||
|
if len(storageMap) > 1 {
|
||||||
|
types := make([]string, 0, len(storageMap))
|
||||||
|
for k := range storageMap {
|
||||||
|
types = append(types, k)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Must provide exactly one layerhandler type. Provided: %v", types)
|
||||||
|
}
|
||||||
|
*layerHandler = storageMap
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var storageType string
|
||||||
|
err = unmarshal(&storageType)
|
||||||
|
if err == nil {
|
||||||
|
*layerHandler = LayerHandler{storageType: Parameters{}}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalYAML implements the yaml.Marshaler interface
|
||||||
|
func (layerHandler LayerHandler) MarshalYAML() (interface{}, error) {
|
||||||
|
if layerHandler.Parameters() == nil {
|
||||||
|
t := layerHandler.Type()
|
||||||
|
if t == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
return map[string]Parameters(layerHandler), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Parse parses an input configuration yaml document into a Configuration struct
|
// Parse parses an input configuration yaml document into a Configuration struct
|
||||||
// This should generally be capable of handling old configuration format versions
|
// This should generally be capable of handling old configuration format versions
|
||||||
//
|
//
|
||||||
|
|
|
@ -31,6 +31,8 @@ type App struct {
|
||||||
|
|
||||||
tokenProvider tokenProvider
|
tokenProvider tokenProvider
|
||||||
|
|
||||||
|
layerHandler storage.LayerHandler
|
||||||
|
|
||||||
accessController auth.AccessController
|
accessController auth.AccessController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +78,16 @@ func NewApp(configuration configuration.Configuration) *App {
|
||||||
app.accessController = accessController
|
app.accessController = accessController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layerHandlerType := configuration.LayerHandler.Type()
|
||||||
|
|
||||||
|
if layerHandlerType != "" {
|
||||||
|
lh, err := storage.GetLayerHandler(layerHandlerType, configuration.LayerHandler.Parameters(), driver)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("unable to configure layer handler (%s): %v", layerHandlerType, err))
|
||||||
|
}
|
||||||
|
app.layerHandler = lh
|
||||||
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,5 +58,13 @@ func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
defer layer.Close()
|
defer layer.Close()
|
||||||
|
|
||||||
|
if lh.layerHandler != nil {
|
||||||
|
handler, _ := lh.layerHandler.Resolve(layer)
|
||||||
|
if handler != nil {
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
http.ServeContent(w, r, layer.Digest().String(), layer.CreatedAt(), layer)
|
http.ServeContent(w, r, layer.Digest().String(), layer.CreatedAt(), layer)
|
||||||
}
|
}
|
||||||
|
|
106
storage/cloudfrontlayerhandler.go
Normal file
106
storage/cloudfrontlayerhandler.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/crowdmob/goamz/cloudfront"
|
||||||
|
"github.com/docker/distribution/storagedriver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cloudFrontLayerHandler provides an simple implementation of layerHandler that
|
||||||
|
// constructs temporary signed CloudFront URLs from the storagedriver layer URL,
|
||||||
|
// then issues HTTP Temporary Redirects to this CloudFront content URL.
|
||||||
|
type cloudFrontLayerHandler struct {
|
||||||
|
cloudfront *cloudfront.CloudFront
|
||||||
|
delegateLayerHandler *delegateLayerHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ LayerHandler = &cloudFrontLayerHandler{}
|
||||||
|
|
||||||
|
// newCloudFrontLayerHandler constructs and returns a new CloudFront
|
||||||
|
// LayerHandler implementation.
|
||||||
|
// Required options: baseurl, privatekey, keypairid
|
||||||
|
func newCloudFrontLayerHandler(storageDriver storagedriver.StorageDriver, options map[string]interface{}) (LayerHandler, error) {
|
||||||
|
base, ok := options["baseurl"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("No baseurl provided")
|
||||||
|
}
|
||||||
|
baseURL, ok := base.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("baseurl must be a string")
|
||||||
|
}
|
||||||
|
pk, ok := options["privatekey"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("No privatekey provided")
|
||||||
|
}
|
||||||
|
pkPath, ok := pk.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("privatekey must be a string")
|
||||||
|
}
|
||||||
|
kpid, ok := options["keypairid"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("No keypairid provided")
|
||||||
|
}
|
||||||
|
keypairID, ok := kpid.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("keypairid must be a string")
|
||||||
|
}
|
||||||
|
|
||||||
|
pkBytes, err := ioutil.ReadFile(pkPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to read privatekey file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode([]byte(pkBytes))
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("Failed to decode private key as an rsa private key")
|
||||||
|
}
|
||||||
|
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lh, err := newDelegateLayerHandler(storageDriver, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dlh := lh.(*delegateLayerHandler)
|
||||||
|
|
||||||
|
cf := cloudfront.New(baseURL, privateKey, keypairID)
|
||||||
|
|
||||||
|
return &cloudFrontLayerHandler{cloudfront: cf, delegateLayerHandler: dlh}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve returns an http.Handler which can serve the contents of the given
|
||||||
|
// Layer, or an error if not supported by the storagedriver.
|
||||||
|
func (lh *cloudFrontLayerHandler) Resolve(layer Layer) (http.Handler, error) {
|
||||||
|
layerURLStr, err := lh.delegateLayerHandler.urlFor(layer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
layerURL, err := url.Parse(layerURLStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfURL, err := lh.cloudfront.CannedSignedURL(layerURL.Path, "", time.Now().Add(20*time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, cfURL, http.StatusTemporaryRedirect)
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// init registers the cloudfront layerHandler backend.
|
||||||
|
func init() {
|
||||||
|
RegisterLayerHandler("cloudfront", LayerHandlerInitFunc(newCloudFrontLayerHandler))
|
||||||
|
}
|
73
storage/delegatelayerhandler.go
Normal file
73
storage/delegatelayerhandler.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/storagedriver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// delegateLayerHandler provides a simple implementation of layerHandler that
|
||||||
|
// simply issues HTTP Temporary Redirects to the URL provided by the
|
||||||
|
// storagedriver for a given Layer.
|
||||||
|
type delegateLayerHandler struct {
|
||||||
|
storageDriver storagedriver.StorageDriver
|
||||||
|
pathMapper *pathMapper
|
||||||
|
duration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ LayerHandler = &delegateLayerHandler{}
|
||||||
|
|
||||||
|
func newDelegateLayerHandler(storageDriver storagedriver.StorageDriver, options map[string]interface{}) (LayerHandler, error) {
|
||||||
|
duration := 20 * time.Minute
|
||||||
|
d, ok := options["duration"]
|
||||||
|
if ok {
|
||||||
|
switch d := d.(type) {
|
||||||
|
case time.Duration:
|
||||||
|
duration = d
|
||||||
|
case string:
|
||||||
|
dur, err := time.ParseDuration(d)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Invalid duration: %s", err)
|
||||||
|
}
|
||||||
|
duration = dur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &delegateLayerHandler{storageDriver: storageDriver, pathMapper: defaultPathMapper, duration: duration}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve returns an http.Handler which can serve the contents of the given
|
||||||
|
// Layer, or an error if not supported by the storagedriver.
|
||||||
|
func (lh *delegateLayerHandler) Resolve(layer Layer) (http.Handler, error) {
|
||||||
|
layerURL, err := lh.urlFor(layer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, layerURL, http.StatusTemporaryRedirect)
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// urlFor returns a download URL for the given layer, or the empty string if
|
||||||
|
// unsupported.
|
||||||
|
func (lh *delegateLayerHandler) urlFor(layer Layer) (string, error) {
|
||||||
|
blobPath, err := resolveBlobPath(lh.storageDriver, lh.pathMapper, layer.Name(), layer.Digest())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
layerURL, err := lh.storageDriver.URLFor(blobPath, map[string]interface{}{"expiry": time.Now().Add(lh.duration)})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return layerURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// init registers the delegate layerHandler backend.
|
||||||
|
func init() {
|
||||||
|
RegisterLayerHandler("delegate", LayerHandlerInitFunc(newDelegateLayerHandler))
|
||||||
|
}
|
50
storage/layerhandler.go
Normal file
50
storage/layerhandler.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/storagedriver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LayerHandler provides middleware for serving the contents of a Layer.
|
||||||
|
type LayerHandler interface {
|
||||||
|
// Resolve returns an http.Handler which can serve the contents of a given
|
||||||
|
// Layer if possible, or nil and an error when unsupported. This may
|
||||||
|
// directly serve the contents of the layer or issue a redirect to another
|
||||||
|
// URL hosting the content.
|
||||||
|
Resolve(layer Layer) (http.Handler, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LayerHandlerInitFunc is the type of a LayerHandler factory function and is
|
||||||
|
// used to register the contsructor for different LayerHandler backends.
|
||||||
|
type LayerHandlerInitFunc func(storageDriver storagedriver.StorageDriver, options map[string]interface{}) (LayerHandler, error)
|
||||||
|
|
||||||
|
var layerHandlers map[string]LayerHandlerInitFunc
|
||||||
|
|
||||||
|
// RegisterLayerHandler is used to register an LayerHandlerInitFunc for
|
||||||
|
// a LayerHandler backend with the given name.
|
||||||
|
func RegisterLayerHandler(name string, initFunc LayerHandlerInitFunc) error {
|
||||||
|
if layerHandlers == nil {
|
||||||
|
layerHandlers = make(map[string]LayerHandlerInitFunc)
|
||||||
|
}
|
||||||
|
if _, exists := layerHandlers[name]; exists {
|
||||||
|
return fmt.Errorf("name already registered: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
layerHandlers[name] = initFunc
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLayerHandler constructs a LayerHandler
|
||||||
|
// with the given options using the named backend.
|
||||||
|
func GetLayerHandler(name string, options map[string]interface{}, storageDriver storagedriver.StorageDriver) (LayerHandler, error) {
|
||||||
|
if layerHandlers != nil {
|
||||||
|
if initFunc, exists := layerHandlers[name]; exists {
|
||||||
|
return initFunc(storageDriver, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no layer handler registered with name: %s", name)
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ func (ls *layerStore) Exists(name string, digest digest.Digest) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ls *layerStore) Fetch(name string, digest digest.Digest) (Layer, error) {
|
func (ls *layerStore) Fetch(name string, digest digest.Digest) (Layer, error) {
|
||||||
blobPath, err := ls.resolveBlobPath(name, digest)
|
blobPath, err := resolveBlobPath(ls.driver, ls.pathMapper, name, digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err := err.(type) {
|
switch err := err.(type) {
|
||||||
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
|
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
|
||||||
|
@ -94,31 +94,3 @@ func (ls *layerStore) newLayerUpload(lus LayerUploadState) LayerUpload {
|
||||||
uploadStore: ls.uploadStore,
|
uploadStore: ls.uploadStore,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveBlobId looks up the blob location in the repositories from a
|
|
||||||
// layer/blob link file, returning blob path or an error on failure.
|
|
||||||
func (ls *layerStore) resolveBlobPath(name string, dgst digest.Digest) (string, error) {
|
|
||||||
pathSpec := layerLinkPathSpec{name: name, digest: dgst}
|
|
||||||
layerLinkPath, err := ls.pathMapper.path(pathSpec)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
layerLinkContent, err := ls.driver.GetContent(layerLinkPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE(stevvooe): The content of the layer link should match the digest.
|
|
||||||
// This layer of indirection is for name-based content protection.
|
|
||||||
|
|
||||||
linked, err := digest.ParseDigest(string(layerLinkContent))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
bp := blobPathSpec{digest: linked}
|
|
||||||
|
|
||||||
return ls.pathMapper.path(bp)
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/distribution/digest"
|
"github.com/docker/distribution/digest"
|
||||||
|
"github.com/docker/distribution/storagedriver"
|
||||||
)
|
)
|
||||||
|
|
||||||
const storagePathVersion = "v2"
|
const storagePathVersion = "v2"
|
||||||
|
@ -44,6 +45,11 @@ type pathMapper struct {
|
||||||
version string // should be a constant?
|
version string // should be a constant?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var defaultPathMapper = &pathMapper{
|
||||||
|
root: "/docker/registry/",
|
||||||
|
version: storagePathVersion,
|
||||||
|
}
|
||||||
|
|
||||||
// path returns the path identified by spec.
|
// path returns the path identified by spec.
|
||||||
func (pm *pathMapper) path(spec pathSpec) (string, error) {
|
func (pm *pathMapper) path(spec pathSpec) (string, error) {
|
||||||
|
|
||||||
|
@ -204,3 +210,31 @@ func digestPathComoponents(dgst digest.Digest) ([]string, error) {
|
||||||
|
|
||||||
return append(prefix, suffix...), nil
|
return append(prefix, suffix...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveBlobPath looks up the blob location in the repositories from a
|
||||||
|
// layer/blob link file, returning blob path or an error on failure.
|
||||||
|
func resolveBlobPath(driver storagedriver.StorageDriver, pm *pathMapper, name string, dgst digest.Digest) (string, error) {
|
||||||
|
pathSpec := layerLinkPathSpec{name: name, digest: dgst}
|
||||||
|
layerLinkPath, err := pm.path(pathSpec)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
layerLinkContent, err := driver.GetContent(layerLinkPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(stevvooe): The content of the layer link should match the digest.
|
||||||
|
// This layer of indirection is for name-based content protection.
|
||||||
|
|
||||||
|
linked, err := digest.ParseDigest(string(layerLinkContent))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
bp := blobPathSpec{digest: linked}
|
||||||
|
|
||||||
|
return pm.path(bp)
|
||||||
|
}
|
||||||
|
|
|
@ -28,11 +28,8 @@ func NewServices(driver storagedriver.StorageDriver) *Services {
|
||||||
|
|
||||||
return &Services{
|
return &Services{
|
||||||
driver: driver,
|
driver: driver,
|
||||||
pathMapper: &pathMapper{
|
|
||||||
// TODO(sday): This should be configurable.
|
// TODO(sday): This should be configurable.
|
||||||
root: "/docker/registry/",
|
pathMapper: defaultPathMapper,
|
||||||
version: storagePathVersion,
|
|
||||||
},
|
|
||||||
layerUploadStore: layerUploadStore,
|
layerUploadStore: layerUploadStore,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -267,7 +267,7 @@ func (d *Driver) Delete(subPath string) error {
|
||||||
|
|
||||||
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
|
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
|
||||||
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
|
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
|
||||||
func (d *Driver) URLFor(path string) (string, error) {
|
func (d *Driver) URLFor(path string, options map[string]interface{}) (string, error) {
|
||||||
return "", storagedriver.ErrUnsupportedMethod
|
return "", storagedriver.ErrUnsupportedMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -254,6 +254,6 @@ func (d *Driver) Delete(path string) error {
|
||||||
|
|
||||||
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
|
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
|
||||||
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
|
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
|
||||||
func (d *Driver) URLFor(path string) (string, error) {
|
func (d *Driver) URLFor(path string, options map[string]interface{}) (string, error) {
|
||||||
return "", storagedriver.ErrUnsupportedMethod
|
return "", storagedriver.ErrUnsupportedMethod
|
||||||
}
|
}
|
||||||
|
|
|
@ -582,12 +582,21 @@ func (d *Driver) Delete(path string) error {
|
||||||
|
|
||||||
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
|
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
|
||||||
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
|
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
|
||||||
func (d *Driver) URLFor(path string) (string, error) {
|
func (d *Driver) URLFor(path string, options map[string]interface{}) (string, error) {
|
||||||
if !storagedriver.PathRegexp.MatchString(path) {
|
if !storagedriver.PathRegexp.MatchString(path) {
|
||||||
return "", storagedriver.InvalidPathError{Path: path}
|
return "", storagedriver.InvalidPathError{Path: path}
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.Bucket.SignedURL(d.s3Path(path), time.Now().Add(24*time.Hour)), nil
|
expiresTime := time.Now().Add(20 * time.Minute)
|
||||||
|
expires, ok := options["expiry"]
|
||||||
|
if ok {
|
||||||
|
et, ok := expires.(time.Time)
|
||||||
|
if ok {
|
||||||
|
expiresTime = et
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.Bucket.SignedURL(d.s3Path(path), expiresTime), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) s3Path(path string) string {
|
func (d *Driver) s3Path(path string) string {
|
||||||
|
|
|
@ -71,9 +71,11 @@ type StorageDriver interface {
|
||||||
// Delete recursively deletes all objects stored at "path" and its subpaths.
|
// Delete recursively deletes all objects stored at "path" and its subpaths.
|
||||||
Delete(path string) error
|
Delete(path string) error
|
||||||
|
|
||||||
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
|
// URLFor returns a URL which may be used to retrieve the content stored at
|
||||||
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
|
// the given path, possibly using the given options.
|
||||||
URLFor(path string) (string, error)
|
// May return an UnsupportedMethodErr in certain StorageDriver
|
||||||
|
// implementations.
|
||||||
|
URLFor(path string, options map[string]interface{}) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PathRegexp is the regular expression which each file path must match.
|
// PathRegexp is the regular expression which each file path must match.
|
||||||
|
|
|
@ -592,7 +592,7 @@ func (suite *DriverSuite) TestURLFor(c *check.C) {
|
||||||
err := suite.StorageDriver.PutContent(filename, contents)
|
err := suite.StorageDriver.PutContent(filename, contents)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
url, err := suite.StorageDriver.URLFor(filename)
|
url, err := suite.StorageDriver.URLFor(filename, nil)
|
||||||
if err == storagedriver.ErrUnsupportedMethod {
|
if err == storagedriver.ErrUnsupportedMethod {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue