mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-03-07 02:33:38 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 437ae2c16c | |||
| 10661f3127 | |||
| 88ac3d82a0 | |||
| 386765ab4e | |||
| 78998182b8 | |||
| 2563391aaf |
@@ -27,21 +27,26 @@ export default tseslint.config(
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
|
||||
// imports
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
"simple-import-sort/imports": "error",
|
||||
|
||||
// brackets
|
||||
// core
|
||||
"curly": "error",
|
||||
"@stylistic/brace-style": ["error", "1tbs"],
|
||||
"eqeqeq": "error",
|
||||
"no-console": "error",
|
||||
"no-eval": "error",
|
||||
|
||||
// stylistic
|
||||
"@stylistic/array-bracket-spacing": ["error", "never"],
|
||||
"@stylistic/arrow-parens": ["error", "as-needed"],
|
||||
"@stylistic/brace-style": ["error", "1tbs"],
|
||||
"@stylistic/comma-dangle": ["error", "always-multiline"],
|
||||
"@stylistic/comma-spacing": ["error", { before: false, after: true }],
|
||||
"@stylistic/eol-last": ["error", "always"],
|
||||
"@stylistic/indent": ["error", 4],
|
||||
"@stylistic/jsx-curly-brace-presence": ["error", { props: "never", children: "never" }],
|
||||
"@stylistic/jsx-quotes": ["error", "prefer-double"],
|
||||
"@stylistic/jsx-self-closing-comp": ["error", { component: true, html: true }],
|
||||
"@stylistic/max-len": ["error", {
|
||||
code: 120,
|
||||
ignoreComments: true,
|
||||
@@ -49,6 +54,7 @@ export default tseslint.config(
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreUrls: true,
|
||||
}],
|
||||
"@stylistic/member-delimiter-style": ["error", { multiline: { delimiter: "semi" }, singleline: { delimiter: "semi" } }],
|
||||
"@stylistic/no-extra-parens": ["error", "all"],
|
||||
"@stylistic/no-multi-spaces": "error",
|
||||
"@stylistic/no-multiple-empty-lines": ["error", { max: 1 }],
|
||||
@@ -58,10 +64,14 @@ export default tseslint.config(
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
|
||||
// typescript
|
||||
"@typescript-eslint/consistent-type-definitions": ["error", "interface"],
|
||||
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
||||
"@typescript-eslint/explicit-function-return-type": ["error", { allowExpressions: true }],
|
||||
"@typescript-eslint/no-deprecated": "error",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||
"@typescript-eslint/prefer-nullish-coalescing": "error",
|
||||
"@typescript-eslint/prefer-optional-chain": "error",
|
||||
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "ahriman-frontend",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "2.20.0-rc4",
|
||||
"version": "2.20.0-rc6",
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"dev": "vite",
|
||||
@@ -11,30 +11,30 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^7.3.8",
|
||||
"@mui/material": "^7.3.8",
|
||||
"@mui/x-data-grid": "^8.27.3",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^7.3.9",
|
||||
"@mui/material": "^7.3.9",
|
||||
"@mui/x-data-grid": "^8.27.4",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"chart.js": "^4.5.1",
|
||||
"react": "^19.2.4",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-syntax-highlighter": "^16.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.3",
|
||||
"@stylistic/eslint-plugin": "^5.9.0",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"typescript": "^5.3.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"vite": "^7.3.1",
|
||||
"vite-tsconfig-paths": "^6.1.1"
|
||||
|
||||
@@ -49,7 +49,7 @@ export class Client {
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
const requestInit: RequestInit = {
|
||||
method: method || (json ? "POST" : "GET"),
|
||||
method: method ?? (json ? "POST" : "GET"),
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
@@ -31,16 +31,16 @@ export default function PackageCountBarChart({ stats }: PackageCountBarChartProp
|
||||
data={{
|
||||
labels: ["packages"],
|
||||
datasets: [
|
||||
{
|
||||
label: "archives",
|
||||
data: [stats.packages ?? 0],
|
||||
backgroundColor: blue[500],
|
||||
},
|
||||
{
|
||||
label: "bases",
|
||||
data: [stats.bases ?? 0],
|
||||
backgroundColor: indigo[300],
|
||||
},
|
||||
{
|
||||
label: "archives",
|
||||
data: [stats.packages ?? 0],
|
||||
backgroundColor: blue[500],
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
@@ -48,7 +48,7 @@ export default function PackageCountBarChart({ stats }: PackageCountBarChartProp
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: { stacked: true },
|
||||
y: { stacked: true },
|
||||
y: { stacked: false },
|
||||
},
|
||||
}}
|
||||
/>;
|
||||
|
||||
@@ -86,12 +86,12 @@ export default function DashboardDialog({ open, onClose }: DashboardDialogProps)
|
||||
|
||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Box sx={{ maxHeight: 300 }}>
|
||||
<Box sx={{ height: 300 }}>
|
||||
<PackageCountBarChart stats={status.stats} />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Box sx={{ maxHeight: 300, display: "flex", justifyContent: "center", alignItems: "center" }}>
|
||||
<Box sx={{ height: 300, display: "flex", justifyContent: "center", alignItems: "center" }}>
|
||||
<StatusPieChart counters={status.packages} />
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function AppLayout(): React.JSX.Element {
|
||||
|
||||
return <Container maxWidth="xl">
|
||||
<Box sx={{ display: "flex", alignItems: "center", py: 1, gap: 1 }}>
|
||||
<a href="https://github.com/arcan1s/ahriman" title="logo">
|
||||
<a href="https://ahriman.readthedocs.io/" title="logo">
|
||||
<img src="/static/logo.svg" width={30} height={30} alt="" />
|
||||
</a>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import path from "path";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
function renameHtml(newName: string): Plugin {
|
||||
function rename(oldName: string, newName: string): Plugin {
|
||||
return {
|
||||
name: "rename-html",
|
||||
name: "rename",
|
||||
enforce: "post",
|
||||
generateBundle(_, bundle) {
|
||||
if (bundle["index.html"]) {
|
||||
bundle["index.html"].fileName = newName;
|
||||
if (bundle[oldName]) {
|
||||
bundle[oldName].fileName = newName;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths(), renameHtml("build-status.jinja2")],
|
||||
plugins: [react(), tsconfigPaths(), rename("index.html", "build-status.jinja2")],
|
||||
base: "/",
|
||||
build: {
|
||||
chunkSizeWarningLimit: 10000,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
pkgbase='ahriman'
|
||||
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
||||
pkgver=2.20.0rc5
|
||||
pkgver=2.20.0rc6
|
||||
pkgrel=1
|
||||
pkgdesc="ArcH linux ReposItory MANager"
|
||||
arch=('any')
|
||||
@@ -17,11 +17,10 @@ source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgbase-$
|
||||
build() {
|
||||
cd "$pkgbase-$pkgver"
|
||||
|
||||
python -m build --wheel --no-isolation
|
||||
npm --prefix "frontend" install --cache "$srcdir/npm-cache"
|
||||
npm --prefix "frontend" run build
|
||||
|
||||
cd "frontend"
|
||||
npm install --cache "$srcdir/npm-cache"
|
||||
npm run build
|
||||
python -m build --wheel --no-isolation
|
||||
}
|
||||
|
||||
package_ahriman() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.TH AHRIMAN "1" "2026\-03\-06" "ahriman 2.20.0rc5" "ArcH linux ReposItory MANager"
|
||||
.TH AHRIMAN "1" "2026\-03\-06" "ahriman 2.20.0rc6" "ArcH linux ReposItory MANager"
|
||||
.SH NAME
|
||||
ahriman \- ArcH linux ReposItory MANager
|
||||
.SH SYNOPSIS
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "2.20.0rc5"
|
||||
__version__ = "2.20.0rc6"
|
||||
|
||||
@@ -138,8 +138,14 @@ class PacmanDatabase(SyncHttpClient):
|
||||
|
||||
Args:
|
||||
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||
|
||||
Raises:
|
||||
PacmanError: on operation error (invalid scheme or incomplete configuration)
|
||||
"""
|
||||
server = next(iter(self.database.servers))
|
||||
try:
|
||||
server = next(iter(self.database.servers))
|
||||
except StopIteration:
|
||||
raise PacmanError("No configured servers available for database") from None
|
||||
filename = f"{self.database.name}.files.tar.gz"
|
||||
url = f"{server}/{filename}"
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class Auth(LazyLogging):
|
||||
Returns:
|
||||
str: login control as html code to insert
|
||||
"""
|
||||
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#login-modal" style="text-decoration: none"><i class="bi bi-box-arrow-in-right"></i> login</button>"""
|
||||
return "<button type=\"button\" class=\"btn btn-link\" data-bs-toggle=\"modal\" data-bs-target=\"#login-modal\" style=\"text-decoration: none\"><i class=\"bi bi-box-arrow-in-right\"></i> login</button>"
|
||||
|
||||
@property
|
||||
def is_external(self) -> bool:
|
||||
|
||||
@@ -116,6 +116,19 @@ class GitRemoteError(RuntimeError):
|
||||
RuntimeError.__init__(self, "Git remote failed")
|
||||
|
||||
|
||||
class GPGError(RuntimeError):
|
||||
"""
|
||||
PGP/GPG related exception
|
||||
"""
|
||||
|
||||
def __init__(self, details: str) -> None:
|
||||
"""
|
||||
Args:
|
||||
details(str): details of the exception
|
||||
"""
|
||||
RuntimeError.__init__(self, f"GPG operation failed: {details}")
|
||||
|
||||
|
||||
class InitializeError(RuntimeError):
|
||||
"""
|
||||
base service initialization exception
|
||||
|
||||
@@ -86,6 +86,11 @@ class ArchiveRotationTrigger(Trigger):
|
||||
package(Package): package which has been updated to check for older versions
|
||||
pacman(Pacman): alpm wrapper instance
|
||||
"""
|
||||
# explicit guard to skip process in case if rotation is disabled
|
||||
# this guard is supposed to speedup process
|
||||
if self.keep_built_packages == 0:
|
||||
return
|
||||
|
||||
packages: dict[tuple[str, str], Package] = {}
|
||||
# we can't use here load_archives, because it ignores versions
|
||||
for full_path in filter(package_like, self.paths.archive_for(package.base).iterdir()):
|
||||
@@ -94,7 +99,7 @@ class ArchiveRotationTrigger(Trigger):
|
||||
|
||||
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
|
||||
to_remove = sorted(packages.values(), key=cmp_to_key(comparator))
|
||||
# 0 will implicitly be translated into [:0], meaning we keep all packages
|
||||
|
||||
for single in to_remove[:-self.keep_built_packages]:
|
||||
self.logger.info("removing version %s of package %s", single.version, single.base)
|
||||
for archive in single.packages.values():
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import BuildError
|
||||
from ahriman.core.exceptions import BuildError, GPGError
|
||||
from ahriman.core.http import SyncHttpClient
|
||||
from ahriman.core.utils import check_output
|
||||
from ahriman.models.sign_settings import SignSettings
|
||||
@@ -147,12 +147,19 @@ class GPG(SyncHttpClient):
|
||||
|
||||
Returns:
|
||||
str: full PGP key fingerprint
|
||||
|
||||
Raises:
|
||||
GPGError: if key is in wrong format
|
||||
"""
|
||||
metadata = check_output("gpg", "--with-colons", "--fingerprint", key, logger=self.logger)
|
||||
# fingerprint line will be like
|
||||
# fpr:::::::::43A663569A07EE1E4ECC55CC7E3A4240CE3C45C2:
|
||||
fingerprint = next(filter(lambda line: line[:3] == "fpr", metadata.splitlines()))
|
||||
return fingerprint.split(":")[-2]
|
||||
metadata = check_output("gpg", "--with-colons", "--fingerprint", key, logger=self.logger)
|
||||
|
||||
try:
|
||||
fingerprint = next(filter(lambda line: line[:3] == "fpr", metadata.splitlines()))
|
||||
return fingerprint.split(":")[-2]
|
||||
except (IndexError, StopIteration):
|
||||
raise GPGError(f"key {key} has invalid metadata") from None
|
||||
|
||||
def key_import(self, server: str, key: str) -> None:
|
||||
"""
|
||||
|
||||
@@ -88,8 +88,12 @@ class User:
|
||||
"""
|
||||
if not self.password:
|
||||
return None
|
||||
algo = next(segment for segment in self.password.split("$") if segment)
|
||||
return f"${algo}$"
|
||||
|
||||
try:
|
||||
algo = next(segment for segment in self.password.split("$") if segment)
|
||||
return f"${algo}$"
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def generate_password(length: int) -> str:
|
||||
|
||||
@@ -25,9 +25,11 @@ class AuthInfoSchema(Schema):
|
||||
authorization information schema
|
||||
"""
|
||||
|
||||
control = fields.String(required=True, metadata={
|
||||
"description": "HTML control for login interface",
|
||||
})
|
||||
control = fields.String(
|
||||
metadata={
|
||||
"description": "HTML control for login interface",
|
||||
"example": "<button type=\"button\" class=\"btn btn-link\" data-bs-toggle=\"modal\" data-bs-target=\"#login-modal\" style=\"text-decoration: none\"><i class=\"bi bi-box-arrow-in-right\"></i> login</button>",
|
||||
})
|
||||
enabled = fields.Boolean(required=True, metadata={
|
||||
"description": "Whether authentication is enabled or not",
|
||||
})
|
||||
@@ -35,5 +37,5 @@ class AuthInfoSchema(Schema):
|
||||
"description": "Whether authorization provider is external (e.g. OAuth)",
|
||||
})
|
||||
username = fields.String(metadata={
|
||||
"description": "Currently authenticated username if any",
|
||||
"description": "Currently authenticated username if available",
|
||||
})
|
||||
|
||||
@@ -27,10 +27,12 @@ class AutoRefreshIntervalSchema(Schema):
|
||||
|
||||
interval = fields.Integer(required=True, metadata={
|
||||
"description": "Auto refresh interval in milliseconds",
|
||||
"example": "60000",
|
||||
})
|
||||
is_active = fields.Boolean(required=True, metadata={
|
||||
"description": "Whether this interval is the default active one",
|
||||
})
|
||||
text = fields.String(required=True, metadata={
|
||||
"description": "Human readable interval description",
|
||||
"example": "1 minute",
|
||||
})
|
||||
|
||||
@@ -40,6 +40,7 @@ class InfoV2Schema(Schema):
|
||||
})
|
||||
index_url = fields.String(metadata={
|
||||
"description": "URL to the repository index page",
|
||||
"example": "https://ahriman.readthedocs.io/",
|
||||
})
|
||||
repositories = fields.Nested(RepositoryIdSchema(many=True), required=True, metadata={
|
||||
"description": "List of loaded repositories",
|
||||
|
||||
@@ -31,7 +31,7 @@ class RepositoryIdSchema(Schema):
|
||||
})
|
||||
id = fields.String(metadata={
|
||||
"description": "Unique repository identifier",
|
||||
"example": "aur-x86_64",
|
||||
"example": "x86_64-aur",
|
||||
})
|
||||
repository = fields.String(metadata={
|
||||
"description": "Repository name",
|
||||
|
||||
@@ -183,6 +183,15 @@ def test_sync_files_local(pacman_database: PacmanDatabase, mocker: MockerFixture
|
||||
copy_mock.assert_called_once_with(Path("/var/core.files.tar.gz"), pytest.helpers.anyvar(int))
|
||||
|
||||
|
||||
def test_sync_files_no_servers(pacman_database: PacmanDatabase) -> None:
|
||||
"""
|
||||
must raise PacmanError if no servers are configured
|
||||
"""
|
||||
pacman_database.database.servers = []
|
||||
with pytest.raises(PacmanError):
|
||||
pacman_database.sync_files(force=False)
|
||||
|
||||
|
||||
def test_sync_files_unknown_source(pacman_database: PacmanDatabase) -> None:
|
||||
"""
|
||||
must raise an exception in case if server scheme is unsupported
|
||||
|
||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import GPGError
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.models.sign_settings import SignSettings
|
||||
|
||||
@@ -113,6 +114,15 @@ fpr:::::::::43A663569A07EE1E4ECC55CC7E3A4240CE3C45C2:""")
|
||||
check_output_mock.assert_called_once_with("gpg", "--with-colons", "--fingerprint", key, logger=gpg.logger)
|
||||
|
||||
|
||||
def test_key_fingerprint_invalid(gpg: GPG, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise GPGError if no fingerprint found in output
|
||||
"""
|
||||
mocker.patch("ahriman.core.sign.gpg.check_output", return_value="no fingerprint here")
|
||||
with pytest.raises(GPGError):
|
||||
gpg.key_fingerprint("0xCE3C45C2")
|
||||
|
||||
|
||||
def test_key_import(gpg: GPG, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must import PGP key from the server
|
||||
|
||||
@@ -12,6 +12,8 @@ def test_algo() -> None:
|
||||
"""
|
||||
assert User(username="user", password=None, access=UserAccess.Read).algo is None
|
||||
assert User(username="user", password="", access=UserAccess.Read).algo is None
|
||||
assert User(username="user", password="$$$", access=UserAccess.Read).algo is None
|
||||
|
||||
assert User(
|
||||
username="user",
|
||||
password="$6$rounds=656000$mWBiecMPrHAL1VgX$oU4Y5HH8HzlvMaxwkNEJjK13ozElyU1wAHBoO/WW5dAaE4YEfnB0X3FxbynKMl4FBdC3Ovap0jINz4LPkNADg0",
|
||||
|
||||
5
tox.toml
5
tox.toml
@@ -268,12 +268,13 @@ commands = [
|
||||
[
|
||||
"git",
|
||||
"add",
|
||||
"package/archlinux/PKGBUILD",
|
||||
"src/ahriman/__init__.py",
|
||||
"docs/_static/architecture.dot",
|
||||
"frontend/package.json",
|
||||
"package/archlinux/PKGBUILD",
|
||||
"package/share/man/man1/ahriman.1",
|
||||
"package/share/bash-completion/completions/_ahriman",
|
||||
"package/share/zsh/site-functions/_ahriman",
|
||||
"src/ahriman/__init__.py",
|
||||
],
|
||||
[
|
||||
"git",
|
||||
|
||||
Reference in New Issue
Block a user