1- import { writeFileSync , promises as fs } from "fs" ;
2- import { join } from "path" ;
3- import { Readable } from "node:stream" ;
4- import type { H3Event } from "h3" ;
5- import { sendStream } from "h3" ;
1+ import { randomUUID } from "node:crypto" ;
2+ import { writeFileSync , promises as fs } from "node:fs" ;
3+ import { join } from "node:path" ;
64import * as tar from "tar" ;
75import axiosInstance from "~/server/config/axios" ;
86import { getErrorMessage } from "~/server/utils/http-error" ;
@@ -13,13 +11,6 @@ import {
1311} from "~/server/utils/imageName" ;
1412import { logger } from "~/server/utils/logger" ;
1513
16- type AssembleParams = {
17- imageName : string ;
18- tag : string ;
19- token : string ;
20- manifest : string ;
21- } ;
22-
2314type DockerConfig = {
2415 digest : string ;
2516 mediaType : string ;
@@ -43,6 +34,18 @@ type DockerManifest = {
4334 platform : DockerPlatform ;
4435} ;
4536
37+ type AssembleBody = {
38+ imageName : string ;
39+ tag : string ;
40+ token : string ;
41+ manifest : DockerManifest ;
42+ } ;
43+
44+ type AssembleResponse = {
45+ downloadId : string ;
46+ fileName : string ;
47+ } ;
48+
4649type LayerJson = {
4750 id : string ;
4851 parent ?: string ;
@@ -52,15 +55,6 @@ type LayerJson = {
5255 os : string ;
5356} ;
5457
55- type AssembleContext = {
56- imageName : string ;
57- repoTagName : string ;
58- fileBaseName : string ;
59- tag : string ;
60- token : string ;
61- manifest : DockerManifest ;
62- } ;
63-
6458const stripSha256 = ( digest : string ) => digest . replace ( "sha256:" , "" ) ;
6559const TAR_OPTIONS = {
6660 preservePaths : true ,
@@ -70,19 +64,20 @@ const TAR_OPTIONS = {
7064 gzip : false ,
7165} as const ;
7266
73- const parseAssembleContext = ( event : H3Event ) : AssembleContext => {
74- const query = getQuery ( event ) as unknown as AssembleParams ;
75- const imageName = normalizeImageName ( query . imageName || "" ) ;
76- if ( ! query . tag || ! query . token || ! query . manifest ) {
67+ const parseAssembleBody = ( body : Partial < AssembleBody > ) => {
68+ const imageName = normalizeImageName ( ( body . imageName || "" ) . trim ( ) ) ;
69+ const tag = ( body . tag || "" ) . trim ( ) ;
70+ const token = ( body . token || "" ) . trim ( ) ;
71+ if ( ! imageName || ! tag || ! token || ! body . manifest ) {
7772 throw createError ( { statusCode : 400 , message : "缺少必要参数" } ) ;
7873 }
7974 return {
8075 imageName,
81- repoTagName : getRepoTagName ( query . imageName || imageName ) ,
82- fileBaseName : getSafeFileBaseName ( query . imageName || imageName ) ,
83- tag : query . tag ,
84- token : query . token ,
85- manifest : JSON . parse ( query . manifest ) as DockerManifest ,
76+ tag ,
77+ token ,
78+ manifest : body . manifest ,
79+ repoTagName : getRepoTagName ( body . imageName || imageName ) ,
80+ fileBaseName : getSafeFileBaseName ( body . imageName || imageName ) ,
8681 } ;
8782} ;
8883
@@ -104,10 +99,8 @@ const processLayer = async (
10499 const layerId = stripSha256 ( layer . digest ) ;
105100 const parentLayer = index > 0 ? manifest . layers [ index - 1 ] : undefined ;
106101 const layerDir = join ( tmpDir , layerId ) ;
107-
108102 await fs . mkdir ( layerDir , { recursive : true } ) ;
109103 writeFileSync ( join ( layerDir , "VERSION" ) , "1.0" ) ;
110-
111104 const layerJson : LayerJson = {
112105 id : layerId ,
113106 parent : parentLayer ? stripSha256 ( parentLayer . digest ) : undefined ,
@@ -116,9 +109,7 @@ const processLayer = async (
116109 architecture : manifest . platform . architecture ,
117110 os : manifest . platform . os ,
118111 } ;
119-
120112 writeFileSync ( join ( layerDir , "json" ) , JSON . stringify ( layerJson , null , 2 ) ) ;
121-
122113 const sourceFile = join (
123114 process . cwd ( ) ,
124115 "downloads" ,
@@ -143,72 +134,49 @@ const writeMetadataFiles = (
143134 Layers : manifest . layers . map ( ( layer ) => `${ stripSha256 ( layer . digest ) } /layer.tar` ) ,
144135 } ,
145136 ] ;
146-
147137 const repositories = { [ repoTagName ] : { [ tag ] : configFileName } } ;
148-
149138 writeFileSync ( join ( tmpDir , "manifest.json" ) , JSON . stringify ( dockerManifest , null , 2 ) ) ;
150139 writeFileSync ( join ( tmpDir , `${ configFileName } .json` ) , JSON . stringify ( configJson , null , 2 ) ) ;
151140 writeFileSync ( join ( tmpDir , "repositories" ) , JSON . stringify ( repositories , null , 2 ) ) ;
152141} ;
153142
154- export default defineEventHandler ( async ( event ) => {
155- const ctx = parseAssembleContext ( event ) ;
156- const { imageName, repoTagName , fileBaseName , tag , token , manifest } = ctx ;
143+ export default defineEventHandler ( async ( event ) : Promise < AssembleResponse > => {
144+ const body = await readBody < Partial < AssembleBody > > ( event ) ;
145+ const { imageName, tag , token , manifest , repoTagName , fileBaseName } = parseAssembleBody ( body ) ;
157146 const startedAt = Date . now ( ) ;
147+ const tempId = randomUUID ( ) ;
148+ const tmpDir = join ( process . cwd ( ) , "tmp" , `${ imageName } -${ Date . now ( ) } ` ) ;
149+ const assembleDir = join ( process . cwd ( ) , "tmp" , "assembled" ) ;
150+ const tarPath = join ( assembleDir , `${ tempId } .tar` ) ;
151+ const metaPath = join ( assembleDir , `${ tempId } .json` ) ;
152+ const fileName = `${ fileBaseName } -${ tag } .tar` ;
158153
159154 logger . info ( "assemble start" , { imageName, tag, layers : manifest . layers . length } ) ;
160-
161- const tmpDir = join ( process . cwd ( ) , "tmp" , `${ imageName } -${ Date . now ( ) } ` ) ;
162155 await fs . mkdir ( tmpDir , { recursive : true } ) ;
163-
164- const cleanup = async ( ) => {
165- await fs . rm ( tmpDir , { recursive : true , force : true } ) ;
166- } ;
156+ await fs . mkdir ( assembleDir , { recursive : true } ) ;
167157
168158 try {
169159 const configJson = await fetchConfigJson ( imageName , token , manifest . config . digest ) ;
170160 const configFileName = stripSha256 ( manifest . config . digest ) ;
171-
172161 await Promise . all (
173162 manifest . layers . map ( ( layer , index ) => processLayer ( tmpDir , imageName , manifest , layer , index ) )
174163 ) ;
175-
176164 writeMetadataFiles ( tmpDir , manifest , repoTagName , tag , configFileName , configJson ) ;
165+ await tar . create ( { cwd : tmpDir , file : tarPath , ...TAR_OPTIONS } , [ "." ] ) ;
166+ await fs . writeFile ( metaPath , JSON . stringify ( { fileName } , null , 2 ) , "utf-8" ) ;
167+ await fs . rm ( tmpDir , { recursive : true , force : true } ) ;
177168
178- setHeader ( event , "Content-Type" , "application/x-tar" ) ;
179- setHeader (
180- event ,
181- "Content-Disposition" ,
182- `attachment; filename="${ fileBaseName } -${ tag } .tar"`
183- ) ;
184-
185- const tarStream = tar . create ( { cwd : tmpDir , ...TAR_OPTIONS } , [ "." ] ) ;
186-
187- tarStream . on ( "close" , async ( ) => {
188- await cleanup ( ) ;
189- logger . info ( "assemble complete" , { imageName, tag, elapsedMs : Date . now ( ) - startedAt } ) ;
190- } ) ;
191- tarStream . on ( "error" , async ( error : unknown ) => {
192- await cleanup ( ) ;
193- logger . error ( "assemble stream error" , {
194- imageName,
195- tag,
196- message : getErrorMessage ( error ) ,
197- } ) ;
169+ logger . info ( "assemble complete" , {
170+ imageName,
171+ tag,
172+ downloadId : tempId ,
173+ elapsedMs : Date . now ( ) - startedAt ,
198174 } ) ;
199-
200- return sendStream ( event , tarStream as unknown as Readable ) ;
175+ return { downloadId : tempId , fileName } ;
201176 } catch ( error : unknown ) {
202- try {
203- await cleanup ( ) ;
204- } catch ( cleanupError : unknown ) {
205- logger . warn ( "assemble cleanup failed" , {
206- imageName,
207- tag,
208- message : getErrorMessage ( cleanupError ) ,
209- } ) ;
210- }
211-
177+ await fs . rm ( tmpDir , { recursive : true , force : true } ) ;
178+ await fs . rm ( tarPath , { force : true } ) ;
179+ await fs . rm ( metaPath , { force : true } ) ;
212180 const message = getErrorMessage ( error ) ;
213181 logger . error ( "assemble failed" , { imageName, tag, message, elapsedMs : Date . now ( ) - startedAt } ) ;
214182 throw createError ( { statusCode : 500 , message } ) ;
0 commit comments