@yetech/estra7ah 3.3.0 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/estra7ah.js +168 -26
- package/package.json +1 -1
package/dist/estra7ah.js
CHANGED
|
@@ -36,6 +36,166 @@ function getRunner() {
|
|
|
36
36
|
throw new Error("Unsupported platform");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// The VPS caps throughput per TCP connection (~2.5 MB/s), not in total, so
|
|
40
|
+
// pulling the binary over several range requests at once is ~N times faster.
|
|
41
|
+
const PARALLEL_CHUNKS = 6;
|
|
42
|
+
|
|
43
|
+
function downloadRange(url, fd, start, end, progressBar, attempt = 0) {
|
|
44
|
+
return axios({
|
|
45
|
+
method: "get",
|
|
46
|
+
url,
|
|
47
|
+
responseType: "stream",
|
|
48
|
+
headers: { Range: `bytes=${start}-${end}` },
|
|
49
|
+
timeout: 0,
|
|
50
|
+
})
|
|
51
|
+
.then(
|
|
52
|
+
(response) =>
|
|
53
|
+
new Promise((resolve, reject) => {
|
|
54
|
+
// A 200 means the server (or a proxy on this plain-HTTP route) ignored
|
|
55
|
+
// the Range header and is sending the whole file: writing that at six
|
|
56
|
+
// different offsets corrupts the binary. Bail out to single-stream.
|
|
57
|
+
if (response.status !== 206) {
|
|
58
|
+
response.data.destroy();
|
|
59
|
+
return reject({ noRangeSupport: true });
|
|
60
|
+
}
|
|
61
|
+
let position = start;
|
|
62
|
+
// With timeout: 0 a dead connection would hang the update forever
|
|
63
|
+
// (progress stuck at 0%). Kill the stream if no data arrives for 30s
|
|
64
|
+
// so the retry/fallback path can take over.
|
|
65
|
+
let watchdog;
|
|
66
|
+
const resetWatchdog = () => {
|
|
67
|
+
clearTimeout(watchdog);
|
|
68
|
+
watchdog = setTimeout(() => {
|
|
69
|
+
response.data.destroy(new Error("download stalled: no data for 30s"));
|
|
70
|
+
}, 30000);
|
|
71
|
+
};
|
|
72
|
+
resetWatchdog();
|
|
73
|
+
response.data.on("close", () => clearTimeout(watchdog));
|
|
74
|
+
response.data.on("data", (chunk) => {
|
|
75
|
+
resetWatchdog();
|
|
76
|
+
fs.writeSync(fd, chunk, 0, chunk.length, position);
|
|
77
|
+
position += chunk.length;
|
|
78
|
+
progressBar.tick(chunk.length);
|
|
79
|
+
});
|
|
80
|
+
response.data.on("end", () => {
|
|
81
|
+
// "end" also fires on some premature disconnects; an incomplete
|
|
82
|
+
// chunk must be retried, never treated as success.
|
|
83
|
+
if (position !== end + 1) {
|
|
84
|
+
return reject({ err: new Error(`chunk ended at ${position}, expected ${end + 1}`), position });
|
|
85
|
+
}
|
|
86
|
+
resolve(position);
|
|
87
|
+
});
|
|
88
|
+
response.data.on("aborted", () => reject({ err: new Error("connection aborted"), position }));
|
|
89
|
+
response.data.on("error", (err) => reject({ err, position }));
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
.catch((failure) => {
|
|
93
|
+
if (failure && failure.noRangeSupport) throw failure;
|
|
94
|
+
const resumeAt = failure.position || start;
|
|
95
|
+
if (attempt < 2) return downloadRange(url, fd, resumeAt, end, progressBar, attempt + 1);
|
|
96
|
+
throw failure.err || failure;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function downloadFileFast(url, destPath) {
|
|
101
|
+
let totalLength = 0;
|
|
102
|
+
try {
|
|
103
|
+
const head = await axios.head(url, { timeout: 10000 });
|
|
104
|
+
if ((head.headers["accept-ranges"] || "").includes("bytes")) {
|
|
105
|
+
totalLength = parseInt(head.headers["content-length"], 10) || 0;
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
// fall through to single-stream download
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!totalLength) return downloadFileStream(url, destPath);
|
|
112
|
+
|
|
113
|
+
const progressBar = new ProgressBar("Downloading [:bar] :percent :etas", {
|
|
114
|
+
width: 40,
|
|
115
|
+
complete: "=",
|
|
116
|
+
incomplete: " ",
|
|
117
|
+
total: totalLength,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Download to a temp file so a failed/interrupted update never corrupts
|
|
121
|
+
// the runner binary we already have.
|
|
122
|
+
const tmpPath = `${destPath}.download`;
|
|
123
|
+
const fd = fs.openSync(tmpPath, "w");
|
|
124
|
+
try {
|
|
125
|
+
fs.ftruncateSync(fd, totalLength);
|
|
126
|
+
const chunkSize = Math.ceil(totalLength / PARALLEL_CHUNKS);
|
|
127
|
+
const jobs = [];
|
|
128
|
+
for (let start = 0; start < totalLength; start += chunkSize) {
|
|
129
|
+
const end = Math.min(start + chunkSize - 1, totalLength - 1);
|
|
130
|
+
jobs.push(downloadRange(url, fd, start, end, progressBar));
|
|
131
|
+
}
|
|
132
|
+
await Promise.all(jobs);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
fs.closeSync(fd);
|
|
135
|
+
try {
|
|
136
|
+
fs.unlinkSync(tmpPath);
|
|
137
|
+
} catch (e) {}
|
|
138
|
+
if (error && error.noRangeSupport) return downloadFileStream(url, destPath);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
fs.closeSync(fd);
|
|
142
|
+
if (fs.statSync(tmpPath).size !== totalLength) {
|
|
143
|
+
fs.unlinkSync(tmpPath);
|
|
144
|
+
throw new Error("downloaded size does not match content-length");
|
|
145
|
+
}
|
|
146
|
+
fs.renameSync(tmpPath, destPath);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function downloadFileStream(url, destPath) {
|
|
150
|
+
return axios({ method: "get", url, responseType: "stream" }).then((response) => {
|
|
151
|
+
const totalLength = parseInt(response.headers["content-length"], 10);
|
|
152
|
+
const progressBar = totalLength
|
|
153
|
+
? new ProgressBar("Downloading [:bar] :percent :etas", {
|
|
154
|
+
width: 40,
|
|
155
|
+
complete: "=",
|
|
156
|
+
incomplete: " ",
|
|
157
|
+
total: totalLength,
|
|
158
|
+
})
|
|
159
|
+
: null;
|
|
160
|
+
const tmpPath = `${destPath}.download`;
|
|
161
|
+
const writer = fs.createWriteStream(tmpPath);
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
let watchdog;
|
|
164
|
+
const fail = (err) => {
|
|
165
|
+
clearTimeout(watchdog);
|
|
166
|
+
writer.destroy();
|
|
167
|
+
try {
|
|
168
|
+
fs.unlinkSync(tmpPath);
|
|
169
|
+
} catch (e) {}
|
|
170
|
+
reject(err);
|
|
171
|
+
};
|
|
172
|
+
const resetWatchdog = () => {
|
|
173
|
+
clearTimeout(watchdog);
|
|
174
|
+
watchdog = setTimeout(() => {
|
|
175
|
+
response.data.destroy(new Error("download stalled: no data for 30s"));
|
|
176
|
+
}, 30000);
|
|
177
|
+
};
|
|
178
|
+
resetWatchdog();
|
|
179
|
+
response.data.on("data", (chunk) => {
|
|
180
|
+
resetWatchdog();
|
|
181
|
+
if (progressBar) progressBar.tick(chunk.length);
|
|
182
|
+
});
|
|
183
|
+
writer.on("error", fail);
|
|
184
|
+
writer.on("finish", () => {
|
|
185
|
+
clearTimeout(watchdog);
|
|
186
|
+
if (totalLength && fs.statSync(tmpPath).size !== totalLength) {
|
|
187
|
+
return fail(new Error("downloaded size does not match content-length"));
|
|
188
|
+
}
|
|
189
|
+
fs.renameSync(tmpPath, destPath);
|
|
190
|
+
resolve();
|
|
191
|
+
});
|
|
192
|
+
response.data.on("aborted", () => fail(new Error("connection aborted")));
|
|
193
|
+
response.data.on("error", fail);
|
|
194
|
+
response.data.pipe(writer);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
39
199
|
// Download the runner executable
|
|
40
200
|
async function downloadRunner() {
|
|
41
201
|
let version = null;
|
|
@@ -60,32 +220,14 @@ async function downloadRunner() {
|
|
|
60
220
|
const runnerPath = `${DIR_NAME}/${getRunner()}`;
|
|
61
221
|
|
|
62
222
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
width: 40,
|
|
72
|
-
complete: "=",
|
|
73
|
-
incomplete: " ",
|
|
74
|
-
total: totalLength,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const writer = fs.createWriteStream(runnerPath);
|
|
78
|
-
|
|
79
|
-
return new Promise((resolve, reject) => {
|
|
80
|
-
response.data.on("data", (chunk) => progressBar.tick(chunk.length));
|
|
81
|
-
writer.on("error", reject);
|
|
82
|
-
writer.on("finish", () => {
|
|
83
|
-
fs.writeFileSync(versionFilePath, version);
|
|
84
|
-
console.log("** Finished DOWNLOADING NEW RUNNER VERSION **");
|
|
85
|
-
resolve();
|
|
86
|
-
});
|
|
87
|
-
response.data.pipe(writer);
|
|
88
|
-
});
|
|
223
|
+
try {
|
|
224
|
+
await downloadFileFast(serverFile, runnerPath);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error("Parallel download failed, retrying with a single stream:", error.message);
|
|
227
|
+
await downloadFileStream(serverFile, runnerPath);
|
|
228
|
+
}
|
|
229
|
+
fs.writeFileSync(versionFilePath, version);
|
|
230
|
+
console.log("** Finished DOWNLOADING NEW RUNNER VERSION **");
|
|
89
231
|
} catch (error) {
|
|
90
232
|
console.error("Error downloading runner:", error.message);
|
|
91
233
|
}
|