UNPKG

screwdriver-api

Version:

API server for the Screwdriver.cd service

118 lines (102 loc) 4.77 kB
'use strict'; const archiver = require('archiver'); const boom = require('@hapi/boom'); const request = require('got'); const joi = require('joi'); const jwt = require('jsonwebtoken'); const logger = require('screwdriver-logger'); const { PassThrough } = require('stream'); const schema = require('screwdriver-data-schema'); const { v4: uuidv4 } = require('uuid'); const idSchema = schema.models.build.base.extract('id'); module.exports = config => ({ method: 'GET', path: '/builds/{id}/artifacts', options: { description: 'Get a zipped file including all build artifacts', notes: 'Redirects to store with proper token', tags: ['api', 'builds', 'artifacts'], auth: { strategies: ['session', 'token'], scope: ['user', 'build', 'pipeline'] }, handler: async (req, h) => { const { name: artifact, id: buildId } = req.params; const { credentials } = req.auth; const { canAccessPipeline } = req.server.plugins.pipelines; const { buildFactory, eventFactory } = req.server.app; return buildFactory.get(buildId) .then(build => { if (!build) { throw boom.notFound('Build does not exist'); } return eventFactory.get(build.eventId); }) .then(event => { if (!event) { throw boom.notFound('Event does not exist'); } return canAccessPipeline(credentials, event.pipelineId, 'pull', req.server.app); }) .then(async () => { const token = jwt.sign({ buildId, artifact, scope: ['user'] }, config.authConfig.jwtPrivateKey, { algorithm: 'RS256', expiresIn: '10m', jwtid: uuidv4() }); const baseUrl = `${config.ecosystem.store}/v1/builds/${buildId}/ARTIFACTS`; // Fetch the manifest const manifest = await request({ url: `${baseUrl}/manifest.txt?token=${token}`, method: 'GET' }).text(); const manifestArray = manifest.trim().split('\n'); // Create a stream and set up archiver const archive = archiver('zip', { zlib: { level: 9 } }); // PassThrough stream to make archiver readable by Hapi const passThrough = new PassThrough(); archive.on('error', (err) => { logger.error('Archiver error:', err); passThrough.emit('error', err); // Propagate the error to the PassThrough stream }); passThrough.on('error', (err) => { logger.error('PassThrough stream error:', err); }); // Pipe the archive to PassThrough so it can be sent as a response archive.pipe(passThrough); // Fetch the artifact files and append to the archive try { for (const file of manifestArray) { if (file) { const fileStream = request.stream(`${baseUrl}/${file}?token=${token}&type=download`); fileStream.on('error', (err) => { logger.error(`Error downloading file: ${file}`, err); archive.emit('error', err); }); archive.append(fileStream, { name: file }); } } // Finalize the archive after all files are appended archive.finalize(); } catch (err) { logger.error('Error while streaming artifact files:', err); archive.emit('error', err); } // Respond with the PassThrough stream (which is now readable by Hapi) return h.response(passThrough) .type('application/zip') .header('Content-Disposition', 'attachment; filename="SD_ARTIFACTS.zip"'); }) .catch(err => { throw err; }); }, validate: { params: joi.object({ id: idSchema }) } } });