@@ -2,6 +2,7 @@ import { createWriteStream, existsSync, mkdirSync, statSync } from "fs";
22import { join } from "path" ;
33import axiosInstance from "~/server/config/axios" ;
44import { normalizeImageName } from "~/server/utils/imageName" ;
5+ import { logger } from "~/server/utils/logger" ;
56
67// 请求参数接口
78type QueryParams = {
@@ -42,6 +43,10 @@ type FileInfo = {
4243} ;
4344
4445const CONCURRENT_DOWNLOADS = 3 ;
46+ const LAYER_TIMEOUT_MS = Math . max (
47+ 1000 ,
48+ parseInt ( process . env . PULL_LAYER_TIMEOUT_MS || "600000" , 10 )
49+ ) ;
4550
4651export default defineEventHandler ( async ( event ) => {
4752 // 设置SSE响应头
@@ -62,6 +67,7 @@ export default defineEventHandler(async (event) => {
6267 }
6368
6469 const layers = JSON . parse ( layersJson ) as DockerLayer [ ] ;
70+ logger . info ( "pull start" , { imageName, layers : layers . length , concurrency : CONCURRENT_DOWNLOADS } ) ;
6571
6672 // 创建下载目录
6773 const downloadDir = join ( process . cwd ( ) , "downloads" , imageName ) ;
@@ -96,9 +102,22 @@ export default defineEventHandler(async (event) => {
96102 event . node . res . write ( `data: ${ JSON . stringify ( progress ) } \n\n` ) ;
97103 ( event . node . res as any ) . flush ?.( ) ;
98104 } ;
105+ const sendError = ( message : string , details ?: string [ ] ) => {
106+ event . node . res . write (
107+ `data: ${ JSON . stringify ( {
108+ error : message ,
109+ details,
110+ } ) } \n\n`
111+ ) ;
112+ ( event . node . res as any ) . flush ?.( ) ;
113+ } ;
99114
100115 // 下载单个层
101116 const downloadLayer = async ( layer : DockerLayer ) : Promise < LayerDownloadResult > => {
117+ const controller = new AbortController ( ) ;
118+ const timeoutId = setTimeout ( ( ) => {
119+ controller . abort ( new Error ( "timeout" ) ) ;
120+ } , LAYER_TIMEOUT_MS ) ;
102121 try {
103122 const fileInfo = checkLayerFile ( layer ) ;
104123
@@ -123,6 +142,7 @@ export default defineEventHandler(async (event) => {
123142 {
124143 headers : { Authorization : `Bearer ${ token } ` } ,
125144 responseType : "stream" ,
145+ signal : controller . signal ,
126146 }
127147 ) ;
128148
@@ -149,6 +169,8 @@ export default defineEventHandler(async (event) => {
149169 } ) ;
150170
151171 await new Promise ( ( resolve , reject ) => {
172+ const onAbort = ( ) => reject ( new Error ( "timeout" ) ) ;
173+ controller . signal . addEventListener ( "abort" , onAbort , { once : true } ) ;
152174 writeStream . on ( "finish" , resolve ) ;
153175 writeStream . on ( "error" , reject ) ;
154176 response . data . on ( "error" , reject ) ;
@@ -162,11 +184,20 @@ export default defineEventHandler(async (event) => {
162184 skipped : false ,
163185 } ;
164186 } catch ( error : any ) {
187+ if ( String ( error ?. message ) . toLowerCase ( ) . includes ( "timeout" ) ) {
188+ return {
189+ success : false ,
190+ digest : layer . digest ,
191+ error : "timeout" ,
192+ } ;
193+ }
165194 return {
166195 success : false ,
167196 digest : layer . digest ,
168197 error : error . message ,
169198 } ;
199+ } finally {
200+ clearTimeout ( timeoutId ) ;
170201 }
171202 } ;
172203
@@ -204,9 +235,27 @@ export default defineEventHandler(async (event) => {
204235 // 检查下载结果
205236 const failedDownloads = results . filter ( ( result ) => ! result . success ) ;
206237 if ( failedDownloads . length > 0 ) {
207- throw new Error (
208- `部分层下载失败: ${ failedDownloads . map ( ( f ) => f . digest ) . join ( ", " ) } `
238+ const timedOut = failedDownloads . filter ( ( f ) => f . error === "timeout" ) ;
239+ if ( timedOut . length > 0 ) {
240+ sendError (
241+ `部分层下载超时(${ timedOut . length } 个),请重试` ,
242+ timedOut . map ( ( f ) => f . digest )
243+ ) ;
244+ logger . warn ( "pull timeout" , {
245+ imageName,
246+ timedOut : timedOut . length ,
247+ } ) ;
248+ return ;
249+ }
250+ sendError (
251+ `部分层下载失败(${ failedDownloads . length } 个),请重试` ,
252+ failedDownloads . map ( ( f ) => f . digest )
209253 ) ;
254+ logger . error ( "pull failed layers" , {
255+ imageName,
256+ failed : failedDownloads . length ,
257+ } ) ;
258+ return ;
210259 }
211260
212261 // 发送下载汇总信息
@@ -223,10 +272,17 @@ export default defineEventHandler(async (event) => {
223272 } ) } \n\n`
224273 ) ;
225274 ( event . node . res as any ) . flush ?.( ) ;
275+ logger . info ( "pull complete" , {
276+ imageName,
277+ total : results . length ,
278+ skipped : skippedLayers ,
279+ downloaded : downloadedLayers ,
280+ } ) ;
226281
227282
228283 return ;
229284 } catch ( error : any ) {
285+ logger . error ( "pull failed" , { imageName, message : error . message } ) ;
230286 throw createError ( {
231287 statusCode : 500 ,
232288 message : error . message ,
0 commit comments