diff --git a/dav/README.md b/dav/README.md index 1641195..de62ab1 100644 --- a/dav/README.md +++ b/dav/README.md @@ -6,28 +6,78 @@ WebDAV client implementation for the unified storage-cli tool. This module provi For general usage and build instructions, see the [main README](../README.md). +## Path Handling Contract + +**IMPORTANT:** Callers are responsible for any path layout or partitioning strategy. The DAV driver does not transform object IDs. + +Object IDs are used exactly as provided: +- Simple paths: `my-blob-id` → stored as `my-blob-id` +- Nested paths: `ab/cd/my-blob-id` → stored as `ab/cd/my-blob-id` + +**Breaking change in v0.0.8+:** The driver no longer applies automatic 2-character path fan-out (e.g., `abcdef...` was previously stored as `ab/cd/abcdef...` in v0.0.7 and earlier). Callers must now include any desired directory structure in the object ID itself. + +This behavior aligns with other storage-cli providers (S3, Azure, GCS, AliOSS), which also pass keys through unchanged. + ## DAV-Specific Configuration -The DAV client requires a JSON configuration file with WebDAV endpoint details and credentials. +The DAV client requires a JSON configuration file with the following structure: + +``` json +{ + "endpoint": " (required - WebDAV server URL)", + "user": " (optional - for Basic Auth)", + "password": " (optional - for Basic Auth)", + "retry_attempts": (optional - default: 3), + "tls": { + "cert": { + "ca": " (optional - PEM-encoded CA certificate)" + } + }, + "secret": " (optional - required for pre-signed URLs)" +} +``` **Usage examples:** ```bash -# Upload an object -storage-cli -s dav -c dav-config.json put local-file.txt remote-object +# Upload a file to WebDAV +storage-cli -s dav -c dav-config.json put local-file.txt remote-blob -# Fetch an object -storage-cli -s dav -c dav-config.json get remote-object local-file.txt +# Download a file from WebDAV +storage-cli -s dav -c dav-config.json get remote-blob local-file.txt -# Delete an object -storage-cli -s dav -c dav-config.json delete remote-object +# Check if blob exists +storage-cli -s dav -c dav-config.json exists remote-blob -# Check if an object exists -storage-cli -s dav -c dav-config.json exists remote-object +# Delete a blob +storage-cli -s dav -c dav-config.json delete remote-blob -# Generate a signed URL (e.g., GET for 1 hour) -storage-cli -s dav -c dav-config.json sign remote-object get 60s +# Generate a pre-signed URL (requires secret in config) +storage-cli -s dav -c dav-config.json sign remote-blob get 1h ``` +## Features + +### Basic Operations (Available) +- **Put** - Upload files to WebDAV server +- **Get** - Download files from WebDAV server +- **Delete** - Delete individual blobs +- **Exists** - Check if a blob exists +- **Sign** - Generate pre-signed URLs with HMAC-SHA256 + +### Advanced Operations (Coming Soon) +The following operations will be available in future releases: +- **List** - List all blobs or filter by prefix +- **Copy** - Server-side blob copying via WebDAV COPY method +- **DeleteRecursive** - Delete all blobs matching a prefix +- **Properties** - Retrieve blob metadata (ContentLength, ETag, LastModified) +- **EnsureStorageExists** - Initialize WebDAV directory structure + +### Automatic Retry Logic +All operations automatically retry on transient errors. Default is 3 retry attempts, configurable via `retry_attempts` in config. + +### TLS/HTTPS Support +Supports HTTPS connections with custom CA certificates for internal or self-signed certificates. + ## Pre-signed URLs The `sign` command generates a pre-signed URL for a specific object, action, and duration. @@ -44,6 +94,13 @@ The generated URL format: ### Unit Tests Run unit tests from the repository root: + ```bash -ginkgo --cover -v -r ./dav/... +go test ./dav/client/... ``` + +Or using ginkgo: +```bash +ginkgo --cover -v -r ./dav/client +``` + diff --git a/dav/app/app.go b/dav/app/app.go deleted file mode 100644 index dfbe1d8..0000000 --- a/dav/app/app.go +++ /dev/null @@ -1,80 +0,0 @@ -package app - -import ( - "errors" - "fmt" - "time" - - davcmd "github.com/cloudfoundry/storage-cli/dav/cmd" - davconfig "github.com/cloudfoundry/storage-cli/dav/config" -) - -type App struct { - runner davcmd.Runner - config davconfig.Config -} - -func New(r davcmd.Runner, c davconfig.Config) *App { - app := &App{runner: r, config: c} - return app -} - -func (app *App) run(args []string) (err error) { - - err = app.runner.SetConfig(app.config) - if err != nil { - err = fmt.Errorf("Invalid CA Certificate: %s", err.Error()) //nolint:staticcheck - return - } - - err = app.runner.Run(args) - return -} - -func (app *App) Put(sourceFilePath string, destinationObject string) error { - return app.run([]string{"put", sourceFilePath, destinationObject}) -} - -func (app *App) Get(sourceObject string, dest string) error { - return app.run([]string{"get", sourceObject, dest}) -} - -func (app *App) Delete(object string) error { - return app.run([]string{"delete", object}) -} - -func (app *App) Exists(object string) (bool, error) { - err := app.run([]string{"exists", object}) - if err != nil { - return false, err - } - return true, nil -} - -func (app *App) Sign(object string, action string, expiration time.Duration) (string, error) { - err := app.run([]string{"sign", object, action, expiration.String()}) - if err != nil { - return "", err - } - return "", nil -} - -func (app *App) List(prefix string) ([]string, error) { - return nil, errors.New("not implemented") -} - -func (app *App) Copy(srcBlob string, dstBlob string) error { - return errors.New("not implemented") -} - -func (app *App) Properties(dest string) error { - return errors.New("not implemented") -} - -func (app *App) EnsureStorageExists() error { - return errors.New("not implemented") -} - -func (app *App) DeleteRecursive(prefix string) error { - return errors.New("not implemented") -} diff --git a/dav/app/app_suite_test.go b/dav/app/app_suite_test.go deleted file mode 100644 index e4657e2..0000000 --- a/dav/app/app_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package app_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "testing" -) - -func TestApp(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Dav App Suite") -} diff --git a/dav/app/app_test.go b/dav/app/app_test.go deleted file mode 100644 index 71d00c2..0000000 --- a/dav/app/app_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package app_test - -import ( - "errors" - "os" - "path/filepath" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - . "github.com/cloudfoundry/storage-cli/dav/app" - davconf "github.com/cloudfoundry/storage-cli/dav/config" -) - -type FakeRunner struct { - Config davconf.Config - SetConfigErr error - RunArgs []string - RunErr error -} - -func (r *FakeRunner) SetConfig(newConfig davconf.Config) (err error) { - r.Config = newConfig - return r.SetConfigErr -} - -func (r *FakeRunner) Run(cmdArgs []string) (err error) { - r.RunArgs = cmdArgs - return r.RunErr -} - -func pathToFixture(file string) string { - pwd, err := os.Getwd() - Expect(err).ToNot(HaveOccurred()) - - fixturePath := filepath.Join(pwd, "../test_assets", file) - - absPath, err := filepath.Abs(fixturePath) - Expect(err).ToNot(HaveOccurred()) - - return absPath -} - -var _ = Describe("App", func() { - - It("reads the CA cert from config", func() { - configFile, _ := os.Open(pathToFixture("dav-cli-config-with-ca.json")) //nolint:errcheck - defer configFile.Close() //nolint:errcheck - davConfig, _ := davconf.NewFromReader(configFile) //nolint:errcheck - - runner := &FakeRunner{} - app := New(runner, davConfig) - err := app.Put("localFile", "remoteFile") - Expect(err).ToNot(HaveOccurred()) - - expectedConfig := davconf.Config{ - User: "some user", - Password: "some pwd", - Endpoint: "https://example.com/some/endpoint", - Secret: "77D47E3A0B0F590B73CF3EBD9BB6761E244F90FA6F28BB39F941B0905789863FBE2861FDFD8195ADC81B72BB5310BC18969BEBBF4656366E7ACD3F0E4186FDDA", - TLS: davconf.TLS{ - Cert: davconf.Cert{ - CA: "ca-cert", - }, - }, - } - - Expect(runner.Config).To(Equal(expectedConfig)) - Expect(runner.Config.TLS.Cert.CA).ToNot(BeNil()) - }) - - It("returns error if CA Cert is invalid", func() { - configFile, _ := os.Open(pathToFixture("dav-cli-config-with-ca.json")) //nolint:errcheck - defer configFile.Close() //nolint:errcheck - davConfig, _ := davconf.NewFromReader(configFile) //nolint:errcheck - - runner := &FakeRunner{ - SetConfigErr: errors.New("invalid cert"), - } - - app := New(runner, davConfig) - err := app.Put("localFile", "remoteFile") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("Invalid CA Certificate: invalid cert")) - - }) - - It("runs the put command", func() { - configFile, _ := os.Open(pathToFixture("dav-cli-config.json")) //nolint:errcheck - defer configFile.Close() //nolint:errcheck - davConfig, _ := davconf.NewFromReader(configFile) //nolint:errcheck - - runner := &FakeRunner{} - - app := New(runner, davConfig) - err := app.Put("localFile", "remoteFile") - Expect(err).ToNot(HaveOccurred()) - - expectedConfig := davconf.Config{ - User: "some user", - Password: "some pwd", - Endpoint: "http://example.com/some/endpoint", - Secret: "77D47E3A0B0F590B73CF3EBD9BB6761E244F90FA6F28BB39F941B0905789863FBE2861FDFD8195ADC81B72BB5310BC18969BEBBF4656366E7ACD3F0E4186FDDA", - } - - Expect(runner.Config).To(Equal(expectedConfig)) - Expect(runner.Config.TLS.Cert.CA).To(BeEmpty()) - Expect(runner.RunArgs).To(Equal([]string{"put", "localFile", "remoteFile"})) - }) - - It("returns error from the cmd runner", func() { - - configFile, _ := os.Open(pathToFixture("dav-cli-config.json")) //nolint:errcheck - defer configFile.Close() //nolint:errcheck - davConfig, _ := davconf.NewFromReader(configFile) //nolint:errcheck - - runner := &FakeRunner{ - RunErr: errors.New("fake-run-error"), - } - - app := New(runner, davConfig) - err := app.Put("localFile", "remoteFile") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("fake-run-error")) - }) - - Context("Checking functionalities", func() { - // var app *App - var davConfig davconf.Config - BeforeEach(func() { - - configFile, _ := os.Open(pathToFixture("dav-cli-config.json")) //nolint:errcheck - defer configFile.Close() //nolint:errcheck - davConfig, _ = davconf.NewFromReader(configFile) //nolint:errcheck - }) - - It("Exists fails", func() { - - runner := &FakeRunner{ - RunErr: errors.New("object does not exist"), - } - app := New(runner, davConfig) - - exist, err := app.Exists("someObject") //nolint:errcheck - - Expect(err.Error()).To(ContainSubstring("object does not exist")) - Expect(exist).To(BeFalse()) - - }) - - It("Sign Fails", func() { - runner := &FakeRunner{ - RunErr: errors.New("can't sign"), - } - - app := New(runner, davConfig) - signedurl, err := app.Sign("someObject", "SomeObject", time.Second*100) - Expect(signedurl).To(BeEmpty()) - Expect(err.Error()).To(ContainSubstring("can't sign")) - - }) - - }) - -}) diff --git a/dav/client/client.go b/dav/client/client.go index cd43926..aa63811 100644 --- a/dav/client/client.go +++ b/dav/client/client.go @@ -1,197 +1,148 @@ package client import ( - "crypto/sha1" "fmt" "io" - "net/http" - "net/url" - "path" + "log/slog" + "os" "strings" "time" - URLsigner "github.com/cloudfoundry/storage-cli/dav/signer" - - bosherr "github.com/cloudfoundry/bosh-utils/errors" "github.com/cloudfoundry/bosh-utils/httpclient" boshlog "github.com/cloudfoundry/bosh-utils/logger" davconf "github.com/cloudfoundry/storage-cli/dav/config" ) -type Client interface { - Get(path string) (content io.ReadCloser, err error) - Put(path string, content io.ReadCloser, contentLength int64) (err error) - Exists(path string) (err error) - Delete(path string) (err error) - Sign(objectID, action string, duration time.Duration) (string, error) +type DavBlobstore struct { + storageClient StorageClient } -func NewClient(config davconf.Config, httpClient httpclient.Client, logger boshlog.Logger) (c Client) { +func New(config davconf.Config) (*DavBlobstore, error) { + logger := boshlog.NewLogger(boshlog.LevelNone) + + var httpClientBase httpclient.Client + var certPool, err = getCertPool(config) + if err != nil { + return nil, fmt.Errorf("failed to create certificate pool: %w", err) + } + + httpClientBase = httpclient.CreateDefaultClient(certPool) + if config.RetryAttempts == 0 { config.RetryAttempts = 3 } - // @todo should a logger now be passed in to this client? - duration := time.Duration(0) retryClient := httpclient.NewRetryClient( - httpClient, + httpClientBase, config.RetryAttempts, - duration, + time.Duration(0), logger, ) - return client{ - config: config, - httpClient: retryClient, - } + storageClient := NewStorageClient(config, retryClient) + + return NewWithStorageClient(storageClient), nil } -type client struct { - config davconf.Config - httpClient httpclient.Client +func NewWithStorageClient(storageClient StorageClient) *DavBlobstore { + return &DavBlobstore{storageClient: storageClient} } -func (c client) Get(path string) (io.ReadCloser, error) { - req, err := c.createReq("GET", path, nil) - if err != nil { - return nil, err - } +func (d *DavBlobstore) Put(sourceFilePath string, dest string) error { + slog.Info("uploading file to webdav", "source", sourceFilePath, "dest", dest) - resp, err := c.httpClient.Do(req) + source, err := os.Open(sourceFilePath) if err != nil { - return nil, bosherr.WrapErrorf(err, "Getting dav blob %s", path) + return fmt.Errorf("failed to open source file: %w", err) } + defer source.Close() //nolint:errcheck - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("Getting dav blob %s: Wrong response code: %d; body: %s", path, resp.StatusCode, c.readAndTruncateBody(resp)) //nolint:staticcheck - } - - return resp.Body, nil -} - -func (c client) Put(path string, content io.ReadCloser, contentLength int64) error { - req, err := c.createReq("PUT", path, content) + fileInfo, err := source.Stat() if err != nil { - return err + return fmt.Errorf("failed to stat source file: %w", err) } - defer content.Close() //nolint:errcheck - req.ContentLength = contentLength - resp, err := c.httpClient.Do(req) + err = d.storageClient.Put(dest, source, fileInfo.Size()) if err != nil { - return bosherr.WrapErrorf(err, "Putting dav blob %s", path) - } - - if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { - return fmt.Errorf("Putting dav blob %s: Wrong response code: %d; body: %s", path, resp.StatusCode, c.readAndTruncateBody(resp)) //nolint:staticcheck + return fmt.Errorf("upload failure: %w", err) } + slog.Debug("successfully uploaded file", "dest", dest) return nil } -func (c client) Exists(path string) error { - req, err := c.createReq("HEAD", path, nil) - if err != nil { - return err - } +func (d *DavBlobstore) Get(source string, dest string) error { + slog.Info("downloading file from webdav", "source", source, "dest", dest) - resp, err := c.httpClient.Do(req) + destFile, err := os.Create(dest) if err != nil { - return bosherr.WrapErrorf(err, "Checking if dav blob %s exists", path) + return fmt.Errorf("failed to create destination file: %w", err) } + defer destFile.Close() //nolint:errcheck - if resp.StatusCode == http.StatusNotFound { - err := fmt.Errorf("%s not found", path) - return bosherr.WrapErrorf(err, "Checking if dav blob %s exists", path) - } - - if resp.StatusCode != http.StatusOK { - err := fmt.Errorf("invalid status: %d", resp.StatusCode) - return bosherr.WrapErrorf(err, "Checking if dav blob %s exists", path) - } - - return nil -} - -func (c client) Delete(path string) error { - req, err := c.createReq("DELETE", path, nil) + content, err := d.storageClient.Get(source) if err != nil { - return bosherr.WrapErrorf(err, "Creating delete request for blob '%s'", path) + return fmt.Errorf("download failure: %w", err) } + defer content.Close() //nolint:errcheck - resp, err := c.httpClient.Do(req) + _, err = io.Copy(destFile, content) if err != nil { - return bosherr.WrapErrorf(err, "Deleting blob '%s'", path) - } - - if resp.StatusCode == http.StatusNotFound { - return nil - } - - if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { - err := fmt.Errorf("invalid status: %d", resp.StatusCode) - return bosherr.WrapErrorf(err, "Deleting blob '%s'", path) + return fmt.Errorf("failed to write to destination file: %w", err) } + slog.Debug("successfully downloaded file", "dest", dest) return nil } -func (c client) Sign(blobID, action string, duration time.Duration) (string, error) { - signer := URLsigner.NewSigner(c.config.Secret) - signTime := time.Now() - - prefixedBlob := fmt.Sprintf("%s/%s", getBlobPrefix(blobID), blobID) - - signedURL, err := signer.GenerateSignedURL(c.config.Endpoint, prefixedBlob, action, signTime, duration) - - if err != nil { - return "", bosherr.WrapErrorf(err, "pre-signing the url") - } - - return signedURL, err +func (d *DavBlobstore) Delete(dest string) error { + slog.Info("deleting file from webdav", "dest", dest) + return d.storageClient.Delete(dest) } -func (c client) createReq(method, blobID string, body io.Reader) (*http.Request, error) { - blobURL, err := url.Parse(c.config.Endpoint) - if err != nil { - return nil, err - } +func (d *DavBlobstore) Exists(dest string) (bool, error) { + slog.Info("checking if file exists on webdav", "dest", dest) + return d.storageClient.Exists(dest) +} - blobPrefix := getBlobPrefix(blobID) +func (d *DavBlobstore) Sign(dest string, action string, expiration time.Duration) (string, error) { + slog.Info("signing url for webdav", "dest", dest, "action", action, "expiration", expiration) - newPath := path.Join(blobURL.Path, blobPrefix, blobID) - if !strings.HasPrefix(newPath, "/") { - newPath = "/" + newPath + action = strings.ToUpper(action) + switch action { + case "GET", "PUT": + signedURL, err := d.storageClient.Sign(dest, action, expiration) + if err != nil { + return "", fmt.Errorf("failed to sign URL: %w", err) + } + return signedURL, nil + default: + return "", fmt.Errorf("action not implemented: %s", action) } +} - blobURL.Path = newPath +// DeleteRecursive is not yet implemented in this refactoring +func (d *DavBlobstore) DeleteRecursive(prefix string) error { + return fmt.Errorf("DeleteRecursive not yet implemented") +} - req, err := http.NewRequest(method, blobURL.String(), body) - if err != nil { - return req, err - } +// List is not yet implemented in this refactoring +func (d *DavBlobstore) List(prefix string) ([]string, error) { + return nil, fmt.Errorf("List not yet implemented") +} - if c.config.User != "" { - req.SetBasicAuth(c.config.User, c.config.Password) - } - return req, nil +// Copy is not yet implemented in this refactoring +func (d *DavBlobstore) Copy(srcBlob string, dstBlob string) error { + return fmt.Errorf("Copy not yet implemented") } -func (c client) readAndTruncateBody(resp *http.Response) string { - body := "" - if resp.Body != nil { - buf := make([]byte, 1024) - n, err := resp.Body.Read(buf) - if err == io.EOF || err == nil { - body = string(buf[0:n]) - } - } - return body +// Properties is not yet implemented in this refactoring +func (d *DavBlobstore) Properties(dest string) error { + return fmt.Errorf("Properties not yet implemented") } -func getBlobPrefix(blobID string) string { - digester := sha1.New() - digester.Write([]byte(blobID)) - return fmt.Sprintf("%02x", digester.Sum(nil)[0]) +// EnsureStorageExists is not yet implemented in this refactoring +func (d *DavBlobstore) EnsureStorageExists() error { + return fmt.Errorf("EnsureStorageExists not yet implemented") } diff --git a/dav/client/client_test.go b/dav/client/client_test.go index a26eab8..b55ff7f 100644 --- a/dav/client/client_test.go +++ b/dav/client/client_test.go @@ -1,298 +1,127 @@ package client_test import ( + "fmt" "io" - "net/http" + "os" "strings" + "github.com/cloudfoundry/storage-cli/dav/client" + "github.com/cloudfoundry/storage-cli/dav/client/clientfakes" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/onsi/gomega/ghttp" - - "github.com/cloudfoundry/bosh-utils/httpclient" - boshlog "github.com/cloudfoundry/bosh-utils/logger" - - . "github.com/cloudfoundry/storage-cli/dav/client" - davconf "github.com/cloudfoundry/storage-cli/dav/config" ) var _ = Describe("Client", func() { - var ( - server *ghttp.Server - config davconf.Config - client Client - logger boshlog.Logger - ) - BeforeEach(func() { - server = ghttp.NewServer() - config.Endpoint = server.URL() - config.User = "some_user" - config.Password = "some password" - logger = boshlog.NewLogger(boshlog.LevelNone) - client = NewClient(config, httpclient.DefaultClient, logger) - }) + Context("Put", func() { + It("uploads a file to a blob", func() { + fakeStorageClient := &clientfakes.FakeStorageClient{} + fakeStorageClient.PutReturns(nil) - disconnectingRequestHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - conn, _, err := w.(http.Hijacker).Hijack() - Expect(err).NotTo(HaveOccurred()) + davBlobstore := client.NewWithStorageClient(fakeStorageClient) - conn.Close() //nolint:errcheck - }) + file, err := os.CreateTemp("", "tmpfile") + Expect(err).NotTo(HaveOccurred()) + defer os.Remove(file.Name()) //nolint:errcheck - Describe("Exists", func() { - It("does not return an error if file exists", func() { - server.AppendHandlers(ghttp.RespondWith(200, "")) - err := client.Exists("/somefile") + _, err = file.WriteString("test content") Expect(err).NotTo(HaveOccurred()) - }) + file.Close() //nolint:errcheck - Context("the file does not exist", func() { - BeforeEach(func() { - server.AppendHandlers( - ghttp.RespondWith(404, ""), - ghttp.RespondWith(404, ""), - ghttp.RespondWith(404, ""), - ) - }) + err = davBlobstore.Put(file.Name(), "target/blob") - It("returns an error saying blob was not found", func() { - err := client.Exists("/somefile") - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("Checking if dav blob /somefile exists: /somefile not found"))) - }) + Expect(err).NotTo(HaveOccurred()) + Expect(fakeStorageClient.PutCallCount()).To(Equal(1)) + path, _, _ := fakeStorageClient.PutArgsForCall(0) + Expect(path).To(Equal("target/blob")) }) - Context("unexpected http status code returned", func() { - BeforeEach(func() { - server.AppendHandlers( - ghttp.RespondWith(601, ""), - ghttp.RespondWith(601, ""), - ghttp.RespondWith(601, ""), - ) - }) + It("fails if the source file does not exist", func() { + fakeStorageClient := &clientfakes.FakeStorageClient{} - It("returns an error saying an unexpected error occurred", func() { - err := client.Exists("/somefile") - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("Checking if dav blob /somefile exists:"))) - }) - }) - }) - - Describe("Delete", func() { - Context("when the file does not exist", func() { - BeforeEach(func() { - server.AppendHandlers( - ghttp.RespondWith(404, ""), - ghttp.RespondWith(404, ""), - ghttp.RespondWith(404, ""), - ) - }) + davBlobstore := client.NewWithStorageClient(fakeStorageClient) + err := davBlobstore.Put("nonexistent/path", "target/blob") - It("does not return an error if file does not exists", func() { - err := client.Delete("/somefile") - Expect(err).NotTo(HaveOccurred()) - }) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to open source file")) + Expect(fakeStorageClient.PutCallCount()).To(Equal(0)) }) + }) - Context("when the file exists", func() { - BeforeEach(func() { - server.AppendHandlers(ghttp.RespondWith(204, "")) - }) + Context("Get", func() { + It("downloads a blob to a file", func() { + fakeStorageClient := &clientfakes.FakeStorageClient{} + content := io.NopCloser(strings.NewReader("test content")) + fakeStorageClient.GetReturns(content, nil) - It("does not return an error", func() { - err := client.Delete("/somefile") - Expect(err).ToNot(HaveOccurred()) - Expect(server.ReceivedRequests()).To(HaveLen(1)) - request := server.ReceivedRequests()[0] - Expect(request.URL.Path).To(Equal("/19/somefile")) - Expect(request.Method).To(Equal("DELETE")) - Expect(request.Header["Authorization"]).To(Equal([]string{"Basic c29tZV91c2VyOnNvbWUgcGFzc3dvcmQ="})) - Expect(request.Host).To(Equal(server.Addr())) - }) - }) + davBlobstore := client.NewWithStorageClient(fakeStorageClient) - Context("when the status code is not in the 2xx range", func() { - It("returns an error saying an unexpected error occurred when the status code is greater than 299", func() { - server.AppendHandlers( - ghttp.RespondWith(300, ""), - ghttp.RespondWith(300, ""), - ghttp.RespondWith(300, ""), - ) + tmpFile, err := os.CreateTemp("", "download") + Expect(err).NotTo(HaveOccurred()) + tmpFile.Close() //nolint:errcheck + defer os.Remove(tmpFile.Name()) //nolint:errcheck - err := client.Delete("/somefile") - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(Equal("Deleting blob '/somefile': invalid status: 300"))) - }) - }) - }) + err = davBlobstore.Get("source/blob", tmpFile.Name()) - Describe("Get", func() { - It("returns the response body from the given path", func() { - server.AppendHandlers(ghttp.RespondWith(200, "response")) + Expect(err).NotTo(HaveOccurred()) + Expect(fakeStorageClient.GetCallCount()).To(Equal(1)) - responseBody, err := client.Get("/") + downloaded, err := os.ReadFile(tmpFile.Name()) Expect(err).NotTo(HaveOccurred()) - buf := make([]byte, 1024) - n, _ := responseBody.Read(buf) //nolint:errcheck - Expect(string(buf[0:n])).To(Equal("response")) + Expect(string(downloaded)).To(Equal("test content")) }) + }) - Context("when the http request fails", func() { - BeforeEach(func() { - server.Close() - }) - - It("returns err", func() { - responseBody, err := client.Get("/") - Expect(responseBody).To(BeNil()) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("Getting dav blob /")) - }) - }) + Context("Delete", func() { + It("deletes a blob", func() { + fakeStorageClient := &clientfakes.FakeStorageClient{} + fakeStorageClient.DeleteReturns(nil) - Context("when the http response code is not 200", func() { - BeforeEach(func() { - server.AppendHandlers( - ghttp.RespondWith(300, "response"), - ghttp.RespondWith(300, "response"), - ghttp.RespondWith(300, "response"), - ) - }) + davBlobstore := client.NewWithStorageClient(fakeStorageClient) + err := davBlobstore.Delete("blob/path") - It("returns err", func() { - responseBody, err := client.Get("/") - Expect(responseBody).To(BeNil()) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("Getting dav blob /: Wrong response code: 300"))) - Expect(server.ReceivedRequests()).To(HaveLen(3)) - }) + Expect(err).NotTo(HaveOccurred()) + Expect(fakeStorageClient.DeleteCallCount()).To(Equal(1)) + Expect(fakeStorageClient.DeleteArgsForCall(0)).To(Equal("blob/path")) }) }) - Describe("Put", func() { - Context("When the put request succeeds", func() { - itUploadsABlob := func() { - body := io.NopCloser(strings.NewReader("content")) - err := client.Put("/", body, int64(7)) - Expect(err).NotTo(HaveOccurred()) - - Expect(server.ReceivedRequests()).To(HaveLen(1)) - req := server.ReceivedRequests()[0] - Expect(req.ContentLength).To(Equal(int64(7))) - } - - It("uploads the given content if the blob does not exist", func() { - server.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWith(201, ""), - ghttp.VerifyBody([]byte("content")), - ), - ) - itUploadsABlob() - }) - - It("uploads the given content if the blob exists", func() { - server.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWith(204, ""), - ghttp.VerifyBody([]byte("content")), - ), - ) - itUploadsABlob() - }) + Context("Exists", func() { + It("returns true if blob exists", func() { + fakeStorageClient := &clientfakes.FakeStorageClient{} + fakeStorageClient.ExistsReturns(true, nil) - It("adds an Authorizatin header to the request", func() { - server.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWith(204, ""), - ghttp.VerifyBody([]byte("content")), - ), - ) - itUploadsABlob() - req := server.ReceivedRequests()[0] - Expect(req.Header.Get("Authorization")).NotTo(BeEmpty()) - }) + davBlobstore := client.NewWithStorageClient(fakeStorageClient) + exists, err := davBlobstore.Exists("blob/path") - Context("when neither user nor password is provided in blobstore options", func() { - BeforeEach(func() { - config.User = "" - config.Password = "" - client = NewClient(config, httpclient.DefaultClient, logger) - }) - - It("sends a request with no Basic Auth header", func() { - server.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWith(204, ""), - ghttp.VerifyBody([]byte("content")), - ), - ) - itUploadsABlob() - req := server.ReceivedRequests()[0] - Expect(req.Header.Get("Authorization")).To(BeEmpty()) - }) - }) + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + Expect(fakeStorageClient.ExistsCallCount()).To(Equal(1)) }) - Context("when the http request fails", func() { - BeforeEach(func() { - server.AppendHandlers( - disconnectingRequestHandler, - disconnectingRequestHandler, - disconnectingRequestHandler, - ) - }) - - It("returns err", func() { - body := io.NopCloser(strings.NewReader("content")) - err := client.Put("/", body, int64(7)) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("Putting dav blob /: Put \"%s/42\": EOF", server.URL()))) - Expect(server.ReceivedRequests()).To(HaveLen(3)) - }) - }) + It("returns false if blob does not exist", func() { + fakeStorageClient := &clientfakes.FakeStorageClient{} + fakeStorageClient.ExistsReturns(false, nil) - Context("when the http response code is not 201 or 204", func() { - BeforeEach(func() { - server.AppendHandlers( - ghttp.RespondWith(300, "response"), - ghttp.RespondWith(300, "response"), - ghttp.RespondWith(300, "response"), - ) - }) + davBlobstore := client.NewWithStorageClient(fakeStorageClient) + exists, err := davBlobstore.Exists("blob/path") - It("returns err", func() { - body := io.NopCloser(strings.NewReader("content")) - err := client.Put("/", body, int64(7)) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("Putting dav blob /: Wrong response code: 300"))) - }) + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeFalse()) }) - }) - Describe("retryable count is configurable", func() { - BeforeEach(func() { - server.AppendHandlers( - disconnectingRequestHandler, - disconnectingRequestHandler, - disconnectingRequestHandler, - disconnectingRequestHandler, - disconnectingRequestHandler, - disconnectingRequestHandler, - disconnectingRequestHandler, - ) - config = davconf.Config{RetryAttempts: 7, Endpoint: server.URL()} - client = NewClient(config, httpclient.DefaultClient, logger) - }) + It("returns error on server error", func() { + fakeStorageClient := &clientfakes.FakeStorageClient{} + fakeStorageClient.ExistsReturns(false, fmt.Errorf("server error")) + + davBlobstore := client.NewWithStorageClient(fakeStorageClient) + exists, err := davBlobstore.Exists("blob/path") - It("tries the specified number of times", func() { - body := io.NopCloser(strings.NewReader("content")) - err := client.Put("/", body, int64(7)) Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("Putting dav blob /: Put \"%s/42\": EOF", server.URL()))) - Expect(server.ReceivedRequests()).To(HaveLen(7)) + Expect(err.Error()).To(ContainSubstring("server error")) + Expect(exists).To(BeFalse()) }) }) }) diff --git a/dav/client/clientfakes/fake_storage_client.go b/dav/client/clientfakes/fake_storage_client.go new file mode 100644 index 0000000..38b20b6 --- /dev/null +++ b/dav/client/clientfakes/fake_storage_client.go @@ -0,0 +1,422 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package clientfakes + +import ( + "io" + "sync" + "time" + + "github.com/cloudfoundry/storage-cli/dav/client" +) + +type FakeStorageClient struct { + DeleteStub func(string) error + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + arg1 string + } + deleteReturns struct { + result1 error + } + deleteReturnsOnCall map[int]struct { + result1 error + } + ExistsStub func(string) (bool, error) + existsMutex sync.RWMutex + existsArgsForCall []struct { + arg1 string + } + existsReturns struct { + result1 bool + result2 error + } + existsReturnsOnCall map[int]struct { + result1 bool + result2 error + } + GetStub func(string) (io.ReadCloser, error) + getMutex sync.RWMutex + getArgsForCall []struct { + arg1 string + } + getReturns struct { + result1 io.ReadCloser + result2 error + } + getReturnsOnCall map[int]struct { + result1 io.ReadCloser + result2 error + } + PutStub func(string, io.ReadCloser, int64) error + putMutex sync.RWMutex + putArgsForCall []struct { + arg1 string + arg2 io.ReadCloser + arg3 int64 + } + putReturns struct { + result1 error + } + putReturnsOnCall map[int]struct { + result1 error + } + SignStub func(string, string, time.Duration) (string, error) + signMutex sync.RWMutex + signArgsForCall []struct { + arg1 string + arg2 string + arg3 time.Duration + } + signReturns struct { + result1 string + result2 error + } + signReturnsOnCall map[int]struct { + result1 string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeStorageClient) Delete(arg1 string) error { + fake.deleteMutex.Lock() + ret, specificReturn := fake.deleteReturnsOnCall[len(fake.deleteArgsForCall)] + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.DeleteStub + fakeReturns := fake.deleteReturns + fake.recordInvocation("Delete", []interface{}{arg1}) + fake.deleteMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeStorageClient) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeStorageClient) DeleteCalls(stub func(string) error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = stub +} + +func (fake *FakeStorageClient) DeleteArgsForCall(i int) string { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + argsForCall := fake.deleteArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeStorageClient) DeleteReturns(result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + fake.deleteReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeStorageClient) DeleteReturnsOnCall(i int, result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + if fake.deleteReturnsOnCall == nil { + fake.deleteReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeStorageClient) Exists(arg1 string) (bool, error) { + fake.existsMutex.Lock() + ret, specificReturn := fake.existsReturnsOnCall[len(fake.existsArgsForCall)] + fake.existsArgsForCall = append(fake.existsArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ExistsStub + fakeReturns := fake.existsReturns + fake.recordInvocation("Exists", []interface{}{arg1}) + fake.existsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStorageClient) ExistsCallCount() int { + fake.existsMutex.RLock() + defer fake.existsMutex.RUnlock() + return len(fake.existsArgsForCall) +} + +func (fake *FakeStorageClient) ExistsCalls(stub func(string) (bool, error)) { + fake.existsMutex.Lock() + defer fake.existsMutex.Unlock() + fake.ExistsStub = stub +} + +func (fake *FakeStorageClient) ExistsArgsForCall(i int) string { + fake.existsMutex.RLock() + defer fake.existsMutex.RUnlock() + argsForCall := fake.existsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeStorageClient) ExistsReturns(result1 bool, result2 error) { + fake.existsMutex.Lock() + defer fake.existsMutex.Unlock() + fake.ExistsStub = nil + fake.existsReturns = struct { + result1 bool + result2 error + }{result1, result2} +} + +func (fake *FakeStorageClient) ExistsReturnsOnCall(i int, result1 bool, result2 error) { + fake.existsMutex.Lock() + defer fake.existsMutex.Unlock() + fake.ExistsStub = nil + if fake.existsReturnsOnCall == nil { + fake.existsReturnsOnCall = make(map[int]struct { + result1 bool + result2 error + }) + } + fake.existsReturnsOnCall[i] = struct { + result1 bool + result2 error + }{result1, result2} +} + +func (fake *FakeStorageClient) Get(arg1 string) (io.ReadCloser, error) { + fake.getMutex.Lock() + ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] + fake.getArgsForCall = append(fake.getArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetStub + fakeReturns := fake.getReturns + fake.recordInvocation("Get", []interface{}{arg1}) + fake.getMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStorageClient) GetCallCount() int { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + return len(fake.getArgsForCall) +} + +func (fake *FakeStorageClient) GetCalls(stub func(string) (io.ReadCloser, error)) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = stub +} + +func (fake *FakeStorageClient) GetArgsForCall(i int) string { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + argsForCall := fake.getArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeStorageClient) GetReturns(result1 io.ReadCloser, result2 error) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + fake.getReturns = struct { + result1 io.ReadCloser + result2 error + }{result1, result2} +} + +func (fake *FakeStorageClient) GetReturnsOnCall(i int, result1 io.ReadCloser, result2 error) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + if fake.getReturnsOnCall == nil { + fake.getReturnsOnCall = make(map[int]struct { + result1 io.ReadCloser + result2 error + }) + } + fake.getReturnsOnCall[i] = struct { + result1 io.ReadCloser + result2 error + }{result1, result2} +} + +func (fake *FakeStorageClient) Put(arg1 string, arg2 io.ReadCloser, arg3 int64) error { + fake.putMutex.Lock() + ret, specificReturn := fake.putReturnsOnCall[len(fake.putArgsForCall)] + fake.putArgsForCall = append(fake.putArgsForCall, struct { + arg1 string + arg2 io.ReadCloser + arg3 int64 + }{arg1, arg2, arg3}) + stub := fake.PutStub + fakeReturns := fake.putReturns + fake.recordInvocation("Put", []interface{}{arg1, arg2, arg3}) + fake.putMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeStorageClient) PutCallCount() int { + fake.putMutex.RLock() + defer fake.putMutex.RUnlock() + return len(fake.putArgsForCall) +} + +func (fake *FakeStorageClient) PutCalls(stub func(string, io.ReadCloser, int64) error) { + fake.putMutex.Lock() + defer fake.putMutex.Unlock() + fake.PutStub = stub +} + +func (fake *FakeStorageClient) PutArgsForCall(i int) (string, io.ReadCloser, int64) { + fake.putMutex.RLock() + defer fake.putMutex.RUnlock() + argsForCall := fake.putArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeStorageClient) PutReturns(result1 error) { + fake.putMutex.Lock() + defer fake.putMutex.Unlock() + fake.PutStub = nil + fake.putReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeStorageClient) PutReturnsOnCall(i int, result1 error) { + fake.putMutex.Lock() + defer fake.putMutex.Unlock() + fake.PutStub = nil + if fake.putReturnsOnCall == nil { + fake.putReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.putReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeStorageClient) Sign(arg1 string, arg2 string, arg3 time.Duration) (string, error) { + fake.signMutex.Lock() + ret, specificReturn := fake.signReturnsOnCall[len(fake.signArgsForCall)] + fake.signArgsForCall = append(fake.signArgsForCall, struct { + arg1 string + arg2 string + arg3 time.Duration + }{arg1, arg2, arg3}) + stub := fake.SignStub + fakeReturns := fake.signReturns + fake.recordInvocation("Sign", []interface{}{arg1, arg2, arg3}) + fake.signMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStorageClient) SignCallCount() int { + fake.signMutex.RLock() + defer fake.signMutex.RUnlock() + return len(fake.signArgsForCall) +} + +func (fake *FakeStorageClient) SignCalls(stub func(string, string, time.Duration) (string, error)) { + fake.signMutex.Lock() + defer fake.signMutex.Unlock() + fake.SignStub = stub +} + +func (fake *FakeStorageClient) SignArgsForCall(i int) (string, string, time.Duration) { + fake.signMutex.RLock() + defer fake.signMutex.RUnlock() + argsForCall := fake.signArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeStorageClient) SignReturns(result1 string, result2 error) { + fake.signMutex.Lock() + defer fake.signMutex.Unlock() + fake.SignStub = nil + fake.signReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeStorageClient) SignReturnsOnCall(i int, result1 string, result2 error) { + fake.signMutex.Lock() + defer fake.signMutex.Unlock() + fake.SignStub = nil + if fake.signReturnsOnCall == nil { + fake.signReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.signReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeStorageClient) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeStorageClient) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ client.StorageClient = new(FakeStorageClient) diff --git a/dav/client/fakes/fake_client.go b/dav/client/fakes/fake_client.go deleted file mode 100644 index 9627637..0000000 --- a/dav/client/fakes/fake_client.go +++ /dev/null @@ -1,37 +0,0 @@ -package fakes - -import ( - "io" -) - -type FakeClient struct { - GetPath string - GetContents io.ReadCloser - GetErr error - - PutPath string - PutContents string - PutContentLength int64 - PutErr error -} - -func NewFakeClient() *FakeClient { - return &FakeClient{} -} - -func (c *FakeClient) Get(path string) (io.ReadCloser, error) { - c.GetPath = path - - return c.GetContents, c.GetErr -} - -func (c *FakeClient) Put(path string, content io.ReadCloser, contentLength int64) error { - c.PutPath = path - contentBytes := make([]byte, contentLength) - content.Read(contentBytes) //nolint:errcheck - defer content.Close() //nolint:errcheck - c.PutContents = string(contentBytes) - c.PutContentLength = contentLength - - return c.PutErr -} diff --git a/dav/client/helpers.go b/dav/client/helpers.go new file mode 100644 index 0000000..c687d24 --- /dev/null +++ b/dav/client/helpers.go @@ -0,0 +1,21 @@ +package client + +import ( + "crypto/x509" + + boshcrypto "github.com/cloudfoundry/bosh-utils/crypto" + davconf "github.com/cloudfoundry/storage-cli/dav/config" +) + +func getCertPool(config davconf.Config) (*x509.CertPool, error) { + if config.TLS.Cert.CA == "" { + return nil, nil + } + + certPool, err := boshcrypto.CertPoolFromPEM([]byte(config.TLS.Cert.CA)) + if err != nil { + return nil, err + } + + return certPool, nil +} diff --git a/dav/client/storage_client.go b/dav/client/storage_client.go new file mode 100644 index 0000000..38ba8dc --- /dev/null +++ b/dav/client/storage_client.go @@ -0,0 +1,168 @@ +package client + +import ( + "fmt" + "io" + "net/http" + "net/url" + "path" + "strings" + "time" + + "github.com/cloudfoundry/bosh-utils/httpclient" + davconf "github.com/cloudfoundry/storage-cli/dav/config" + URLsigner "github.com/cloudfoundry/storage-cli/dav/signer" +) + +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . StorageClient + +type StorageClient interface { + Get(path string) (content io.ReadCloser, err error) + Put(path string, content io.ReadCloser, contentLength int64) (err error) + Exists(path string) (bool, error) + Delete(path string) (err error) + Sign(objectID, action string, duration time.Duration) (string, error) +} + +type storageClient struct { + config davconf.Config + httpClient httpclient.Client +} + +func NewStorageClient(config davconf.Config, httpClient httpclient.Client) StorageClient { + return &storageClient{ + config: config, + httpClient: httpClient, + } +} + +func (c *storageClient) Get(path string) (io.ReadCloser, error) { + req, err := c.createReq("GET", path, nil) + if err != nil { + return nil, err + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("getting dav blob %q: %w", path, err) + } + + if resp.StatusCode != http.StatusOK { + defer resp.Body.Close() //nolint:errcheck + return nil, fmt.Errorf("getting dav blob %q: wrong response code: %d; body: %s", path, resp.StatusCode, c.readAndTruncateBody(resp)) + } + + return resp.Body, nil +} + +func (c *storageClient) Put(path string, content io.ReadCloser, contentLength int64) error { + defer content.Close() //nolint:errcheck + + req, err := c.createReq("PUT", path, content) + if err != nil { + return err + } + + req.ContentLength = contentLength + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("putting dav blob %q: %w", path, err) + } + defer resp.Body.Close() //nolint:errcheck + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("putting dav blob %q: wrong response code: %d; body: %s", path, resp.StatusCode, c.readAndTruncateBody(resp)) + } + + return nil +} + +func (c *storageClient) Exists(path string) (bool, error) { + req, err := c.createReq("HEAD", path, nil) + if err != nil { + return false, err + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return false, fmt.Errorf("checking if dav blob %q exists: %w", path, err) + } + defer resp.Body.Close() //nolint:errcheck + + if resp.StatusCode == http.StatusNotFound { + return false, nil + } + + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("checking if dav blob %q exists: invalid status: %d", path, resp.StatusCode) + } + + return true, nil +} + +func (c *storageClient) Delete(path string) error { + req, err := c.createReq("DELETE", path, nil) + if err != nil { + return fmt.Errorf("creating delete request for blob %q: %w", path, err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("deleting blob %q: %w", path, err) + } + defer resp.Body.Close() //nolint:errcheck + + if resp.StatusCode == http.StatusNotFound { + return nil + } + + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return fmt.Errorf("deleting blob %q: invalid status %d", path, resp.StatusCode) + } + + return nil +} + +func (c *storageClient) Sign(blobID, action string, duration time.Duration) (string, error) { + signer := URLsigner.NewSigner(c.config.Secret) + signTime := time.Now() + + signedURL, err := signer.GenerateSignedURL(c.config.Endpoint, blobID, action, signTime, duration) + if err != nil { + return "", fmt.Errorf("pre-signing the url: %w", err) + } + + return signedURL, nil +} + +func (c *storageClient) createReq(method, blobID string, body io.Reader) (*http.Request, error) { + blobURL, err := url.Parse(c.config.Endpoint) + if err != nil { + return nil, err + } + + newPath := path.Join(blobURL.Path, blobID) + if !strings.HasPrefix(newPath, "/") { + newPath = "/" + newPath + } + + blobURL.Path = newPath + + req, err := http.NewRequest(method, blobURL.String(), body) + if err != nil { + return req, err + } + + if c.config.User != "" { + req.SetBasicAuth(c.config.User, c.config.Password) + } + return req, nil +} + +func (c *storageClient) readAndTruncateBody(resp *http.Response) string { + if resp.Body == nil { + return "" + } + bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) //nolint:errcheck + return string(bodyBytes) +} diff --git a/dav/cmd/cmd.go b/dav/cmd/cmd.go deleted file mode 100644 index 6f69763..0000000 --- a/dav/cmd/cmd.go +++ /dev/null @@ -1,5 +0,0 @@ -package cmd - -type Cmd interface { - Run(args []string) (err error) -} diff --git a/dav/cmd/cmd_suite_test.go b/dav/cmd/cmd_suite_test.go deleted file mode 100644 index 8d36bcd..0000000 --- a/dav/cmd/cmd_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package cmd_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "testing" -) - -func TestCmd(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Dav Cmd Suite") -} diff --git a/dav/cmd/delete.go b/dav/cmd/delete.go deleted file mode 100644 index f291828..0000000 --- a/dav/cmd/delete.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "errors" - - davclient "github.com/cloudfoundry/storage-cli/dav/client" -) - -type DeleteCmd struct { - client davclient.Client -} - -func newDeleteCmd(client davclient.Client) (cmd DeleteCmd) { - cmd.client = client - return -} - -func (cmd DeleteCmd) Run(args []string) (err error) { - if len(args) != 1 { - err = errors.New("Incorrect usage, delete needs remote blob path") //nolint:staticcheck - return - } - err = cmd.client.Delete(args[0]) - return -} diff --git a/dav/cmd/delete_test.go b/dav/cmd/delete_test.go deleted file mode 100644 index 912c68b..0000000 --- a/dav/cmd/delete_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package cmd_test - -import ( - "net/http" - "net/http/httptest" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - . "github.com/cloudfoundry/storage-cli/dav/cmd" - - boshlog "github.com/cloudfoundry/bosh-utils/logger" - testcmd "github.com/cloudfoundry/storage-cli/dav/cmd/testing" - davconf "github.com/cloudfoundry/storage-cli/dav/config" -) - -func runDelete(config davconf.Config, args []string) error { - logger := boshlog.NewLogger(boshlog.LevelNone) - factory := NewFactory(logger) - factory.SetConfig(config) //nolint:errcheck - - cmd, err := factory.Create("delete") - Expect(err).ToNot(HaveOccurred()) - - return cmd.Run(args) -} - -var _ = Describe("DeleteCmd", func() { - var ( - handler func(http.ResponseWriter, *http.Request) - requestedBlob string - ts *httptest.Server - config davconf.Config - ) - - BeforeEach(func() { - requestedBlob = "0ca907f2-dde8-4413-a304-9076c9d0978b" - - handler = func(w http.ResponseWriter, r *http.Request) { - req := testcmd.NewHTTPRequest(r) - - username, password, err := req.ExtractBasicAuth() - Expect(err).ToNot(HaveOccurred()) - Expect(req.URL.Path).To(Equal("/0d/" + requestedBlob)) - Expect(req.Method).To(Equal("DELETE")) - Expect(username).To(Equal("some user")) - Expect(password).To(Equal("some pwd")) - - w.WriteHeader(http.StatusOK) - } - }) - - AfterEach(func() { - ts.Close() - }) - - AssertDeleteBehavior := func() { - It("with valid args", func() { - err := runDelete(config, []string{requestedBlob}) - Expect(err).ToNot(HaveOccurred()) - }) - - It("returns err with incorrect arg count", func() { - err := runDelete(davconf.Config{}, []string{}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("Incorrect usage")) - }) - } - - Context("with http endpoint", func() { - BeforeEach(func() { - ts = httptest.NewServer(http.HandlerFunc(handler)) - config = davconf.Config{ - User: "some user", - Password: "some pwd", - Endpoint: ts.URL, - } - - }) - - AssertDeleteBehavior() - }) - - Context("with https endpoint", func() { - BeforeEach(func() { - ts = httptest.NewTLSServer(http.HandlerFunc(handler)) - - rootCa, err := testcmd.ExtractRootCa(ts) - Expect(err).ToNot(HaveOccurred()) - - config = davconf.Config{ - User: "some user", - Password: "some pwd", - Endpoint: ts.URL, - TLS: davconf.TLS{ - Cert: davconf.Cert{ - CA: rootCa, - }, - }, - } - }) - - AssertDeleteBehavior() - }) -}) diff --git a/dav/cmd/exists.go b/dav/cmd/exists.go deleted file mode 100644 index 220ccc6..0000000 --- a/dav/cmd/exists.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "errors" - - davclient "github.com/cloudfoundry/storage-cli/dav/client" -) - -type ExistsCmd struct { - client davclient.Client -} - -func newExistsCmd(client davclient.Client) (cmd ExistsCmd) { - cmd.client = client - return -} - -func (cmd ExistsCmd) Run(args []string) (err error) { - if len(args) != 1 { - err = errors.New("Incorrect usage, exists needs remote blob path") //nolint:staticcheck - return - } - err = cmd.client.Exists(args[0]) - return -} diff --git a/dav/cmd/exists_test.go b/dav/cmd/exists_test.go deleted file mode 100644 index 0d01ce7..0000000 --- a/dav/cmd/exists_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package cmd_test - -import ( - "net/http" - "net/http/httptest" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - boshlog "github.com/cloudfoundry/bosh-utils/logger" - . "github.com/cloudfoundry/storage-cli/dav/cmd" - testcmd "github.com/cloudfoundry/storage-cli/dav/cmd/testing" - davconf "github.com/cloudfoundry/storage-cli/dav/config" -) - -func runExists(config davconf.Config, args []string) error { - logger := boshlog.NewLogger(boshlog.LevelNone) - factory := NewFactory(logger) - factory.SetConfig(config) //nolint:errcheck - - cmd, err := factory.Create("exists") - Expect(err).ToNot(HaveOccurred()) - - return cmd.Run(args) -} - -var _ = Describe("Exists", func() { - var ( - handler func(http.ResponseWriter, *http.Request) - requestedBlob string - ts *httptest.Server - config davconf.Config - ) - - BeforeEach(func() { - requestedBlob = "0ca907f2-dde8-4413-a304-9076c9d0978b" - - handler = func(w http.ResponseWriter, r *http.Request) { - req := testcmd.NewHTTPRequest(r) - - username, password, err := req.ExtractBasicAuth() - Expect(err).ToNot(HaveOccurred()) - Expect(req.URL.Path).To(Equal("/0d/" + requestedBlob)) - Expect(req.Method).To(Equal("HEAD")) - Expect(username).To(Equal("some user")) - Expect(password).To(Equal("some pwd")) - - w.WriteHeader(200) - } - }) - - AfterEach(func() { - ts.Close() - }) - - AssertExistsBehavior := func() { - It("with valid args", func() { - err := runExists(config, []string{requestedBlob}) - Expect(err).ToNot(HaveOccurred()) - }) - - It("with incorrect arg count", func() { - err := runExists(davconf.Config{}, []string{}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("Incorrect usage")) - }) - } - - Context("with http endpoint", func() { - BeforeEach(func() { - ts = httptest.NewServer(http.HandlerFunc(handler)) - config = davconf.Config{ - User: "some user", - Password: "some pwd", - Endpoint: ts.URL, - } - - }) - - AssertExistsBehavior() - }) - - Context("with https endpoint", func() { - BeforeEach(func() { - ts = httptest.NewTLSServer(http.HandlerFunc(handler)) - - rootCa, err := testcmd.ExtractRootCa(ts) - Expect(err).ToNot(HaveOccurred()) - - config = davconf.Config{ - User: "some user", - Password: "some pwd", - Endpoint: ts.URL, - TLS: davconf.TLS{ - Cert: davconf.Cert{ - CA: rootCa, - }, - }, - } - }) - - AssertExistsBehavior() - }) -}) diff --git a/dav/cmd/factory.go b/dav/cmd/factory.go deleted file mode 100644 index 6b68025..0000000 --- a/dav/cmd/factory.go +++ /dev/null @@ -1,62 +0,0 @@ -package cmd - -import ( - "crypto/x509" - "fmt" - - boshcrypto "github.com/cloudfoundry/bosh-utils/crypto" - boshhttpclient "github.com/cloudfoundry/bosh-utils/httpclient" - boshlog "github.com/cloudfoundry/bosh-utils/logger" - - davclient "github.com/cloudfoundry/storage-cli/dav/client" - davconf "github.com/cloudfoundry/storage-cli/dav/config" -) - -type Factory interface { - Create(name string) (cmd Cmd, err error) - SetConfig(config davconf.Config) (err error) -} - -func NewFactory(logger boshlog.Logger) Factory { - return &factory{ - cmds: make(map[string]Cmd), - logger: logger, - } -} - -type factory struct { - config davconf.Config //nolint:unused - cmds map[string]Cmd - logger boshlog.Logger -} - -func (f *factory) Create(name string) (cmd Cmd, err error) { - cmd, found := f.cmds[name] - if !found { - err = fmt.Errorf("Could not find command with name %s", name) //nolint:staticcheck - } - return -} - -func (f *factory) SetConfig(config davconf.Config) (err error) { - var httpClient boshhttpclient.Client - var certPool *x509.CertPool - - if len(config.TLS.Cert.CA) != 0 { - certPool, err = boshcrypto.CertPoolFromPEM([]byte(config.TLS.Cert.CA)) - } - - httpClient = boshhttpclient.CreateDefaultClient(certPool) - - client := davclient.NewClient(config, httpClient, f.logger) - - f.cmds = map[string]Cmd{ - "put": newPutCmd(client), - "get": newGetCmd(client), - "exists": newExistsCmd(client), - "delete": newDeleteCmd(client), - "sign": newSignCmd(client), - } - - return -} diff --git a/dav/cmd/factory_test.go b/dav/cmd/factory_test.go deleted file mode 100644 index 46378a6..0000000 --- a/dav/cmd/factory_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package cmd_test - -import ( - "reflect" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - boshlog "github.com/cloudfoundry/bosh-utils/logger" - . "github.com/cloudfoundry/storage-cli/dav/cmd" - davconf "github.com/cloudfoundry/storage-cli/dav/config" -) - -func buildFactory() (factory Factory) { - config := davconf.Config{User: "some user"} - logger := boshlog.NewLogger(boshlog.LevelNone) - factory = NewFactory(logger) - factory.SetConfig(config) //nolint:errcheck - return -} - -var _ = Describe("Factory", func() { - Describe("Create", func() { - It("factory create a put command", func() { - factory := buildFactory() - cmd, err := factory.Create("put") - - Expect(err).ToNot(HaveOccurred()) - Expect(reflect.TypeOf(cmd)).To(Equal(reflect.TypeOf(PutCmd{}))) - }) - - It("factory create a get command", func() { - factory := buildFactory() - cmd, err := factory.Create("get") - - Expect(err).ToNot(HaveOccurred()) - Expect(reflect.TypeOf(cmd)).To(Equal(reflect.TypeOf(GetCmd{}))) - }) - - It("factory create a delete command", func() { - factory := buildFactory() - cmd, err := factory.Create("delete") - - Expect(err).ToNot(HaveOccurred()) - Expect(reflect.TypeOf(cmd)).To(Equal(reflect.TypeOf(DeleteCmd{}))) - }) - - It("factory create when cmd is unknown", func() { - factory := buildFactory() - _, err := factory.Create("some unknown cmd") - - Expect(err).To(HaveOccurred()) - }) - }) - - Describe("SetConfig", func() { - It("returns an error if CaCert is given but invalid", func() { - factory := buildFactory() - config := davconf.Config{ - TLS: davconf.TLS{ - Cert: davconf.Cert{ - CA: "--- INVALID CERTIFICATE ---", - }, - }, - } - - err := factory.SetConfig(config) - Expect(err).To(HaveOccurred()) - }) - It("does not return an error if CaCert is valid", func() { - factory := buildFactory() - cert := `-----BEGIN CERTIFICATE----- -MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS -MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw -MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB -iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4 -iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul -rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO -BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw -AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA -AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9 -tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs -h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM -fblo6RBxUQ== ------END CERTIFICATE-----` - config := davconf.Config{ - TLS: davconf.TLS{ - Cert: davconf.Cert{ - CA: cert, - }, - }, - } - - err := factory.SetConfig(config) - Expect(err).ToNot(HaveOccurred()) - }) - It("does not return an error if CaCert is not provided", func() { - factory := buildFactory() - config := davconf.Config{ - TLS: davconf.TLS{ - Cert: davconf.Cert{ - CA: "", - }, - }, - } - - err := factory.SetConfig(config) - Expect(err).ToNot(HaveOccurred()) - }) - }) -}) diff --git a/dav/cmd/get.go b/dav/cmd/get.go deleted file mode 100644 index 3009585..0000000 --- a/dav/cmd/get.go +++ /dev/null @@ -1,39 +0,0 @@ -package cmd - -import ( - "errors" - "io" - "os" - - davclient "github.com/cloudfoundry/storage-cli/dav/client" -) - -type GetCmd struct { - client davclient.Client -} - -func newGetCmd(client davclient.Client) (cmd GetCmd) { - cmd.client = client - return -} - -func (cmd GetCmd) Run(args []string) (err error) { - if len(args) != 2 { - err = errors.New("Incorrect usage, get needs remote blob path and local file destination") //nolint:staticcheck - return - } - - readCloser, err := cmd.client.Get(args[0]) - if err != nil { - return - } - defer readCloser.Close() //nolint:errcheck - - targetFile, err := os.Create(args[1]) - if err != nil { - return - } - - _, err = io.Copy(targetFile, readCloser) - return -} diff --git a/dav/cmd/get_test.go b/dav/cmd/get_test.go deleted file mode 100644 index 0ab58a7..0000000 --- a/dav/cmd/get_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package cmd_test - -import ( - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - boshlog "github.com/cloudfoundry/bosh-utils/logger" - . "github.com/cloudfoundry/storage-cli/dav/cmd" - testcmd "github.com/cloudfoundry/storage-cli/dav/cmd/testing" - davconf "github.com/cloudfoundry/storage-cli/dav/config" -) - -func runGet(config davconf.Config, args []string) error { - logger := boshlog.NewLogger(boshlog.LevelNone) - factory := NewFactory(logger) - factory.SetConfig(config) //nolint:errcheck - - cmd, err := factory.Create("get") - Expect(err).ToNot(HaveOccurred()) - - return cmd.Run(args) -} - -func getFileContent(path string) string { - file, err := os.Open(path) - Expect(err).ToNot(HaveOccurred()) - - fileBytes, err := io.ReadAll(file) - Expect(err).ToNot(HaveOccurred()) - - return string(fileBytes) -} - -var _ = Describe("GetCmd", func() { - var ( - handler func(http.ResponseWriter, *http.Request) - targetFilePath string - requestedBlob string - ts *httptest.Server - config davconf.Config - ) - - BeforeEach(func() { - requestedBlob = "0ca907f2-dde8-4413-a304-9076c9d0978b" - targetFilePath = filepath.Join(os.TempDir(), "testRunGetCommand.txt") - - handler = func(w http.ResponseWriter, r *http.Request) { - req := testcmd.NewHTTPRequest(r) - - username, password, err := req.ExtractBasicAuth() - Expect(err).ToNot(HaveOccurred()) - Expect(req.URL.Path).To(Equal("/0d/" + requestedBlob)) - Expect(req.Method).To(Equal("GET")) - Expect(username).To(Equal("some user")) - Expect(password).To(Equal("some pwd")) - - w.Write([]byte("this is your blob")) //nolint:errcheck - } - - }) - - AfterEach(func() { - os.RemoveAll(targetFilePath) //nolint:errcheck - ts.Close() - }) - - AssertGetBehavior := func() { - It("get run with valid args", func() { - err := runGet(config, []string{requestedBlob, targetFilePath}) - Expect(err).ToNot(HaveOccurred()) - Expect(getFileContent(targetFilePath)).To(Equal("this is your blob")) - }) - - It("get run with incorrect arg count", func() { - err := runGet(davconf.Config{}, []string{}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("Incorrect usage")) - }) - } - - Context("with http endpoint", func() { - BeforeEach(func() { - ts = httptest.NewServer(http.HandlerFunc(handler)) - - config = davconf.Config{ - User: "some user", - Password: "some pwd", - Endpoint: ts.URL, - } - }) - - AssertGetBehavior() - }) - - Context("with https endpoint", func() { - BeforeEach(func() { - ts = httptest.NewTLSServer(http.HandlerFunc(handler)) - - rootCa, err := testcmd.ExtractRootCa(ts) - Expect(err).ToNot(HaveOccurred()) - - config = davconf.Config{ - User: "some user", - Password: "some pwd", - Endpoint: ts.URL, - TLS: davconf.TLS{ - Cert: davconf.Cert{ - CA: rootCa, - }, - }, - } - }) - - AssertGetBehavior() - }) -}) diff --git a/dav/cmd/put.go b/dav/cmd/put.go deleted file mode 100644 index 44f6d84..0000000 --- a/dav/cmd/put.go +++ /dev/null @@ -1,35 +0,0 @@ -package cmd - -import ( - "errors" - "os" - - davclient "github.com/cloudfoundry/storage-cli/dav/client" -) - -type PutCmd struct { - client davclient.Client -} - -func newPutCmd(client davclient.Client) (cmd PutCmd) { - cmd.client = client - return -} - -func (cmd PutCmd) Run(args []string) error { - if len(args) != 2 { - return errors.New("Incorrect usage, put needs local file and remote blob destination") //nolint:staticcheck - } - - file, err := os.OpenFile(args[0], os.O_RDWR, os.ModeExclusive) - if err != nil { - return err - } - - fileInfo, err := file.Stat() - if err != nil { - return err - } - - return cmd.client.Put(args[1], file, fileInfo.Size()) -} diff --git a/dav/cmd/put_test.go b/dav/cmd/put_test.go deleted file mode 100644 index f7af661..0000000 --- a/dav/cmd/put_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package cmd_test - -import ( - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - boshlog "github.com/cloudfoundry/bosh-utils/logger" - - . "github.com/cloudfoundry/storage-cli/dav/cmd" - testcmd "github.com/cloudfoundry/storage-cli/dav/cmd/testing" - davconf "github.com/cloudfoundry/storage-cli/dav/config" -) - -func runPut(config davconf.Config, args []string) error { - logger := boshlog.NewLogger(boshlog.LevelNone) - factory := NewFactory(logger) - factory.SetConfig(config) //nolint:errcheck - - cmd, err := factory.Create("put") - Expect(err).ToNot(HaveOccurred()) - - return cmd.Run(args) -} - -func fileBytes(path string) []byte { - file, err := os.Open(path) - Expect(err).ToNot(HaveOccurred()) - - content, err := io.ReadAll(file) - Expect(err).ToNot(HaveOccurred()) - - return content -} - -var _ = Describe("PutCmd", func() { - Describe("Run", func() { - var ( - handler func(http.ResponseWriter, *http.Request) - config davconf.Config - ts *httptest.Server - sourceFilePath string - targetBlob string - serverWasHit bool - ) - BeforeEach(func() { - pwd, err := os.Getwd() - Expect(err).ToNot(HaveOccurred()) - - sourceFilePath = filepath.Join(pwd, "../test_assets/cat.jpg") - targetBlob = "some-other-awesome-guid" - serverWasHit = false - - handler = func(w http.ResponseWriter, r *http.Request) { - defer GinkgoRecover() - serverWasHit = true - req := testcmd.NewHTTPRequest(r) - - username, password, err := req.ExtractBasicAuth() - Expect(err).ToNot(HaveOccurred()) - Expect(req.URL.Path).To(Equal("/d1/" + targetBlob)) - Expect(req.Method).To(Equal("PUT")) - Expect(req.ContentLength).To(Equal(int64(1718186))) - Expect(username).To(Equal("some user")) - Expect(password).To(Equal("some pwd")) - - expectedBytes := fileBytes(sourceFilePath) - actualBytes, _ := io.ReadAll(r.Body) //nolint:errcheck - Expect(expectedBytes).To(Equal(actualBytes)) - - w.WriteHeader(201) - } - }) - - AfterEach(func() { - defer ts.Close() - }) - - AssertPutBehavior := func() { - It("uploads the blob with valid args", func() { - err := runPut(config, []string{sourceFilePath, targetBlob}) - Expect(err).ToNot(HaveOccurred()) - Expect(serverWasHit).To(BeTrue()) - }) - - It("returns err with incorrect arg count", func() { - err := runPut(davconf.Config{}, []string{}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("Incorrect usage")) - }) - } - - Context("with http endpoint", func() { - BeforeEach(func() { - ts = httptest.NewServer(http.HandlerFunc(handler)) - config = davconf.Config{ - User: "some user", - Password: "some pwd", - Endpoint: ts.URL, - } - - }) - - AssertPutBehavior() - }) - - Context("with https endpoint", func() { - BeforeEach(func() { - ts = httptest.NewTLSServer(http.HandlerFunc(handler)) - - rootCa, err := testcmd.ExtractRootCa(ts) - Expect(err).ToNot(HaveOccurred()) - - config = davconf.Config{ - User: "some user", - Password: "some pwd", - Endpoint: ts.URL, - TLS: davconf.TLS{ - Cert: davconf.Cert{ - CA: rootCa, - }, - }, - } - }) - - AssertPutBehavior() - }) - }) -}) diff --git a/dav/cmd/runner.go b/dav/cmd/runner.go deleted file mode 100644 index 0fbf423..0000000 --- a/dav/cmd/runner.go +++ /dev/null @@ -1,40 +0,0 @@ -package cmd - -import ( - "errors" - - davconf "github.com/cloudfoundry/storage-cli/dav/config" -) - -type Runner interface { - SetConfig(newConfig davconf.Config) (err error) - Run(cmdArgs []string) (err error) -} - -func NewRunner(factory Factory) Runner { - return runner{ - factory: factory, - } -} - -type runner struct { - factory Factory -} - -func (r runner) Run(cmdArgs []string) (err error) { - if len(cmdArgs) == 0 { - err = errors.New("Missing command name") //nolint:staticcheck - return - } - - cmd, err := r.factory.Create(cmdArgs[0]) - if err != nil { - return - } - - return cmd.Run(cmdArgs[1:]) -} - -func (r runner) SetConfig(newConfig davconf.Config) (err error) { - return r.factory.SetConfig(newConfig) -} diff --git a/dav/cmd/runner_test.go b/dav/cmd/runner_test.go deleted file mode 100644 index 2087b1a..0000000 --- a/dav/cmd/runner_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package cmd_test - -import ( - "errors" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - . "github.com/cloudfoundry/storage-cli/dav/cmd" - davconf "github.com/cloudfoundry/storage-cli/dav/config" -) - -type FakeFactory struct { - CreateName string - CreateCmd *FakeCmd - CreateErr error - - Config davconf.Config - SetConfigErr error -} - -func (f *FakeFactory) Create(name string) (cmd Cmd, err error) { - f.CreateName = name - cmd = f.CreateCmd - err = f.CreateErr - return -} - -func (f *FakeFactory) SetConfig(config davconf.Config) (err error) { - f.Config = config - return f.SetConfigErr -} - -type FakeCmd struct { - RunArgs []string - RunErr error -} - -func (cmd *FakeCmd) Run(args []string) (err error) { - cmd.RunArgs = args - err = cmd.RunErr - return -} - -var _ = Describe("Runner", func() { - Describe("Run", func() { - It("run can run a command and return its error", func() { - factory := &FakeFactory{ - CreateCmd: &FakeCmd{ - RunErr: errors.New("fake-run-error"), - }, - } - cmdRunner := NewRunner(factory) - - err := cmdRunner.Run([]string{"put", "foo", "bar"}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("fake-run-error")) - - Expect(factory.CreateName).To(Equal("put")) - Expect(factory.CreateCmd.RunArgs).To(Equal([]string{"foo", "bar"})) - }) - - It("run expects at least one argument", func() { - factory := &FakeFactory{ - CreateCmd: &FakeCmd{}, - } - cmdRunner := NewRunner(factory) - - err := cmdRunner.Run([]string{}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("Missing command name")) - }) - - It("accepts exactly one argument", func() { - factory := &FakeFactory{ - CreateCmd: &FakeCmd{}, - } - cmdRunner := NewRunner(factory) - - err := cmdRunner.Run([]string{"put"}) - Expect(err).ToNot(HaveOccurred()) - - Expect(factory.CreateName).To(Equal("put")) - Expect(factory.CreateCmd.RunArgs).To(Equal([]string{})) - }) - }) - - Describe("SetConfig", func() { - It("delegates to factory", func() { - factory := &FakeFactory{} - cmdRunner := NewRunner(factory) - conf := davconf.Config{User: "foo"} - - err := cmdRunner.SetConfig(conf) - - Expect(factory.Config).To(Equal(conf)) - Expect(err).ToNot(HaveOccurred()) - }) - It("propagates errors", func() { - setConfigErr := errors.New("some error") - factory := &FakeFactory{ - SetConfigErr: setConfigErr, - } - cmdRunner := NewRunner(factory) - conf := davconf.Config{User: "foo"} - - err := cmdRunner.SetConfig(conf) - Expect(err).To(HaveOccurred()) - }) - }) -}) diff --git a/dav/cmd/sign.go b/dav/cmd/sign.go deleted file mode 100644 index 27b9ac6..0000000 --- a/dav/cmd/sign.go +++ /dev/null @@ -1,41 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "time" - - davclient "github.com/cloudfoundry/storage-cli/dav/client" -) - -type SignCmd struct { - client davclient.Client -} - -func newSignCmd(client davclient.Client) (cmd SignCmd) { - cmd.client = client - return -} - -func (cmd SignCmd) Run(args []string) (err error) { - if len(args) != 3 { - err = errors.New("incorrect usage, sign requires: ") - return - } - - objectID, action := args[0], args[1] - - expiration, err := time.ParseDuration(args[2]) - if err != nil { - err = fmt.Errorf("expiration should be a duration value eg: 45s or 1h43m. Got: %s", args[2]) - return - } - - signedURL, err := cmd.client.Sign(objectID, action, expiration) - if err != nil { - return err - } - - fmt.Print(signedURL) - return -} diff --git a/dav/cmd/sign_test.go b/dav/cmd/sign_test.go deleted file mode 100644 index 09a570d..0000000 --- a/dav/cmd/sign_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package cmd_test - -import ( - "bytes" - "io" - "os" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - . "github.com/cloudfoundry/storage-cli/dav/cmd" - - boshlog "github.com/cloudfoundry/bosh-utils/logger" - - davconf "github.com/cloudfoundry/storage-cli/dav/config" -) - -func runSign(config davconf.Config, args []string) error { - logger := boshlog.NewLogger(boshlog.LevelNone) - factory := NewFactory(logger) - factory.SetConfig(config) //nolint:errcheck - - cmd, err := factory.Create("sign") - Expect(err).ToNot(HaveOccurred()) - - return cmd.Run(args) -} - -var _ = Describe("SignCmd", func() { - var ( - objectID = "0ca907f2-dde8-4413-a304-9076c9d0978b" - config davconf.Config - ) - - It("with valid args", func() { - old := os.Stdout // keep backup of the real stdout - r, w, _ := os.Pipe() //nolint:errcheck - os.Stdout = w - - err := runSign(config, []string{objectID, "get", "15m"}) - - outC := make(chan string) - // copy the output in a separate goroutine so printing can't block indefinitely - go func() { - var buf bytes.Buffer - io.Copy(&buf, r) //nolint:errcheck - outC <- buf.String() - }() - - // back to normal state - w.Close() //nolint:errcheck - os.Stdout = old // restoring the real stdout - out := <-outC - - Expect(err).ToNot(HaveOccurred()) - Expect(out).To(HavePrefix("signed/")) - Expect(out).To(ContainSubstring(objectID)) - Expect(out).To(ContainSubstring("?e=")) - Expect(out).To(ContainSubstring("&st=")) - Expect(out).To(ContainSubstring("&ts=")) - }) - - It("returns err with incorrect arg count", func() { - err := runSign(davconf.Config{}, []string{}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("incorrect usage")) - }) - - It("returns err with non-implemented action", func() { - err := runSign(davconf.Config{}, []string{objectID, "delete", "15m"}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("action not implemented")) - }) - - It("returns err with incorrect duration", func() { - err := runSign(davconf.Config{}, []string{objectID, "put", "15"}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("expiration should be a duration value")) - }) -}) diff --git a/dav/cmd/testing/http_request.go b/dav/cmd/testing/http_request.go deleted file mode 100644 index 912d363..0000000 --- a/dav/cmd/testing/http_request.go +++ /dev/null @@ -1,47 +0,0 @@ -package testing - -import ( - "encoding/base64" - "errors" - "net/http" - "strings" -) - -type HTTPRequest struct { - *http.Request -} - -func NewHTTPRequest(req *http.Request) (testReq HTTPRequest) { - return HTTPRequest{req} -} - -func (req HTTPRequest) ExtractBasicAuth() (username, password string, err error) { - authHeader := req.Header["Authorization"] - if len(authHeader) != 1 { - err = errors.New("Missing basic auth header") //nolint:staticcheck - return - } - - encodedAuth := authHeader[0] - encodedAuthParts := strings.Split(encodedAuth, " ") - if len(encodedAuthParts) != 2 { - err = errors.New("Invalid basic auth header format") //nolint:staticcheck - return - } - - clearAuth, err := base64.StdEncoding.DecodeString(encodedAuthParts[1]) - if len(encodedAuthParts) != 2 { - err = errors.New("Invalid basic auth header encoding") //nolint:staticcheck - return - } - - clearAuthParts := strings.Split(string(clearAuth), ":") - if len(clearAuthParts) != 2 { - err = errors.New("Invalid basic auth header encoded username and pwd") //nolint:staticcheck - return - } - - username = clearAuthParts[0] - password = clearAuthParts[1] - return -} diff --git a/dav/cmd/testing/testing_suite_test.go b/dav/cmd/testing/testing_suite_test.go deleted file mode 100644 index e1ac225..0000000 --- a/dav/cmd/testing/testing_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package testing_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "testing" -) - -func TestTesting(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Dav Testing Suite") -} diff --git a/dav/cmd/testing/tls_server.go b/dav/cmd/testing/tls_server.go deleted file mode 100644 index 6bdeb96..0000000 --- a/dav/cmd/testing/tls_server.go +++ /dev/null @@ -1,31 +0,0 @@ -package testing - -import ( - "bytes" - "crypto/x509" - "encoding/pem" - "net/http/httptest" -) - -func ExtractRootCa(server *httptest.Server) (rootCaStr string, err error) { - rootCa := new(bytes.Buffer) - - cert, err := x509.ParseCertificate(server.TLS.Certificates[0].Certificate[0]) - if err != nil { - panic(err.Error()) - } - // TODO: Replace above with following on Go 1.9 - //cert := server.Certificate() - - block := &pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - } - - err = pem.Encode(rootCa, block) - if err != nil { - return "", err - } - - return rootCa.String(), nil -} diff --git a/dav/test_assets/cat.jpg b/dav/test_assets/cat.jpg deleted file mode 100644 index 995d644..0000000 Binary files a/dav/test_assets/cat.jpg and /dev/null differ diff --git a/dav/test_assets/dav-cli-config-with-ca.json b/dav/test_assets/dav-cli-config-with-ca.json deleted file mode 100644 index fdec3de..0000000 --- a/dav/test_assets/dav-cli-config-with-ca.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "user":"some user", - "password":"some pwd", - "endpoint":"https://example.com/some/endpoint", - "secret": "77D47E3A0B0F590B73CF3EBD9BB6761E244F90FA6F28BB39F941B0905789863FBE2861FDFD8195ADC81B72BB5310BC18969BEBBF4656366E7ACD3F0E4186FDDA", - "tls": { - "cert": { - "ca":"ca-cert" - } - } -} diff --git a/dav/test_assets/dav-cli-config.json b/dav/test_assets/dav-cli-config.json deleted file mode 100644 index 5defc00..0000000 --- a/dav/test_assets/dav-cli-config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "user":"some user", - "password":"some pwd", - "endpoint":"http://example.com/some/endpoint", - "secret": "77D47E3A0B0F590B73CF3EBD9BB6761E244F90FA6F28BB39F941B0905789863FBE2861FDFD8195ADC81B72BB5310BC18969BEBBF4656366E7ACD3F0E4186FDDA" -} diff --git a/storage/factory.go b/storage/factory.go index a64634d..1e677c2 100644 --- a/storage/factory.go +++ b/storage/factory.go @@ -5,13 +5,11 @@ import ( "fmt" "os" - boshlog "github.com/cloudfoundry/bosh-utils/logger" alioss "github.com/cloudfoundry/storage-cli/alioss/client" aliossconfig "github.com/cloudfoundry/storage-cli/alioss/config" azurebs "github.com/cloudfoundry/storage-cli/azurebs/client" azureconfigbs "github.com/cloudfoundry/storage-cli/azurebs/config" - davapp "github.com/cloudfoundry/storage-cli/dav/app" - davcmd "github.com/cloudfoundry/storage-cli/dav/cmd" + dav "github.com/cloudfoundry/storage-cli/dav/client" davconfig "github.com/cloudfoundry/storage-cli/dav/config" gcs "github.com/cloudfoundry/storage-cli/gcs/client" gcsconfig "github.com/cloudfoundry/storage-cli/gcs/config" @@ -92,12 +90,12 @@ var newDavClient = func(configFile *os.File) (Storager, error) { return nil, err } - logger := boshlog.NewLogger(boshlog.LevelNone) - cmdFactory := davcmd.NewFactory(logger) - - cmdRunner := davcmd.NewRunner(cmdFactory) + davClient, err := dav.New(davConfig) + if err != nil { + return nil, err + } - return davapp.New(cmdRunner, davConfig), nil + return davClient, nil } func NewStorageClient(storageType string, configFile *os.File) (Storager, error) {