Skip to content

Commit 3fcca28

Browse files
committed
refactor: optimize docker server APIs readability and type safety
1 parent 595a6a0 commit 3fcca28

File tree

9 files changed

+503
-604
lines changed

9 files changed

+503
-604
lines changed
Lines changed: 130 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,240 +1,216 @@
11
import { writeFileSync, promises as fs } from "fs";
22
import { join } from "path";
3-
import * as tar from "tar";
3+
import { Readable } from "node:stream";
4+
import type { H3Event } from "h3";
45
import { sendStream } from "h3";
6+
import * as tar from "tar";
57
import axiosInstance from "~/server/config/axios";
8+
import { getErrorMessage } from "~/server/utils/http-error";
69
import {
7-
normalizeImageName,
810
getRepoTagName,
911
getSafeFileBaseName,
12+
normalizeImageName,
1013
} from "~/server/utils/imageName";
11-
import { getErrorMessage } from "~/server/utils/http-error";
1214
import { logger } from "~/server/utils/logger";
1315

14-
// 请求参数接口
1516
type AssembleParams = {
1617
imageName: string;
1718
tag: string;
1819
token: string;
1920
manifest: string;
2021
};
2122

22-
// Docker配置接口
2323
type DockerConfig = {
2424
digest: string;
2525
mediaType: string;
2626
size: number;
2727
};
2828

29-
// Docker层接口
3029
type DockerLayer = {
3130
digest: string;
3231
mediaType: string;
3332
size: number;
3433
};
3534

36-
// Docker平台接口
3735
type DockerPlatform = {
3836
architecture: string;
3937
os: string;
4038
};
4139

42-
// Docker清单接口
4340
type DockerManifest = {
4441
config: DockerConfig;
4542
layers: DockerLayer[];
4643
platform: DockerPlatform;
4744
};
4845

49-
// 层JSON配置接口
5046
type LayerJson = {
5147
id: string;
5248
parent?: string;
5349
created: string;
54-
container_config: {
55-
Cmd: string[];
56-
};
50+
container_config: { Cmd: string[] };
5751
architecture: string;
5852
os: string;
5953
};
6054

55+
type AssembleContext = {
56+
imageName: string;
57+
repoTagName: string;
58+
fileBaseName: string;
59+
tag: string;
60+
token: string;
61+
manifest: DockerManifest;
62+
};
63+
64+
const stripSha256 = (digest: string) => digest.replace("sha256:", "");
65+
const TAR_OPTIONS = {
66+
preservePaths: true,
67+
follow: true,
68+
noPax: true,
69+
noMtime: true,
70+
gzip: false,
71+
} as const;
72+
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) {
77+
throw createError({ statusCode: 400, message: "缺少必要参数" });
78+
}
79+
return {
80+
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,
86+
};
87+
};
88+
89+
const fetchConfigJson = async (imageName: string, token: string, digest: string) => {
90+
const response = await axiosInstance.get(
91+
`https://registry-1.docker.io/v2/${imageName}/blobs/${digest}`,
92+
{ headers: { Authorization: `Bearer ${token}` } }
93+
);
94+
return response.data;
95+
};
96+
97+
const processLayer = async (
98+
tmpDir: string,
99+
imageName: string,
100+
manifest: DockerManifest,
101+
layer: DockerLayer,
102+
index: number
103+
) => {
104+
const layerId = stripSha256(layer.digest);
105+
const parentLayer = index > 0 ? manifest.layers[index - 1] : undefined;
106+
const layerDir = join(tmpDir, layerId);
107+
108+
await fs.mkdir(layerDir, { recursive: true });
109+
writeFileSync(join(layerDir, "VERSION"), "1.0");
110+
111+
const layerJson: LayerJson = {
112+
id: layerId,
113+
parent: parentLayer ? stripSha256(parentLayer.digest) : undefined,
114+
created: new Date().toISOString(),
115+
container_config: { Cmd: ["baselayer"] },
116+
architecture: manifest.platform.architecture,
117+
os: manifest.platform.os,
118+
};
119+
120+
writeFileSync(join(layerDir, "json"), JSON.stringify(layerJson, null, 2));
121+
122+
const sourceFile = join(
123+
process.cwd(),
124+
"downloads",
125+
imageName,
126+
`${layer.digest.replace(":", "_")}.tar`
127+
);
128+
await fs.copyFile(sourceFile, join(layerDir, "layer.tar"));
129+
};
130+
131+
const writeMetadataFiles = (
132+
tmpDir: string,
133+
manifest: DockerManifest,
134+
repoTagName: string,
135+
tag: string,
136+
configFileName: string,
137+
configJson: unknown
138+
) => {
139+
const dockerManifest = [
140+
{
141+
Config: `${configFileName}.json`,
142+
RepoTags: [`${repoTagName}:${tag}`],
143+
Layers: manifest.layers.map((layer) => `${stripSha256(layer.digest)}/layer.tar`),
144+
},
145+
];
146+
147+
const repositories = { [repoTagName]: { [tag]: configFileName } };
148+
149+
writeFileSync(join(tmpDir, "manifest.json"), JSON.stringify(dockerManifest, null, 2));
150+
writeFileSync(join(tmpDir, `${configFileName}.json`), JSON.stringify(configJson, null, 2));
151+
writeFileSync(join(tmpDir, "repositories"), JSON.stringify(repositories, null, 2));
152+
};
153+
61154
export default defineEventHandler(async (event) => {
62-
const query = getQuery(event);
63-
const {
64-
imageName: rawImageName,
65-
tag,
66-
token,
67-
manifest: manifestJson,
68-
} = query as unknown as AssembleParams;
69-
const imageName = normalizeImageName(rawImageName || "");
70-
const repoTagName = getRepoTagName(rawImageName || imageName);
71-
const fileBaseName = getSafeFileBaseName(rawImageName || imageName);
72-
const manifest = JSON.parse(manifestJson) as DockerManifest;
155+
const ctx = parseAssembleContext(event);
156+
const { imageName, repoTagName, fileBaseName, tag, token, manifest } = ctx;
73157
const startedAt = Date.now();
74-
logger.info("assemble start", {
75-
imageName,
76-
tag,
77-
layers: manifest.layers?.length || 0,
78-
});
79158

80-
// 创建临时目录
159+
logger.info("assemble start", { imageName, tag, layers: manifest.layers.length });
160+
81161
const tmpDir = join(process.cwd(), "tmp", `${imageName}-${Date.now()}`);
82162
await fs.mkdir(tmpDir, { recursive: true });
83163

84-
// 获取配置文件
85-
const fetchConfig = async () => {
86-
const response = await axiosInstance.get(
87-
`https://registry-1.docker.io/v2/${imageName}/blobs/${manifest.config.digest}`,
88-
{
89-
headers: { Authorization: `Bearer ${token}` },
90-
}
91-
);
92-
return response.data;
93-
};
94-
95-
// 处理单个层
96-
const processLayer = async (layer: DockerLayer, index: number) => {
97-
const layerId = layer.digest.replace("sha256:", "");
98-
const layerDir = join(tmpDir, layerId);
99-
100-
await fs.mkdir(layerDir, { recursive: true });
101-
102-
// 写入版本文件
103-
writeFileSync(join(layerDir, "VERSION"), "1.0");
104-
105-
// 创建层配置
106-
const layerJson: LayerJson = {
107-
id: layerId,
108-
parent:
109-
index > 0
110-
? manifest.layers[index - 1].digest.replace("sha256:", "")
111-
: undefined,
112-
created: new Date().toISOString(),
113-
container_config: {
114-
Cmd: ["baselayer"],
115-
},
116-
architecture: manifest.platform.architecture,
117-
os: manifest.platform.os,
118-
};
119-
120-
// 写入层配置
121-
writeFileSync(join(layerDir, "json"), JSON.stringify(layerJson, null, 2));
122-
123-
// 复制层文件
124-
const sourceFile = join(
125-
process.cwd(),
126-
"downloads",
127-
imageName,
128-
`${layer.digest.replace(":", "_")}.tar`
129-
);
130-
await fs.copyFile(sourceFile, join(layerDir, "layer.tar"));
164+
const cleanup = async () => {
165+
await fs.rm(tmpDir, { recursive: true, force: true });
131166
};
132167

133168
try {
134-
// 获取配置
135-
const configJson = await fetchConfig();
136-
const configFileName = manifest.config.digest.replace("sha256:", "");
169+
const configJson = await fetchConfigJson(imageName, token, manifest.config.digest);
170+
const configFileName = stripSha256(manifest.config.digest);
137171

138-
// 处理所有层
139172
await Promise.all(
140-
manifest.layers.map((layer, index) => processLayer(layer, index))
173+
manifest.layers.map((layer, index) => processLayer(tmpDir, imageName, manifest, layer, index))
141174
);
142175

143-
// 写入manifest.json
144-
const manifestJson = [
145-
{
146-
Config: `${configFileName}.json`,
147-
RepoTags: [`${repoTagName}:${tag}`],
148-
Layers: manifest.layers.map(
149-
(layer) => `${layer.digest.replace("sha256:", "")}/layer.tar`
150-
),
151-
},
152-
];
153-
writeFileSync(
154-
join(tmpDir, "manifest.json"),
155-
JSON.stringify(manifestJson, null, 2)
156-
);
157-
158-
// 写入配置文件
159-
writeFileSync(
160-
join(tmpDir, `${configFileName}.json`),
161-
JSON.stringify(configJson, null, 2)
162-
);
176+
writeMetadataFiles(tmpDir, manifest, repoTagName, tag, configFileName, configJson);
163177

164-
// 写入repositories文件
165-
const repositoriesJson = {
166-
[repoTagName]: {
167-
[tag]: configFileName,
168-
},
169-
};
170-
writeFileSync(
171-
join(tmpDir, "repositories"),
172-
JSON.stringify(repositoriesJson, null, 2)
173-
);
174-
175-
// 设置响应头
176178
setHeader(event, "Content-Type", "application/x-tar");
177179
setHeader(
178180
event,
179181
"Content-Disposition",
180182
`attachment; filename="${fileBaseName}-${tag}.tar"`
181183
);
182184

183-
const tarStream = tar.create(
184-
{
185-
cwd: tmpDir,
186-
preservePaths: true,
187-
follow: true,
188-
noPax: true,
189-
noMtime: true,
190-
gzip: false,
191-
},
192-
["."]
193-
);
194-
195-
let cleaned = false;
196-
const cleanup = async () => {
197-
if (cleaned) return;
198-
cleaned = true;
199-
await fs.rm(tmpDir, { recursive: true, force: true });
200-
};
185+
const tarStream = tar.create({ cwd: tmpDir, ...TAR_OPTIONS }, ["."]);
201186

202187
tarStream.on("close", async () => {
203188
await cleanup();
204-
logger.info("assemble complete", {
205-
imageName,
206-
tag,
207-
elapsedMs: Date.now() - startedAt,
208-
});
189+
logger.info("assemble complete", { imageName, tag, elapsedMs: Date.now() - startedAt });
209190
});
210-
tarStream.on("error", async (err) => {
191+
tarStream.on("error", async (error: unknown) => {
211192
await cleanup();
212193
logger.error("assemble stream error", {
213194
imageName,
214195
tag,
215-
message: String(err?.message || err),
196+
message: getErrorMessage(error),
216197
});
217198
});
218199

219-
return sendStream(event, tarStream);
200+
return sendStream(event, tarStream as unknown as Readable);
220201
} catch (error: unknown) {
221-
const message = getErrorMessage(error);
222-
// 清理临时目录
223202
try {
224-
await fs.rm(tmpDir, { recursive: true, force: true });
225-
} catch (e) {
226-
console.warn("清理临时目录失败:", e);
203+
await cleanup();
204+
} catch (cleanupError: unknown) {
205+
logger.warn("assemble cleanup failed", {
206+
imageName,
207+
tag,
208+
message: getErrorMessage(cleanupError),
209+
});
227210
}
228211

229-
logger.error("assemble failed", {
230-
imageName,
231-
tag,
232-
message,
233-
elapsedMs: Date.now() - startedAt,
234-
});
235-
throw createError({
236-
statusCode: 500,
237-
message,
238-
});
212+
const message = getErrorMessage(error);
213+
logger.error("assemble failed", { imageName, tag, message, elapsedMs: Date.now() - startedAt });
214+
throw createError({ statusCode: 500, message });
239215
}
240216
});

0 commit comments

Comments
 (0)