Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion api/router.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from ninja import NinjaAPI

from api.routes import releng
from api.routes import mirrors, packages, public, releng, todolists

api = NinjaAPI(
version="1",
Expand All @@ -11,3 +11,7 @@
)

api.add_router("/v1/releng/", releng.router)
api.add_router("/v1/mirrors/", mirrors.router)
api.add_router("/v1/master-keys/", public.router)
api.add_router("/v1/packages/", packages.router)
api.add_router("/v1/todo/", todolists.router)
143 changes: 143 additions & 0 deletions api/routes/mirrors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from datetime import timedelta

from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.timezone import now
from ninja import Router

from api.schemas.mirrors import (
MirrorDetailsSchema,
MirrorLocationSchema,
MirrorLocationsSchema,
MirrorLogSchema,
MirrorStatusSchema,
MirrorUrlSchema,
MirrorUrlWithLogsSchema,
)
from mirrors.models import CheckLocation, Mirror
from mirrors.utils import DEFAULT_CUTOFF, get_mirror_statuses

router = Router(tags=["mirrors"])


def _td_seconds(value: timedelta) -> int:
return value.days * 24 * 3600 + value.seconds


def _td_seconds_or_none(value: timedelta | None) -> int | None:
if value is None:
return None
return _td_seconds(value)


def _url_to_schema(url) -> MirrorUrlSchema:
return MirrorUrlSchema(
url=url.url,
protocol=url.protocol.protocol,
last_sync=url.last_sync,
completion_pct=url.completion_pct,
delay=_td_seconds_or_none(url.delay),
duration_avg=url.duration_avg,
duration_stddev=url.duration_stddev,
score=url.score,
active=url.active,
country=str(url.country.name),
country_code=url.country.code,
isos=url.mirror.isos,
ipv4=url.has_ipv4,
ipv6=url.has_ipv6,
details=url.get_full_url(),
)


def _url_with_logs_to_schema(url, cutoff_time) -> MirrorUrlWithLogsSchema:
base = _url_to_schema(url)
logs = [
MirrorLogSchema(
check_time=log.check_time,
last_sync=log.last_sync,
duration=log.duration,
is_success=log.is_success,
location_id=log.location_id,
error=log.error or None,
)
for log in url.logs.filter(check_time__gte=cutoff_time).order_by('check_time')
]
return MirrorUrlWithLogsSchema(**base.dict(), logs=logs)


@router.get("/status/", response=MirrorStatusSchema, url_name="mirror-status")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this one is missing @cache_control(max_age=311)

def status(request):
info = get_mirror_statuses()
return MirrorStatusSchema(
version=3,
urls=[_url_to_schema(u) for u in info['urls']],
cutoff=_td_seconds(info["cutoff"]),
last_check=info['last_check'],
num_checks=info['num_checks'],
check_frequency=_td_seconds_or_none(info["check_frequency"]),
)


@router.get("/status/tier/{tier}/", response=MirrorStatusSchema, url_name="mirror-status-tier")
def status_tier(request, tier: int):
if tier not in [t[0] for t in Mirror.TIER_CHOICES]:
raise Http404
info = get_mirror_statuses()
urls = [u for u in info['urls'] if u.mirror.tier == tier]
return MirrorStatusSchema(
version=3,
urls=[_url_to_schema(u) for u in urls],
cutoff=_td_seconds(info["cutoff"]),
last_check=info['last_check'],
num_checks=info['num_checks'],
check_frequency=_td_seconds_or_none(info["check_frequency"]),
)


@router.get("/locations/", response=MirrorLocationsSchema, url_name="mirror-locations")
def locations(request):
return MirrorLocationsSchema(
version=1,
locations=[
MirrorLocationSchema(
id=loc.pk,
hostname=loc.hostname,
source_ip=loc.source_ip,
country=str(loc.country.name),
country_code=loc.country.code,
ip_version=loc.ip_version,
)
for loc in CheckLocation.objects.all().order_by('pk')
],
)


@router.get("/{name}/", response=MirrorDetailsSchema, url_name="mirror-details")
def mirror_details(request, name: str):
authorized = request.user.is_authenticated
mirror = get_object_or_404(Mirror, name=name)
if not authorized and (not mirror.public or not mirror.active):
raise Http404
info = get_mirror_statuses(mirror_id=mirror.id, show_all=authorized)
cutoff_time = now() - DEFAULT_CUTOFF

admin_email = None
alternate_email = None
if authorized and request.user.has_perm('mirrors.change_mirror'):
admin_email = mirror.admin_email
alternate_email = mirror.alternate_email

return MirrorDetailsSchema(
version=5,
tier=mirror.tier,
upstream=mirror.upstream.name if mirror.upstream else None,
details=mirror.get_full_url(),
urls=[_url_with_logs_to_schema(u, cutoff_time) for u in info['urls']],
cutoff=_td_seconds(info["cutoff"]),
last_check=info['last_check'],
num_checks=info['num_checks'],
check_frequency=_td_seconds_or_none(info["check_frequency"]),
admin_email=admin_email,
alternate_email=alternate_email,
)
166 changes: 166 additions & 0 deletions api/routes/packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import json
from collections import defaultdict

from django.contrib.auth.models import User
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from django.views.decorators.cache import cache_control
from ninja import Router
from ninja.decorators import decorate_view

from api.schemas.packages import (
PackageFilesSchema,
PackageSchema,
PackageSearchSchema,
)
from main.models import Package, PackageFile, Soname
from packages.models import PackageRelation
from packages.utils import DEPENDENCY_TYPES, PackageJSONEncoder, attach_maintainers
from packages.views.search import PackageSearchForm, parse_form

router = Router(tags=["packages"])


def _pkg_to_schema(pkg: Package) -> PackageSchema:
all_deps = list(pkg.depends.all())
deps_by_type = {
name: [str(d) for d in all_deps if d.deptype == deptype]
for deptype, name in DEPENDENCY_TYPES
}
return PackageSchema(
pkgname=pkg.pkgname,
pkgbase=pkg.pkgbase,
repo=pkg.repo.name.lower(),
arch=pkg.arch.name.lower(),
pkgver=pkg.pkgver,
pkgrel=pkg.pkgrel,
epoch=pkg.epoch,
pkgdesc=pkg.pkgdesc or None,
url=pkg.url or None,
filename=pkg.filename,
compressed_size=pkg.compressed_size,
installed_size=pkg.installed_size,
build_date=pkg.build_date,
last_update=pkg.last_update,
flag_date=pkg.flag_date,
maintainers=[u.username for u in pkg.maintainers],
packager=pkg.packager.username if pkg.packager else None,
groups=[g.name for g in pkg.groups.all()],
licenses=[lic.name for lic in pkg.licenses.all()],
conflicts=[str(c) for c in pkg.conflicts.all()],
provides=[str(p) for p in pkg.provides.all()],
replaces=[str(r) for r in pkg.replaces.all()],
depends=deps_by_type['depends'],
optdepends=deps_by_type['optdepends'],
makedepends=deps_by_type['makedepends'],
checkdepends=deps_by_type['checkdepends'],
)


def _get_package(name: str, repo: str, arch: str) -> Package:
return get_object_or_404(
Package.objects.normal(),
pkgname=name, repo__name__iexact=repo, arch__name=arch,
)


@router.get("/search/", response=PackageSearchSchema, url_name="package-search")
def search(request):
limit = 250
container = {
'version': 2,
'limit': limit,
'valid': False,
'results': [],
}

if request.GET:
form = PackageSearchForm(data=request.GET,
show_staging=request.user.is_authenticated)
if form.is_valid():
form_limit = form.cleaned_data.get('limit', limit)
limit = min(limit, form_limit) if form_limit else limit
container['limit'] = limit

packages = Package.objects.select_related('arch', 'repo', 'packager')
if not request.user.is_authenticated:
packages = packages.filter(repo__staging=False)
packages = parse_form(form, packages)

paginator = Paginator(packages, limit)
container['num_pages'] = paginator.num_pages
container['count'] = paginator.count

page = form.cleaned_data.get('page')
try:
page = int(page) if page else 1
except ValueError:
return HttpResponseBadRequest('page parameter is not a number')
container['page'] = page
try:
packages = paginator.page(page)
except PageNotAnInteger:
packages = paginator.page(1)
except EmptyPage:
packages = paginator.page(paginator.num_pages)

attach_maintainers(packages)
container['results'] = packages
container['valid'] = True

to_json = json.dumps(container, ensure_ascii=False, cls=PackageJSONEncoder)
return HttpResponse(to_json, content_type='application/json')


@router.get("/{repo}/{arch}/{name}/", response=PackageSchema, url_name="package-details")
def details(request, repo: str, arch: str, name: str):
return _pkg_to_schema(_get_package(name, repo, arch))


@router.get("/{repo}/{arch}/{name}/files/", response=PackageFilesSchema, url_name="package-files")
def files(request, repo: str, arch: str, name: str):
pkg = _get_package(name, repo, arch)
# files are inserted in sorted order, so preserve that
fileslist = PackageFile.objects.filter(pkg=pkg).order_by('id')
dir_count = sum(1 for f in fileslist if f.is_directory)
files_count = len(fileslist) - dir_count
return PackageFilesSchema(
pkgname=pkg.pkgname,
repo=pkg.repo.name.lower(),
arch=pkg.arch.name.lower(),
pkg_last_update=pkg.last_update,
files_last_update=pkg.files_last_update,
files_count=files_count,
dir_count=dir_count,
files=[f.directory + (f.filename or '') for f in fileslist],
)


@router.get("/{repo}/{arch}/{name}/sonames/", response=list[str], url_name="package-sonames")
def sonames(request, repo: str, arch: str, name: str):
pkg = _get_package(name, repo, arch)
return list(Soname.objects.filter(pkg=pkg).values_list('name', flat=True))


@router.get("/pkgbase-maintainer", response=dict[str, list[str]], url_name="pkgbase-maintainer")
@decorate_view(cache_control(public=True, max_age=300))
def pkgbase_maintainer(request):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint was cached before max_age=300

pkgbases = Package.objects.all().values('pkgbase')
rels = PackageRelation.objects.filter(
type=PackageRelation.MAINTAINER, pkgbase__in=pkgbases
).values_list('pkgbase', 'user_id').order_by().distinct()

user_ids = {rel[1] for rel in rels}
users = User.objects.in_bulk(user_ids)

maintainers = defaultdict(list)
for pkgbase_name, user_id in rels:
maintainers[pkgbase_name].append(users[user_id].username)

mapping = {}
for row in pkgbases:
pkgbase_name = row['pkgbase']
if pkgbase_name not in mapping:
mapping[pkgbase_name] = maintainers[pkgbase_name]
return mapping
48 changes: 48 additions & 0 deletions api/routes/public.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from datetime import datetime, timezone

from django.contrib.auth.models import User
from django.db.models import Q
from django.views.decorators.cache import cache_page
from ninja import Router
from ninja.decorators import decorate_view

from api.schemas.public import PGPEdgeSchema, PGPKeysSchema, PGPNodeSchema
from devel.models import MasterKey, PGPSignature, UserProfile

router = Router(tags=["public"])


@router.get("/", response=PGPKeysSchema, url_name="pgp-keys")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was cached before @cache_page(1789)

@decorate_view(cache_page(1789))
def master_keys(request):
profile_ids = UserProfile.allowed_repos.through.objects.values('userprofile_id')
users = User.objects.filter(
is_active=True, userprofile__id__in=profile_ids
).order_by('first_name', 'last_name')
nodes = [
PGPNodeSchema(
name=user.get_full_name(),
key=user.userprofile.pgp_key or None,
group='packager',
)
for user in users
]

master = MasterKey.objects.select_related('owner').filter(revoked__isnull=True)
nodes.extend(
PGPNodeSchema(
name='Master Key (%s)' % key.owner.get_full_name(),
key=key.pgp_key or None,
group='master',
)
for key in master
)

not_expired = Q(expires__gt=datetime.now(timezone.utc)) | Q(expires__isnull=True)
signatures = PGPSignature.objects.filter(not_expired, revoked__isnull=True)
edges = [
PGPEdgeSchema(signee=sig.signee, signer=sig.signer)
for sig in signatures
]

return PGPKeysSchema(nodes=nodes, edges=edges)
Loading
Loading