|
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