""" This blueprint deals with everything under /api/projects/. Used environment variables: GITEA_URL: The URL (either intenral or external) to the Gitea instance to pull data from. GITEA_TOKEN: The API token for the crawler user. Only requires repository.read. """ from urllib.parse import urlencode, urlparse, urlunparse, ParseResult from io import BytesIO import logging import string import os import requests import flask import magic log = logging.getLogger('blueprints.projects') class Blueprint(flask.Blueprint): """ Holds the required data for this blueprint to work. Automatically gets the UID for the logged in user, and gives access to a simple constructURL() function to make API calls without specifying the token each time. """ def __init__(self, *args, **kwargs): self._baseURL = urlparse(os.getenv('GITEA_URL')) self._apiKey = os.getenv('GITEA_TOKEN') self.giteaUID = None try: r = requests.get(self.constructURL('/api/v1/user'), timeout=5) if r.ok: self.giteaUID = r.json().get('id') else: log.error('Cannot get Gitea user ID. This blueprint will not work.') except requests.exceptions.ConnectionError: log.error('Failed to connect to Gitea server. This blueprint will not work.') super().__init__(*args, **kwargs) def constructURL(self, apiPath: str, query: dict=None) -> str: """ Construcs an API url from the path and queries. """ if query is None: query = {} query.update({'token': self._apiKey}) newURL = ParseResult(scheme=self._baseURL.scheme, netloc=self._baseURL.netloc, path=apiPath, params='', query=urlencode(query), fragment='') return urlunparse(newURL) projectsBP = Blueprint('projectsAPI', __name__) def generateStrippedProjectData(data: dict) -> dict: """ Generates the stripped down project data that the front-end expects. """ # ----- Languages ----- # Get the languages path and request it path = urlparse(data['languages_url']).path r = requests.get(projectsBP.constructURL(path), timeout=5) languages = r.json() # Convert all languages from bytes to percentages languages = {lang: size / sum(languages.values()) for lang, size in languages.items()} # ----- Image ----- if not data.get('avatar_url', '') == '': # Get the avatar URL avatarUrl = urlparse(data['avatar_url']) # Parse the URL to get just the hash imgHash = avatarUrl.path.split('/')[-1:][0] # Create an API path for that image imgPath = f'/api/projects/img/{imgHash}' else: imgPath = None # ----- Git URL ----- if not data.get('private'): gitUrl = data.get('html_url') else: gitUrl = None newData = {} newData.update({'name': data.get('name')}) newData.update({'desc': data.get('description')}) newData.update({'issues': data.get('open_issues_count')}) newData.update({'langs': languages}) newData.update({'gitUrl': gitUrl}) newData.update({'imgPath': imgPath}) return newData @projectsBP.route('/api/projects', strict_slashes=False) def projectsList(): """ Returns a filtered list of Gitea projects. """ # Request for all Gitea projects that the crawler account is a contributor in r = requests.get(projectsBP.constructURL('/api/v1/repos/search', {'exclusive': False, 'uid': projectsBP.giteaUID}), timeout=5) if r.ok: # Filter down every repo with generateStrippedProjectData() projects = [generateStrippedProjectData(data) for data in r.json().get('data')] return projects else: flask.abort(500) @projectsBP.route('/api/projects/img/', strict_slashes=False) def img(imgHash: str): """ Forwards images from Gitea's /repo-avatars. """ # Filter the hash imgHash = ''.join([c for c in imgHash if c in string.ascii_lowercase+string.digits]) # Make the request the Gitea r = requests.get(projectsBP.constructURL(f'/repo-avatars/{imgHash}'), timeout=5) # Get the MIME type mime = magic.from_buffer(BytesIO(r.content).read(128), mime=True) # Send the image content = BytesIO(r.content) return flask.send_file(content, mime)