diff --git a/cookbook_download.go b/cookbook_download.go index 8f982f9..91f8b33 100644 --- a/cookbook_download.go +++ b/cookbook_download.go @@ -5,6 +5,8 @@ package chef import ( + "crypto/md5" + "fmt" "io" "os" "path" @@ -17,11 +19,11 @@ func (c *CookbookService) Download(name, version string) error { return err } - return c.DownloadAt(name, version, cwd) + return c.DownloadTo(name, version, cwd) } -// DownloadAt downloads a cookbook to the specified local directory on disk -func (c *CookbookService) DownloadAt(name, version, localDir string) error { +// DownloadTo downloads a cookbook to the specified local directory on disk +func (c *CookbookService) DownloadTo(name, version, localDir string) error { // If the version is set to 'latest' or it is empty ("") then, // we will set the version to '_latest' which is the default endpoint if version == "" || version == "latest" { @@ -60,6 +62,12 @@ func (c *CookbookService) DownloadAt(name, version, localDir string) error { return nil } +// DownloadAt is a deprecated alias for DownloadTo +func (c *CookbookService) DownloadAt(name, version, localDir string) error { + err := c.DownloadTo(name, version, localDir) + return err +} + // downloadCookbookItems downloads all the provided cookbook items into the provided // local path, it also ensures that the provided directory exists by creating it func (c *CookbookService) downloadCookbookItems(items []CookbookItem, itemType, localPath string) error { @@ -73,8 +81,7 @@ func (c *CookbookService) downloadCookbookItems(items []CookbookItem, itemType, } for _, item := range items { - itemPath := path.Join(localPath, item.Name) - if err := c.downloadCookbookFile(item.Url, itemPath); err != nil { + if err := c.downloadCookbookFile(item, localPath); err != nil { return err } } @@ -83,11 +90,14 @@ func (c *CookbookService) downloadCookbookItems(items []CookbookItem, itemType, } // downloadCookbookFile downloads a single cookbook file to disk -func (c *CookbookService) downloadCookbookFile(url, file string) error { - request, err := c.client.NewRequest("GET", url, nil) +func (c *CookbookService) downloadCookbookFile(item CookbookItem, localPath string) error { + filePath := path.Join(localPath, item.Name) + + request, err := c.client.NewRequest("GET", item.Url, nil) if err != nil { return err } + response, err := c.client.Do(request, nil) if response != nil { defer response.Body.Close() @@ -96,13 +106,42 @@ func (c *CookbookService) downloadCookbookFile(url, file string) error { return err } - f, err := os.Create(file) + f, err := os.Create(filePath) if err != nil { return err } + defer f.Close() if _, err := io.Copy(f, response.Body); err != nil { return err } - return nil + + if verifyMD5Checksum(filePath, item.Checksum) { + return nil + } + + return fmt.Errorf( + "cookbook file '%s' checksum mismatch. (expected:%s)", + filePath, + item.Checksum, + ) +} + +func verifyMD5Checksum(filePath, checksum string) bool { + file, err := os.Open(filePath) + if err != nil { + return false + } + defer file.Close() + + hash := md5.New() + if _, err := io.Copy(hash, file); err != nil { + return false + } + + md5String := fmt.Sprintf("%x", hash.Sum(nil)) + if md5String == checksum { + return true + } + return false } diff --git a/cookbook_download_test.go b/cookbook_download_test.go index d5708b3..9b6b1f2 100644 --- a/cookbook_download_test.go +++ b/cookbook_download_test.go @@ -72,7 +72,7 @@ func TestCookbooksDownloadEmptyWithVersion(t *testing.T) { assert.Nil(t, err) } -func TestCookbooksDownloadAt(t *testing.T) { +func TestCookbooksDownloadTo(t *testing.T) { setup() defer teardown() @@ -93,7 +93,7 @@ func TestCookbooksDownloadAt(t *testing.T) { { "name": "default.rb", "path": "recipes/default.rb", - "checksum": "320sdk2w38020827kdlsdkasbd5454b6", + "checksum": "8e751ed8663cb9b97499956b6a20b0de", "specificity": "default", "url": "` + server.URL + `/bookshelf/foo/default_rb" } @@ -103,7 +103,7 @@ func TestCookbooksDownloadAt(t *testing.T) { { "name": "metadata.rb", "path": "metadata.rb", - "checksum": "14963c5b685f3a15ea90ae51bd5454b6", + "checksum": "6607f3131919e82dc4ba4c026fcfee9f", "specificity": "default", "url": "` + server.URL + `/bookshelf/foo/metadata_rb" } @@ -130,7 +130,7 @@ func TestCookbooksDownloadAt(t *testing.T) { fmt.Fprintf(w, "log 'this is a resource'") }) - err = client.Cookbooks.DownloadAt("foo", "0.2.1", tempDir) + err = client.Cookbooks.DownloadTo("foo", "0.2.1", tempDir) assert.Nil(t, err) var ( @@ -152,3 +152,102 @@ func TestCookbooksDownloadAt(t *testing.T) { assert.Equal(t, "log 'this is a resource'", string(recipeBytes)) } } + +func TestCookbooksDownloadAt(t *testing.T) { + setup() + defer teardown() + + mockedCookbookResponseFile := ` +{ + "version": "0.2.1", + "name": "foo-0.2.1", + "cookbook_name": "foo", + "frozen?": false, + "chef_type": "cookbook_version", + "json_class": "Chef::CookbookVersion", + "attributes": [], + "definitions": [], + "files": [], + "libraries": [], + "providers": [], + "recipes": [ + { + "name": "default.rb", + "path": "recipes/default.rb", + "checksum": "8e751ed8663cb9b97499956b6a20b0de", + "specificity": "default", + "url": "` + server.URL + `/bookshelf/foo/default_rb" + } + ], + "resources": [], + "root_files": [ + { + "name": "metadata.rb", + "path": "metadata.rb", + "checksum": "6607f3131919e82dc4ba4c026fcfee9f", + "specificity": "default", + "url": "` + server.URL + `/bookshelf/foo/metadata_rb" + } + ], + "templates": [], + "metadata": {}, + "access": {} +} +` + + tempDir, err := ioutil.TempDir("", "foo-cookbook") + if err != nil { + t.Error(err) + } + defer os.RemoveAll(tempDir) // clean up + + mux.HandleFunc("/cookbooks/foo/0.2.1", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, string(mockedCookbookResponseFile)) + }) + mux.HandleFunc("/bookshelf/foo/metadata_rb", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "name 'foo'") + }) + mux.HandleFunc("/bookshelf/foo/default_rb", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "log 'this is a resource'") + }) + + err = client.Cookbooks.DownloadAt("foo", "0.2.1", tempDir) + assert.Nil(t, err) + + var ( + cookbookPath = path.Join(tempDir, "foo-0.2.1") + metadataPath = path.Join(cookbookPath, "metadata.rb") + recipesPath = path.Join(cookbookPath, "recipes") + defaultPath = path.Join(recipesPath, "default.rb") + ) + assert.DirExistsf(t, cookbookPath, "the cookbook directory should exist") + assert.DirExistsf(t, recipesPath, "the recipes directory should exist") + if assert.FileExistsf(t, metadataPath, "a metadata.rb file should exist") { + metadataBytes, err := ioutil.ReadFile(metadataPath) + assert.Nil(t, err) + assert.Equal(t, "name 'foo'", string(metadataBytes)) + } + if assert.FileExistsf(t, defaultPath, "the default.rb recipes should exist") { + recipeBytes, err := ioutil.ReadFile(defaultPath) + assert.Nil(t, err) + assert.Equal(t, "log 'this is a resource'", string(recipeBytes)) + } +} + +func TestVerifyMD5Checksum(t *testing.T) { + tempDir, err := ioutil.TempDir("", "md5-test") + if err != nil { + t.Error(err) + } + defer os.RemoveAll(tempDir) // clean up + + var ( + // if someone changes the test data, + // you have to also update the below md5 sum + testData = []byte("hello\nchef\n") + filePath = path.Join(tempDir, "dat") + ) + err = ioutil.WriteFile(filePath, testData, 0644) + assert.Nil(t, err) + assert.True(t, verifyMD5Checksum(filePath, "70bda176ac4db06f1f66f96ae0693be1")) +}