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)
141 changes: 141 additions & 0 deletions api/routes/mirrors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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) -> int:
return value.days * 24 * 3600 + value.seconds


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


def _url_to_schema(url) -> MirrorUrlSchema:
return MirrorUrlSchema(
url=url.url,
protocol=str(url.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")
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,
)
163 changes: 163 additions & 0 deletions api/routes/packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
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 ninja import Router

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")
def pkgbase_maintainer(request):
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
45 changes: 45 additions & 0 deletions api/routes/public.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from datetime import datetime, timezone

from django.contrib.auth.models import User
from django.db.models import Q
from ninja import Router

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")
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)
19 changes: 19 additions & 0 deletions api/routes/todolists.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import json

from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from ninja import Router

from api.schemas.todolists import TodolistSchema
from todolists.models import Todolist
from todolists.views import TodoListJSONEncoder

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


@router.get("/{slug}/", response=TodolistSchema, url_name="todolist-details")
def todolist_details(request, slug: str):
# Same as with /todo/{slug}/json
todolist = get_object_or_404(Todolist, slug=slug)
to_json = json.dumps(todolist, ensure_ascii=False, cls=TodoListJSONEncoder)
return HttpResponse(to_json, content_type='application/json')
Loading
Loading