@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.
Files changed (2) hide show
  1. package/dist/estra7ah.js +168 -26
  2. 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
- const response = await axios({
64
- method: "get",
65
- url: serverFile,
66
- responseType: "stream",
67
- });
68
-
69
- const totalLength = parseInt(response.headers["content-length"], 10);
70
- const progressBar = new ProgressBar("Downloading [:bar] :percent :etas", {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yetech/estra7ah",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "Estra7ah Manager",
5
5
  "author": "Abdullah Al-taheri",
6
6
  "keywords": [