Changeset View
Changeset View
Standalone View
Standalone View
lilybuild/lilybuild/artifacts/resource.py
| import json | import json | ||||
| from klein import Klein | from klein import Klein | ||||
| from twisted.internet import defer | from twisted.internet import defer | ||||
| from twisted.web.static import File | from twisted.web.static import File | ||||
| from buildbot.data.resultspec import ResultSpec, Property | from buildbot.data.resultspec import ResultSpec, Property | ||||
| import os | import os | ||||
| import re | |||||
| POSSIBLE_ARTIFACT_TYPES = { | POSSIBLE_ARTIFACT_TYPES = { | ||||
| 'archive': 'artifacts.tar', | 'archive': 'artifacts.tar', | ||||
| 'reports': 'reports.tar', | 'reports': 'reports.tar', | ||||
| } | } | ||||
| BAD_REQUEST = 'BAD_REQUEST' | BAD_REQUEST = 'BAD_REQUEST' | ||||
| NOT_FOUND = 'NOT_FOUND' | NOT_FOUND = 'NOT_FOUND' | ||||
| def validate_artifact_type(at): | def validate_artifact_type(at): | ||||
| return at in POSSIBLE_ARTIFACT_TYPES | return at in POSSIBLE_ARTIFACT_TYPES | ||||
| REF_NAME_REGEX = re.compile(r'^[A-Za-z0-9_\-]+$') | |||||
| def validate_ref_name(ref_name): | |||||
| return re.match(REF_NAME_REGEX, ref_name) | |||||
| class Api: | class Api: | ||||
| app = Klein() | app = Klein() | ||||
| def __init__(self, ep): | def __init__(self, ep): | ||||
| self.ep = ep | self.ep = ep | ||||
| def lbc(self): | def lbc(self): | ||||
| return self.ep.config['lbc'] | return self.ep.config['lbc'] | ||||
| Show All 16 Lines | def getArtifact(self, request, build_id, job_index, artifact_type): | ||||
| else: | else: | ||||
| raise FileNotFoundError('Not Found') | raise FileNotFoundError('Not Found') | ||||
| except: | except: | ||||
| request.setResponseCode(404) | request.setResponseCode(404) | ||||
| request.setHeader('Content-Type', 'application/json') | request.setHeader('Content-Type', 'application/json') | ||||
| return json.dumps({'error': NOT_FOUND}) | return json.dumps({'error': NOT_FOUND}) | ||||
| @defer.inlineCallbacks | @defer.inlineCallbacks | ||||
| def get_artifact_location(self, request, build_id, job_index, artifact_type): | def get_artifact_location(self, request, build_id, job_index, artifact_type, job_name=None): | ||||
| result_spec = ResultSpec( | props_to_get = [ | ||||
| # see popProperties in ResultSpec | |||||
| properties=[Property(b'property', 'eq', [ | |||||
| 'lilybuild_repo_id', | 'lilybuild_repo_id', | ||||
| 'lilybuild_source', | 'lilybuild_source', | ||||
| ])] | ] | ||||
| if job_index is None and job_name is None: | |||||
| raise FileNotFoundError('Not Found') | |||||
| if job_index is None and job_name is not None: | |||||
| props_to_get.append('lilybuild_job_map') | |||||
| result_spec = ResultSpec( | |||||
| # see popProperties in ResultSpec | |||||
| properties=[Property(b'property', 'eq', props_to_get)] | |||||
| ) | ) | ||||
| build = yield self.ep.master.data.get_with_resultspec( | build = yield self.ep.master.data.get_with_resultspec( | ||||
| ('builds', build_id), result_spec | ('builds', build_id), result_spec | ||||
| ) | ) | ||||
| if not build: | if not build: | ||||
| raise FileNotFoundError('Not Found') | raise FileNotFoundError('Not Found') | ||||
| if 'lilybuild_repo_id' not in build['properties']: | if 'lilybuild_repo_id' not in build['properties']: | ||||
| raise FileNotFoundError('Not Found') | raise FileNotFoundError('Not Found') | ||||
| if 'lilybuild_source' not in build['properties']: | if 'lilybuild_source' not in build['properties']: | ||||
| source = 'none' | source = 'none' | ||||
| else: | else: | ||||
| source = build['properties']['lilybuild_source'][0] | source = build['properties']['lilybuild_source'][0] | ||||
| if job_index is None: | |||||
| if 'lilybuild_job_map' not in build['properties']: | |||||
| raise FileNotFoundError('Not Found') | |||||
| job_map = build['properties']['lilybuild_job_map'][0] | |||||
| if job_name not in job_map: | |||||
| raise FileNotFoundError('Not Found') | |||||
| job_index = job_map[job_name] | |||||
| # Things not pushed to staging area is considered private | # Things not pushed to staging area is considered private | ||||
| # and can only be seen by project members | # and can only be seen by project members | ||||
| if source == 'arc-patch': | if source == 'arc-patch': | ||||
| # see ForgejoAuthz | # see ForgejoAuthz | ||||
| yield self.www().assertUserAllowed(request, ('lilybuild_artifacts', 'builds', build_id, 'jobs', job_index, 'artifacts', artifact_type), 'get', {}) | yield self.www().assertUserAllowed(request, ('lilybuild_artifacts', 'builds', build_id, 'jobs', job_index, 'artifacts', artifact_type), 'get', {}) | ||||
| repo_id = build['properties']['lilybuild_repo_id'][0] | repo_id = build['properties']['lilybuild_repo_id'][0] | ||||
| fn = POSSIBLE_ARTIFACT_TYPES[artifact_type] | fn = POSSIBLE_ARTIFACT_TYPES[artifact_type] | ||||
| res = os.path.join( | res = os.path.join( | ||||
| self.lbc().storage_dir, | self.lbc().storage_dir, | ||||
| 'repos', str(repo_id), | 'repos', str(repo_id), | ||||
| 'builds', str(build_id), | 'builds', str(build_id), | ||||
| 'jobs', str(job_index), | 'jobs', str(job_index), | ||||
| 'artifacts', fn | 'artifacts', fn | ||||
| ) | ) | ||||
| return res | return res | ||||
| @app.route('/repos/<int:repo_id>/latest/<ref_name>/artifacts/<artifact_type>', methods=['GET']) | |||||
| @defer.inlineCallbacks | |||||
| def getLatestArtifact(self, request, repo_id, ref_name, artifact_type): | |||||
| if not validate_ref_name(ref_name): | |||||
| request.setResponseCode(400) | |||||
| request.setHeader('Content-Type', 'application/json') | |||||
| return json.dumps({'error': f'Bad ref_name format: {ref_name}'}) | |||||
| if not validate_artifact_type(artifact_type): | |||||
| request.setResponseCode(400) | |||||
| request.setHeader('Content-Type', 'application/json') | |||||
| return json.dumps({'error': BAD_REQUEST}) | |||||
| is_good = True | |||||
| if b'good' in request.args: | |||||
| good = request.args[b'good'][0] | |||||
| if good == b'1': | |||||
| is_good = True | |||||
| elif good == b'0': | |||||
| is_good = False | |||||
| else: | |||||
| request.setResponseCode(400) | |||||
| request.setHeader('Content-Type', 'application/json') | |||||
| return json.dumps({'error': 'query `good` must be 0 or 1'}) | |||||
| if b'job' not in request.args: | |||||
| request.setResponseCode(400) | |||||
| request.setHeader('Content-Type', 'application/json') | |||||
| return json.dumps({'error': 'query `job` missing'}) | |||||
| job_name = request.args[b'job'][0].decode() | |||||
| link_name = os.path.join( | |||||
| self.lbc().storage_dir, | |||||
| 'repos', str(repo_id), | |||||
| 'latest-good' if is_good else 'latest', ref_name | |||||
| ) | |||||
| try: | |||||
| target = os.readlink(link_name) | |||||
| build_id = int(os.path.basename(target)) | |||||
| artifact_file = yield self.get_artifact_location(request, build_id, None, artifact_type, job_name) | |||||
| if os.path.exists(artifact_file): | |||||
| return File(artifact_file) | |||||
| else: | |||||
| raise FileNotFoundError('Not Found') | |||||
| except: | |||||
| request.setResponseCode(404) | |||||
| request.setHeader('Content-Type', 'application/json') | |||||
| return json.dumps({'error': NOT_FOUND}) | |||||