@torrent-tv/proxy 2.101.20 → 2.101.21
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/package.json +1 -1
- package/routes/stream/get.js +86 -48
package/package.json
CHANGED
package/routes/stream/get.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { open } from "node:fs/promises";
|
|
10
|
+
import { Readable } from "node:stream";
|
|
10
11
|
import { parseRange } from "../../utils/parse-range.js";
|
|
11
12
|
import { logger } from "../../utils/logger.js";
|
|
12
13
|
|
|
@@ -109,49 +110,66 @@ function wholeFileFor(torrentPool, sourceKey, fileIndex) {
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
/**
|
|
112
|
-
*
|
|
113
|
+
* Acquire the whole file a response can continue from, if there is one.
|
|
113
114
|
*
|
|
114
|
-
* Any failure to
|
|
115
|
+
* Any failure to acquire it leaves the response on the torrent, which can still
|
|
115
116
|
* answer; a file the disk owner removed is also forgotten here, as the route
|
|
116
117
|
* does before headers.
|
|
117
118
|
*
|
|
118
|
-
* @param {{ wholeFiles?: Map<string, { path: string, length: number, name: string }> }} torrentPool
|
|
119
|
+
* @param {{ wholeFiles?: Map<string, { path: string, length: number, name: string }>, acquireWholeFile: (source: { sourceKey: string, fileIndex: number }) => Promise<{ length: number, stream: (start: number, end: number, onRead: (length: number) => void) => import("node:stream").Readable | null, release: () => Promise<unknown> } | null> }} torrentPool
|
|
119
120
|
* @param {string} sourceKey
|
|
120
121
|
* @param {number} fileIndex
|
|
121
122
|
* @param {number} length - The length the response was promised for.
|
|
122
|
-
* @returns {Promise<import("node:
|
|
123
|
+
* @returns {Promise<{ length: number, stream: (start: number, end: number, onRead: (length: number) => void) => import("node:stream").Readable | null, release: () => Promise<unknown> } | null>}
|
|
123
124
|
*/
|
|
124
|
-
async function
|
|
125
|
+
async function acquireWholeFileRead(torrentPool, sourceKey, fileIndex, length) {
|
|
125
126
|
const whole = wholeFileFor(torrentPool, sourceKey, fileIndex);
|
|
126
127
|
if (!whole || whole.length !== length) {
|
|
127
128
|
return null;
|
|
128
129
|
}
|
|
129
130
|
try {
|
|
130
|
-
|
|
131
|
+
const read = await torrentPool.acquireWholeFile({ sourceKey, fileIndex });
|
|
132
|
+
if (!read) {
|
|
133
|
+
forgetWholeFile(torrentPool, sourceKey, fileIndex, whole);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (read.length !== length) {
|
|
137
|
+
await read.release();
|
|
138
|
+
forgetWholeFile(torrentPool, sourceKey, fileIndex, whole);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return read;
|
|
131
142
|
} catch (error) {
|
|
132
143
|
if (error.code === "ENOENT") {
|
|
133
|
-
|
|
134
|
-
if (torrentPool.wholeFiles.get(key) === whole) torrentPool.wholeFiles.delete(key);
|
|
144
|
+
forgetWholeFile(torrentPool, sourceKey, fileIndex, whole);
|
|
135
145
|
}
|
|
136
146
|
return null;
|
|
137
147
|
}
|
|
138
148
|
}
|
|
139
149
|
|
|
150
|
+
function forgetWholeFile(torrentPool, sourceKey, fileIndex, whole) {
|
|
151
|
+
const key = `${sourceKey.slice("torrent:".length).toLowerCase()}/${fileIndex}`;
|
|
152
|
+
if (torrentPool.wholeFiles?.get(key) === whole) torrentPool.wholeFiles.delete(key);
|
|
153
|
+
}
|
|
154
|
+
|
|
140
155
|
/**
|
|
141
156
|
* Write bytes `from`..`end` of an open file to a response, each write waited
|
|
142
157
|
* for, and close the file.
|
|
143
158
|
*
|
|
144
159
|
* @param {import("node:http").ServerResponse} raw
|
|
145
|
-
* @param {import("node:
|
|
160
|
+
* @param {{ stream: (start: number, end: number, onRead: (length: number) => void) => import("node:stream").Readable | null, release: () => Promise<unknown> }} read
|
|
146
161
|
* @param {number} from - Inclusive.
|
|
147
162
|
* @param {number} end - Inclusive.
|
|
148
163
|
* @param {(length: number) => void} onWritten
|
|
149
164
|
* @returns {Promise<number>} Bytes written.
|
|
150
165
|
*/
|
|
151
|
-
async function writeFromWholeFile(raw,
|
|
166
|
+
async function writeFromWholeFile(raw, read, from, end, onWritten) {
|
|
152
167
|
let written = 0;
|
|
168
|
+
let stream = null;
|
|
153
169
|
try {
|
|
154
|
-
|
|
170
|
+
stream = read.stream(from, end, () => {});
|
|
171
|
+
if (!stream) throw new Error("Completed file did not provide the admitted range.");
|
|
172
|
+
for await (const chunk of stream) {
|
|
155
173
|
if (raw.writableEnded || raw.destroyed) {
|
|
156
174
|
break;
|
|
157
175
|
}
|
|
@@ -162,7 +180,8 @@ async function writeFromWholeFile(raw, handle, from, end, onWritten) {
|
|
|
162
180
|
onWritten(chunk.length);
|
|
163
181
|
}
|
|
164
182
|
} finally {
|
|
165
|
-
|
|
183
|
+
stream?.destroy();
|
|
184
|
+
await read.release();
|
|
166
185
|
}
|
|
167
186
|
return written;
|
|
168
187
|
}
|
|
@@ -173,35 +192,41 @@ async function writeFromWholeFile(raw, handle, from, end, onWritten) {
|
|
|
173
192
|
* @param {import("fastify").FastifyRequest} req
|
|
174
193
|
* @param {import("fastify").FastifyReply} reply
|
|
175
194
|
* @param {{ path: string, length: number, name: string }} file
|
|
176
|
-
* @param {import("node:
|
|
195
|
+
* @param {{ stream: (start: number, end: number, onRead: (length: number) => void) => import("node:stream").Readable | null, release: () => Promise<unknown> }} read
|
|
177
196
|
* @returns {Promise<void> | void}
|
|
178
197
|
*/
|
|
179
|
-
function serveWholeFile(req, reply, file,
|
|
198
|
+
function serveWholeFile(req, reply, file, read) {
|
|
180
199
|
const disposition = `inline; filename*=UTF-8''${encodeURIComponent(file.name)}`;
|
|
181
|
-
if (req.method === "HEAD") {
|
|
182
|
-
reply.hijack();
|
|
183
|
-
reply.raw.writeHead(200, {
|
|
184
|
-
"Accept-Ranges": "bytes",
|
|
185
|
-
"Content-Type": "application/octet-stream",
|
|
186
|
-
"Content-Length": String(file.length),
|
|
187
|
-
"Content-Disposition": disposition
|
|
188
|
-
});
|
|
189
|
-
reply.raw.end();
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
200
|
reply.header("Accept-Ranges", "bytes");
|
|
193
201
|
reply.header("Content-Type", "application/octet-stream");
|
|
194
202
|
reply.header("Content-Disposition", disposition);
|
|
195
203
|
const range = parseRange(req.headers.range, file.length);
|
|
204
|
+
if (file.length === 0) {
|
|
205
|
+
reply.header("Content-Length", "0");
|
|
206
|
+
void read.release().catch(() => undefined);
|
|
207
|
+
return reply.send(Readable.from([]));
|
|
208
|
+
}
|
|
209
|
+
const start = range?.start ?? 0;
|
|
210
|
+
const end = range?.end ?? file.length - 1;
|
|
211
|
+
const stream = read.stream(start, end, () => {});
|
|
212
|
+
if (!stream) {
|
|
213
|
+
void read.release().catch(() => undefined);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
196
216
|
if (!range) {
|
|
197
217
|
reply.header("Content-Length", String(file.length));
|
|
198
|
-
|
|
218
|
+
} else {
|
|
219
|
+
reply.code(206);
|
|
220
|
+
reply.header("Content-Length", String(end - start + 1));
|
|
221
|
+
reply.header("Content-Range", `bytes ${start}-${end}/${file.length}`);
|
|
222
|
+
}
|
|
223
|
+
void read.release().catch(() => undefined);
|
|
224
|
+
try {
|
|
225
|
+
return reply.send(stream);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
stream.destroy();
|
|
228
|
+
throw error;
|
|
199
229
|
}
|
|
200
|
-
const { start, end } = range;
|
|
201
|
-
reply.code(206);
|
|
202
|
-
reply.header("Content-Length", String(end - start + 1));
|
|
203
|
-
reply.header("Content-Range", `bytes ${start}-${end}/${file.length}`);
|
|
204
|
-
return reply.send(handle.createReadStream({ start, end }));
|
|
205
230
|
}
|
|
206
231
|
|
|
207
232
|
export async function handleStreamGet(req, reply, { sourceRegistry, torrentPool, noteInputBytes = null, noteInputWaiting = null }) {
|
|
@@ -224,23 +249,36 @@ export async function handleStreamGet(req, reply, { sourceRegistry, torrentPool,
|
|
|
224
249
|
// nothing the torrent would have told us.
|
|
225
250
|
const whole = wholeFileFor(torrentPool, sourceKey, fileIndex);
|
|
226
251
|
if (whole) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// Open before committing headers: the disk owner may have removed an
|
|
230
|
-
// announced file. An open descriptor also preserves a live POSIX read.
|
|
231
|
-
handle = await open(whole.path, "r");
|
|
232
|
-
} catch (error) {
|
|
233
|
-
if (error.code !== "ENOENT") throw error;
|
|
234
|
-
const key = `${sourceKey.slice("torrent:".length).toLowerCase()}/${fileIndex}`;
|
|
235
|
-
if (torrentPool.wholeFiles.get(key) === whole) torrentPool.wholeFiles.delete(key);
|
|
236
|
-
}
|
|
237
|
-
if (handle) {
|
|
238
|
-
if (req.method === "HEAD") await handle.close();
|
|
252
|
+
if (req.method === "HEAD") {
|
|
253
|
+
let handle;
|
|
239
254
|
try {
|
|
240
|
-
|
|
255
|
+
handle = await open(whole.path, "r");
|
|
241
256
|
} catch (error) {
|
|
242
|
-
|
|
243
|
-
|
|
257
|
+
if (error.code !== "ENOENT") throw error;
|
|
258
|
+
forgetWholeFile(torrentPool, sourceKey, fileIndex, whole);
|
|
259
|
+
}
|
|
260
|
+
if (handle) {
|
|
261
|
+
await handle.close();
|
|
262
|
+
reply.hijack();
|
|
263
|
+
reply.raw.writeHead(200, {
|
|
264
|
+
"Accept-Ranges": "bytes",
|
|
265
|
+
"Content-Type": "application/octet-stream",
|
|
266
|
+
"Content-Length": String(whole.length),
|
|
267
|
+
"Content-Disposition": `inline; filename*=UTF-8''${encodeURIComponent(whole.name)}`
|
|
268
|
+
});
|
|
269
|
+
reply.raw.end();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
const read = await acquireWholeFileRead(torrentPool, sourceKey, fileIndex, whole.length);
|
|
274
|
+
if (read) {
|
|
275
|
+
try {
|
|
276
|
+
const sent = serveWholeFile(req, reply, whole, read);
|
|
277
|
+
if (sent) return sent;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
await read.release().catch(() => undefined);
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
244
282
|
}
|
|
245
283
|
}
|
|
246
284
|
}
|
|
@@ -338,7 +376,7 @@ export async function handleStreamGet(req, reply, { sourceRegistry, torrentPool,
|
|
|
338
376
|
fragment.release();
|
|
339
377
|
break;
|
|
340
378
|
}
|
|
341
|
-
continueFrom = await
|
|
379
|
+
continueFrom = await acquireWholeFileRead(torrentPool, sourceKey, fileIndex, file.length);
|
|
342
380
|
if (continueFrom) {
|
|
343
381
|
fragment.release();
|
|
344
382
|
break;
|
|
@@ -368,7 +406,7 @@ export async function handleStreamGet(req, reply, { sourceRegistry, torrentPool,
|
|
|
368
406
|
}
|
|
369
407
|
} catch (error) {
|
|
370
408
|
if (sent >= contentLength || req.raw.aborted || reply.raw.destroyed) throw error;
|
|
371
|
-
continueFrom = await
|
|
409
|
+
continueFrom = await acquireWholeFileRead(torrentPool, sourceKey, fileIndex, file.length);
|
|
372
410
|
if (!continueFrom) throw error;
|
|
373
411
|
}
|
|
374
412
|
if (continueFrom) {
|