Repository: actions/toolkit (ts)

Issue:
@actions/tool-cache downloadTool does not save file with file extension
I am trying to write a GitHub action tool to download & setup Nuget.exe however by passing the URL

`const nugetPath = await tc.downloadTool("https://dist.nuget.org/win-x86-commandline/latest/nuget.exe");`

This saves the file as something like this
`temp/b438b0f2-af22-456a-8a77-ddc4152465b9` minus the .exe file extension

I could workaround it by manually renaming it with using NodeJS fs APIs but this feels a bit wrong to me.


Pull request description:
overload downloadTool to accept destination path
fixes #60 

Patch:
diff --git a/packages/tool-cache/src/tool-cache.ts b/packages/tool-cache/src/tool-cache.ts
index b0b695d..972eea0 100644
--- a/packages/tool-cache/src/tool-cache.ts
+++ b/packages/tool-cache/src/tool-cache.ts
@@ -48,9 +48,13 @@ if (!tempDirectory || !cacheRoot) {
  * Download a tool from an url and stream it into a file
  *
  * @param url       url of tool to download
+ * @param dest      path to download tool
  * @returns         path to downloaded tool
  */
-export async function downloadTool(url: string): Promise<string> {
+export async function downloadTool(
+  url: string,
+  dest?: string
+): Promise<string> {
   // Wrap in a promise so that we can resolve from within stream callbacks
   return new Promise<string>(async (resolve, reject) => {
     try {
@@ -58,14 +62,13 @@ export async function downloadTool(url: string): Promise<string> {
         allowRetries: true,
         maxRetries: 3
       })
-      const destPath = path.join(tempDirectory, uuidV4())
-
-      await io.mkdirP(tempDirectory)
+      dest = dest || path.join(tempDirectory, uuidV4())
+      await io.mkdirP(path.dirname(dest))
       core.debug(`Downloading ${url}`)
-      core.debug(`Downloading ${destPath}`)
+      core.debug(`Downloading ${dest}`)
 
-      if (fs.existsSync(destPath)) {
-        throw new Error(`Destination file path ${destPath} already exists`)
+      if (fs.existsSync(dest)) {
+        throw new Error(`Destination file path ${dest} already exists`)
       }
 
       const response: httpm.HttpClientResponse = await http.get(url)
@@ -80,13 +83,13 @@ export async function downloadTool(url: string): Promise<string> {
         throw err
       }
 
-      const file: NodeJS.WritableStream = fs.createWriteStream(destPath)
+      const file: NodeJS.WritableStream = fs.createWriteStream(dest)
       file.on('open', async () => {
         try {
           const stream = response.message.pipe(file)
           stream.on('close', () => {
             core.debug('download complete')
-            resolve(destPath)
+            resolve(dest)
           })
         } catch (err) {
           core.debug(


Test patch:
diff --git a/packages/tool-cache/__tests__/tool-cache.test.ts b/packages/tool-cache/__tests__/tool-cache.test.ts
index d316fe4..be176f1 100644
--- a/packages/tool-cache/__tests__/tool-cache.test.ts
+++ b/packages/tool-cache/__tests__/tool-cache.test.ts
@@ -47,6 +47,47 @@ describe('@actions/tool-cache', function() {
     expect(fs.statSync(downPath).size).toBe(35)
   })
 
+  it('downloads a 35 byte file (dest)', async () => {
+    const dest = 'test-download-file'
+    try {
+      const downPath: string = await tc.downloadTool(
+        'http://example.com/bytes/35',
+        dest
+      )
+
+      expect(downPath).toEqual(dest)
+      expect(fs.existsSync(downPath)).toBeTruthy()
+      expect(fs.statSync(downPath).size).toBe(35)
+    } finally {
+      try {
+        await fs.promises.unlink(dest)
+      } catch {
+        // intentionally empty
+      }
+    }
+  })
+
+  it('downloads a 35 byte file (dest requires mkdirp)', async () => {
+    const dest = 'test-download-dir/test-download-file'
+    try {
+      const downPath: string = await tc.downloadTool(
+        'http://example.com/bytes/35',
+        dest
+      )
+
+      expect(downPath).toEqual(dest)
+      expect(fs.existsSync(downPath)).toBeTruthy()
+      expect(fs.statSync(downPath).size).toBe(35)
+    } finally {
+      try {
+        await fs.promises.unlink(dest)
+        await fs.promises.rmdir(path.dirname(dest))
+      } catch {
+        // intentionally empty
+      }
+    }
+  })
+
   it('downloads a 35 byte file after a redirect', async () => {
     nock('http://example.com')
       .get('/redirect-to')