laster 0.0.2

This commit is contained in:
2026-05-26 15:43:56 +07:00
parent e2c4881bb7
commit 8ceb1bb1df
24 changed files with 583 additions and 40 deletions

View File

@@ -1,6 +1,7 @@
require('dotenv').config({ quiet: true });
const crypto = require('crypto');
const { execFile } = require('child_process');
const fs = require('fs');
const fsp = require('fs/promises');
const path = require('path');
@@ -15,6 +16,7 @@ const app = express();
const port = Number(process.env.PORT || 3000);
const uploadDir = path.join(__dirname, 'uploads', 'packages');
const agentPackageDir = path.resolve(process.env.AGENT_PACKAGE_DIR || path.join(uploadDir, 'agent'));
const agentDebianPackageName = 'local-installer-agent';
const authCookieName = 'robot_installer_session';
const sessionMaxAgeMs = Number(process.env.SESSION_MAX_AGE_MS || 1000 * 60 * 60 * 8);
const authSecret = process.env.AUTH_SECRET || process.env.SESSION_SECRET || 'robot-installer-dev-secret';
@@ -32,6 +34,7 @@ const agentVersionCollator = new Intl.Collator('en', {
numeric: true,
sensitivity: 'base'
});
let didWarnAgentMetadataToolMissing = false;
app.get('/healthz', (req, res) => {
res.status(200).json({ status: 'ok' });
@@ -598,13 +601,112 @@ function getAgentPackageDownloadPath(fileName) {
return `/packages/agent/${encodeURIComponent(fileName)}`;
}
async function getAgentPackageFromEntry(entry) {
const match = /^local-installer-agent_([^/\\]+)_([^/\\]+)\.deb$/.exec(entry.name);
function parseAgentPackageFileName(fileName) {
const match = /^local-installer-agent_([^/\\]+)_([^/\\]+)\.deb$/.exec(fileName);
if (!match) return null;
const [, version, packageArch] = match;
return {
version: match[1],
arch: match[2]
};
}
function parseDebControlOutput(output) {
return String(output || '')
.split(/\r?\n/)
.reduce((metadata, line) => {
const match = /^([^:]+):\s*(.*)$/.exec(line);
if (!match) return metadata;
const key = match[1].trim().toLowerCase();
const value = match[2].trim();
if (key === 'package') metadata.package = value;
if (key === 'version') metadata.version = value;
if (key === 'architecture') metadata.architecture = value;
return metadata;
}, {});
}
function isDebMetadataInspectionUnavailable(error) {
const message = String(error?.message || '');
return error?.code === 'ENOENT'
|| /unable to execute decompressing archive/i.test(message)
|| /member 'control\.tar' \([^)]+\): No such file or directory/i.test(message);
}
async function readDebPackageMetadata(filePath) {
return new Promise((resolve, reject) => {
execFile(
'dpkg-deb',
['-f', filePath, 'Package', 'Version', 'Architecture'],
{ windowsHide: true, timeout: 10000, maxBuffer: 1024 * 1024 },
(error, stdout) => {
if (error) {
if (isDebMetadataInspectionUnavailable(error)) error.metadataToolMissing = true;
reject(error);
return;
}
const metadata = parseDebControlOutput(stdout);
if (!metadata.package || !metadata.version || !metadata.architecture) {
reject(new Error('Missing Package, Version, or Architecture in deb control metadata.'));
return;
}
resolve(metadata);
}
);
});
}
async function readDebPackageMetadataIfAvailable(filePath) {
try {
return await readDebPackageMetadata(filePath);
} catch (error) {
if (error.metadataToolMissing) {
if (!didWarnAgentMetadataToolMissing) {
console.warn('dpkg-deb metadata inspection is not available; using agent package filename metadata.');
didWarnAgentMetadataToolMissing = true;
}
return null;
}
console.warn(`Cannot inspect agent package metadata for ${path.basename(filePath)}:`, error.message);
return null;
}
}
async function getAgentPackageFromEntry(entry) {
const fileNameMetadata = parseAgentPackageFileName(entry.name);
if (!fileNameMetadata) return null;
const filePath = path.join(agentPackageDir, entry.name);
const stat = await fsp.stat(filePath);
const debMetadata = await readDebPackageMetadataIfAvailable(filePath);
if (debMetadata?.package && debMetadata.package !== agentDebianPackageName) {
console.warn(`Ignoring ${entry.name}: deb package is ${debMetadata.package}, expected ${agentDebianPackageName}.`);
return null;
}
if (debMetadata) {
const metadataArch = normalizeAgentArch(debMetadata.architecture);
const fileNameArch = normalizeAgentArch(fileNameMetadata.arch);
if (debMetadata.version !== fileNameMetadata.version || metadataArch !== fileNameArch) {
console.warn(
`Ignoring ${entry.name}: filename metadata does not match deb metadata ` +
`(deb version=${debMetadata.version}, deb arch=${debMetadata.architecture}).`
);
return null;
}
}
const version = fileNameMetadata.version;
const packageArch = fileNameMetadata.arch;
return {
fileName: entry.name,
@@ -824,6 +926,35 @@ async function getArtifactFromUpload(file) {
};
}
async function getDebUploadMetadataValidationMessage(file, packageCode, version) {
if (!file || path.extname(file.originalname).toLowerCase() !== '.deb') return null;
let debMetadata = null;
try {
debMetadata = await readDebPackageMetadata(file.path);
} catch (error) {
if (error.metadataToolMissing) {
console.warn('dpkg-deb metadata inspection is not available; skipping package metadata validation.');
return null;
}
return `Khong doc duoc metadata trong file .deb: ${error.message}`;
}
const metadataErrors = [];
if (debMetadata.package !== packageCode) {
metadataErrors.push(`package=${debMetadata.package}`);
}
if (debMetadata.version !== version) {
metadataErrors.push(`version=${debMetadata.version}`);
}
return metadataErrors.length > 0
? `File .deb khong khop thong tin package (${metadataErrors.join(', ')}). Package code va version tren web phai khop Package/Version trong file .deb.`
: null;
}
async function listMissingManifestPackageFiles(manifest) {
const missingPackageFiles = [];
@@ -849,6 +980,7 @@ async function listMissingManifestPackageFiles(manifest) {
componentId: component.componentId,
packageName: component.packageName,
version: component.version,
filePath: packageVersion?.filePath || '',
downloadUrl: component.downloadUrl
});
}
@@ -1233,7 +1365,7 @@ app.get('/api/apps/:appCode/versions/:version/manifest', asyncRoute(async (req,
if (missingPackageFiles.length > 0) {
res.status(409).json({
error: 'Package file is missing on the package server',
detail: 'Upload the package file to this web-server storage or re-upload the package version.',
detail: 'Upload the package file to this web-server storage or re-upload the same package version from the Packages page.',
missingPackageFiles
});
return;
@@ -1343,6 +1475,47 @@ app.post('/agent/packages', requireAdmin, agentUpload.single('agentFile'), async
return;
}
let debMetadata = null;
try {
debMetadata = await readDebPackageMetadata(req.file.path);
} catch (error) {
if (!error.metadataToolMissing) {
await removeUploadedFile(req.file);
redirectWithNotice(res, '/agent', 'warning', `Khong doc duoc metadata trong file .deb: ${error.message}`);
return;
}
console.warn('dpkg-deb metadata inspection is not available; skipping uploaded agent package metadata validation.');
}
if (debMetadata) {
const metadataArch = normalizeAgentArch(debMetadata.architecture);
const metadataErrors = [];
if (debMetadata.package !== agentDebianPackageName) {
metadataErrors.push(`package=${debMetadata.package}`);
}
if (debMetadata.version !== version) {
metadataErrors.push(`version=${debMetadata.version}`);
}
if (metadataArch !== arch) {
metadataErrors.push(`arch=${debMetadata.architecture}`);
}
if (metadataErrors.length > 0) {
await removeUploadedFile(req.file);
redirectWithNotice(
res,
'/agent',
'warning',
`File .deb khong khop thong tin upload (${metadataErrors.join(', ')}).`
);
return;
}
}
const targetFileName = `local-installer-agent_${version}_${arch}.deb`;
const targetPath = path.join(agentPackageDir, targetFileName);
const isUpdate = fs.existsSync(targetPath);
@@ -1488,6 +1661,15 @@ app.post('/packages', upload.single('packageFile'), asyncRoute(async (req, res)
return;
}
if (packageType === 'deb') {
const metadataMessage = await getDebUploadMetadataValidationMessage(req.file, packageCode, version);
if (metadataMessage) {
await removeUploadedFile(req.file);
redirectWithNotice(res, '/packages', 'warning', metadataMessage);
return;
}
}
await repository.createPackageWithVersion({
packageCode,
packageName,
@@ -1516,6 +1698,8 @@ app.post('/packages', upload.single('packageFile'), asyncRoute(async (req, res)
}));
app.post('/package-versions', upload.single('packageFile'), asyncRoute(async (req, res) => {
let versionInput = null;
try {
const artifact = await getArtifactFromUpload(req.file);
const version = String(req.body.version || '').trim();
@@ -1532,7 +1716,23 @@ app.post('/package-versions', upload.single('packageFile'), asyncRoute(async (re
return;
}
await repository.addPackageVersion({
const packageItem = await repository.getPackageById(req.body.packageId);
if (!packageItem) {
await removeUploadedFile(req.file);
redirectWithNotice(res, '/packages', 'warning', 'Khong tim thay package can cap nhat.');
return;
}
if (packageItem.type === 'deb') {
const metadataMessage = await getDebUploadMetadataValidationMessage(req.file, packageItem.code, version);
if (metadataMessage) {
await removeUploadedFile(req.file);
redirectWithNotice(res, `/packages/${req.body.packageId}`, 'warning', metadataMessage);
return;
}
}
versionInput = {
packageId: req.body.packageId,
version,
releaseDate: req.body.releaseDate,
@@ -1541,12 +1741,33 @@ app.post('/package-versions', upload.single('packageFile'), asyncRoute(async (re
fileSizeBytes: artifact.fileSizeBytes,
checksum: artifact.checksum,
changeLog: req.body.changeLog
});
};
await repository.addPackageVersion(versionInput);
redirectWithNotice(res, `/packages/${req.body.packageId}`, 'success', 'Đã cập nhật version mới và đặt làm latest.');
} catch (error) {
await removeUploadedFile(req.file);
if (error.code === 'DUPLICATE_PACKAGE') {
if (versionInput && (versionInput.filePath || versionInput.dockerImage)) {
try {
const replacedVersionId = await repository.replacePackageVersionArtifact(versionInput);
if (replacedVersionId) {
redirectWithNotice(
res,
`/packages/${versionInput.packageId}`,
'success',
'Da re-upload package version hien co va cap nhat artifact.'
);
return;
}
} catch (replaceError) {
await removeUploadedFile(req.file);
throw replaceError;
}
}
await removeUploadedFile(req.file);
redirectWithNotice(res, `/packages/${req.body.packageId || ''}`, 'warning', 'Version này đã tồn tại trong package.');
return;
}