├── .gitignore ├── CHANGELOG.md ├── README.md ├── binaries ├── darwin │ └── amd64 │ │ └── cf-download ├── linux │ └── amd64 │ │ └── cf-download └── windows │ └── amd64 │ └── cf-download.exe ├── cmd_exec ├── cmd_exec.go └── cmd_exec_fake │ └── cmd_exec.go ├── dir_parser ├── parser.go ├── parser_suite_test.go └── parser_test.go ├── downloader ├── download.go ├── download_suite_test.go ├── download_test.go └── testFiles │ ├── app_content │ ├── app.go │ └── server.go │ ├── ignore.go │ ├── ignoreDir │ └── hello.txt │ └── notignored.go ├── filter └── filter.go ├── license.txt ├── main.go ├── main_suite_test.go └── main_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | main -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.0 (Sep 13, 2016) 2 | 3 | * Support for paths within applications 4 | 5 | FEATURES: 6 | 7 | * Allow downloading of specific files and directories in running applications 8 | 9 | ## 1.1.0 (Jul 1, 2016) 10 | 11 | * Refactor for Windows 12 | 13 | FEATURES: 14 | 15 | * Improved Windows support 16 | * Improved error handling 17 | 18 | ## 1.0.0 (Apr 24, 2015) 19 | 20 | * Initial Release 21 | 22 | FEATURES: 23 | 24 | * Allow downloading of running applications 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CF DOWNLOAD 2 | ### A Cloud Foundry cli plugin for downloading your application contents after staging 3 | 4 | 5 | 6 | ##Installation 7 | #####Install from CLI (Recommended) 8 | ``` 9 | $ cf add-plugin-repo CF-Community http://plugins.cloudfoundry.org/ 10 | $ cf install-plugin cf-download -r CF-Community 11 | ``` 12 | 13 | ##### Install from binary 14 | 1. download binary (See Download Section below) 15 | 2. **cd path/to/downloaded/binary** 16 | 3. If you've already installed the plugin and are updating, you must first run **cf uninstall-plugin cf-download** 17 | 4. Then install the plugin with **cf install-plugin cf-download** 18 | * If you get a permission error run: **chmod +x cf-download** on the binary 19 | 5. Verify the plugin installed by looking for it with **cf plugins** 20 | 21 | ##### Download Binaries 22 | 23 | ###### Mac: [64-bit](https://github.com/ibmjstart/cf-download/blob/master/binaries/darwin/amd64/cf-download?raw=true) 24 | ###### Windows: [64-bit](https://github.com/ibmjstart/cf-download/blob/master/binaries/windows/amd64/cf-download.exe?raw=true) 25 | ###### Linux: [64-bit](https://github.com/ibmjstart/cf-download/blob/master/binaries/linux/amd64/cf-download?raw=true) 26 | 27 | *** 28 | 29 | ## Usage 30 | 31 | cf download APP_NAME [PATH...] [--overwrite] [--file] [--verbose] [--omit omitted_path] [-i instance] 32 | 33 | If no "PATH" is specified, the downloaded app files will be put in a new directory "APP_NAME" that's created within your working directory. 34 | If "PATH" is specified, the directory or file specified will be placed directly in your working directory. 35 | 36 | ### Path Argument 37 | The path argument is optional but, if included, should come immediately after the app name. It determines the starting directory that all the files will be downloaded from. By default, the entire app is downloaded starting from the root. However if desired, one could use **some/starting/path** to only download files within the **path** directory. 38 | 39 | The path can point to a single file (or be a path to a single file) to be downloaded if the **--file** flag is specified. Note: this works similarly to "cf files [path]". 40 | 41 | The last element of a path can contain standard glob characters (*, ?, [ - ]). 42 | 43 | Any number of path arguments can be passed as long as they all come immideately after the app name, but they must all be directories or all be files (if the **--file** flag is specified). 44 | 45 | ### Flags: 46 | 1. The **--overwrite** flag is needed if the download directory, "APP_NAME-download", is already taken. Using the flag, that directory will be overwritten. 47 | 2. The **--file** flag is needed if **PATH** points to a single file to be downloaded, and not a directory. 48 | 3. The **--verbose** flag is used to see more detailed output as the downloads are happening. 49 | 4. The **--omit [omitted_path]** flag is useful when certain files or directories are not wanted. You can exclude a file by typing **--omit path/to/file**. Multiple things can be omitted by delimiting the paths with semicolons and putting quotes around the entire parameter like so: **--omit "path/to/file; another/path/to/file"** 50 | 5. The **-i [instance]** flag will download from the given app instance. By default, the instance number is 0. 51 | 52 | *** 53 | 54 | ## Improving performance: 55 | Projects usually have a enormous amount of dependencies installed by package managers, we highly recommend not downloading these dependencies. Using the --omit flag, you can avoid downloading these dependencies and significantly reduce download times. 56 | 57 | #### Java/Liberty: 58 | We highly recommend you not download the app/.java and app/.liberty directories in your java/liberty projects. They are very large and contain many permission issues the prevent proper downloads. It is best to omit them. 59 | 60 | #### Node.js: 61 | npm will download dependencies to the node_modules folder in the app directory. By omitting app/node_modules you will greatly decrease download times. You can run npm install locally on your package.json after completing a download. 62 | 63 | #### PHP: 64 | Composer is a popular PHP package manager that installs dependencies to a folder called vendors. It is recommended you omit this folder from the download to ensure a quick and error free download. example: **--omit /vendors** 65 | 66 | *** 67 | 68 | ## Notes and FAQ: 69 | #### .cfignore: 70 | All directories and files within the .cfignore file will be omitted. Each entry should be on its own line and that the .cfignore file must be in the same working directory. Instead of using many --omit parameters, it's easier to use the .cfignore file. 71 | 72 | #### Stuck Download: 73 | In case your download seems to be stuck, we recommend terminating and redownloading using the --verbose flag. When the download stalls you can see which files were being downloaded and what could be causing the issue. It is also important to note that you do not always need to pull every file from your application. Many files can be found elsewhere and should be omitted. These files are usually a part of a buildpack or dependencies that can easily be installed using a package manager. Refer back to the "Improving performance" section for suggestions on which files can be omitted. 74 | 75 | #### Downloading Jar files: 76 | Projects containing jar files can trigger antivirus software while being downloaded. you can either temporarily disable network antivirus protection or exclude directories containing jar files. 77 | 78 | #### I am getting a lot of 502 errors, why?: 79 | On rare occasions the cf cli api that the plugin uses can get overburdened by the plugin. This will display a lot of 502 error messages to the command line. The best thing to do in this case is wait a couple minutes and try again later. The Api will hopefully return to full capacity and allow downloads to complete. In the unlikely case that you experience this often, create an issue on this repo and we can explore solutions. 80 | 81 | #### Error: "App not found, or the app is in stopped state (This can also be caused by api failure)": 82 | This error is caused when the cf cli api fails. Best solution is to wait and try again, when the api recovers. 83 | -------------------------------------------------------------------------------- /binaries/darwin/amd64/cf-download: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibmjstart/cf-download/6a11f64ee902407709e2c21822645288810c2171/binaries/darwin/amd64/cf-download -------------------------------------------------------------------------------- /binaries/linux/amd64/cf-download: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibmjstart/cf-download/6a11f64ee902407709e2c21822645288810c2171/binaries/linux/amd64/cf-download -------------------------------------------------------------------------------- /binaries/windows/amd64/cf-download.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibmjstart/cf-download/6a11f64ee902407709e2c21822645288810c2171/binaries/windows/amd64/cf-download.exe -------------------------------------------------------------------------------- /cmd_exec/cmd_exec.go: -------------------------------------------------------------------------------- 1 | package cmd_exec 2 | 3 | import "os/exec" 4 | 5 | type CmdExec interface { 6 | GetFile(appName, readPath, instance string) ([]byte, error) 7 | } 8 | 9 | type cmdExec struct { 10 | } 11 | 12 | func NewCmdExec() CmdExec { 13 | return &cmdExec{} 14 | } 15 | 16 | func (c *cmdExec) GetFile(appName, readPath, instance string) ([]byte, error) { 17 | // call cf files using os/exec 18 | cmd := exec.Command("cf", "files", appName, readPath, "-i", instance) 19 | output, err := cmd.CombinedOutput() 20 | 21 | return output, err 22 | } 23 | -------------------------------------------------------------------------------- /cmd_exec/cmd_exec_fake/cmd_exec.go: -------------------------------------------------------------------------------- 1 | package cmd_exec_fake 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | ) 7 | 8 | type FakeCmdExec interface { 9 | GetFile(appName, readPath, instance string) ([]byte, error) 10 | SetOutput(output string) 11 | SetFakeDir(flag bool) 12 | } 13 | 14 | type cmdExec struct { 15 | output string 16 | useFakeDir bool 17 | } 18 | 19 | func NewCmdExec() FakeCmdExec { 20 | return &cmdExec{} 21 | } 22 | 23 | func (c *cmdExec) SetOutput(output string) { 24 | c.output = output 25 | } 26 | 27 | func (c *cmdExec) SetFakeDir(flag bool) { 28 | c.useFakeDir = flag 29 | } 30 | 31 | func (c *cmdExec) GetFile(appName, readPath, instance string) ([]byte, error) { 32 | var output []byte 33 | if c.useFakeDir == false { 34 | return []byte(c.output), nil 35 | } 36 | 37 | // Needs to be appended to every response (for parser) 38 | startString := "Getting files for app payToWin in org jstart / space koldus as email@us.ibm.com...\nOK\n" 39 | 40 | fileInfo, _ := os.Stat(readPath) 41 | if fileInfo.IsDir() { 42 | file, _ := os.Open(readPath) 43 | dirFiles, _ := file.Readdir(0) 44 | var dirString string 45 | for _, val := range dirFiles { 46 | if val.IsDir() { 47 | dirString += "\n" + val.Name() + "/ -" 48 | } else { 49 | dirString += "\n" + val.Name() + " 1B" 50 | } 51 | } 52 | 53 | output = []byte(startString + dirString) 54 | 55 | } else { 56 | fileContents, _ := ioutil.ReadFile(readPath) 57 | output = []byte(startString + string(fileContents)) 58 | } 59 | return output, nil 60 | } 61 | -------------------------------------------------------------------------------- /dir_parser/parser.go: -------------------------------------------------------------------------------- 1 | package dir_parser 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ibmjstart/cf-download/cmd_exec" 6 | "github.com/mgutz/ansi" 7 | "os" 8 | "regexp" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type Parser interface { 14 | ExecParseDir(readPath string) ([]string, []string) 15 | GetFailedDownloads() []string 16 | GetDirectory(readPath string) (string, string) 17 | } 18 | 19 | type parser struct { 20 | cmdExec cmd_exec.CmdExec 21 | appName string 22 | instance string 23 | onWindows bool 24 | verbose bool 25 | failedDownloads []string 26 | } 27 | 28 | func NewParser(cmdExec cmd_exec.CmdExec, appName, instance string, onWindows, verbose bool) *parser { 29 | return &parser{ 30 | cmdExec: cmdExec, 31 | appName: appName, 32 | instance: instance, 33 | onWindows: onWindows, 34 | verbose: verbose, 35 | } 36 | } 37 | 38 | /* 39 | * execParseDir() uses os/exec to shell out commands to cf files with the given readPath. The returned 40 | * text contains file and directory structure which is then parsed into two slices, dirs and files. dirs 41 | * contains the names of directories in readPath, files contians the file names. dirs and files are returned 42 | * to be downloaded by download() and downloadFile() respectively. 43 | */ 44 | func (p *parser) ExecParseDir(readPath string) ([]string, []string) { 45 | dir, status := p.GetDirectory(readPath) 46 | 47 | if status == "OK" { 48 | // parse the returned output into files and dirs slices 49 | filesSlice := strings.Fields(dir) 50 | var files, dirs []string 51 | var name string 52 | for i := 0; i < len(filesSlice); i++ { 53 | if strings.HasSuffix(filesSlice[i], "/") { 54 | name += filesSlice[i] 55 | dirs = append(dirs, name) 56 | name = "" 57 | } else if isDelimiter(filesSlice[i]) { 58 | if len(name) > 0 { 59 | name = strings.TrimSuffix(name, " ") 60 | files = append(files, name) 61 | } 62 | name = "" 63 | } else { 64 | name += filesSlice[i] + " " 65 | } 66 | } 67 | return files, dirs 68 | } else { 69 | //error was already logged in GetDirectory if --verbose was used 70 | if readPath == "/" { 71 | os.Exit(1) 72 | } 73 | } 74 | 75 | return nil, nil 76 | } 77 | 78 | /* 79 | * getDirectory will return the directory as a string ready for parsing. 80 | * There is a status code returned as well, this is not necessary but helps with testing. 81 | */ 82 | func (p *parser) GetDirectory(readPath string) (string, string) { 83 | 84 | // make the cf files call using exec 85 | output, err := p.cmdExec.GetFile(p.appName, readPath, p.instance) 86 | dirSlice := strings.SplitAfterN(string(output), "\n", 3) 87 | 88 | // if cf files fails to get directory, retry (this code is not covered in tests) 89 | if len(dirSlice) < 2 { 90 | iterations := 0 91 | for len(dirSlice) < 2 && iterations < 10 { 92 | time.Sleep(3 * time.Second) 93 | output, err = p.cmdExec.GetFile(p.appName, readPath, p.instance) 94 | dirSlice = strings.SplitAfterN(string(output), "\n", 3) 95 | iterations++ 96 | } 97 | } 98 | 99 | if len(dirSlice) >= 2 && strings.Contains(dirSlice[1], "OK") { 100 | if strings.Contains(dirSlice[2], "No files found") { 101 | return "", "noFiles" 102 | } 103 | return dirSlice[2], "OK" 104 | } else { 105 | message := createMessage(" Server Error: '"+readPath+"' not downloaded", "yellow", p.onWindows) 106 | 107 | p.failedDownloads = append(p.failedDownloads, message) 108 | 109 | if p.verbose { 110 | fmt.Println(message) 111 | // check for other errors 112 | if err != nil { 113 | PrintSlice(dirSlice) 114 | } 115 | } 116 | 117 | return string(output), "Failed" 118 | } 119 | } 120 | 121 | func (p *parser) GetFailedDownloads() []string { 122 | return p.failedDownloads 123 | } 124 | 125 | func isDelimiter(str string) bool { 126 | match, _ := regexp.MatchString("^[0-9]([0-9]|.)*(G|M|B|K)$", str) 127 | if match == true || str == "-" { 128 | return true 129 | } 130 | return false 131 | } 132 | 133 | // prints slices in readable format 134 | func PrintSlice(slice []string) error { 135 | for index, val := range slice { 136 | fmt.Println(index, ": ", val) 137 | } 138 | return nil 139 | } 140 | 141 | func createMessage(message, color string, onWindows bool) string { 142 | errmsg := ansi.Color(message, color) 143 | if onWindows == true { 144 | errmsg = message 145 | } 146 | 147 | return errmsg 148 | } 149 | -------------------------------------------------------------------------------- /dir_parser/parser_suite_test.go: -------------------------------------------------------------------------------- 1 | package dir_parser_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestDirParser(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "DirParser Suite") 13 | } 14 | -------------------------------------------------------------------------------- /dir_parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package dir_parser_test 2 | 3 | import ( 4 | "github.com/ibmjstart/cf-download/cmd_exec/cmd_exec_fake" 5 | . "github.com/ibmjstart/cf-download/dir_parser" 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | //unit tests 11 | var _ = Describe("DirParser", func() { 12 | var p Parser 13 | var cmdExec cmd_exec_fake.FakeCmdExec 14 | 15 | BeforeEach(func() { 16 | cmdExec = cmd_exec_fake.NewCmdExec() 17 | p = NewParser(cmdExec, "TestApp", "0", false, false) 18 | }) 19 | Describe("Test getFailedDownloads()", func() { 20 | It("Should return empty []string because no directory string downloads have failed.", func() { 21 | fails := p.GetFailedDownloads() 22 | Ω(len(fails)).To(Equal(0)) 23 | }) 24 | }) 25 | 26 | Describe("Test ExecParseDir()", func() { 27 | It("Should return 8 files and 3 directories", func() { 28 | cmdExec.SetOutput("Getting files for app smithInTheHouse in org jstart / space evans as email@us.ibm.com...\nOK\n\n.npmignore 136B\nLICENSE 1.1K\nREADME.md 5.3K\nReadme_zh-cn.md 28.4K\nbin/ -\ncomponent.json 282B\nindex.js 95B\njade-language.md 20.0K\njade.js 757.2K\njade.md 11.3K\nlib/ -\nnode_modules/ -\npackage.json 2.0K\nruntime.js 5.1K") 29 | files, directories := p.ExecParseDir("readPath") 30 | Ω(len(files)).To(Equal(11)) 31 | Ω(files[0]).To(Equal(".npmignore")) 32 | Ω(files[1]).To(Equal("LICENSE")) 33 | Ω(files[2]).To(Equal("README.md")) 34 | Ω(files[3]).To(Equal("Readme_zh-cn.md")) 35 | Ω(files[4]).To(Equal("component.json")) 36 | Ω(files[5]).To(Equal("index.js")) 37 | Ω(files[6]).To(Equal("jade-language.md")) 38 | Ω(files[7]).To(Equal("jade.js")) 39 | Ω(files[8]).To(Equal("jade.md")) 40 | Ω(files[9]).To(Equal("package.json")) 41 | Ω(files[10]).To(Equal("runtime.js")) 42 | Ω(len(directories)).To(Equal(3)) 43 | Ω(directories[0]).To(Equal("bin/")) 44 | Ω(directories[1]).To(Equal("lib/")) 45 | Ω(directories[2]).To(Equal("node_modules/")) 46 | }) 47 | }) 48 | 49 | Describe("Test GetDirectory()", func() { 50 | It("test when app is not found", func() { 51 | cmdExec.SetOutput("Getting files for app\nFAILED\nApp APP_NAME not found") 52 | _, status := p.GetDirectory("") 53 | Ω(status).To(Equal("Failed")) 54 | }) 55 | It("test empty directory", func() { 56 | cmdExec.SetOutput("Getting files for app\nOK\nNo files found") 57 | _, status := p.GetDirectory("") 58 | Ω(status).To(Equal("noFiles")) 59 | }) 60 | It("test unkown api error", func() { 61 | cmdExec.SetOutput("FAILED\nServer error, status code: 500, error code: 10001, message: An unknown error occurred.\n") 62 | _, status := p.GetDirectory("") 63 | Ω(status).To(Equal("Failed")) 64 | }) 65 | It("test when app is stopped", func() { 66 | cmdExec.SetOutput("Getting files for app\nFAILED\nerror code: 190001") 67 | _, status := p.GetDirectory("") 68 | Ω(status).To(Equal("Failed")) 69 | }) 70 | It("test when 502 error occurs", func() { 71 | cmdExec.SetOutput("Getting files for app\nstatus code: 502\n ") 72 | _, status := p.GetDirectory("") 73 | Ω(status).To(Equal("Failed")) 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /downloader/download.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "github.com/ibmjstart/cf-download/cmd_exec" 14 | "github.com/ibmjstart/cf-download/dir_parser" 15 | "github.com/ibmjstart/cf-download/filter" 16 | "github.com/mgutz/ansi" 17 | ) 18 | 19 | type Downloader interface { 20 | Download(files, dirs []string, readPath, writePath string, filterList []string) error 21 | DownloadFile(readPath, writePath string) error 22 | WriteFile(readPath, writePath string, output []byte, err error) error 23 | CheckDownload(readPath string, file []string, err error) error 24 | GetFilesDownloadedCount() int 25 | GetFailedDownloads() []string 26 | } 27 | 28 | type downloader struct { 29 | cmdExec cmd_exec.CmdExec 30 | appName string 31 | instance string 32 | verbose bool 33 | onWindows bool 34 | failedDownloads []string 35 | filesDownloaded int 36 | parser dir_parser.Parser 37 | wg *sync.WaitGroup 38 | } 39 | 40 | func NewDownloader(cmdExec cmd_exec.CmdExec, WG *sync.WaitGroup, appName, instance string, verbose, onWindows bool) *downloader { 41 | 42 | return &downloader{ 43 | cmdExec: cmdExec, 44 | appName: appName, 45 | instance: instance, 46 | verbose: verbose, 47 | onWindows: onWindows, 48 | parser: dir_parser.NewParser(cmdExec, appName, instance, onWindows, verbose), 49 | wg: WG, 50 | } 51 | } 52 | 53 | // error struct that allows appending error messages 54 | type cliError struct { 55 | err error 56 | errMsg string 57 | } 58 | 59 | /* 60 | * given file and directory names, download() will download the files from 61 | * 'readPath' and write them to disk on the 'writepath'. 62 | * the function calls it's self recursively for each directory as it travels down the tree. 63 | * Each call runs on a seperate go routine and and calls a go routine for every 64 | * file download. 65 | */ 66 | func (d *downloader) Download(files, dirs []string, readPath, writePath string, filterList []string) error { 67 | defer d.wg.Done() 68 | 69 | //create dir if does not exist 70 | err := os.MkdirAll(writePath, 0755) 71 | check(err, "Error D1: failed to create directory.") 72 | 73 | // download each file 74 | for _, val := range files { 75 | fileWPath := writePath + val 76 | fileRPath := readPath + val 77 | 78 | if filter.CheckToFilter(fileRPath, filterList) { 79 | continue 80 | } 81 | 82 | d.wg.Add(1) 83 | go d.DownloadFile(fileRPath, fileWPath) 84 | } 85 | 86 | // call download on every sub directory 87 | for _, val := range dirs { 88 | dirWPath := writePath + filepath.FromSlash(val) 89 | dirRPath := readPath + val 90 | 91 | if filter.CheckToFilter(strings.TrimSuffix(dirRPath, "/"), filterList) { 92 | continue 93 | } 94 | 95 | err := os.MkdirAll(dirWPath, 0755) 96 | check(err, "Error D2: failed to create directory.") 97 | 98 | files, dirs = d.parser.ExecParseDir(dirRPath) 99 | 100 | d.wg.Add(1) 101 | d.Download(files, dirs, dirRPath, dirWPath, filterList) 102 | } 103 | return nil 104 | } 105 | 106 | /* 107 | * downloadFile() takes a 'readPath' which corresponds to a file in the cf app. The file is 108 | * downloaded using the cmd_exec package which uses the os/exec library to call cf files with the given readPath. The output is 109 | * written to a file at writePath. 110 | */ 111 | func (d *downloader) DownloadFile(readPath, writePath string) error { 112 | defer d.wg.Done() 113 | 114 | output, err := d.cmdExec.GetFile(d.appName, readPath, d.instance) 115 | 116 | d.WriteFile(readPath, writePath, output, err) 117 | 118 | return nil 119 | } 120 | 121 | func (d *downloader) WriteFile(readPath, writePath string, output []byte, err error) error { 122 | file := strings.SplitAfterN(string(output), "\n", 3) 123 | 124 | // check for invalid files or download issues 125 | downloadErr := d.CheckDownload(readPath, file, err) 126 | 127 | if downloadErr == nil { 128 | fileAsString := file[2] 129 | 130 | // there is currently an issue open to change the behavior for empty files 131 | // https://github.com/cloudfoundry/cli/issues/869 132 | if strings.Contains(fileAsString, "No files found") { 133 | fileAsString = "" 134 | } 135 | 136 | if d.verbose { 137 | fmt.Printf("Writing file: %s\n", readPath) 138 | } 139 | 140 | // write downloaded file to writePath 141 | err = ioutil.WriteFile(writePath, []byte(fileAsString), 0644) 142 | 143 | for i := 1; i <= 32 && err != nil; i *= 2 { 144 | time.Sleep(time.Duration(i) * time.Second) 145 | err = ioutil.WriteFile(writePath, []byte(fileAsString), 0644) 146 | } 147 | 148 | if err == nil { 149 | // increment download counter for commandline display 150 | // see consoleWriter() in main.go 151 | d.filesDownloaded++ 152 | } else { 153 | errMsg := createMessage(" Write Error: '"+readPath+"' encountered error while writing to local file", "yellow", d.onWindows) 154 | d.failedDownloads = append(d.failedDownloads, errMsg) 155 | if d.verbose { 156 | fmt.Println(errMsg) 157 | fmt.Println(err) 158 | } 159 | } 160 | 161 | } 162 | 163 | return err 164 | } 165 | 166 | func (d *downloader) CheckDownload(readPath string, file []string, err error) error { 167 | if len(file) >= 2 && strings.Contains(file[1], "OK") { 168 | return nil 169 | } else { 170 | errMsg := createMessage(" Server Error: '"+readPath+"' not downloaded", "yellow", d.onWindows) 171 | 172 | d.failedDownloads = append(d.failedDownloads, errMsg) 173 | 174 | if d.verbose { 175 | fmt.Println(errMsg) 176 | // check for other errors 177 | if err != nil && len(file) >= 2 { 178 | PrintSlice(file) 179 | } 180 | } 181 | return errors.New("download failed") 182 | } 183 | } 184 | 185 | func (d *downloader) GetFilesDownloadedCount() int { 186 | return d.filesDownloaded 187 | } 188 | 189 | func (d *downloader) GetFailedDownloads() []string { 190 | return d.failedDownloads 191 | } 192 | 193 | // error check function 194 | func check(e error, errMsg string) { 195 | if e != nil { 196 | fmt.Println("\nError: ", e) 197 | if errMsg != "" { 198 | fmt.Println("Message: ", errMsg) 199 | } 200 | os.Exit(1) 201 | } 202 | } 203 | 204 | // prints slices in readable format 205 | func PrintSlice(slice []string) error { 206 | for index, val := range slice { 207 | fmt.Println(index, ": ", val) 208 | } 209 | return nil 210 | } 211 | 212 | func createMessage(message, color string, onWindows bool) string { 213 | errmsg := ansi.Color(message, color) 214 | if onWindows == true { 215 | errmsg = message 216 | } 217 | 218 | return errmsg 219 | } 220 | -------------------------------------------------------------------------------- /downloader/download_suite_test.go: -------------------------------------------------------------------------------- 1 | package downloader_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestDownloader(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Downloader Suite") 13 | } 14 | -------------------------------------------------------------------------------- /downloader/download_test.go: -------------------------------------------------------------------------------- 1 | package downloader_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/ibmjstart/cf-download/cmd_exec/cmd_exec_fake" 6 | . "github.com/ibmjstart/cf-download/downloader" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | "io/ioutil" 10 | "os" 11 | "sync" 12 | ) 13 | 14 | var _ = Describe("Downloader tests", func() { 15 | var ( 16 | wg sync.WaitGroup 17 | d Downloader 18 | cmdExec cmd_exec_fake.FakeCmdExec 19 | currentDirectory string 20 | ) 21 | 22 | currentDirectory, _ = os.Getwd() 23 | os.MkdirAll(currentDirectory+"/testFiles/", 0755) 24 | 25 | cmdExec = cmd_exec_fake.NewCmdExec() 26 | d = NewDownloader(cmdExec, &wg, "appName", "0", false, false) 27 | 28 | // downloadfile also tests the following functions 29 | // WriteFile(), CheckDownload() 30 | Describe("Test DownloadFile function", func() { 31 | Context("download and create a file containing only 'helloWorld'", func() { 32 | It("create test1.txt", func() { 33 | writePath := currentDirectory + "/testFiles/test1.txt" 34 | cmdExec.SetOutput("Getting files for app payToWin in org jstart / space koldus as email@us.ibm.com...\nOK\nHello World") 35 | wg.Add(1) 36 | go d.DownloadFile("", writePath) 37 | wg.Wait() 38 | 39 | fileContents, err := ioutil.ReadFile(writePath) 40 | Ω(err).To(BeNil()) 41 | Ω(string(fileContents)).To(Equal("Hello World")) 42 | writePath = currentDirectory + "/testFiles/test2.txt" 43 | cmdExec.SetOutput("Getting files for app payToWin in org jstart / space koldus as email@us.ibm.com...\nOK\nLorem ipsum is a pseudo-Latin text used in web design, typography, layout, and printing in place of English to emphasise design elements over content. It's also called placeholder (or filler) text. It's a convenient tool for mock-ups. It helps to outline the visual elements of a document or presentation, eg typography, font, or layout. Lorem ipsum is mostly a part of a Latin text by the classical author and philosopher Cicero. Its words and letters have been changed by addition or removal, so to deliberately render its content nonsensical; it's not genuine, correct, or comprehensible Latin anymore. While lorem ipsum's still resembles classical Latin, it actually has no meaning whatsoever. As Cicero's text doesn't contain the letters K, W, or Z, alien to latin, these, and others are often inserted randomly to mimic the typographic appearence of European languages, as are digraphs not to be found in the original.") 44 | wg.Add(1) 45 | go d.DownloadFile("", writePath) 46 | wg.Wait() 47 | 48 | fileInfo, err := os.Stat(writePath) 49 | Ω(err).To(BeNil()) 50 | Ω(fileInfo.Name()).To(Equal("test2.txt")) 51 | Ω(fileInfo.Size()).To(BeEquivalentTo(924)) 52 | Ω(fileInfo.IsDir()).To(BeFalse()) 53 | os.RemoveAll("testFiles/test1.txt") 54 | os.RemoveAll("testFiles/test2.txt") 55 | }) 56 | }) 57 | }) 58 | 59 | Describe("Test checkDownload Function", func() { 60 | Context("when we recieve permission error", func() { 61 | It("Should return server error", func() { 62 | falseFile := make([]string, 3) 63 | falseFile[0] = "Getting files for app app_name in org org_name / space spacey as user@us.ibm.com" 64 | falseFile[1] = "FAILED" 65 | 66 | err := d.CheckDownload("/app/node_modules/express/application.js", falseFile, nil) 67 | Expect(err).To(Equal(errors.New("download failed"))) 68 | }) 69 | }) 70 | 71 | Context("when we recieve an empty FAILED file", func() { 72 | It("Should return server error", func() { 73 | falseFile := make([]string, 1) 74 | falseFile[0] = "" 75 | 76 | // Throw away Stdout 77 | oldStdout := os.Stdout 78 | os.Stdout = nil 79 | 80 | err := d.CheckDownload("/app/node_modules/express/application.js", falseFile, nil) 81 | 82 | // restore Stdout 83 | os.Stdout = oldStdout 84 | Expect(err.Error()).To(Equal("download failed")) 85 | }) 86 | }) 87 | 88 | Context("when we recieve 502 error", func() { 89 | It("Should return server error", func() { 90 | falseFile := make([]string, 3) 91 | falseFile[0] = "Getting files for app app_name in org org_name / space spacey as user@us.ibm.com" 92 | falseFile[1] = "status code: 502" 93 | 94 | // Throw away Stdout 95 | oldStdout := os.Stdout 96 | os.Stdout = nil 97 | 98 | err := d.CheckDownload("/app/node_modules/express/application.js", falseFile, nil) 99 | 100 | // restore Stdout 101 | os.Stdout = oldStdout 102 | Expect(err.Error()).To(Equal("download failed")) 103 | }) 104 | }) 105 | 106 | Context("when we recieve 500 error", func() { 107 | It("Should return server error", func() { 108 | falseFile := make([]string, 3) 109 | falseFile[0] = "Getting files for app app_name in org org_name / space spacey as user@us.ibm.com" 110 | falseFile[1] = "status code: 500" 111 | 112 | // Throw away Stdout 113 | oldStdout := os.Stdout 114 | os.Stdout = nil 115 | 116 | err := d.CheckDownload("/app/node_modules/express/application.js", falseFile, nil) 117 | 118 | // restore Stdout 119 | os.Stdout = oldStdout 120 | Expect(err.Error()).To(Equal("download failed")) 121 | }) 122 | }) 123 | 124 | Context("when we recieve 400 error", func() { 125 | It("Should return server error", func() { 126 | falseFile := make([]string, 3) 127 | falseFile[0] = "Getting files for app app_name in org org_name / space spacey as user@us.ibm.com" 128 | falseFile[1] = "status code: 400" 129 | 130 | // Throw away Stdout 131 | oldStdout := os.Stdout 132 | os.Stdout = nil 133 | 134 | err := d.CheckDownload("/app/node_modules/express/application.js", falseFile, nil) 135 | 136 | // restore Stdout 137 | os.Stdout = oldStdout 138 | Expect(err.Error()).To(Equal("download failed")) 139 | }) 140 | }) 141 | 142 | Context("when we recieve no error", func() { 143 | It("Should return no error", func() { 144 | falseFile := make([]string, 3) 145 | falseFile[0] = "Getting files for app app_name in org org_name / space spacey as user@us.ibm.com" 146 | falseFile[1] = "OK" 147 | 148 | err := d.CheckDownload("/app/node_modules/express/application.js", falseFile, nil) 149 | Expect(err).To(BeNil()) 150 | }) 151 | }) 152 | 153 | }) 154 | 155 | Describe("Test GetFilesDownloadedCount Function", func() { 156 | It("after downloading 2 files", func() { 157 | count := d.GetFilesDownloadedCount() 158 | Ω(count).To(Equal(2)) 159 | }) 160 | }) 161 | 162 | Describe("Test getFailedDownloads()", func() { 163 | It("Should have 5 failed download from previous CheckDownload Test", func() { 164 | fails := d.GetFailedDownloads() 165 | Ω(len(fails)).To(Equal(5)) 166 | }) 167 | }) 168 | 169 | /* 170 | * The Following test call the download function on a test directory (testFiles) which exists in cf-download/downloader/ 171 | * The directory will be loaded into the download function and written to a new folder called test-download. The test suite 172 | * will verify that the directory was loaded and written correctly. Once everything is verified then the test-download directory 173 | * will be deleted as to not interfere with the next test. This not only test the entire download functionality but also the 174 | * parsing and all flags. 175 | */ 176 | Describe("Test Download() Function", func() { 177 | Context("download the entire directory (no filter)", func() { 178 | It("should download and write the files to the testFiles Directory", func() { 179 | d = NewDownloader(cmdExec, &wg, "appName", "0", false, false) 180 | readPath := currentDirectory 181 | writePath := currentDirectory + "/test-download" 182 | 183 | // turn on directory faking 184 | cmdExec.SetFakeDir(true) 185 | // make sure to turn it off after test 186 | defer cmdExec.SetFakeDir(false) 187 | 188 | files := []string{} 189 | dirs := []string{"/testFiles/"} 190 | filterList := []string{currentDirectory + "/testFiles/.DS_Store", currentDirectory + "/testFiles/app_content/.DS_Store"} 191 | 192 | wg.Add(1) 193 | go d.Download(files, dirs, readPath, writePath, filterList) 194 | wg.Wait() 195 | 196 | // test root structure 197 | rootInfo, _ := os.Stat(writePath + "/testFiles/") 198 | Ω(rootInfo.IsDir()).To(BeTrue()) 199 | 200 | rootFile, _ := os.Open(writePath + "/testFiles/") 201 | rootContents, _ := rootFile.Readdir(0) 202 | rootFile.Close() 203 | Ω(rootContents[0].Name()).To(Equal("app_content")) 204 | Ω(rootContents[1].Name()).To(Equal("ignore.go")) 205 | Ω(rootContents[2].Name()).To(Equal("ignoreDir")) 206 | Ω(rootContents[3].Name()).To(Equal("notignored.go")) 207 | 208 | // test the contents of the app_contents directory 209 | Ω(rootContents[0].IsDir()).To(BeTrue()) 210 | appContentFolder, _ := os.Open(writePath + "/testFiles/" + rootContents[0].Name()) 211 | appContents, _ := appContentFolder.Readdir(0) 212 | appContentFolder.Close() 213 | Ω(appContents[0].Name()).To(Equal("app.go")) 214 | Ω(appContents[1].Name()).To(Equal("server.go")) 215 | 216 | // delete the folder after testing 217 | os.RemoveAll(writePath) 218 | }) 219 | }) 220 | }) 221 | 222 | Describe("Test Download() Function", func() { 223 | Context("download the fake directory filtering out ignore.go and ignoreDir", func() { 224 | It("should download and write the files to the testFiles Directory", func() { 225 | d = NewDownloader(cmdExec, &wg, "appName", "0", false, false) 226 | readPath := currentDirectory 227 | writePath := currentDirectory + "/test-download" 228 | 229 | // turn on directory faking 230 | cmdExec.SetFakeDir(true) 231 | // make sure to turn it off after test 232 | defer cmdExec.SetFakeDir(false) 233 | 234 | files := []string{} 235 | dirs := []string{"/testFiles/"} 236 | filterList := []string{currentDirectory + "/testFiles/ignore.go", currentDirectory + "/testFiles/ignoreDir", currentDirectory + "/testFiles/.DS_Store", currentDirectory + "/testFiles/app_content/.DS_Store"} 237 | 238 | wg.Add(1) 239 | go d.Download(files, dirs, readPath, writePath, filterList) 240 | wg.Wait() 241 | 242 | // test root structure 243 | rootInfo, _ := os.Stat(writePath + "/testFiles/") 244 | Ω(rootInfo.IsDir()).To(BeTrue()) 245 | 246 | rootFile, _ := os.Open(writePath + "/testFiles/") 247 | rootContents, _ := rootFile.Readdir(0) 248 | rootFile.Close() 249 | Ω(rootContents[0].Name()).To(Equal("app_content")) 250 | Ω(rootContents[1].Name()).To(Equal("notignored.go")) 251 | 252 | // test the contents of the app_contents directory 253 | Ω(rootContents[0].IsDir()).To(BeTrue()) 254 | appContentFolder, _ := os.Open(writePath + "/testFiles/" + rootContents[0].Name()) 255 | appContents, _ := appContentFolder.Readdir(0) 256 | appContentFolder.Close() 257 | Ω(appContents[0].Name()).To(Equal("app.go")) 258 | Ω(appContents[1].Name()).To(Equal("server.go")) 259 | 260 | // delete the folder after testing 261 | os.RemoveAll(writePath) 262 | 263 | }) 264 | }) 265 | }) 266 | }) 267 | -------------------------------------------------------------------------------- /downloader/testFiles/app_content/app.go: -------------------------------------------------------------------------------- 1 | hello user -------------------------------------------------------------------------------- /downloader/testFiles/app_content/server.go: -------------------------------------------------------------------------------- 1 | server time -------------------------------------------------------------------------------- /downloader/testFiles/ignore.go: -------------------------------------------------------------------------------- 1 | ignored -------------------------------------------------------------------------------- /downloader/testFiles/ignoreDir/hello.txt: -------------------------------------------------------------------------------- 1 | World -------------------------------------------------------------------------------- /downloader/testFiles/notignored.go: -------------------------------------------------------------------------------- 1 | not ignored -------------------------------------------------------------------------------- /filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "strings" 7 | ) 8 | 9 | func GetFilterList(omitString string, verbose bool) []string { 10 | // POST: FCTVAL== slice of strings (paths and files) to filter 11 | filterList := []string{} // filtered list to be returned 12 | 13 | // Add .cfignore files to filterList 14 | content, err := ioutil.ReadFile(".cfignore") 15 | 16 | if err != nil && verbose { 17 | fmt.Println("[ Info: ", err, "]") 18 | } else { 19 | lines := strings.Split(string(content), "\n") // get each line in .cfignore 20 | 21 | if verbose && len(lines) > 1 { 22 | fmt.Println("[ Info: using .cfignore ] \nContents: ") 23 | for _, val := range lines { 24 | fmt.Println(val) 25 | } 26 | fmt.Println("") 27 | } else if len(lines) > 0 { 28 | if lines[0] != "" { 29 | fmt.Println("[ Info: using .cfignore ]") 30 | } 31 | } 32 | 33 | filterList = append(filterList, lines[0:]...) 34 | } 35 | 36 | // Add the path from the --omit param to filterList 37 | allOmits := strings.Split(omitString, ";") 38 | 39 | // Add omitted strings to the filter list 40 | filterList = append(filterList, allOmits[0:]...) 41 | 42 | var returnList []string // filtered strings to be returned 43 | 44 | // Remove any trailing forward slashes in the filterList[ex: app/ becomes app] 45 | for i, _ := range filterList { 46 | filterList[i] = strings.TrimSpace(filterList[i]) 47 | 48 | if filterList[i] != "" { 49 | filterList[i] = strings.TrimPrefix(filterList[i], "/") 50 | filterList[i] = "/" + filterList[i] 51 | filterList[i] = strings.TrimSuffix(filterList[i], "/") 52 | 53 | returnList = append(returnList, filterList[i]) 54 | } 55 | } 56 | 57 | return returnList 58 | } 59 | 60 | func CheckToFilter(filePath string, filterList []string) bool { 61 | 62 | for _, item := range filterList { 63 | 64 | // ignore files in ignore list and the cfignore file 65 | if filePath == item { 66 | return true 67 | } 68 | } 69 | 70 | return false 71 | } 72 | 73 | // prints slices in readable format 74 | func PrintSlice(slice []string) error { 75 | for index, val := range slice { 76 | fmt.Println(index, ": ", val) 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Apache License, Version 2.0 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 13 | 14 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 15 | 16 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 17 | 18 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 19 | 20 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 21 | 22 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 23 | 24 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 25 | 26 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 27 | 28 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 29 | 30 | 2. Grant of Copyright License. 31 | 32 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 33 | 34 | 3. Grant of Patent License. 35 | 36 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 37 | 38 | 4. Redistribution. 39 | 40 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 41 | 42 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 43 | You must cause any modified files to carry prominent notices stating that You changed the files; and 44 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 45 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 46 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 47 | 48 | 5. Submission of Contributions. 49 | 50 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 51 | 52 | 6. Trademarks. 53 | 54 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 55 | 56 | 7. Disclaimer of Warranty. 57 | 58 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 59 | 60 | 8. Limitation of Liability. 61 | 62 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 63 | 64 | 9. Accepting Warranty or Additional Liability. 65 | 66 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 67 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * IBM jStart team cf download cli Plugin 3 | * A plugin for downloading contents of a running app's file directory 4 | * 5 | * Authors: Miguel Clement, Jake Eden 6 | * Date: 3/5/2015 7 | * 8 | * for cross platform compiling use gox (https://github.com/mitchellh/gox) 9 | * gox compile command: gox -output="binaries/{{.OS}}/{{.Arch}}/cf-download" -osarch="linux/amd64 darwin/amd64 windows/amd64" 10 | */ 11 | 12 | package main 13 | 14 | import ( 15 | "flag" 16 | "fmt" 17 | "github.com/cloudfoundry/cli/plugin" 18 | "github.com/ibmjstart/cf-download/cmd_exec" 19 | "github.com/ibmjstart/cf-download/dir_parser" 20 | "github.com/ibmjstart/cf-download/downloader" 21 | "github.com/ibmjstart/cf-download/filter" 22 | "github.com/mgutz/ansi" 23 | "os" 24 | "os/exec" 25 | "path/filepath" 26 | "runtime" 27 | "strconv" 28 | "strings" 29 | "sync" 30 | "time" 31 | ) 32 | 33 | /* 34 | * This is the struct implementing the interface defined by the core CLI. It can 35 | * be found at "github.com/cloudfoundry/cli/plugin/plugin.go" 36 | */ 37 | type DownloadPlugin struct{} 38 | 39 | // contains flag values 40 | type flagVal struct { 41 | Omit_flag string 42 | OverWrite_flag bool 43 | Instance_flag string 44 | Verbose_flag bool 45 | File_flag bool 46 | } 47 | 48 | // contains local and server paths 49 | type pathVal struct { 50 | RootWorkingDirectoryLocal string 51 | StartingPathServer string 52 | } 53 | 54 | var ( 55 | appName string 56 | filesDownloaded int 57 | failedDownloads []string 58 | parser dir_parser.Parser 59 | dloader downloader.Downloader 60 | ) 61 | 62 | // global wait group for all download threads 63 | var wg sync.WaitGroup 64 | 65 | /* 66 | * This function must be implemented by any plugin because it is part of the 67 | * plugin interface defined by the core CLI. 68 | * 69 | * Run(....) is the entry point when the core CLI is invoking a command defined 70 | * by a plugin. The first parameter, plugin.CliConnection, is a struct that can 71 | * be used to invoke cli commands. The second paramter, args, is a slice of 72 | * strings. args[0] will be the name of the command, and will be followed by 73 | * any additional arguments a cli user typed in. 74 | * 75 | * Any error handling should be handled with the plugin itself (this means printing 76 | * user facing errors). The CLI will exit 0 if the plugin exits 0 and will exit 77 | * 1 should the plugin exits nonzero. 78 | */ 79 | 80 | func (c *DownloadPlugin) Run(cliConnection plugin.CliConnection, args []string) { 81 | if args[0] != "download" { 82 | os.Exit(0) 83 | } 84 | 85 | // start time for download timer 86 | start := time.Now() 87 | 88 | // disables ansi text color on windows 89 | onWindows := IsWindows() 90 | 91 | if len(args) < 2 { 92 | fmt.Println(createMessage("\nError: Missing App Name", "red+b", onWindows)) 93 | printHelp() 94 | os.Exit(1) 95 | } 96 | 97 | // parse input flags 98 | flagVals, paths := ParseArgs(args) 99 | 100 | cmdExec := cmd_exec.NewCmdExec() 101 | parser = dir_parser.NewParser(cmdExec, appName, flagVals.Instance_flag, onWindows, flagVals.Verbose_flag) 102 | 103 | // get list of paths to download 104 | paths = ExpandGlobs(cmdExec, paths, flagVals.Instance_flag) 105 | 106 | // get list of things to not download 107 | filterList := filter.GetFilterList(flagVals.Omit_flag, flagVals.Verbose_flag) 108 | 109 | // get server path to download from and local path to download to 110 | workingDir, err := os.Getwd() 111 | check(err, "Called by: Getwd") 112 | pathVals := GetDirectoryContext(workingDir, paths, flagVals.File_flag) 113 | 114 | // ensure cf_trace is disabled, otherwise parsing breaks 115 | if os.Getenv("CF_TRACE") == "true" { 116 | fmt.Println("\nError: environment variable CF_TRACE is set to true. This prevents download from succeeding.") 117 | return 118 | } 119 | 120 | // download files at each input path 121 | for _, v := range pathVals { 122 | // prevent overwriting files 123 | if Exists(v.RootWorkingDirectoryLocal) && flagVals.OverWrite_flag == false { 124 | fmt.Println("\nError: destination path", v.RootWorkingDirectoryLocal, "already exists.\n\nDelete it or rerun the command with the '--overwrite' flag.") 125 | os.Exit(1) 126 | } 127 | 128 | // remove files to be overwritten 129 | if flagVals.OverWrite_flag { 130 | err := os.RemoveAll(v.RootWorkingDirectoryLocal) 131 | check(err, "Cannot remove "+v.RootWorkingDirectoryLocal+" for overwrite.") 132 | } 133 | 134 | dloader = downloader.NewDownloader(cmdExec, &wg, appName, flagVals.Instance_flag, flagVals.Verbose_flag, onWindows) 135 | 136 | // stop consoleWriter 137 | quit := make(chan int) 138 | 139 | // disable consoleWriter if verbose 140 | if flagVals.Verbose_flag == false { 141 | go consoleWriter(quit) 142 | } 143 | 144 | if flagVals.File_flag { 145 | // create directory for single file 146 | err := os.MkdirAll(strings.TrimSuffix(v.RootWorkingDirectoryLocal, filepath.Base(v.RootWorkingDirectoryLocal)), 0755) 147 | check(err, "Error D1: failed to create directory.") 148 | 149 | // start download of single file 150 | wg.Add(1) 151 | dloader.DownloadFile(v.StartingPathServer, v.RootWorkingDirectoryLocal) 152 | } else { 153 | // parse the input directory 154 | files, dirs := parser.ExecParseDir(v.StartingPathServer) 155 | 156 | // start download of directory 157 | wg.Add(1) 158 | dloader.Download(files, dirs, v.StartingPathServer, v.RootWorkingDirectoryLocal, filterList) 159 | } 160 | 161 | // wait for download goRoutines 162 | wg.Wait() 163 | 164 | // stop console writer 165 | if flagVals.Verbose_flag == false { 166 | quit <- 0 167 | } 168 | } 169 | 170 | // return completion status to user 171 | getFailedDownloads() 172 | PrintCompletionInfo(start, onWindows) 173 | } 174 | 175 | /* 176 | * --------------------------------------------------------------------------------------- 177 | * --------------------------------- Helper Functions ------------------------------------ 178 | * --------------------------------------------------------------------------------------- 179 | */ 180 | 181 | /* 182 | * This function returns a list of files that failed to download. 183 | */ 184 | func getFailedDownloads() { 185 | failedDownloads = append(parser.GetFailedDownloads(), dloader.GetFailedDownloads()...) 186 | } 187 | 188 | /* 189 | * This function returns a list of pathVal structs that contain the locations 190 | * to download each input path to and from. 191 | */ 192 | func GetDirectoryContext(workingDir string, paths []string, isFile bool) []pathVal { 193 | var pathVals []pathVal 194 | 195 | //rootWorkingDir := workingDir + "/" 196 | localPath := workingDir + "/" 197 | startingPath := "/" 198 | 199 | if len(paths) == 0 { 200 | // create appName directory if downloading whole app 201 | addPathVals := pathVal{ 202 | RootWorkingDirectoryLocal: localPath + appName + "/", 203 | StartingPathServer: startingPath, 204 | } 205 | 206 | addPathVals.RootWorkingDirectoryLocal = filepath.FromSlash(addPathVals.RootWorkingDirectoryLocal) 207 | pathVals = append(pathVals, addPathVals) 208 | } else { 209 | // append each path provided as an argument 210 | for _, v := range paths { 211 | // add trailing backslash if path is not to a file 212 | if !strings.HasSuffix(v, "/") && !isFile { 213 | v += "/" 214 | } 215 | // remove leading backslash 216 | if strings.HasPrefix(v, "/") { 217 | v = strings.TrimPrefix(v, "/") 218 | } 219 | 220 | addPathVals := pathVal{ 221 | RootWorkingDirectoryLocal: localPath + filepath.Base(v), 222 | StartingPathServer: startingPath + v, 223 | } 224 | 225 | // ensure trailing backslash is added to local root directory 226 | if !isFile { 227 | addPathVals.RootWorkingDirectoryLocal += "/" 228 | } 229 | 230 | addPathVals.RootWorkingDirectoryLocal = filepath.FromSlash(addPathVals.RootWorkingDirectoryLocal) 231 | pathVals = append(pathVals, addPathVals) 232 | } 233 | } 234 | 235 | return pathVals 236 | } 237 | 238 | /* 239 | * This function sets the flag values and determines download paths based on 240 | * input arguments. 241 | */ 242 | func ParseArgs(args []string) (flagVal, []string) { 243 | // create flagSet f1 244 | f1 := flag.NewFlagSet("f1", flag.ContinueOnError) 245 | 246 | // create flags 247 | omitp := f1.String("omit", "", "--omit path/to/some/file") 248 | overWritep := f1.Bool("overwrite", false, "--overwrite") 249 | instancep := f1.Int("i", 0, "-i [instanceNum]") 250 | verbosep := f1.Bool("verbose", false, "--verbose") 251 | filep := f1.Bool("file", false, "--file") 252 | 253 | // get paths 254 | var paths []string 255 | for i, v := range args { 256 | if strings.HasPrefix(v, "-") { 257 | break 258 | } else if i > 1 { 259 | paths = append(paths, v) 260 | } 261 | } 262 | 263 | err := f1.Parse(args[(2 + len(paths)):]) 264 | 265 | // check for misplaced flags 266 | appName = args[1] 267 | if strings.HasPrefix(appName, "-") || strings.HasPrefix(appName, "--") { 268 | fmt.Println(createMessage("\nError: App name begins with '-' or '--'. correct flag usage: 'cf download APP_NAME [--flags]'", "red+b", IsWindows())) 269 | printHelp() 270 | os.Exit(1) 271 | } 272 | 273 | // check for parsing errors, display usage 274 | if err != nil { 275 | fmt.Println("\nError: ", err, "\n") 276 | printHelp() 277 | os.Exit(1) 278 | } 279 | 280 | flagVals := flagVal{ 281 | Omit_flag: string(*omitp), 282 | OverWrite_flag: bool(*overWritep), 283 | Instance_flag: strconv.Itoa(*instancep), 284 | Verbose_flag: *verbosep, 285 | File_flag: *filep, 286 | } 287 | 288 | return flagVals, paths 289 | } 290 | 291 | /* 292 | * This function prints the current number of files downloaded. It is polled every 350 milleseconds 293 | * and disabled if the verbose flag is set to true. 294 | */ 295 | func consoleWriter(quit chan int) { 296 | count := 0 297 | for { 298 | filesDownloaded := dloader.GetFilesDownloadedCount() 299 | select { 300 | case <-quit: 301 | fmt.Println("\rFiles downloaded:", filesDownloaded, " ") 302 | return 303 | default: 304 | switch count = (count + 1) % 4; count { 305 | case 0: 306 | fmt.Printf("\rFiles downloaded: %d \\ ", filesDownloaded) 307 | case 1: 308 | fmt.Printf("\rFiles downloaded: %d | ", filesDownloaded) 309 | case 2: 310 | fmt.Printf("\rFiles downloaded: %d / ", filesDownloaded) 311 | case 3: 312 | fmt.Printf("\rFiles downloaded: %d --", filesDownloaded) 313 | } 314 | time.Sleep(350 * time.Millisecond) 315 | } 316 | } 317 | } 318 | 319 | /* 320 | * This function prints all the info you see at program finish. 321 | */ 322 | func PrintCompletionInfo(start time.Time, onWindows bool) { 323 | // let user know if any files were inaccessible 324 | fmt.Println("") 325 | if len(failedDownloads) == 1 { 326 | fmt.Println("1 file or directory was not downloaded (permissions issue or corrupt):") 327 | } else if len(failedDownloads) > 1 { 328 | fmt.Println(len(failedDownloads), "files or directories were not downloaded (permissions issue or corrupt):") 329 | } 330 | PrintSlice(failedDownloads) 331 | 332 | if len(failedDownloads) > 100 { 333 | fmt.Println("\nYou had over 100 failed downloads, we highly recommend you omit the failed file's open parent directories using the omit flag.\n") 334 | } 335 | 336 | // display runtime 337 | elapsed := time.Since(start) 338 | elapsedString := strings.Split(elapsed.String(), ".")[0] 339 | elapsedString = strings.TrimSuffix(elapsedString, ".") + "s" 340 | fmt.Println("\nDownload time: " + elapsedString) 341 | 342 | msg := ansi.Color(appName+" Successfully Downloaded!", "green+b") 343 | if onWindows == true { 344 | msg = "Successfully Downloaded!" 345 | } 346 | fmt.Println(msg) 347 | } 348 | 349 | /* 350 | * This function checks for errors and prints error messages. 351 | */ 352 | func check(e error, errMsg string) { 353 | if e != nil { 354 | fmt.Println("\nError: ", e) 355 | if errMsg != "" { 356 | fmt.Println("Message: ", errMsg) 357 | } 358 | os.Exit(1) 359 | } 360 | } 361 | 362 | /* 363 | * This function prints slices in a readable format. 364 | */ 365 | func PrintSlice(slice []string) error { 366 | for index, val := range slice { 367 | fmt.Println(index+1, ": ", val) 368 | } 369 | return nil 370 | } 371 | 372 | /* 373 | * This function returns true if the OS is Windows. 374 | */ 375 | func IsWindows() bool { 376 | return runtime.GOOS == "windows" 377 | } 378 | 379 | /* 380 | * This function returns whether the given file or directory exists locally or not. 381 | */ 382 | func Exists(path string) bool { 383 | _, err := os.Stat(path) 384 | if err == nil { 385 | return true 386 | } 387 | if os.IsNotExist(err) { 388 | return false 389 | } 390 | check(err, "Error E0.") 391 | return false 392 | } 393 | 394 | /* 395 | * This function expands given input globs into matching paths on the server. 396 | */ 397 | func ExpandGlobs(cmdExec cmd_exec.CmdExec, paths []string, instance string) []string { 398 | var newPaths []string 399 | // iterate over each input path 400 | for _, v := range paths { 401 | // check if path is a glob 402 | if strings.ContainsAny(v, "*?[]") { 403 | dir := filepath.Dir(v) 404 | out, _ := cmdExec.GetFile(appName, dir, instance) 405 | // split the body line by line 406 | body := strings.Split(string(out), "\n") 407 | // the first 3 lines contain execution info from cf files and the last 2 are blank 408 | body = body[3 : len(body)-2] 409 | 410 | //iterate over glob's directory 411 | for _, w := range body { 412 | cur := strings.SplitN(w, " ", 2)[0] 413 | match, err := filepath.Match(filepath.Base(v), strings.TrimSuffix(cur, "/")) 414 | check(err, "") 415 | // append path to return list if glob pattern matches 416 | if match && !strings.HasPrefix(cur, ".") { 417 | newPaths = append(newPaths, filepath.Dir(v)+"/"+cur) 418 | } 419 | } 420 | } else { 421 | //not a glob, append original path to return list 422 | newPaths = append(newPaths, v) 423 | } 424 | } 425 | return newPaths 426 | } 427 | 428 | /* 429 | * This function formats color coded error messages. 430 | */ 431 | func createMessage(message, color string, onWindows bool) string { 432 | errmsg := ansi.Color(message, color) 433 | if onWindows == true { 434 | errmsg = message 435 | } 436 | 437 | return errmsg 438 | } 439 | 440 | /* 441 | * This function prints the help information for the cf download command. 442 | */ 443 | func printHelp() { 444 | cmd := exec.Command("cf", "help", "download") 445 | output, _ := cmd.CombinedOutput() 446 | fmt.Printf("%s", output) 447 | } 448 | 449 | /* 450 | * This function must be implemented as part of the plugin interface defined by 451 | * the core CLI. 452 | * 453 | * GetMetadata() returns a PluginMetadata struct. The first field, Name, 454 | * determines the name of the plugin which should generally be without spaces. 455 | * If there are spaces in the name a user will need to properly quote the name 456 | * during uninstall otherwise the name will be treated as seperate arguments. 457 | * The second value is a slice of Command structs. Our slice only contains one 458 | * Command Struct, but could contain any number of them. The first field Name 459 | * defines the command `cf basic-plugin-command` once installed into the CLI. The 460 | * second field, HelpText, is used by the core CLI to display help information 461 | * to the user in the core commands `cf help`, `cf`, or `cf -h`. 462 | */ 463 | func (c *DownloadPlugin) GetMetadata() plugin.PluginMetadata { 464 | return plugin.PluginMetadata{ 465 | Name: "cf-download", 466 | Version: plugin.VersionType{ 467 | Major: 1, 468 | Minor: 2, 469 | Build: 0, 470 | }, 471 | Commands: []plugin.Command{ 472 | plugin.Command{ 473 | Name: "download", 474 | HelpText: "Download contents of a running app's file directory", 475 | 476 | // UsageDetails is optional, it is used to show help of usage of each command 477 | UsageDetails: plugin.Usage{ 478 | Usage: "cf download APP_NAME [PATH...] [--overwrite] [--file] [--verbose] [--omit ommited_paths] [-i instance_num]", 479 | Options: map[string]string{ 480 | "-overwrite": "Overwrite existing files", 481 | "-file": "Specify a file", 482 | "-verbose": "Verbose output", 483 | "-omit \"path/to/file\"": "Omit directories or files (delimited by semicolons)", 484 | "i": "Instance", 485 | }, 486 | }, 487 | }, 488 | }, 489 | } 490 | } 491 | 492 | /* 493 | * Unlike most Go programs, the `Main()` function will not be used to run all of the 494 | * commands provided in your plugin. Main will be used to initialize the plugin 495 | * process, as well as any dependencies you might require for your 496 | * plugin. 497 | */ 498 | func main() { 499 | 500 | // Any initialization for your plugin can be handled here 501 | 502 | // Note: The plugin's main() method is invoked at install time to collect 503 | // metadata. The plugin will exit 0 and the Run([]string) method will not be 504 | // invoked. 505 | 506 | // About debug Locally: 507 | // The plugin interface hides panics from stdout, so in order to get panic info, 508 | // you can run this plugin outside of the plugin architecture by setting debuglocally = true. 509 | 510 | // Example usage for local run: go run main.go download APP_NAME --overwrite 2> err.txt 511 | // Note the lack of 'cf' 512 | 513 | debugLocally := false 514 | if debugLocally { 515 | var run DownloadPlugin 516 | run.Run(nil, os.Args[1:]) 517 | } else { 518 | plugin.Start(new(DownloadPlugin)) 519 | } 520 | 521 | // Plugin code should be written in the Run([]string) method, 522 | // ensuring the plugin environment is bootstrapped. 523 | } 524 | -------------------------------------------------------------------------------- /main_suite_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestCfDownload(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "CfDownload Suite") 13 | } 14 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | . "github.com/ibmjstart/cf-download" 5 | "github.com/ibmjstart/cf-download/cmd_exec/cmd_exec_fake" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | // unit tests of individual functions 16 | var _ = Describe("CfDownload", func() { 17 | Describe("Test ParseArgs functionality", func() { 18 | 19 | Context("Check if overWrite flag works", func() { 20 | It("Should set the overwrite_flag", func() { 21 | args := [...]string{"download", "app", "app/files/htdocs", "--overwrite"} 22 | 23 | flagVals, _ := ParseArgs(args[:]) 24 | Expect(flagVals.OverWrite_flag).To(BeTrue()) 25 | Expect(flagVals.File_flag).To(BeFalse()) 26 | Expect(flagVals.Instance_flag).To(Equal("0")) 27 | Expect(flagVals.Verbose_flag).To(BeFalse()) 28 | Expect(flagVals.Omit_flag).To(Equal("")) 29 | }) 30 | }) 31 | 32 | Context("Check if file flag works", func() { 33 | It("Should set the file_flag", func() { 34 | args := [...]string{"download", "app", "--file"} 35 | 36 | flagVals, _ := ParseArgs(args[:]) 37 | Expect(flagVals.OverWrite_flag).To(BeFalse()) 38 | Expect(flagVals.File_flag).To(BeTrue()) 39 | Expect(flagVals.Instance_flag).To(Equal("0")) 40 | Expect(flagVals.Verbose_flag).To(BeFalse()) 41 | Expect(flagVals.Omit_flag).To(Equal("")) 42 | }) 43 | }) 44 | 45 | Context("Check if verbose flag works", func() { 46 | It("Should set the verbose_flag", func() { 47 | args := [...]string{"download", "app", "--verbose"} 48 | 49 | flagVals, _ := ParseArgs(args[:]) 50 | Expect(flagVals.OverWrite_flag).To(BeFalse()) 51 | Expect(flagVals.File_flag).To(BeFalse()) 52 | Expect(flagVals.Instance_flag).To(Equal("0")) 53 | Expect(flagVals.Verbose_flag).To(BeTrue()) 54 | Expect(flagVals.Omit_flag).To(Equal("")) 55 | }) 56 | }) 57 | 58 | Context("Check if instance (i) flag works", func() { 59 | It("Should set the instance_flag", func() { 60 | args := [...]string{"download", "app", "--i", "3"} 61 | 62 | flagVals, _ := ParseArgs(args[:]) 63 | Expect(flagVals.OverWrite_flag).To(BeFalse()) 64 | Expect(flagVals.File_flag).To(BeFalse()) 65 | Expect(flagVals.Instance_flag).To(Equal("3")) 66 | Expect(flagVals.Verbose_flag).To(BeFalse()) 67 | Expect(flagVals.Omit_flag).To(Equal("")) 68 | }) 69 | }) 70 | 71 | Context("Check if omit flag works", func() { 72 | It("Should set the omit_flag", func() { 73 | args := [...]string{"download", "app", "--omit", "app/node_modules"} 74 | 75 | flagVals, _ := ParseArgs(args[:]) 76 | Expect(flagVals.OverWrite_flag).To(BeFalse()) 77 | Expect(flagVals.File_flag).To(BeFalse()) 78 | Expect(flagVals.Instance_flag).To(Equal("0")) 79 | Expect(flagVals.Verbose_flag).To(BeFalse()) 80 | Expect(flagVals.Omit_flag).To(Equal("app/node_modules")) 81 | }) 82 | }) 83 | 84 | Context("Check if correct number of paths are returned", func() { 85 | It("Should return 0 paths", func() { 86 | args := [...]string{"download", "app"} 87 | 88 | _, paths := ParseArgs(args[:]) 89 | Expect(len(paths)).To(Equal(0)) 90 | }) 91 | 92 | It("Should return 1 path", func() { 93 | args := [...]string{"download", "app", "path/to/file"} 94 | 95 | _, paths := ParseArgs(args[:]) 96 | Expect(len(paths)).To(Equal(1)) 97 | }) 98 | 99 | It("Should return 2 paths", func() { 100 | args := [...]string{"download", "app", "path/to/file", "path/to/other/file"} 101 | 102 | _, paths := ParseArgs(args[:]) 103 | Expect(len(paths)).To(Equal(2)) 104 | }) 105 | }) 106 | }) 107 | 108 | Describe("test directoryContext parsing", func() { 109 | 110 | It("Should return correct strings", func() { 111 | paths := [...]string{"app/src/node"} 112 | 113 | currentDirectory, _ := os.Getwd() 114 | currentDirectory = filepath.ToSlash(currentDirectory) 115 | pathVals := GetDirectoryContext(currentDirectory, paths[:], false) 116 | 117 | correctSuffix := strings.HasSuffix(pathVals[0].RootWorkingDirectoryLocal, filepath.FromSlash("/cf-download/node/")) 118 | Expect(correctSuffix).To(BeTrue()) 119 | 120 | Expect(pathVals[0].StartingPathServer).To(Equal("/app/src/node/")) 121 | }) 122 | 123 | It("should still return /app/src/node/ for startingPath (INPUT has leading and trailing slash)", func() { 124 | paths := [...]string{"/app/src/node/"} 125 | 126 | currentDirectory, _ := os.Getwd() 127 | currentDirectory = filepath.ToSlash(currentDirectory) 128 | pathVals := GetDirectoryContext(currentDirectory, paths[:], false) 129 | 130 | correctSuffix := strings.HasSuffix(pathVals[0].RootWorkingDirectoryLocal, filepath.FromSlash("/cf-download/node/")) 131 | Expect(correctSuffix).To(BeTrue()) 132 | 133 | Expect(pathVals[0].StartingPathServer).To(Equal("/app/src/node/")) 134 | }) 135 | 136 | It("should still return /app/src/node/ for startingPath (INPUT only has trailing slash)", func() { 137 | paths := [...]string{"app/src/node/"} 138 | 139 | currentDirectory, _ := os.Getwd() 140 | currentDirectory = filepath.ToSlash(currentDirectory) 141 | pathVals := GetDirectoryContext(currentDirectory, paths[:], false) 142 | 143 | correctSuffix := strings.HasSuffix(pathVals[0].RootWorkingDirectoryLocal, filepath.FromSlash("/cf-download/node/")) 144 | Expect(correctSuffix).To(BeTrue()) 145 | 146 | Expect(pathVals[0].StartingPathServer).To(Equal("/app/src/node/")) 147 | }) 148 | 149 | It("should still return /app/src/node/ for startingPath (INPUT only has leading slash)", func() { 150 | paths := [...]string{"/app/src/node"} 151 | 152 | currentDirectory, _ := os.Getwd() 153 | currentDirectory = filepath.ToSlash(currentDirectory) 154 | pathVals := GetDirectoryContext(currentDirectory, paths[:], false) 155 | 156 | correctSuffix := strings.HasSuffix(pathVals[0].RootWorkingDirectoryLocal, filepath.FromSlash("/cf-download/node/")) 157 | Expect(correctSuffix).To(BeTrue()) 158 | 159 | Expect(pathVals[0].StartingPathServer).To(Equal("/app/src/node/")) 160 | }) 161 | 162 | It("should return /app/src/file.html for startingPath (--file flag specified)", func() { 163 | paths := [...]string{"/app/src/file.html"} 164 | 165 | currentDirectory, _ := os.Getwd() 166 | currentDirectory = filepath.ToSlash(currentDirectory) 167 | pathVals := GetDirectoryContext(currentDirectory, paths[:], true) 168 | 169 | correctSuffix := strings.HasSuffix(pathVals[0].RootWorkingDirectoryLocal, filepath.FromSlash("/cf-download/file.html")) 170 | Expect(correctSuffix).To(BeTrue()) 171 | 172 | Expect(pathVals[0].StartingPathServer).To(Equal("/app/src/file.html")) 173 | }) 174 | 175 | It("should return two staringPaths, /app/src/ and /app/logs/", func() { 176 | paths := [...]string{"/app/src/", "app/logs/"} 177 | 178 | currentDirectory, _ := os.Getwd() 179 | currentDirectory = filepath.ToSlash(currentDirectory) 180 | pathVals := GetDirectoryContext(currentDirectory, paths[:], false) 181 | 182 | correctSuffix := strings.HasSuffix(pathVals[0].RootWorkingDirectoryLocal, filepath.FromSlash("/cf-download/src/")) 183 | Expect(correctSuffix).To(BeTrue()) 184 | correctSuffix = strings.HasSuffix(pathVals[1].RootWorkingDirectoryLocal, filepath.FromSlash("/cf-download/logs/")) 185 | Expect(correctSuffix).To(BeTrue()) 186 | 187 | Expect(pathVals[0].StartingPathServer).To(Equal("/app/src/")) 188 | Expect(pathVals[1].StartingPathServer).To(Equal("/app/logs/")) 189 | }) 190 | 191 | }) 192 | 193 | Describe("test expandGlobs parsing", func() { 194 | It("should return x.txt, y.txt, a.go and ab.go", func() { 195 | cmdExec := cmd_exec_fake.NewCmdExec() 196 | cmdExec.SetOutput("Getting files for app Test in org test / space dev as user...\nOK\n\nxyz.txt 220B\na.go 675B\nab.go 333B\nyz.go 123B\n\n") 197 | cmdExec.SetFakeDir(false) 198 | 199 | paths := make([]string, 1) 200 | paths[0] = "*.txt" 201 | paths = ExpandGlobs(cmdExec, paths, "0") 202 | Expect(paths[0]).To(Equal("./xyz.txt")) 203 | 204 | paths[0] = "?.go" 205 | paths = ExpandGlobs(cmdExec, paths, "0") 206 | Expect(paths[0]).To(Equal("./a.go")) 207 | 208 | paths[0] = "[a-z]b.go" 209 | paths = ExpandGlobs(cmdExec, paths, "0") 210 | Expect(paths[0]).To(Equal("./ab.go")) 211 | }) 212 | }) 213 | 214 | Describe("test error catching in run() [MUST HAVE PLUGIN INSTALLED TO PASS]", func() { 215 | Context("when appname begins with -- or -", func() { 216 | It("Should print error, because user has flags before appname", func() { 217 | cmd := exec.Command("cf", "download", "--appname") 218 | output, _ := cmd.CombinedOutput() 219 | Expect(strings.Contains(string(output), "Error: App name begins with '-' or '--'. correct flag usage: 'cf download APP_NAME [--flags]'")).To(BeTrue()) 220 | }) 221 | 222 | It("Should print error, because user not specified an appName", func() { 223 | cmd := exec.Command("cf", "download") 224 | output, _ := cmd.CombinedOutput() 225 | Expect(strings.Contains(string(output), "Error: Missing App Name")).To(BeTrue()) 226 | }) 227 | 228 | It("Should print error, test overwrite flag functionality", func() { 229 | // create directory that needs to be overwritten 230 | os.Mkdir("test", 755) 231 | 232 | cmd := exec.Command("cf", "download", "test") 233 | output, _ := cmd.CombinedOutput() 234 | 235 | // clean up 236 | os.RemoveAll("test") 237 | 238 | Expect(strings.Contains(string(output), "already exists.")).To(BeTrue()) 239 | }) 240 | 241 | It("Should print error, instance flag not int", func() { 242 | cmd := exec.Command("cf", "download", "test", "-i", "hello") 243 | output, _ := cmd.CombinedOutput() 244 | Expect(strings.Contains(string(output), "Error: invalid value ")).To(BeTrue()) 245 | }) 246 | 247 | It("Should print error, invalid flag", func() { 248 | cmd := exec.Command("cf", "download", "test", "-ooverwrite") 249 | output, _ := cmd.CombinedOutput() 250 | Expect(strings.Contains(string(output), "Error: flag provided but not defined: -ooverwrite")).To(BeTrue()) 251 | }) 252 | }) 253 | }) 254 | 255 | }) 256 | --------------------------------------------------------------------------------