laster 0.0.2
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user