upppppppp1 commited on
Commit
28d7780
·
verified ·
1 Parent(s): b4440d6

Create index.js

Browse files
Files changed (1) hide show
  1. index.js +363 -0
index.js ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import crc32 from './crc32.js'
2
+ import http from 'axios'
3
+ import express from 'express'
4
+
5
+ const app = express()
6
+ const port = 7860
7
+
8
+ app.use(express.json())
9
+ app.use(express.urlencoded({ extended: true }))
10
+
11
+ // 带取消令牌的请求函数
12
+ const createRequestWithCancel = (cancelToken) => {
13
+ return {
14
+ get: (url, config = {}) => http.get(url, { ...config, cancelToken }),
15
+ post: (url, data, config = {}) => http.post(url, data, { ...config, cancelToken }),
16
+ head: (url, config = {}) => http.head(url, { ...config, cancelToken })
17
+ }
18
+ }
19
+
20
+ // 带重试功能的分片下载函数
21
+ async function downloadChunkWithRetry(request, finalUrl, start, end, partNumber, maxRetries = 30) {
22
+ let lastError = null
23
+
24
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
25
+ try {
26
+ const response = await request.get(finalUrl, {
27
+ headers: {
28
+ 'Range': `bytes=${start}-${end}`
29
+ },
30
+ responseType: 'arraybuffer'
31
+ })
32
+
33
+ if (response.status !== 206 && response.status !== 200) {
34
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
35
+ }
36
+
37
+ const arrayBuffer = response.data
38
+
39
+ if (arrayBuffer.byteLength !== (end - start + 1)) {
40
+ throw new Error(`分片大小不匹配: 期望 ${end - start + 1} 字节, 实际 ${arrayBuffer.byteLength} 字节`)
41
+ }
42
+
43
+ console.log(`分片 ${partNumber} 下载成功 (尝试 ${attempt})`)
44
+ return arrayBuffer
45
+
46
+ } catch (error) {
47
+ // 如果是取消请求,直接抛出
48
+ if (http.isCancel(error)) {
49
+ throw error
50
+ }
51
+
52
+ lastError = error
53
+ console.warn(`分片 ${partNumber} 下载失败 (尝试 ${attempt}/${maxRetries}):`, error.message)
54
+
55
+ if (attempt < maxRetries) {
56
+ // 指数退避策略,等待时间逐渐增加
57
+ // const waitTime = Math.min(500 * Math.pow(2, attempt - 1), 5000)
58
+ const waitTime = 300
59
+ console.log(`等待 ${waitTime}ms 后重试分片 ${partNumber}`)
60
+ await new Promise(resolve => setTimeout(resolve, waitTime))
61
+ }
62
+ }
63
+ }
64
+
65
+ throw new Error(`分片 ${partNumber} 下载失败,已达到最大重试次数: ${lastError.message}`)
66
+ }
67
+
68
+ // 带重试功能的上传函数
69
+ async function uploadChunkWithRetry(request, uploadUrl, arrayBuffer, partNumber, crc32_text, uploadid, authorization, maxRetries = 10) {
70
+ let lastError = null
71
+
72
+ const host = [
73
+ 'tos-d-x-hl.snssdk.com',
74
+ 'tos-d-ct-hl.snssdk.com',
75
+ 'tos-d-cu-hl.snssdk.com',
76
+ 'tos-hl-x.snssdk.com',
77
+ 'tos-cu-hl.snssdk.com'
78
+ ]
79
+
80
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
81
+ const upload_host = host[Math.floor(Math.random() * host.length)]
82
+ try {
83
+ const upload_result = await request.post(uploadUrl.replace('tos-d-x-hl.snssdk.com', upload_host), arrayBuffer, {
84
+ params: {
85
+ uploadid: uploadid,
86
+ part_number: partNumber,
87
+ phase: 'transfer'
88
+ },
89
+ headers: {
90
+ 'content-crc32': crc32_text,
91
+ 'authorization': authorization
92
+ }
93
+ })
94
+
95
+ if (upload_result.data.data && upload_result.data.data.crc32 == crc32_text) {
96
+ console.log(`分片 ${partNumber} 上传成功 ${upload_host} (尝试 ${attempt}) ${crc32_text}`)
97
+ return {
98
+ crc32: crc32_text,
99
+ part_number: partNumber,
100
+ status: 'success'
101
+ }
102
+ } else {
103
+ throw new Error(`CRC32 校验失败: 期望 ${crc32_text}, 实际 ${upload_result.data.data?.crc32}`)
104
+ }
105
+ } catch (error) {
106
+ // 如果是取消请求,直接抛出
107
+ if (http.isCancel(error)) {
108
+ throw error
109
+ }
110
+
111
+ lastError = error
112
+ console.warn(`分片 ${partNumber} ${upload_host} (尝试 ${attempt}/${maxRetries}):`, error.message)
113
+
114
+ if (attempt < maxRetries) {
115
+ // const waitTime = Math.min(500 * Math.pow(2, attempt - 1), 5000)
116
+ const waitTime = 300
117
+ console.log(`等待 ${waitTime}ms 后重试上传分片 ${partNumber}`)
118
+ await new Promise(resolve => setTimeout(resolve, waitTime))
119
+ }
120
+ }
121
+ }
122
+
123
+ throw new Error(`分片 ${partNumber} 上传失败,已达到最大重试次数: ${lastError.message}`)
124
+ }
125
+
126
+ // 并发控制函数
127
+ async function runWithConcurrency(tasks, maxConcurrency = 50, cancelToken) {
128
+ const results = []
129
+ const executing = new Set()
130
+
131
+ for (let i = 0; i < tasks.length; i++) {
132
+ // 检查是否已取消
133
+ if (cancelToken && cancelToken.reason) {
134
+ console.log('检测到取消信号,停止创建新任务')
135
+ break
136
+ }
137
+
138
+ const task = tasks[i]
139
+
140
+ // 如果当前执行的任务数达到最大并发数,等待其中一个完成
141
+ if (executing.size >= maxConcurrency) {
142
+ await Promise.race(executing)
143
+ }
144
+
145
+ const promise = task().then(result => {
146
+ executing.delete(promise)
147
+ return result
148
+ }).catch(error => {
149
+ executing.delete(promise)
150
+ throw error
151
+ })
152
+
153
+ executing.add(promise)
154
+ results.push(promise)
155
+ }
156
+
157
+ // 等待所有剩余任务完成
158
+ return Promise.allSettled(results)
159
+ }
160
+
161
+ app.get('*', async (req, res) => {
162
+ // 创建取消令牌
163
+ const cancelTokenSource = http.CancelToken.source()
164
+ let isRequestCancelled = false
165
+
166
+ // 监听连接关闭事件
167
+ req.on('close', () => {
168
+ if (!res.headersSent) {
169
+ console.log('用户断开连接,取消所有请求...')
170
+ isRequestCancelled = true
171
+ cancelTokenSource.cancel('用户取消请求')
172
+ }
173
+ })
174
+
175
+ try {
176
+ const videoUrl = req.originalUrl.replace(/^\//, '')
177
+ if (/favicon.ico/gim.test(videoUrl)) {
178
+ return
179
+ }
180
+
181
+ // 创建带取消令牌的请求实例
182
+ const request = createRequestWithCancel(cancelTokenSource.token)
183
+
184
+ // 获取上传信息
185
+ const upload_info = await request.get('https://api.emmmm.eu.org.cdn.cloudflare.net/upload/cn')
186
+ console.log(upload_info.data)
187
+ const upload_part_info = await request.post(`${upload_info.data.url}?uploadmode=part&phase=init`, null, {
188
+ headers: {
189
+ 'authorization': upload_info.data.authorization
190
+ }
191
+ })
192
+
193
+ const uploadid = upload_part_info.data.data.uploadid
194
+ const upload_list = {}
195
+
196
+ // 检查是否已取消
197
+ if (isRequestCancelled) {
198
+ return
199
+ }
200
+
201
+ // 1. 首先获取文件信息
202
+ const headResponse = await request.head(videoUrl, {
203
+ maxRedirects: 5
204
+ })
205
+
206
+ const finalUrl = headResponse.request.res.responseUrl || videoUrl
207
+ const contentLength = headResponse.headers['content-length']
208
+ const acceptRanges = headResponse.headers['accept-ranges']
209
+
210
+ // 检查服务器是否支持范围请求
211
+ if (!acceptRanges || acceptRanges === 'none' || !contentLength) {
212
+ return res.json({
213
+ video_url: videoUrl,
214
+ message: '不支持范围请求'
215
+ })
216
+ }
217
+
218
+ const fileSize = parseInt(contentLength)
219
+ const chunkSize = 2000 * 1024
220
+ const threadCount = Math.ceil(fileSize / chunkSize)
221
+
222
+ console.log(`文件大小: ${fileSize} bytes, 分片数: ${threadCount}`)
223
+
224
+ // 2. 创建所有分片任务
225
+ const tasks = []
226
+ for (let i = 0; i < threadCount; i++) {
227
+ const start = i * chunkSize
228
+ const end = Math.min(start + chunkSize - 1, fileSize - 1)
229
+ const partNumber = i + 1
230
+
231
+ tasks.push(() => (async () => {
232
+ try {
233
+ // 检查是否已取消
234
+ if (isRequestCancelled) {
235
+ throw new http.Cancel('请求已取消')
236
+ }
237
+
238
+ // 下载分片(带重试)
239
+ const arrayBuffer = await downloadChunkWithRetry(request, finalUrl, start, end, partNumber)
240
+
241
+ // 检查是否已取消
242
+ if (isRequestCancelled) {
243
+ throw new http.Cancel('请求已取消')
244
+ }
245
+
246
+ // 计算 CRC32
247
+ const crc32_text = crc32(arrayBuffer)
248
+
249
+ // 上传分片(带重试)
250
+ const uploadResult = await uploadChunkWithRetry(
251
+ request,
252
+ upload_info.data.url,
253
+ arrayBuffer,
254
+ partNumber,
255
+ crc32_text,
256
+ uploadid,
257
+ upload_info.data.authorization
258
+ )
259
+
260
+ // 保存到上传列表
261
+ upload_list[partNumber] = crc32_text
262
+
263
+ return uploadResult
264
+
265
+ } catch (error) {
266
+ if (http.isCancel(error)) {
267
+ console.log(`分片 ${partNumber} 处理被取消`)
268
+ throw error
269
+ }
270
+ console.error(`分片 ${partNumber} 处理失败:`, error.message)
271
+ return {
272
+ part_number: partNumber,
273
+ status: 'failed',
274
+ error: error.message
275
+ }
276
+ }
277
+ })())
278
+ }
279
+
280
+ // 3. 并行执行所有分片任务,但限制最大并发数为100
281
+ const chunksResults = await runWithConcurrency(tasks, 100, cancelTokenSource)
282
+
283
+ // 检查是否已取消
284
+ if (isRequestCancelled) {
285
+ return
286
+ }
287
+
288
+ // 处理任务结果
289
+ const chunks = chunksResults.map(result =>
290
+ result.status === 'fulfilled' ? result.value : result.reason
291
+ )
292
+
293
+ // 过滤掉取消错误
294
+ const validChunks = chunks.filter(chunk => !(chunk instanceof Error && http.isCancel(chunk)))
295
+
296
+ // 检查是否所有分片都成功处理
297
+ const failedChunks = validChunks.filter(chunk => chunk && chunk.status === 'failed')
298
+ if (failedChunks.length > 0) {
299
+ return res.status(500).json({
300
+ video_url: videoUrl,
301
+ message: '部分分片处理失败',
302
+ failed_chunks: failedChunks,
303
+ success_count: validChunks.length - failedChunks.length,
304
+ failed_count: failedChunks.length
305
+ })
306
+ }
307
+
308
+ // 检查是否有足够的成功分片
309
+ const successChunks = validChunks.filter(chunk => chunk && chunk.status === 'success')
310
+ if (successChunks.length === 0) {
311
+ return res.status(500).json({
312
+ video_url: videoUrl,
313
+ message: '所有分片处理失败'
314
+ })
315
+ }
316
+
317
+ const finish = Object.entries(upload_list).map(i => i.join(':')).join(',')
318
+
319
+ const upload_result = await request.post(`${upload_info.data.url}?uploadid=${uploadid}&uploadmode=part&phase=finish`, finish, {
320
+ headers: {
321
+ 'authorization': upload_info.data.authorization
322
+ }
323
+ })
324
+ console.log(upload_result.data)
325
+
326
+ if (upload_result.data.code == 2000) {
327
+ return res.json({
328
+ vid: upload_info.data.vid,
329
+ url: `https://data.emmmm.eu.org/parse/zj/${upload_info.data.uri}`,
330
+ message: '文件上传成功',
331
+ total_chunks: successChunks.length
332
+ })
333
+ }
334
+
335
+ return res.json({
336
+ video_url: videoUrl,
337
+ chunks: successChunks,
338
+ upload_list,
339
+ fileSize,
340
+ chunkSize,
341
+ threadCount
342
+ })
343
+
344
+ } catch (error) {
345
+ // 如果是取消请求,不发送错误响应
346
+ if (http.isCancel(error)) {
347
+ console.log('请求已被用户取消')
348
+ return
349
+ }
350
+
351
+ console.error('处理失败:', error)
352
+ if (!res.headersSent) {
353
+ res.status(500).json({
354
+ error: `下载失败: ${error.message}`,
355
+ video_url: req.path.replace(/^\//, '')
356
+ })
357
+ }
358
+ }
359
+ })
360
+
361
+ app.listen(port, () => {
362
+ console.log(`http://localhost:${port}`)
363
+ })