mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-04-01 06:03:39 +00:00
feat: allow to use single web instance for all repositories (#114)
* Allow to use single web instance for any repository * some improvements * drop includes from user home directory, introduce new variables to docker The old solution didn't actually work as expected, because devtools configuration belongs to filesystem (as well as sudo one), so it was still required to run setup command. In order to handle additional repositories, the POSTSETUP and PRESETUP commands variables have been introduced. FAQ has been updated as well * raise 404 in case if repository is unknown
This commit is contained in:
@@ -18,14 +18,16 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp_cors import CorsViewMixin # type: ignore[import-untyped]
|
||||
from aiohttp.web import HTTPBadRequest, Request, StreamResponse, View
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Request, StreamResponse, View
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from ahriman.core.auth import Auth
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.spawn import Spawn
|
||||
from ahriman.core.status.watcher import Watcher
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.user_access import UserAccess
|
||||
|
||||
|
||||
@@ -56,15 +58,25 @@ class BaseView(View, CorsViewMixin):
|
||||
return configuration
|
||||
|
||||
@property
|
||||
def service(self) -> Watcher:
|
||||
def services(self) -> dict[RepositoryId, Watcher]:
|
||||
"""
|
||||
get status watcher instance
|
||||
get all loaded watchers
|
||||
|
||||
Returns:
|
||||
Watcher: build status watcher instance
|
||||
dict[RepositoryId, Watcher]: map of loaded watchers per known repository
|
||||
"""
|
||||
watcher: Watcher = self.request.app["watcher"]
|
||||
return watcher
|
||||
watchers: dict[RepositoryId, Watcher] = self.request.app["watcher"]
|
||||
return watchers
|
||||
|
||||
@property
|
||||
def sign(self) -> GPG:
|
||||
"""
|
||||
get GPG control instance
|
||||
|
||||
Returns:
|
||||
GPG: GPG wrapper instance
|
||||
"""
|
||||
return GPG(self.configuration)
|
||||
|
||||
@property
|
||||
def spawner(self) -> Spawn:
|
||||
@@ -197,8 +209,8 @@ class BaseView(View, CorsViewMixin):
|
||||
HTTPBadRequest: if supplied parameters are invalid
|
||||
"""
|
||||
try:
|
||||
limit = int(self.request.query.getone("limit", default=-1))
|
||||
offset = int(self.request.query.getone("offset", default=0))
|
||||
limit = int(self.request.query.get("limit", default=-1))
|
||||
offset = int(self.request.query.get("offset", default=0))
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
@@ -210,6 +222,40 @@ class BaseView(View, CorsViewMixin):
|
||||
|
||||
return limit, offset
|
||||
|
||||
def repository_id(self) -> RepositoryId:
|
||||
"""
|
||||
extract repository from request
|
||||
|
||||
Returns:
|
||||
RepositoryIde: repository if possible to construct and first one otherwise
|
||||
"""
|
||||
architecture = self.request.query.get("architecture")
|
||||
name = self.request.query.get("repository")
|
||||
|
||||
if architecture and name:
|
||||
return RepositoryId(architecture, name)
|
||||
return next(iter(sorted(self.services.keys())))
|
||||
|
||||
def service(self, repository_id: RepositoryId | None = None) -> Watcher:
|
||||
"""
|
||||
get status watcher instance
|
||||
|
||||
Args:
|
||||
repository_id(RepositoryId | None, optional): repository unique identifier (Default value = None)
|
||||
|
||||
Returns:
|
||||
Watcher: build status watcher instance. If no repository provided, it will return the first one
|
||||
|
||||
Raises:
|
||||
HTTPNotFound: if no repository found
|
||||
"""
|
||||
if repository_id is None:
|
||||
repository_id = self.repository_id()
|
||||
try:
|
||||
return self.services[repository_id]
|
||||
except KeyError:
|
||||
raise HTTPNotFound(reason=f"Repository {repository_id.id} is unknown")
|
||||
|
||||
async def username(self) -> str | None:
|
||||
"""
|
||||
extract username from request if any
|
||||
|
||||
@@ -37,7 +37,10 @@ class IndexView(BaseView):
|
||||
* enabled - whether authorization is enabled by configuration or not, boolean, required
|
||||
* username - authenticated username if any, string, null means not authenticated
|
||||
* index_url - url to the repository index, string, optional
|
||||
* repository - repository name, string, required
|
||||
* repositories - list of repositories unique identifiers, required
|
||||
* id - unique repository identifier, string, required
|
||||
* repository - repository name, string, required
|
||||
* architecture - repository architecture, string, required
|
||||
|
||||
Attributes:
|
||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||
@@ -64,5 +67,11 @@ class IndexView(BaseView):
|
||||
return {
|
||||
"auth": auth,
|
||||
"index_url": self.configuration.get("web", "index_url", fallback=None),
|
||||
"repository": self.service.repository.name,
|
||||
"repositories": [
|
||||
{
|
||||
"id": repository.id,
|
||||
**repository.view(),
|
||||
}
|
||||
for repository in sorted(self.services)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
|
||||
from aiohttp.web import HTTPBadRequest, Response, json_response
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@@ -46,11 +46,13 @@ class AddView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
@aiohttp_apispec.json_schema(PackageNamesSchema)
|
||||
async def post(self) -> Response:
|
||||
"""
|
||||
@@ -68,7 +70,8 @@ class AddView(BaseView):
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
repository_id = self.repository_id()
|
||||
username = await self.username()
|
||||
process_id = self.spawner.packages_add(packages, username, now=True)
|
||||
process_id = self.spawner.packages_add(repository_id, packages, username, now=True)
|
||||
|
||||
return json_response({"process_id": process_id})
|
||||
|
||||
@@ -48,7 +48,7 @@ class PGPView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Package base is unknown", "schema": ErrorSchema},
|
||||
404: {"description": "PGP key is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
@@ -67,15 +67,15 @@ class PGPView(BaseView):
|
||||
HTTPNotFound: if key wasn't found or service was unable to fetch it
|
||||
"""
|
||||
try:
|
||||
key = self.get_non_empty(self.request.query.getone, "key")
|
||||
server = self.get_non_empty(self.request.query.getone, "server")
|
||||
key = self.get_non_empty(self.request.query.get, "key")
|
||||
server = self.get_non_empty(self.request.query.get, "server")
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
try:
|
||||
key = self.service.repository.sign.key_download(server, key)
|
||||
key = self.sign.key_download(server, key)
|
||||
except Exception:
|
||||
raise HTTPNotFound
|
||||
raise HTTPNotFound(reason=f"Key {key} is unknown")
|
||||
|
||||
return json_response({"key": key})
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class ProcessView(BaseView):
|
||||
200: {"description": "Success response", "schema": ProcessSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Not found", "schema": ErrorSchema},
|
||||
404: {"description": "Process is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
|
||||
@@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
|
||||
from aiohttp.web import HTTPBadRequest, Response, json_response
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@@ -46,11 +46,13 @@ class RebuildView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
@aiohttp_apispec.json_schema(PackageNamesSchema)
|
||||
async def post(self) -> Response:
|
||||
"""
|
||||
@@ -69,7 +71,8 @@ class RebuildView(BaseView):
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
repository_id = self.repository_id()
|
||||
username = await self.username()
|
||||
process_id = self.spawner.packages_rebuild(depends_on, username)
|
||||
process_id = self.spawner.packages_rebuild(repository_id, depends_on, username)
|
||||
|
||||
return json_response({"process_id": process_id})
|
||||
|
||||
@@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
|
||||
from aiohttp.web import HTTPBadRequest, Response, json_response
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@@ -46,11 +46,13 @@ class RemoveView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
@aiohttp_apispec.json_schema(PackageNamesSchema)
|
||||
async def post(self) -> Response:
|
||||
"""
|
||||
@@ -68,6 +70,7 @@ class RemoveView(BaseView):
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
process_id = self.spawner.packages_remove(packages)
|
||||
repository_id = self.repository_id()
|
||||
process_id = self.spawner.packages_remove(repository_id, packages)
|
||||
|
||||
return json_response({"process_id": process_id})
|
||||
|
||||
@@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
|
||||
from aiohttp.web import HTTPBadRequest, Response, json_response
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@@ -46,11 +46,13 @@ class RequestView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
@aiohttp_apispec.json_schema(PackageNamesSchema)
|
||||
async def post(self) -> Response:
|
||||
"""
|
||||
@@ -69,6 +71,7 @@ class RequestView(BaseView):
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
username = await self.username()
|
||||
process_id = self.spawner.packages_add(packages, username, now=False)
|
||||
repository_id = self.repository_id()
|
||||
process_id = self.spawner.packages_add(repository_id, packages, username, now=False)
|
||||
|
||||
return json_response({"process_id": process_id})
|
||||
|
||||
@@ -69,7 +69,7 @@ class SearchView(BaseView):
|
||||
"""
|
||||
try:
|
||||
search: list[str] = self.get_non_empty(lambda key: self.request.query.getall(key, default=[]), "for")
|
||||
packages = AUR.multisearch(*search, pacman=self.service.repository.pacman)
|
||||
packages = AUR.multisearch(*search)
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
|
||||
from aiohttp.web import HTTPBadRequest, Response, json_response
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, ProcessIdSchema, UpdateFlagsSchema
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, ProcessIdSchema, UpdateFlagsSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@@ -46,11 +46,13 @@ class UpdateView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
@aiohttp_apispec.json_schema(UpdateFlagsSchema)
|
||||
async def post(self) -> Response:
|
||||
"""
|
||||
@@ -67,8 +69,10 @@ class UpdateView(BaseView):
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
repository_id = self.repository_id()
|
||||
username = await self.username()
|
||||
process_id = self.spawner.packages_update(
|
||||
repository_id,
|
||||
username,
|
||||
aur=data.get("aur", True),
|
||||
local=data.get("local", True),
|
||||
|
||||
@@ -25,8 +25,9 @@ from aiohttp.web import HTTPBadRequest, HTTPCreated, HTTPNotFound
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, FileSchema
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, FileSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@@ -100,12 +101,13 @@ class UploadView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Not found", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown or endpoint is disabled", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
@aiohttp_apispec.form_schema(FileSchema)
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
@@ -125,7 +127,10 @@ class UploadView(BaseView):
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
max_body_size = self.configuration.getint("web", "max_body_size", fallback=None)
|
||||
target = self.configuration.repository_paths.packages
|
||||
|
||||
paths_root = self.configuration.repository_paths.root
|
||||
repository_id = self.repository_id()
|
||||
target = RepositoryPaths(paths_root, repository_id).packages
|
||||
|
||||
files = []
|
||||
while (part := await reader.next()) is not None:
|
||||
|
||||
@@ -25,7 +25,7 @@ from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.util import pretty_datetime
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, LogsSchema, PackageNameSchema
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@@ -51,12 +51,14 @@ class LogsView(BaseView):
|
||||
204: {"description": "Success response"},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [DELETE_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
async def delete(self) -> None:
|
||||
"""
|
||||
delete package logs
|
||||
@@ -65,7 +67,7 @@ class LogsView(BaseView):
|
||||
HTTPNoContent: on success response
|
||||
"""
|
||||
package_base = self.request.match_info["package"]
|
||||
self.service.logs_remove(package_base, None)
|
||||
self.service().logs_remove(package_base, None)
|
||||
|
||||
raise HTTPNoContent
|
||||
|
||||
@@ -77,13 +79,14 @@ class LogsView(BaseView):
|
||||
200: {"description": "Success response", "schema": LogsSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Package base is unknown", "schema": ErrorSchema},
|
||||
404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get last package logs
|
||||
@@ -97,10 +100,10 @@ class LogsView(BaseView):
|
||||
package_base = self.request.match_info["package"]
|
||||
|
||||
try:
|
||||
_, status = self.service.package_get(package_base)
|
||||
_, status = self.service().package_get(package_base)
|
||||
except UnknownPackageError:
|
||||
raise HTTPNotFound
|
||||
logs = self.service.logs_get(package_base)
|
||||
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
|
||||
logs = self.service().logs_get(package_base)
|
||||
|
||||
response = {
|
||||
"package_base": package_base,
|
||||
@@ -118,6 +121,7 @@ class LogsView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
@@ -143,6 +147,6 @@ class LogsView(BaseView):
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
self.service.logs_update(LogRecordId(package_base, version), created, record)
|
||||
self.service().logs_update(LogRecordId(package_base, version), created, record)
|
||||
|
||||
raise HTTPNoContent
|
||||
|
||||
@@ -25,7 +25,8 @@ from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PackageStatusSchema, PackageStatusSimplifiedSchema
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PackageStatusSchema, \
|
||||
PackageStatusSimplifiedSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@@ -51,12 +52,14 @@ class PackageView(BaseView):
|
||||
204: {"description": "Success response"},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [DELETE_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
async def delete(self) -> None:
|
||||
"""
|
||||
delete package base from status page
|
||||
@@ -65,7 +68,7 @@ class PackageView(BaseView):
|
||||
HTTPNoContent: on success response
|
||||
"""
|
||||
package_base = self.request.match_info["package"]
|
||||
self.service.package_remove(package_base)
|
||||
self.service().package_remove(package_base)
|
||||
|
||||
raise HTTPNoContent
|
||||
|
||||
@@ -77,13 +80,14 @@ class PackageView(BaseView):
|
||||
200: {"description": "Success response", "schema": PackageStatusSchema(many=True)},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Package base is unknown", "schema": ErrorSchema},
|
||||
404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get current package base status
|
||||
@@ -95,16 +99,18 @@ class PackageView(BaseView):
|
||||
HTTPNotFound: if no package was found
|
||||
"""
|
||||
package_base = self.request.match_info["package"]
|
||||
repository_id = self.repository_id()
|
||||
|
||||
try:
|
||||
package, status = self.service.package_get(package_base)
|
||||
package, status = self.service(repository_id).package_get(package_base)
|
||||
except UnknownPackageError:
|
||||
raise HTTPNotFound
|
||||
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
|
||||
|
||||
response = [
|
||||
{
|
||||
"package": package.view(),
|
||||
"status": status.view()
|
||||
"status": status.view(),
|
||||
"repository": repository_id.view(),
|
||||
}
|
||||
]
|
||||
return json_response(response)
|
||||
@@ -118,12 +124,14 @@ class PackageView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
@aiohttp_apispec.json_schema(PackageStatusSimplifiedSchema)
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
@@ -143,7 +151,7 @@ class PackageView(BaseView):
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
try:
|
||||
self.service.package_update(package_base, status, package)
|
||||
self.service().package_update(package_base, status, package)
|
||||
except UnknownPackageError:
|
||||
raise HTTPBadRequest(reason=f"Package {package_base} is unknown, but no package body set")
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ from aiohttp.web import HTTPNoContent, Response, json_response
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageStatusSchema, PaginationSchema
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageStatusSchema, PaginationSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ class PackagesView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
@@ -68,12 +69,16 @@ class PackagesView(BaseView):
|
||||
limit, offset = self.page()
|
||||
stop = offset + limit if limit >= 0 else None
|
||||
|
||||
comparator: Callable[[tuple[Package, BuildStatus]], str] = lambda pair: pair[0].base
|
||||
repository_id = self.repository_id()
|
||||
packages = self.service(repository_id).packages
|
||||
|
||||
comparator: Callable[[tuple[Package, BuildStatus]], str] = lambda items: items[0].base
|
||||
response = [
|
||||
{
|
||||
"package": package.view(),
|
||||
"status": status.view()
|
||||
} for package, status in itertools.islice(sorted(self.service.packages, key=comparator), offset, stop)
|
||||
"status": status.view(),
|
||||
"repository": repository_id.view(),
|
||||
} for package, status in itertools.islice(sorted(packages, key=comparator), offset, stop)
|
||||
]
|
||||
|
||||
return json_response(response)
|
||||
@@ -86,11 +91,13 @@ class PackagesView(BaseView):
|
||||
204: {"description": "Success response"},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
reload all packages from repository
|
||||
@@ -98,6 +105,6 @@ class PackagesView(BaseView):
|
||||
Raises:
|
||||
HTTPNoContent: on success response
|
||||
"""
|
||||
self.service.load()
|
||||
self.service().load()
|
||||
|
||||
raise HTTPNoContent
|
||||
|
||||
65
src/ahriman/web/views/v1/status/repositories.py
Normal file
65
src/ahriman/web/views/v1/status/repositories.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#
|
||||
# Copyright (c) 2021-2023 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import aiohttp_apispec # type: ignore[import-untyped]
|
||||
|
||||
from aiohttp.web import Response, json_response
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
class RepositoriesView(BaseView):
|
||||
"""
|
||||
repositories view
|
||||
|
||||
Attributes:
|
||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Read
|
||||
ROUTES = ["/api/v1/repositories"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Status"],
|
||||
summary="Available repositories",
|
||||
description="List available repositories",
|
||||
responses={
|
||||
200: {"description": "Success response", "schema": RepositoryIdSchema(many=True)},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get list of available repositories
|
||||
|
||||
Returns:
|
||||
Response: 200 with service status object
|
||||
"""
|
||||
repositories = [
|
||||
repository_id.view()
|
||||
for repository_id in sorted(self.services)
|
||||
]
|
||||
|
||||
return json_response(repositories)
|
||||
@@ -26,7 +26,7 @@ from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, InternalStatusSchema, StatusSchema
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, InternalStatusSchema, StatusSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class StatusView(BaseView):
|
||||
200: {"description": "Success response", "schema": InternalStatusSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
@@ -63,12 +64,13 @@ class StatusView(BaseView):
|
||||
Returns:
|
||||
Response: 200 with service status object
|
||||
"""
|
||||
counters = Counters.from_packages(self.service.packages)
|
||||
repository_id = self.repository_id()
|
||||
counters = Counters.from_packages(self.service(repository_id).packages)
|
||||
status = InternalStatus(
|
||||
status=self.service.status,
|
||||
architecture=self.service.repository_id.architecture,
|
||||
status=self.service(repository_id).status,
|
||||
architecture=repository_id.architecture,
|
||||
packages=counters,
|
||||
repository=self.service.repository_id.name,
|
||||
repository=repository_id.name,
|
||||
version=__version__,
|
||||
)
|
||||
|
||||
@@ -83,11 +85,13 @@ class StatusView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
@aiohttp_apispec.json_schema(StatusSchema)
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
@@ -103,6 +107,6 @@ class StatusView(BaseView):
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
self.service.status_update(status)
|
||||
self.service().status_update(status)
|
||||
|
||||
raise HTTPNoContent
|
||||
|
||||
@@ -75,7 +75,7 @@ class LoginView(BaseView):
|
||||
if not isinstance(oauth_provider, OAuth): # there is actually property, but mypy does not like it anyway
|
||||
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
|
||||
|
||||
code = self.request.query.getone("code", default=None)
|
||||
code = self.request.query.get("code")
|
||||
if not code:
|
||||
raise HTTPFound(oauth_provider.get_oauth_url())
|
||||
|
||||
|
||||
@@ -23,8 +23,7 @@ from aiohttp.web import HTTPNotFound, Response, json_response
|
||||
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PaginationSchema
|
||||
from ahriman.web.schemas.logs_schema import LogsSchemaV2
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchemaV2, PackageNameSchema, PaginationSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@@ -48,7 +47,7 @@ class LogsView(BaseView):
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Package base is unknown", "schema": ErrorSchema},
|
||||
404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
@@ -70,10 +69,10 @@ class LogsView(BaseView):
|
||||
limit, offset = self.page()
|
||||
|
||||
try:
|
||||
_, status = self.service.package_get(package_base)
|
||||
_, status = self.service().package_get(package_base)
|
||||
except UnknownPackageError:
|
||||
raise HTTPNotFound
|
||||
logs = self.service.logs_get(package_base, limit, offset)
|
||||
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
|
||||
logs = self.service().logs_get(package_base, limit, offset)
|
||||
|
||||
response = {
|
||||
"package_base": package_base,
|
||||
|
||||
Reference in New Issue
Block a user