|
1 | 1 | import fetch from "node-fetch"; |
2 | 2 | import * as vscode from "vscode"; |
3 | | -import * as fs from "fs"; |
4 | 3 | import * as stream from "stream"; |
| 4 | +import * as fs from "fs"; |
| 5 | +import * as os from "os"; |
| 6 | +import * as path from "path"; |
5 | 7 | import * as util from "util"; |
6 | 8 | import { log, assert } from "./util"; |
7 | 9 |
|
@@ -87,7 +89,7 @@ export async function download( |
87 | 89 | } |
88 | 90 |
|
89 | 91 | /** |
90 | | - * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. |
| 92 | + * Downloads file from `url` and stores it at `destFilePath` with `mode` (unix permissions). |
91 | 93 | * `onProgress` callback is called on recieveing each chunk of bytes |
92 | 94 | * to track the progress of downloading, it gets the already read and total |
93 | 95 | * amount of bytes to read as its parameters. |
@@ -118,13 +120,46 @@ async function downloadFile( |
118 | 120 | onProgress(readBytes, totalBytes); |
119 | 121 | }); |
120 | 122 |
|
121 | | - const destFileStream = fs.createWriteStream(destFilePath, { mode }); |
122 | | - |
123 | | - await pipeline(res.body, destFileStream); |
124 | | - return new Promise<void>(resolve => { |
125 | | - destFileStream.on("close", resolve); |
126 | | - destFileStream.destroy(); |
127 | | - // This workaround is awaiting to be removed when vscode moves to newer nodejs version: |
128 | | - // https://github.com/rust-analyzer/rust-analyzer/issues/3167 |
| 123 | + // Put the artifact into a temporary folder to prevent partially downloaded files when user kills vscode |
| 124 | + await withTempFile(async tempFilePath => { |
| 125 | + const destFileStream = fs.createWriteStream(tempFilePath, { mode }); |
| 126 | + await pipeline(res.body, destFileStream); |
| 127 | + await new Promise<void>(resolve => { |
| 128 | + destFileStream.on("close", resolve); |
| 129 | + destFileStream.destroy(); |
| 130 | + // This workaround is awaiting to be removed when vscode moves to newer nodejs version: |
| 131 | + // https://github.com/rust-analyzer/rust-analyzer/issues/3167 |
| 132 | + }); |
| 133 | + await moveFile(tempFilePath, destFilePath); |
129 | 134 | }); |
130 | 135 | } |
| 136 | + |
| 137 | +async function withTempFile(scope: (tempFilePath: string) => Promise<void>) { |
| 138 | + // Based on the great article: https://advancedweb.hu/secure-tempfiles-in-nodejs-without-dependencies/ |
| 139 | + |
| 140 | + // `.realpath()` should handle the cases where os.tmpdir() contains symlinks |
| 141 | + const osTempDir = await fs.promises.realpath(os.tmpdir()); |
| 142 | + |
| 143 | + const tempDir = await fs.promises.mkdtemp(path.join(osTempDir, "rust-analyzer")); |
| 144 | + |
| 145 | + try { |
| 146 | + return await scope(path.join(tempDir, "file")); |
| 147 | + } finally { |
| 148 | + // We are good citizens :D |
| 149 | + void fs.promises.rmdir(tempDir, { recursive: true }).catch(log.error); |
| 150 | + } |
| 151 | +}; |
| 152 | + |
| 153 | +async function moveFile(src: fs.PathLike, dest: fs.PathLike) { |
| 154 | + try { |
| 155 | + await fs.promises.rename(src, dest); |
| 156 | + } catch (err) { |
| 157 | + if (err.code === 'EXDEV') { |
| 158 | + // We are probably moving the file across partitions/devices |
| 159 | + await fs.promises.copyFile(src, dest); |
| 160 | + await fs.promises.unlink(src); |
| 161 | + } else { |
| 162 | + log.error(`Failed to rename the file ${src} -> ${dest}`, err); |
| 163 | + } |
| 164 | + } |
| 165 | +} |
0 commit comments