mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-12 21:45:47 +00:00
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
c6306631e6 | |||
97b906c536 | |||
435375721d | |||
4c5caba6b7 | |||
b83df9d2c5 | |||
f2ea76aab9 | |||
471b1c1331 | |||
bd770aac2f | |||
6abe35ef8c | |||
fdc27a9ebf | |||
b729096a25 | |||
390b9da29e | |||
256376df85 | |||
939a94d889 |
@ -140,6 +140,14 @@ ahriman.web.schemas.logs\_schema module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.schemas.logs\_search\_schema module
|
||||||
|
-----------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.schemas.logs_search_schema
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.schemas.oauth2\_schema module
|
ahriman.web.schemas.oauth2\_schema module
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
|
@ -166,6 +166,7 @@ Reporting to web service related settings. In most cases there is fallback to we
|
|||||||
Web server settings. This feature requires ``aiohttp`` libraries to be installed.
|
Web server settings. This feature requires ``aiohttp`` libraries to be installed.
|
||||||
|
|
||||||
* ``address`` - optional address in form ``proto://host:port`` (``port`` can be omitted in case of default ``proto`` ports), will be used instead of ``http://{host}:{port}`` in case if set, string, optional. This option is required in case if ``OAuth`` provider is used.
|
* ``address`` - optional address in form ``proto://host:port`` (``port`` can be omitted in case of default ``proto`` ports), will be used instead of ``http://{host}:{port}`` in case if set, string, optional. This option is required in case if ``OAuth`` provider is used.
|
||||||
|
* ``autorefresh_intervals`` - enable page auto refresh options, space separated list of integers, optional. The first defined interval will be used as default. If no intervals set, the auto refresh buttons will be disabled. If first element of the list equals ``0``, auto refresh will be disabled by default.
|
||||||
* ``enable_archive_upload`` - allow to upload packages via HTTP (i.e. call of ``/api/v1/service/upload`` uri), boolean, optional, default ``no``.
|
* ``enable_archive_upload`` - allow to upload packages via HTTP (i.e. call of ``/api/v1/service/upload`` uri), boolean, optional, default ``no``.
|
||||||
* ``host`` - host to bind, string, optional.
|
* ``host`` - host to bind, string, optional.
|
||||||
* ``index_url`` - full URL of the repository index page, string, optional.
|
* ``index_url`` - full URL of the repository index page, string, optional.
|
||||||
|
@ -28,6 +28,10 @@ allow_read_only = yes
|
|||||||
; External address of the web service. Will be used for some features like OAuth. If none set will be generated as
|
; External address of the web service. Will be used for some features like OAuth. If none set will be generated as
|
||||||
; address = http://${web:host}:${web:port}
|
; address = http://${web:host}:${web:port}
|
||||||
;address = http://${web:host}:${web:port}
|
;address = http://${web:host}:${web:port}
|
||||||
|
; Enable page auto refresh. Intervals are given in seconds. Default interval is always the first element of the list.
|
||||||
|
; If no intervals set, auto refresh will be disabled. 0 can only be the first element and will disable auto refresh
|
||||||
|
; by default.
|
||||||
|
autorefresh_intervals = 5 1 10 30 60
|
||||||
; Enable file upload endpoint used by some triggers.
|
; Enable file upload endpoint used by some triggers.
|
||||||
;enable_archive_upload = no
|
;enable_archive_upload = no
|
||||||
; Address to bind the server.
|
; Address to bind the server.
|
||||||
|
@ -80,10 +80,28 @@
|
|||||||
<button type="button" class="btn btn-secondary" onclick="reload()">
|
<button type="button" class="btn btn-secondary" onclick="reload()">
|
||||||
<i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span>
|
<i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{% if autorefresh_intervals %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<input id="table-autoreload-button" type="checkbox" class="btn-check" autocomplete="off" onclick="toggleTableAutoReload()" checked>
|
||||||
|
<label for="table-autoreload-button" class="btn btn-outline-secondary" title="toggle auto reload"><i class="bi bi-clock"></i></label>
|
||||||
|
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<span class="visually-hidden">select interval</span>
|
||||||
|
</button>
|
||||||
|
<ul id="table-autoreload-input" class="dropdown-menu">
|
||||||
|
{% for interval in autorefresh_intervals %}
|
||||||
|
<li><a class="dropdown-item {{ "active" if interval.is_active }}" onclick="toggleTableAutoReload({{ interval.interval }})" data-interval="{{ interval.interval }}">{{ interval.text }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table id="packages"
|
<table id="packages"
|
||||||
data-classes="table table-hover"
|
data-classes="table table-hover"
|
||||||
|
data-cookie="true"
|
||||||
|
data-cookie-id-table="ahriman-packages"
|
||||||
|
data-cookie-storage="localStorage"
|
||||||
data-export-options='{"fileName": "packages"}'
|
data-export-options='{"fileName": "packages"}'
|
||||||
data-filter-control="true"
|
data-filter-control="true"
|
||||||
data-filter-control-visible="false"
|
data-filter-control-visible="false"
|
||||||
@ -102,8 +120,8 @@
|
|||||||
data-sortable="true"
|
data-sortable="true"
|
||||||
data-sort-name="base"
|
data-sort-name="base"
|
||||||
data-sort-order="asc"
|
data-sort-order="asc"
|
||||||
data-toggle="table"
|
data-toolbar="#toolbar"
|
||||||
data-toolbar="#toolbar">
|
data-unique-id="id">
|
||||||
<thead class="table-primary">
|
<thead class="table-primary">
|
||||||
<tr>
|
<tr>
|
||||||
<th data-checkbox="true"></th>
|
<th data-checkbox="true"></th>
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
function createAlert(title, message, clz, action, id) {
|
function createAlert(title, message, clz, action, id) {
|
||||||
id ??= md5(title + message); // MD5 id from the content
|
id ??= md5(title + message); // MD5 id from the content
|
||||||
if (alertPlaceholder.querySelector(`#alert-${id}`)) return; // check if there are duplicates
|
if (alertPlaceholder.querySelector(`#alert-${id}`)) {
|
||||||
|
return; // check if there are duplicates
|
||||||
|
}
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
wrapper.id = `alert-${id}`;
|
wrapper.id = `alert-${id}`;
|
||||||
|
@ -51,6 +51,87 @@
|
|||||||
const dashboardPackagesCountChartCanvas = document.getElementById("dashboard-packages-count-chart");
|
const dashboardPackagesCountChartCanvas = document.getElementById("dashboard-packages-count-chart");
|
||||||
let dashboardPackagesCountChart = null;
|
let dashboardPackagesCountChart = null;
|
||||||
|
|
||||||
|
function statusLoad() {
|
||||||
|
const badgeClass = status => {
|
||||||
|
if (status === "pending") return "btn-outline-warning";
|
||||||
|
if (status === "building") return "btn-outline-warning";
|
||||||
|
if (status === "failed") return "btn-outline-danger";
|
||||||
|
if (status === "success") return "btn-outline-success";
|
||||||
|
return "btn-outline-secondary";
|
||||||
|
};
|
||||||
|
|
||||||
|
makeRequest(
|
||||||
|
"/api/v1/status",
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
architecture: repository.architecture,
|
||||||
|
repository: repository.repository,
|
||||||
|
},
|
||||||
|
convert: response => response.json(),
|
||||||
|
},
|
||||||
|
data => {
|
||||||
|
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
|
||||||
|
|
||||||
|
dashboardButton.classList.remove(...dashboardButton.classList);
|
||||||
|
dashboardButton.classList.add("btn");
|
||||||
|
dashboardButton.classList.add(badgeClass(data.status.status));
|
||||||
|
|
||||||
|
dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
|
||||||
|
dashboardModalHeader.classList.add("modal-header");
|
||||||
|
headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
|
||||||
|
|
||||||
|
dashboardName.textContent = data.repository;
|
||||||
|
dashboardArchitecture.textContent = data.architecture;
|
||||||
|
dashboardStatus.textContent = data.status.status;
|
||||||
|
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
|
||||||
|
|
||||||
|
if (dashboardPackagesStatusesChart) {
|
||||||
|
const labels = [
|
||||||
|
"unknown",
|
||||||
|
"pending",
|
||||||
|
"building",
|
||||||
|
"failed",
|
||||||
|
"success",
|
||||||
|
];
|
||||||
|
dashboardPackagesStatusesChart.config.data = {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: "packages in status",
|
||||||
|
data: labels.map(label => data.packages[label]),
|
||||||
|
backgroundColor: [
|
||||||
|
"rgb(55, 58, 60)",
|
||||||
|
"rgb(255, 117, 24)",
|
||||||
|
"rgb(255, 117, 24)",
|
||||||
|
"rgb(255, 0, 57)",
|
||||||
|
"rgb(63, 182, 24)", // copy-paste from current style
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
dashboardPackagesStatusesChart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dashboardPackagesCountChart) {
|
||||||
|
dashboardPackagesCountChart.config.data = {
|
||||||
|
labels: ["packages"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "archives",
|
||||||
|
data: [data.stats.packages],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "bases",
|
||||||
|
data: [data.stats.bases],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
dashboardPackagesCountChart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboardCanvas.hidden = data.status.total > 0;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ready(_ => {
|
ready(_ => {
|
||||||
dashboardPackagesStatusesChart = new Chart(dashboardPackagesStatusesChartCanvas, {
|
dashboardPackagesStatusesChart = new Chart(dashboardPackagesStatusesChartCanvas, {
|
||||||
type: "pie",
|
type: "pie",
|
||||||
|
@ -80,8 +80,7 @@
|
|||||||
data-classes="table table-hover"
|
data-classes="table table-hover"
|
||||||
data-sortable="true"
|
data-sortable="true"
|
||||||
data-sort-name="timestamp"
|
data-sort-name="timestamp"
|
||||||
data-sort-order="desc"
|
data-sort-order="desc">
|
||||||
data-toggle="table">
|
|
||||||
<thead class="table-primary">
|
<thead class="table-primary">
|
||||||
<tr>
|
<tr>
|
||||||
<th data-align="right" data-field="timestamp">date</th>
|
<th data-align="right" data-field="timestamp">date</th>
|
||||||
@ -98,10 +97,24 @@
|
|||||||
<input id="package-info-refresh-input" type="checkbox" class="form-check-input" value="" checked>
|
<input id="package-info-refresh-input" type="checkbox" class="form-check-input" value="" checked>
|
||||||
<label for="package-info-refresh-input" class="form-check-label">update pacman databases</label>
|
<label for="package-info-refresh-input" class="form-check-label">update pacman databases</label>
|
||||||
|
|
||||||
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
|
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
|
||||||
<button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button>
|
<button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button>
|
{% if autorefresh_intervals %}
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button>
|
||||||
|
<div class="btn-group dropup">
|
||||||
|
<input id="package-info-autoreload-button" type="checkbox" class="btn-check" autocomplete="off" onclick="togglePackageInfoAutoReload()" checked>
|
||||||
|
<label for="package-info-autoreload-button" class="btn btn-outline-secondary" title="toggle auto reload"><i class="bi bi-clock"></i></label>
|
||||||
|
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<span class="visually-hidden">select interval</span>
|
||||||
|
</button>
|
||||||
|
<ul id="package-info-autoreload-input" class="dropdown-menu">
|
||||||
|
{% for interval in autorefresh_intervals %}
|
||||||
|
<li><a class="dropdown-item {{ "active" if interval.is_active }}" onclick="togglePackageInfoAutoReload({{ interval.interval }})" data-interval="{{ interval.interval }}">{{ interval.text }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -140,6 +153,10 @@
|
|||||||
|
|
||||||
const packageInfoRefreshInput = document.getElementById("package-info-refresh-input");
|
const packageInfoRefreshInput = document.getElementById("package-info-refresh-input");
|
||||||
|
|
||||||
|
const packageInfoAutoReloadButton = document.getElementById("package-info-autoreload-button");
|
||||||
|
const packageInfoAutoReloadInput = document.getElementById("package-info-autoreload-input");
|
||||||
|
let packageInfoAutoReloadTask = null;
|
||||||
|
|
||||||
function clearChart() {
|
function clearChart() {
|
||||||
packageInfoEventsUpdateChartCanvas.hidden = true;
|
packageInfoEventsUpdateChartCanvas.hidden = true;
|
||||||
if (packageInfoEventsUpdateChart) {
|
if (packageInfoEventsUpdateChart) {
|
||||||
@ -148,6 +165,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function convertLogs(data, filter) {
|
||||||
|
return data
|
||||||
|
.filter((filter || Boolean))
|
||||||
|
.map(log_record => `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
async function copyChanges() {
|
async function copyChanges() {
|
||||||
const changes = packageInfoChangesInput.textContent;
|
const changes = packageInfoChangesInput.textContent;
|
||||||
await copyToClipboard(changes, packageInfoChangesCopyButton);
|
await copyToClipboard(changes, packageInfoChangesCopyButton);
|
||||||
@ -291,6 +315,69 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadLogs(packageBase, onFailure) {
|
function loadLogs(packageBase, onFailure) {
|
||||||
|
const sortFn = (left, right) => left.process_id.localeCompare(right.process_id) || left.version.localeCompare(right.version);
|
||||||
|
const compareFn = (left, right) => left.process_id === right.process_id && left.version === right.version;
|
||||||
|
|
||||||
|
makeRequest(
|
||||||
|
`/api/v2/packages/${packageBase}/logs`,
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
architecture: repository.architecture,
|
||||||
|
head: true,
|
||||||
|
repository: repository.repository,
|
||||||
|
},
|
||||||
|
convert: response => response.json(),
|
||||||
|
},
|
||||||
|
data => {
|
||||||
|
const currentVersions = Array.from(packageInfoLogsVersions.children)
|
||||||
|
.map(el => {
|
||||||
|
return {
|
||||||
|
process_id: el.dataset.processId,
|
||||||
|
version: el.dataset.version,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort(sortFn);
|
||||||
|
const newVersions = data
|
||||||
|
.map(el => {
|
||||||
|
return {
|
||||||
|
process_id: el.process_id,
|
||||||
|
version: el.version,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort(sortFn);
|
||||||
|
|
||||||
|
if (currentVersions.equals(newVersions, compareFn))
|
||||||
|
loadLogsActive(packageBase);
|
||||||
|
else
|
||||||
|
loadLogsAll(packageBase, onFailure);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLogsActive(packageBase) {
|
||||||
|
const activeLogSelector = packageInfoLogsVersions.querySelector(".active");
|
||||||
|
|
||||||
|
if (activeLogSelector) {
|
||||||
|
makeRequest(
|
||||||
|
`/api/v2/packages/${packageBase}/logs`,
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
architecture: repository.architecture,
|
||||||
|
repository: repository.repository,
|
||||||
|
version: activeLogSelector.dataset.version,
|
||||||
|
process_id: activeLogSelector.dataset.processId,
|
||||||
|
},
|
||||||
|
convert: response => response.json(),
|
||||||
|
},
|
||||||
|
data => {
|
||||||
|
activeLogSelector.dataset.logs = convertLogs(data);
|
||||||
|
activeLogSelector.click();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLogsAll(packageBase, onFailure) {
|
||||||
makeRequest(
|
makeRequest(
|
||||||
`/api/v2/packages/${packageBase}/logs`,
|
`/api/v2/packages/${packageBase}/logs`,
|
||||||
{
|
{
|
||||||
@ -319,15 +406,19 @@
|
|||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.classList.add("dropdown-item");
|
link.classList.add("dropdown-item");
|
||||||
|
|
||||||
|
link.dataset.version = version.version;
|
||||||
|
link.dataset.processId = version.process_id;
|
||||||
|
link.dataset.logs = convertLogs(data, log_record => log_record.version === version.version && log_record.process_id === version.process_id);
|
||||||
|
|
||||||
link.textContent = new Date(1000 * version.created).toISOStringShort();
|
link.textContent = new Date(1000 * version.created).toISOStringShort();
|
||||||
link.href = "#";
|
link.href = "#";
|
||||||
link.onclick = _ => {
|
link.onclick = _ => {
|
||||||
const logs = data
|
// check if we are at the bottom of the code block
|
||||||
.filter(log_record => log_record.version === version.version && log_record.process_id === version.process_id)
|
const isScrolledToBottom = packageInfoLogsInput.scrollTop + packageInfoLogsInput.clientHeight >= packageInfoLogsInput.scrollHeight;
|
||||||
.map(log_record => `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`);
|
packageInfoLogsInput.textContent = link.dataset.logs;
|
||||||
|
|
||||||
packageInfoLogsInput.textContent = logs.join("\n");
|
|
||||||
highlight(packageInfoLogsInput);
|
highlight(packageInfoLogsInput);
|
||||||
|
if (isScrolledToBottom)
|
||||||
|
packageInfoLogsInput.scrollTop = packageInfoLogsInput.scrollHeight; // scroll to the new end
|
||||||
|
|
||||||
Array.from(packageInfoLogsVersions.children).forEach(el => el.classList.remove("active"));
|
Array.from(packageInfoLogsVersions.children).forEach(el => el.classList.remove("active"));
|
||||||
link.classList.add("active");
|
link.classList.add("active");
|
||||||
@ -403,12 +494,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function packageInfoRemove() {
|
function packageInfoRemove() {
|
||||||
const packageBase = packageInfoModal.package;
|
const packageBase = packageInfoModal.dataset.package;
|
||||||
packagesRemove([packageBase]);
|
packagesRemove([packageBase]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function packageInfoUpdate() {
|
function packageInfoUpdate() {
|
||||||
const packageBase = packageInfoModal.package;
|
const packageBase = packageInfoModal.dataset.package;
|
||||||
packagesAdd(packageBase, [], repository, {refresh: packageInfoRefreshInput.checked});
|
packagesAdd(packageBase, [], repository, {refresh: packageInfoRefreshInput.checked});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,10 +507,10 @@
|
|||||||
const isPackageBaseSet = packageBase !== undefined;
|
const isPackageBaseSet = packageBase !== undefined;
|
||||||
if (isPackageBaseSet) {
|
if (isPackageBaseSet) {
|
||||||
// set package base as currently used
|
// set package base as currently used
|
||||||
packageInfoModal.package = packageBase;
|
packageInfoModal.dataset.package = packageBase;
|
||||||
} else {
|
} else {
|
||||||
// read package base from the current window attribute
|
// read package base from the current window attribute
|
||||||
packageBase = packageInfoModal.package;
|
packageBase = packageInfoModal.dataset.package;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFailure = error => {
|
const onFailure = error => {
|
||||||
@ -438,10 +529,27 @@
|
|||||||
|
|
||||||
if (isPackageBaseSet) {
|
if (isPackageBaseSet) {
|
||||||
bootstrap.Modal.getOrCreateInstance(packageInfoModal).show();
|
bootstrap.Modal.getOrCreateInstance(packageInfoModal).show();
|
||||||
|
{% if autorefresh_intervals %}
|
||||||
|
togglePackageInfoAutoReload();
|
||||||
|
{% endif %}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function togglePackageInfoAutoReload(interval) {
|
||||||
|
clearInterval(packageInfoAutoReloadTask);
|
||||||
|
packageInfoAutoReloadTask = toggleAutoReload(packageInfoAutoReloadButton, interval, packageInfoAutoReloadInput, _ => {
|
||||||
|
if (!hasActiveSelection()) {
|
||||||
|
const packageBase = packageInfoModal.dataset.package;
|
||||||
|
// we only poll status and logs here
|
||||||
|
loadPackage(packageBase);
|
||||||
|
loadLogs(packageBase);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ready(_ => {
|
ready(_ => {
|
||||||
|
packageInfoEventsTable.bootstrapTable({});
|
||||||
|
|
||||||
packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, {
|
packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, {
|
||||||
type: "line",
|
type: "line",
|
||||||
data: {},
|
data: {},
|
||||||
@ -468,6 +576,11 @@
|
|||||||
packageInfoChangesInput.textContent = "";
|
packageInfoChangesInput.textContent = "";
|
||||||
packageInfoEventsTable.bootstrapTable("load", []);
|
packageInfoEventsTable.bootstrapTable("load", []);
|
||||||
clearChart();
|
clearChart();
|
||||||
|
|
||||||
|
clearInterval(packageInfoAutoReloadTask);
|
||||||
|
packageInfoAutoReloadTask = null; // not really required (?) but lets clear everything
|
||||||
});
|
});
|
||||||
|
|
||||||
|
restoreAutoReloadSettings(packageInfoAutoReloadButton, packageInfoAutoReloadInput);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -10,6 +10,10 @@
|
|||||||
const dashboardButton = document.getElementById("dashboard-button");
|
const dashboardButton = document.getElementById("dashboard-button");
|
||||||
const versionBadge = document.getElementById("badge-version");
|
const versionBadge = document.getElementById("badge-version");
|
||||||
|
|
||||||
|
const tableAutoReloadButton = document.getElementById("table-autoreload-button");
|
||||||
|
const tableAutoReloadInput = document.getElementById("table-autoreload-input");
|
||||||
|
let tableAutoReloadTask = null;
|
||||||
|
|
||||||
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
||||||
makeRequest(
|
makeRequest(
|
||||||
uri,
|
uri,
|
||||||
@ -55,6 +59,41 @@
|
|||||||
return table.bootstrapTable("getSelections").map(row => row.id);
|
return table.bootstrapTable("getSelections").map(row => row.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function packagesLoad(onFailure) {
|
||||||
|
makeRequest(
|
||||||
|
"/api/v1/packages",
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
architecture: repository.architecture,
|
||||||
|
repository: repository.repository,
|
||||||
|
},
|
||||||
|
convert: response => response.json(),
|
||||||
|
},
|
||||||
|
data => {
|
||||||
|
const payload = data
|
||||||
|
.map(description => {
|
||||||
|
const package_base = description.package.base;
|
||||||
|
const web_url = description.package.remote.web_url;
|
||||||
|
return {
|
||||||
|
id: package_base,
|
||||||
|
base: web_url ? safeLink(web_url, package_base, package_base).outerHTML : safe(package_base),
|
||||||
|
version: safe(description.package.version),
|
||||||
|
packager: description.package.packager ? safe(description.package.packager) : "",
|
||||||
|
packages: listToTable(Object.keys(description.package.packages)),
|
||||||
|
groups: listToTable(extractListProperties(description.package, "groups")),
|
||||||
|
licenses: listToTable(extractListProperties(description.package, "licenses")),
|
||||||
|
timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(),
|
||||||
|
status: description.status.status,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTable(table, payload);
|
||||||
|
table.bootstrapTable("hideLoading");
|
||||||
|
},
|
||||||
|
onFailure,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function packagesRemove(packages) {
|
function packagesRemove(packages) {
|
||||||
packages = packages ?? getSelection();
|
packages = packages ?? getSelection();
|
||||||
const onSuccess = update => `Packages ${update} have been removed`;
|
const onSuccess = update => `Packages ${update} have been removed`;
|
||||||
@ -88,130 +127,22 @@
|
|||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
table.bootstrapTable("showLoading");
|
table.bootstrapTable("showLoading");
|
||||||
|
const onFailure = error => {
|
||||||
const badgeClass = status => {
|
if ((error.status === 401) || (error.status === 403)) {
|
||||||
if (status === "pending") return "btn-outline-warning";
|
// authorization error
|
||||||
if (status === "building") return "btn-outline-warning";
|
const text = "In order to see statuses you must login first.";
|
||||||
if (status === "failed") return "btn-outline-danger";
|
table.find("tr.unauthorized").remove();
|
||||||
if (status === "success") return "btn-outline-success";
|
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${safe(text)}</td></tr>`);
|
||||||
return "btn-outline-secondary";
|
table.bootstrapTable("hideLoading");
|
||||||
|
} else {
|
||||||
|
// other errors
|
||||||
|
const message = details => `Could not load list of packages: ${details}`;
|
||||||
|
showFailure("Load failure", message, error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
makeRequest(
|
packagesLoad(onFailure);
|
||||||
"/api/v1/packages",
|
statusLoad();
|
||||||
{
|
|
||||||
query: {
|
|
||||||
architecture: repository.architecture,
|
|
||||||
repository: repository.repository,
|
|
||||||
},
|
|
||||||
convert: response => response.json(),
|
|
||||||
},
|
|
||||||
data => {
|
|
||||||
const payload = data.map(description => {
|
|
||||||
const package_base = description.package.base;
|
|
||||||
const web_url = description.package.remote.web_url;
|
|
||||||
return {
|
|
||||||
id: package_base,
|
|
||||||
base: web_url ? safeLink(web_url, package_base, package_base).outerHTML : safe(package_base),
|
|
||||||
version: safe(description.package.version),
|
|
||||||
packager: description.package.packager ? safe(description.package.packager) : "",
|
|
||||||
packages: listToTable(Object.keys(description.package.packages)),
|
|
||||||
groups: listToTable(extractListProperties(description.package, "groups")),
|
|
||||||
licenses: listToTable(extractListProperties(description.package, "licenses")),
|
|
||||||
timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(),
|
|
||||||
status: description.status.status,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
table.bootstrapTable("load", payload);
|
|
||||||
table.bootstrapTable("uncheckAll");
|
|
||||||
table.bootstrapTable("hideLoading");
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
if ((error.status === 401) || (error.status === 403)) {
|
|
||||||
// authorization error
|
|
||||||
const text = "In order to see statuses you must login first.";
|
|
||||||
table.find("tr.unauthorized").remove();
|
|
||||||
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${safe(text)}</td></tr>`);
|
|
||||||
table.bootstrapTable("hideLoading");
|
|
||||||
} else {
|
|
||||||
// other errors
|
|
||||||
const message = details => `Could not load list of packages: ${details}`;
|
|
||||||
showFailure("Load failure", message, error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
makeRequest(
|
|
||||||
"/api/v1/status",
|
|
||||||
{
|
|
||||||
query: {
|
|
||||||
architecture: repository.architecture,
|
|
||||||
repository: repository.repository,
|
|
||||||
},
|
|
||||||
convert: response => response.json(),
|
|
||||||
},
|
|
||||||
data => {
|
|
||||||
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
|
|
||||||
|
|
||||||
dashboardButton.classList.remove(...dashboardButton.classList);
|
|
||||||
dashboardButton.classList.add("btn");
|
|
||||||
dashboardButton.classList.add(badgeClass(data.status.status));
|
|
||||||
|
|
||||||
dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
|
|
||||||
dashboardModalHeader.classList.add("modal-header");
|
|
||||||
headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
|
|
||||||
|
|
||||||
dashboardName.textContent = data.repository;
|
|
||||||
dashboardArchitecture.textContent = data.architecture;
|
|
||||||
dashboardStatus.textContent = data.status.status;
|
|
||||||
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
|
|
||||||
|
|
||||||
if (dashboardPackagesStatusesChart) {
|
|
||||||
const labels = [
|
|
||||||
"unknown",
|
|
||||||
"pending",
|
|
||||||
"building",
|
|
||||||
"failed",
|
|
||||||
"success",
|
|
||||||
];
|
|
||||||
dashboardPackagesStatusesChart.config.data = {
|
|
||||||
labels: labels,
|
|
||||||
datasets: [{
|
|
||||||
label: "packages in status",
|
|
||||||
data: labels.map(label => data.packages[label]),
|
|
||||||
backgroundColor: [
|
|
||||||
"rgb(55, 58, 60)",
|
|
||||||
"rgb(255, 117, 24)",
|
|
||||||
"rgb(255, 117, 24)",
|
|
||||||
"rgb(255, 0, 57)",
|
|
||||||
"rgb(63, 182, 24)", // copy-paste from current style
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
dashboardPackagesStatusesChart.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dashboardPackagesCountChart) {
|
|
||||||
dashboardPackagesCountChart.config.data = {
|
|
||||||
labels: ["packages"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "archives",
|
|
||||||
data: [data.stats.packages],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "bases",
|
|
||||||
data: [data.stats.bases],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
dashboardPackagesCountChart.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
dashboardCanvas.hidden = data.status.total > 0;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectRepository() {
|
function selectRepository() {
|
||||||
@ -230,7 +161,24 @@
|
|||||||
return {classes: cellClass(value)};
|
return {classes: cellClass(value)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleTableAutoReload(interval) {
|
||||||
|
clearInterval(tableAutoReloadTask);
|
||||||
|
tableAutoReloadTask = toggleAutoReload(tableAutoReloadButton, interval, tableAutoReloadInput, _ => {
|
||||||
|
if (!dashboardModal.classList.contains("show") &&
|
||||||
|
!hasActiveDropdown()) {
|
||||||
|
packagesLoad();
|
||||||
|
statusLoad();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ready(_ => {
|
ready(_ => {
|
||||||
|
const onCheckFunction = function () {
|
||||||
|
if (packageRemoveButton) {
|
||||||
|
packageRemoveButton.disabled = !getSelection().length;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
document.querySelectorAll("#repositories a").forEach(element => {
|
document.querySelectorAll("#repositories a").forEach(element => {
|
||||||
element.onclick = _ => {
|
element.onclick = _ => {
|
||||||
repository = {
|
repository = {
|
||||||
@ -245,49 +193,55 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", _ => {
|
table.bootstrapTable({
|
||||||
if (packageRemoveButton) {
|
onCheck: onCheckFunction,
|
||||||
packageRemoveButton.disabled = !table.bootstrapTable("getSelections").length;
|
onCheckAll: onCheckFunction,
|
||||||
}
|
onClickRow: (data, row, cell) => {
|
||||||
});
|
if (0 === cell || "base" === cell) {
|
||||||
table.on("click-row.bs.table", (self, data, row, cell) => {
|
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
|
||||||
if (0 === cell || "base" === cell) {
|
table.bootstrapTable(method, {field: "id", values: [data.id]});
|
||||||
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
|
} else showPackageInfo(data.id);
|
||||||
table.bootstrapTable(method, {field: "id", values: [data.id]});
|
},
|
||||||
} else showPackageInfo(data.id);
|
onCreatedControls: _ => {
|
||||||
});
|
new easepick.create({
|
||||||
table.on("created-controls.bs.table", _ => {
|
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
|
||||||
new easepick.create({
|
css: [
|
||||||
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
|
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
|
||||||
css: [
|
],
|
||||||
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
|
grid: 2,
|
||||||
],
|
calendars: 2,
|
||||||
grid: 2,
|
autoApply: false,
|
||||||
calendars: 2,
|
locale: {
|
||||||
autoApply: false,
|
cancel: "Clear",
|
||||||
locale: {
|
},
|
||||||
cancel: "Clear",
|
RangePlugin: {
|
||||||
},
|
tooltip: false,
|
||||||
RangePlugin: {
|
},
|
||||||
tooltip: false,
|
plugins: [
|
||||||
},
|
"RangePlugin",
|
||||||
plugins: [
|
],
|
||||||
"RangePlugin",
|
setup: picker => {
|
||||||
],
|
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
|
||||||
setup: picker => {
|
// replace "Cancel" behaviour to "Clear"
|
||||||
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
|
picker.onClickCancelButton = element => {
|
||||||
// replace "Cancel" behaviour to "Clear"
|
if (picker.isCancelButton(element)) {
|
||||||
picker.onClickCancelButton = element => {
|
picker.clear();
|
||||||
if (picker.isCancelButton(element)) {
|
picker.hide();
|
||||||
picker.clear();
|
table.bootstrapTable("triggerSearch");
|
||||||
picker.hide();
|
}
|
||||||
table.bootstrapTable("triggerSearch");
|
};
|
||||||
}
|
},
|
||||||
};
|
});
|
||||||
},
|
},
|
||||||
});
|
onUncheck: onCheckFunction,
|
||||||
|
onUncheckAll: onCheckFunction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
restoreAutoReloadSettings(tableAutoReloadButton, tableAutoReloadInput);
|
||||||
|
|
||||||
selectRepository();
|
selectRepository();
|
||||||
|
{% if autorefresh_intervals %}
|
||||||
|
toggleTableAutoReload();
|
||||||
|
{% endif %}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -53,8 +53,7 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
|||||||
data-show-search-clear-button="true"
|
data-show-search-clear-button="true"
|
||||||
data-sortable="true"
|
data-sortable="true"
|
||||||
data-sort-name="base"
|
data-sort-name="base"
|
||||||
data-sort-order="asc"
|
data-sort-order="asc">
|
||||||
data-toggle="table">
|
|
||||||
<thead class="table-primary">
|
<thead class="table-primary">
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th>
|
<th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th>
|
||||||
@ -128,36 +127,38 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
ready(_ => {
|
ready(_ => {
|
||||||
table.on("created-controls.bs.table", _ => {
|
table.bootstrapTable({
|
||||||
new easepick.create({
|
onCreatedControls: _ => {
|
||||||
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
|
new easepick.create({
|
||||||
css: [
|
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
|
||||||
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
|
css: [
|
||||||
],
|
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
|
||||||
grid: 2,
|
],
|
||||||
calendars: 2,
|
grid: 2,
|
||||||
autoApply: false,
|
calendars: 2,
|
||||||
locale: {
|
autoApply: false,
|
||||||
cancel: "Clear",
|
locale: {
|
||||||
},
|
cancel: "Clear",
|
||||||
RangePlugin: {
|
},
|
||||||
tooltip: false,
|
RangePlugin: {
|
||||||
},
|
tooltip: false,
|
||||||
plugins: [
|
},
|
||||||
"RangePlugin",
|
plugins: [
|
||||||
],
|
"RangePlugin",
|
||||||
setup: picker => {
|
],
|
||||||
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
|
setup: picker => {
|
||||||
// replace "Cancel" behaviour to "Clear"
|
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
|
||||||
picker.onClickCancelButton = element => {
|
// replace "Cancel" behaviour to "Clear"
|
||||||
if (picker.isCancelButton(element)) {
|
picker.onClickCancelButton = element => {
|
||||||
picker.clear();
|
if (picker.isCancelButton(element)) {
|
||||||
picker.hide();
|
picker.clear();
|
||||||
table.bootstrapTable("triggerSearch");
|
picker.hide();
|
||||||
}
|
table.bootstrapTable("triggerSearch");
|
||||||
};
|
}
|
||||||
},
|
};
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,23 +1,24 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/js-md5@0.8.3/src/md5.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/js-md5@0.8.3/src/md5.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.30.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.33.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/cookie/bootstrap-table-cookie.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function copyToClipboard(text, button) {
|
async function copyToClipboard(text, button) {
|
||||||
@ -58,6 +59,15 @@
|
|||||||
return value.includes(dataList[index].toLowerCase());
|
return value.includes(dataList[index].toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasActiveSelection() {
|
||||||
|
return !document.getSelection().isCollapsed; // not sure if it is a valid way, but I guess so
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasActiveDropdown() {
|
||||||
|
return Array.from(document.querySelectorAll(".dropdown-menu"))
|
||||||
|
.some(el => el.classList.contains("show"));
|
||||||
|
}
|
||||||
|
|
||||||
function headerClass(status) {
|
function headerClass(status) {
|
||||||
if (status === "pending") return ["bg-warning"];
|
if (status === "pending") return ["bg-warning"];
|
||||||
if (status === "building") return ["bg-warning"];
|
if (status === "building") return ["bg-warning"];
|
||||||
@ -106,6 +116,12 @@
|
|||||||
.catch(error => onFailure && onFailure(error));
|
.catch(error => onFailure && onFailure(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readOptional(extractor, callback) {
|
||||||
|
for (let value = extractor(); !!value; value = null) {
|
||||||
|
callback(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ready(fn) {
|
function ready(fn) {
|
||||||
if (document.readyState === "complete" || document.readyState === "interactive") {
|
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||||
setTimeout(fn, 1);
|
setTimeout(fn, 1);
|
||||||
@ -114,6 +130,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restoreAutoReloadSettings(toggle, intervalSelector) {
|
||||||
|
readOptional(() => localStorage.getItem(`ahriman-${toggle.id}-refresh-enabled`), value => toggle.checked = value === "true");
|
||||||
|
readOptional(() => localStorage.getItem(`ahriman-${toggle.id}-refresh-interval`), value => toggleActiveElement(intervalSelector, "interval", value));
|
||||||
|
}
|
||||||
|
|
||||||
function safe(string) {
|
function safe(string) {
|
||||||
return String(string)
|
return String(string)
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
@ -133,7 +154,86 @@
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
Date.prototype.toISOStringShort = function() {
|
function toggleActiveElement(selector, dataType, value) {
|
||||||
|
const targetElement = selector.querySelector(`a[data-${dataType}="${value}"]`);
|
||||||
|
if (targetElement?.classList?.contains("active")) {
|
||||||
|
return; // element is already active, skip processing
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.from(selector.children).forEach(il => {
|
||||||
|
Array.from(il.children).forEach(el => el.classList.remove("active"));
|
||||||
|
});
|
||||||
|
targetElement?.classList?.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAutoReload(toggle, interval, intervalSelector, callback) {
|
||||||
|
if (interval) {
|
||||||
|
toggle.checked = true; // toggle reload
|
||||||
|
} else {
|
||||||
|
interval = intervalSelector.querySelector(".active")?.dataset?.interval; // find active element
|
||||||
|
}
|
||||||
|
|
||||||
|
let intervalId = null;
|
||||||
|
if (interval) {
|
||||||
|
if (toggle.checked) {
|
||||||
|
// refresh UI
|
||||||
|
toggleActiveElement(intervalSelector, "interval", interval);
|
||||||
|
// finally create timer task
|
||||||
|
intervalId = setInterval(callback, interval);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toggle.checked = false; // no active interval found, disable toggle
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(`ahriman-${toggle.id}-refresh-enabled`, toggle.checked);
|
||||||
|
localStorage.setItem(`ahriman-${toggle.id}-refresh-interval`, interval);
|
||||||
|
return intervalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTable(table, rows) {
|
||||||
|
// instead of using load method here, we just update rows manually to avoid table reinitialization
|
||||||
|
const currentData = table.bootstrapTable("getData").reduce((accumulator, row) => {
|
||||||
|
accumulator[row.id] = row["0"];
|
||||||
|
return accumulator;
|
||||||
|
}, {});
|
||||||
|
// insert or update rows
|
||||||
|
rows.forEach(row => {
|
||||||
|
if (Object.hasOwn(currentData, row.id)) {
|
||||||
|
row["0"] = currentData[row.id]; // copy checkbox state
|
||||||
|
table.bootstrapTable("updateByUniqueId", {
|
||||||
|
id: row.id,
|
||||||
|
row: row,
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
table.bootstrapTable("insertRow", {index: 0, row: row});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// remove old rows
|
||||||
|
const newData = rows.map(value => value.id);
|
||||||
|
Object.keys(currentData).forEach(id => {
|
||||||
|
if (!newData.includes(id)) {
|
||||||
|
table.bootstrapTable("removeByUniqueId", id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.prototype.equals = function (right, comparator) {
|
||||||
|
let index = this.length;
|
||||||
|
if (index !== right.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (index--) {
|
||||||
|
if (!comparator(this[index], right[index])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date.prototype.toISOStringShort = function () {
|
||||||
const pad = number => String(number).padStart(2, "0");
|
const pad = number => String(number).padStart(2, "0");
|
||||||
return `${this.getFullYear()}-${pad(this.getMonth() + 1)}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`;
|
return `${this.getFullYear()}-${pad(this.getMonth() + 1)}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.7/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.pre-scrollable {
|
.pre-scrollable {
|
||||||
|
@ -8,7 +8,7 @@ services:
|
|||||||
AHRIMAN_OUTPUT: console
|
AHRIMAN_OUTPUT: console
|
||||||
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
||||||
AHRIMAN_PORT: 8080
|
AHRIMAN_PORT: 8080
|
||||||
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
||||||
AHRIMAN_REPOSITORY: ahriman-demo
|
AHRIMAN_REPOSITORY: ahriman-demo
|
||||||
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ services:
|
|||||||
AHRIMAN_OUTPUT: console
|
AHRIMAN_OUTPUT: console
|
||||||
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
||||||
AHRIMAN_PORT: 8080
|
AHRIMAN_PORT: 8080
|
||||||
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
||||||
AHRIMAN_REPOSITORY: ahriman-demo
|
AHRIMAN_REPOSITORY: ahriman-demo
|
||||||
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ services:
|
|||||||
AHRIMAN_OUTPUT: console
|
AHRIMAN_OUTPUT: console
|
||||||
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
||||||
AHRIMAN_PORT: 8080
|
AHRIMAN_PORT: 8080
|
||||||
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
||||||
AHRIMAN_REPOSITORY: ahriman-demo
|
AHRIMAN_REPOSITORY: ahriman-demo
|
||||||
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ services:
|
|||||||
AHRIMAN_OUTPUT: console
|
AHRIMAN_OUTPUT: console
|
||||||
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
||||||
AHRIMAN_PORT: 8080
|
AHRIMAN_PORT: 8080
|
||||||
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
||||||
AHRIMAN_REPOSITORY: ahriman-demo
|
AHRIMAN_REPOSITORY: ahriman-demo
|
||||||
AHRIMAN_REPOSITORY_SERVER: http://frontend/repo/$$repo/$$arch
|
AHRIMAN_REPOSITORY_SERVER: http://frontend/repo/$$repo/$$arch
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ services:
|
|||||||
AHRIMAN_PACMAN_MIRROR: https://de.mirror.archlinux32.org/$$arch/$$repo
|
AHRIMAN_PACMAN_MIRROR: https://de.mirror.archlinux32.org/$$arch/$$repo
|
||||||
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
||||||
AHRIMAN_PORT: 8080
|
AHRIMAN_PORT: 8080
|
||||||
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
||||||
AHRIMAN_REPOSITORY: ahriman-demo
|
AHRIMAN_REPOSITORY: ahriman-demo
|
||||||
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@ services:
|
|||||||
AHRIMAN_OUTPUT: console
|
AHRIMAN_OUTPUT: console
|
||||||
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
||||||
AHRIMAN_PORT: 8080
|
AHRIMAN_PORT: 8080
|
||||||
AHRIMAN_POSTSETUP_COMMAND: ahriman --architecture x86_64 --repository another-demo service-setup --build-as-user ahriman --packager 'ahriman bot <ahriman@example.com>'
|
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
||||||
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
AHRIMAN_PRESETUP_COMMAND: ahriman --architecture x86_64 --repository another-demo service-setup --build-as-user ahriman --packager 'ahriman bot <ahriman@example.com>'
|
||||||
AHRIMAN_REPOSITORY: ahriman-demo
|
AHRIMAN_REPOSITORY: ahriman-demo
|
||||||
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ services:
|
|||||||
AHRIMAN_OAUTH_CLIENT_SECRET: ${AHRIMAN_OAUTH_CLIENT_SECRET}
|
AHRIMAN_OAUTH_CLIENT_SECRET: ${AHRIMAN_OAUTH_CLIENT_SECRET}
|
||||||
AHRIMAN_OUTPUT: console
|
AHRIMAN_OUTPUT: console
|
||||||
AHRIMAN_PORT: 8080
|
AHRIMAN_PORT: 8080
|
||||||
AHRIMAN_PRESETUP_COMMAND: sudo -u ahriman ahriman user-add ${AHRIMAN_OAUTH_USER} -R full -p ""
|
AHRIMAN_POSTSETUP_COMMAND: sudo -u ahriman ahriman user-add ${AHRIMAN_OAUTH_USER} -R full -p ""
|
||||||
AHRIMAN_REPOSITORY: ahriman-demo
|
AHRIMAN_REPOSITORY: ahriman-demo
|
||||||
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
AHRIMAN_DEBUG: yes
|
AHRIMAN_DEBUG: yes
|
||||||
AHRIMAN_OUTPUT: console
|
AHRIMAN_OUTPUT: console
|
||||||
AHRIMAN_PRESETUP_COMMAND: sudo -u ahriman gpg --import /run/secrets/key
|
AHRIMAN_POSTSETUP_COMMAND: sudo -u ahriman gpg --import /run/secrets/key
|
||||||
AHRIMAN_REPOSITORY: ahriman-demo
|
AHRIMAN_REPOSITORY: ahriman-demo
|
||||||
|
|
||||||
configs:
|
configs:
|
||||||
|
@ -8,7 +8,7 @@ services:
|
|||||||
AHRIMAN_OUTPUT: console
|
AHRIMAN_OUTPUT: console
|
||||||
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
|
||||||
AHRIMAN_PORT: 8080
|
AHRIMAN_PORT: 8080
|
||||||
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
|
||||||
AHRIMAN_REPOSITORY: ahriman-demo
|
AHRIMAN_REPOSITORY: ahriman-demo
|
||||||
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
|
||||||
|
|
||||||
|
@ -72,16 +72,17 @@ class Setup(Handler):
|
|||||||
|
|
||||||
application = Application(repository_id, configuration, report=report)
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
|
||||||
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
|
with application.repository.paths.preserve_owner():
|
||||||
Setup.executable_create(application.repository.paths, repository_id)
|
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
|
||||||
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
|
Setup.executable_create(application.repository.paths, repository_id)
|
||||||
Setup.configuration_create_devtools(
|
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
|
||||||
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
|
Setup.configuration_create_devtools(
|
||||||
Setup.configuration_create_sudo(application.repository.paths, repository_id)
|
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
|
||||||
|
Setup.configuration_create_sudo(application.repository.paths, repository_id)
|
||||||
|
|
||||||
application.repository.repo.init()
|
application.repository.repo.init()
|
||||||
# lazy database sync
|
# lazy database sync
|
||||||
application.repository.pacman.handle # pylint: disable=pointless-statement
|
application.repository.pacman.handle # pylint: disable=pointless-statement
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
@ -280,6 +281,5 @@ class Setup(Handler):
|
|||||||
command = Setup.build_command(paths.root, repository_id)
|
command = Setup.build_command(paths.root, repository_id)
|
||||||
command.unlink(missing_ok=True)
|
command.unlink(missing_ok=True)
|
||||||
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)
|
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)
|
||||||
paths.chown(command) # we would like to keep owner inside ahriman's home
|
|
||||||
|
|
||||||
arguments = [_set_service_setup_parser]
|
arguments = [_set_service_setup_parser]
|
||||||
|
@ -130,8 +130,8 @@ class Pacman(LazyLogging):
|
|||||||
return # database for some reason deos not exist
|
return # database for some reason deos not exist
|
||||||
|
|
||||||
self.logger.info("copy pacman database %s from operating system root to ahriman's home %s", src, dst)
|
self.logger.info("copy pacman database %s from operating system root to ahriman's home %s", src, dst)
|
||||||
shutil.copy(src, dst)
|
with self.repository_paths.preserve_owner(dst.parent):
|
||||||
self.repository_paths.chown(dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
|
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
|
||||||
"""
|
"""
|
||||||
@ -267,7 +267,8 @@ class Pacman(LazyLogging):
|
|||||||
Package: list of packages which were returned by the query
|
Package: list of packages which were returned by the query
|
||||||
"""
|
"""
|
||||||
def is_package_provided(package: Package) -> bool:
|
def is_package_provided(package: Package) -> bool:
|
||||||
return package_name in package.provides
|
provides = [trim_package(name) for name in package.provides]
|
||||||
|
return package_name in provides
|
||||||
|
|
||||||
for database in self.handle.get_syncdbs():
|
for database in self.handle.get_syncdbs():
|
||||||
yield from filter(is_package_provided, database.search(package_name))
|
yield from filter(is_package_provided, database.search(package_name))
|
||||||
|
@ -146,7 +146,7 @@ class AUR(Remote):
|
|||||||
# search api provides reduced models
|
# search api provides reduced models
|
||||||
for stub in self.package_search(package_name, pacman=pacman, search_by="provides")
|
for stub in self.package_search(package_name, pacman=pacman, search_by="provides")
|
||||||
# verity that found package actually provides it
|
# verity that found package actually provides it
|
||||||
if package_name in (package := self.package_info(stub.package_base, pacman=pacman)).provides
|
if package_name in (package := self.package_info(stub.name, pacman=pacman)).provides
|
||||||
]
|
]
|
||||||
|
|
||||||
def package_search(self, *keywords: str, pacman: Pacman | None, search_by: str | None) -> list[AURPackage]:
|
def package_search(self, *keywords: str, pacman: Pacman | None, search_by: str | None) -> list[AURPackage]:
|
||||||
|
@ -94,6 +94,15 @@ class Remote(SyncHttpClient):
|
|||||||
for package in portion
|
for package in portion
|
||||||
if package.name in packages or not packages
|
if package.name in packages or not packages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# simple check for duplicates. This method will remove all packages under base if there is
|
||||||
|
# a package named exactly as its base
|
||||||
|
packages = {
|
||||||
|
package.name: package
|
||||||
|
for package in packages.values()
|
||||||
|
if package.package_base not in packages or package.package_base == package.name
|
||||||
|
}
|
||||||
|
|
||||||
return list(packages.values())
|
return list(packages.values())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
import configparser
|
import configparser
|
||||||
import shlex
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
@ -85,9 +86,10 @@ class Configuration(configparser.RawConfigParser):
|
|||||||
empty_lines_in_values=not allow_multi_key,
|
empty_lines_in_values=not allow_multi_key,
|
||||||
interpolation=ShellInterpolator(),
|
interpolation=ShellInterpolator(),
|
||||||
converters={
|
converters={
|
||||||
|
"intlist": lambda value: list(map(int, shlex.split(value))),
|
||||||
"list": shlex.split,
|
"list": shlex.split,
|
||||||
"path": self._convert_path,
|
"path": self._convert_path,
|
||||||
"pathlist": lambda value: [self._convert_path(element) for element in shlex.split(value)],
|
"pathlist": lambda value: list(map(self._convert_path, shlex.split(value))),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -236,6 +238,8 @@ class Configuration(configparser.RawConfigParser):
|
|||||||
|
|
||||||
# pylint and mypy are too stupid to find these methods
|
# pylint and mypy are too stupid to find these methods
|
||||||
# pylint: disable=missing-function-docstring,unused-argument
|
# pylint: disable=missing-function-docstring,unused-argument
|
||||||
|
def getintlist(self, *args: Any, **kwargs: Any) -> list[int]: ... # type: ignore[empty-body]
|
||||||
|
|
||||||
def getlist(self, *args: Any, **kwargs: Any) -> list[str]: ... # type: ignore[empty-body]
|
def getlist(self, *args: Any, **kwargs: Any) -> list[str]: ... # type: ignore[empty-body]
|
||||||
|
|
||||||
def getpath(self, *args: Any, **kwargs: Any) -> Path: ... # type: ignore[empty-body]
|
def getpath(self, *args: Any, **kwargs: Any) -> Path: ... # type: ignore[empty-body]
|
||||||
|
@ -324,6 +324,15 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
|||||||
"empty": False,
|
"empty": False,
|
||||||
"is_url": ["http", "https"],
|
"is_url": ["http", "https"],
|
||||||
},
|
},
|
||||||
|
"autorefresh_intervals": {
|
||||||
|
"type": "list",
|
||||||
|
"coerce": "list",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"coerce": "integer",
|
||||||
|
"min": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
"enable_archive_upload": {
|
"enable_archive_upload": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"coerce": "boolean",
|
"coerce": "boolean",
|
||||||
|
@ -29,13 +29,15 @@ class LogsOperations(Operations):
|
|||||||
logs operations
|
logs operations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0,
|
def logs_get(self, package_base: str, version: str | None = None, process_id: str | None = None,
|
||||||
repository_id: RepositoryId | None = None) -> list[LogRecord]:
|
limit: int = -1, offset: int = 0, repository_id: RepositoryId | None = None) -> list[LogRecord]:
|
||||||
"""
|
"""
|
||||||
extract logs for specified package base
|
extract logs for specified package base
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base to extract logs
|
package_base(str): package base to extract logs
|
||||||
|
version(str | None, optional): package version to filter (Default value = None)
|
||||||
|
process_id(str | None, optional): process identifier to filter (Default value = None)
|
||||||
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||||
offset(int, optional): records offset (Default value = 0)
|
offset(int, optional): records offset (Default value = 0)
|
||||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||||
@ -52,12 +54,17 @@ class LogsOperations(Operations):
|
|||||||
"""
|
"""
|
||||||
select created, message, version, process_id from (
|
select created, message, version, process_id from (
|
||||||
select * from logs
|
select * from logs
|
||||||
where package_base = :package_base and repository = :repository
|
where package_base = :package_base
|
||||||
|
and repository = :repository
|
||||||
|
and (:version is null or version = :version)
|
||||||
|
and (:process_id is null or process_id = :process_id)
|
||||||
order by created desc limit :limit offset :offset
|
order by created desc limit :limit offset :offset
|
||||||
) order by created asc
|
) order by created asc
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
"package_base": package_base,
|
"package_base": package_base,
|
||||||
|
"version": version,
|
||||||
|
"process_id": process_id,
|
||||||
"repository": repository_id.id,
|
"repository": repository_id.id,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"offset": offset,
|
"offset": offset,
|
||||||
|
@ -94,9 +94,13 @@ class SQLite(
|
|||||||
sqlite3.register_adapter(list, json.dumps)
|
sqlite3.register_adapter(list, json.dumps)
|
||||||
sqlite3.register_converter("json", json.loads)
|
sqlite3.register_converter("json", json.loads)
|
||||||
|
|
||||||
if self._configuration.getboolean("settings", "apply_migrations", fallback=True):
|
if not self._configuration.getboolean("settings", "apply_migrations", fallback=True):
|
||||||
|
return
|
||||||
|
if self._repository_id.is_empty:
|
||||||
|
return # do not perform migration on empty repository identifier (e.g. multirepo command)
|
||||||
|
|
||||||
|
with self._repository_paths.preserve_owner():
|
||||||
self.with_connection(lambda connection: Migrations.migrate(connection, self._configuration))
|
self.with_connection(lambda connection: Migrations.migrate(connection, self._configuration))
|
||||||
self._repository_paths.chown(self.path)
|
|
||||||
|
|
||||||
def package_clear(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
|
def package_clear(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -72,8 +72,8 @@ class SyncHttpClient(LazyLogging):
|
|||||||
"""
|
"""
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
python_version = ".".join(map(str, sys.version_info[:3])) # just major.minor.patch
|
python_version = ".".join(map(str, sys.version_info[:3])) # just major.minor.patch
|
||||||
session.headers["User-Agent"] = f"ahriman/{__version__}" \
|
session.headers["User-Agent"] = f"ahriman/{__version__} " \
|
||||||
f"{requests.utils.default_user_agent()}" \
|
f"{requests.utils.default_user_agent()} " \
|
||||||
f"python/{python_version}"
|
f"python/{python_version}"
|
||||||
|
|
||||||
return session
|
return session
|
||||||
|
@ -203,12 +203,15 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
# this method does not raise NotImplementedError because it is actively used as dummy client for http log
|
# this method does not raise NotImplementedError because it is actively used as dummy client for http log
|
||||||
|
|
||||||
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[LogRecord]:
|
def package_logs_get(self, package_base: str, version: str | None = None, process_id: str | None = None,
|
||||||
|
limit: int = -1, offset: int = 0) -> list[LogRecord]:
|
||||||
"""
|
"""
|
||||||
get package logs
|
get package logs
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base
|
package_base(str): package base
|
||||||
|
version(str | None, optional): package version to search (Default value = None)
|
||||||
|
process_id(str | None, optional): process identifier to search (Default value = None)
|
||||||
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||||
offset(int, optional): records offset (Default value = 0)
|
offset(int, optional): records offset (Default value = 0)
|
||||||
|
|
||||||
|
@ -152,19 +152,22 @@ class LocalClient(Client):
|
|||||||
"""
|
"""
|
||||||
self.database.logs_insert(log_record, self.repository_id)
|
self.database.logs_insert(log_record, self.repository_id)
|
||||||
|
|
||||||
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[LogRecord]:
|
def package_logs_get(self, package_base: str, version: str | None = None, process_id: str | None = None,
|
||||||
|
limit: int = -1, offset: int = 0) -> list[LogRecord]:
|
||||||
"""
|
"""
|
||||||
get package logs
|
get package logs
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base
|
package_base(str): package base
|
||||||
|
version(str | None, optional): package version to search (Default value = None)
|
||||||
|
process_id(str | None, optional): process identifier to search (Default value = None)
|
||||||
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||||
offset(int, optional): records offset (Default value = 0)
|
offset(int, optional): records offset (Default value = 0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[LogRecord]: package logs
|
list[LogRecord]: package logs
|
||||||
"""
|
"""
|
||||||
return self.database.logs_get(package_base, limit, offset, self.repository_id)
|
return self.database.logs_get(package_base, version, process_id, limit, offset, self.repository_id)
|
||||||
|
|
||||||
def package_logs_remove(self, package_base: str, version: str | None) -> None:
|
def package_logs_remove(self, package_base: str, version: str | None) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -109,7 +109,7 @@ class Watcher(LazyLogging):
|
|||||||
|
|
||||||
package_logs_add: Callable[[LogRecord], None]
|
package_logs_add: Callable[[LogRecord], None]
|
||||||
|
|
||||||
package_logs_get: Callable[[str, int, int], list[LogRecord]]
|
package_logs_get: Callable[[str, str | None, str | None, int, int], list[LogRecord]]
|
||||||
|
|
||||||
package_logs_remove: Callable[[str, str | None], None]
|
package_logs_remove: Callable[[str, str | None], None]
|
||||||
|
|
||||||
|
@ -326,12 +326,15 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
self.make_request("POST", self._logs_url(log_record.log_record_id.package_base),
|
self.make_request("POST", self._logs_url(log_record.log_record_id.package_base),
|
||||||
params=self.repository_id.query(), json=payload, suppress_errors=True)
|
params=self.repository_id.query(), json=payload, suppress_errors=True)
|
||||||
|
|
||||||
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[LogRecord]:
|
def package_logs_get(self, package_base: str, version: str | None = None, process_id: str | None = None,
|
||||||
|
limit: int = -1, offset: int = 0) -> list[LogRecord]:
|
||||||
"""
|
"""
|
||||||
get package logs
|
get package logs
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base
|
package_base(str): package base
|
||||||
|
version(str | None, optional): package version to search (Default value = None)
|
||||||
|
process_id(str | None, optional): process identifier to search (Default value = None)
|
||||||
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||||
offset(int, optional): records offset (Default value = 0)
|
offset(int, optional): records offset (Default value = 0)
|
||||||
|
|
||||||
@ -339,6 +342,10 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
list[LogRecord]: package logs
|
list[LogRecord]: package logs
|
||||||
"""
|
"""
|
||||||
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
||||||
|
if version is not None:
|
||||||
|
query.append(("version", version))
|
||||||
|
if process_id is not None:
|
||||||
|
query.append(("process_id", process_id))
|
||||||
|
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
response = self.make_request("GET", self._logs_url(package_base), params=query)
|
response = self.make_request("GET", self._logs_url(package_base), params=query)
|
||||||
|
@ -51,6 +51,7 @@ __all__ = [
|
|||||||
"parse_version",
|
"parse_version",
|
||||||
"partition",
|
"partition",
|
||||||
"pretty_datetime",
|
"pretty_datetime",
|
||||||
|
"pretty_interval",
|
||||||
"pretty_size",
|
"pretty_size",
|
||||||
"safe_filename",
|
"safe_filename",
|
||||||
"srcinfo_property",
|
"srcinfo_property",
|
||||||
@ -353,6 +354,28 @@ def pretty_datetime(timestamp: datetime.datetime | float | int | None) -> str:
|
|||||||
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_interval(interval: int) -> str:
|
||||||
|
"""
|
||||||
|
convert time interval to string
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interval(int): time interval in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: pretty printable interval as string
|
||||||
|
"""
|
||||||
|
minutes, seconds = divmod(interval, 60)
|
||||||
|
hours, minutes = divmod(minutes, 60)
|
||||||
|
return " ".join([
|
||||||
|
f"{value} {description}{"s" if value > 1 else ""}"
|
||||||
|
for value, description in [
|
||||||
|
(hours, "hour"),
|
||||||
|
(minutes, "minute"),
|
||||||
|
(seconds, "second"),
|
||||||
|
] if value > 0
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
def pretty_size(size: float | None, level: int = 0) -> str:
|
def pretty_size(size: float | None, level: int = 0) -> str:
|
||||||
"""
|
"""
|
||||||
convert size to string
|
convert size to string
|
||||||
|
@ -25,7 +25,7 @@ from dataclasses import dataclass, field, fields
|
|||||||
from pyalpm import Package # type: ignore[import-not-found]
|
from pyalpm import Package # type: ignore[import-not-found]
|
||||||
from typing import Any, Self
|
from typing import Any, Self
|
||||||
|
|
||||||
from ahriman.core.utils import filter_json, full_version
|
from ahriman.core.utils import filter_json, full_version, trim_package
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@ -103,6 +103,17 @@ class AURPackage:
|
|||||||
keywords: list[str] = field(default_factory=list)
|
keywords: list[str] = field(default_factory=list)
|
||||||
groups: list[str] = field(default_factory=list)
|
groups: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
"""
|
||||||
|
update packages lists accordingly
|
||||||
|
"""
|
||||||
|
object.__setattr__(self, "depends", [trim_package(package) for package in self.depends])
|
||||||
|
object.__setattr__(self, "make_depends", [trim_package(package) for package in self.make_depends])
|
||||||
|
object.__setattr__(self, "opt_depends", [trim_package(package) for package in self.opt_depends])
|
||||||
|
object.__setattr__(self, "check_depends", [trim_package(package) for package in self.check_depends])
|
||||||
|
object.__setattr__(self, "conflicts", [trim_package(package) for package in self.conflicts])
|
||||||
|
object.__setattr__(self, "provides", [trim_package(package) for package in self.provides])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, dump: dict[str, Any]) -> Self:
|
def from_json(cls, dump: dict[str, Any]) -> Self:
|
||||||
"""
|
"""
|
||||||
|
@ -83,12 +83,13 @@ class PackageDescription:
|
|||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
"""
|
"""
|
||||||
update dependencies list accordingly
|
update packages lists accordingly
|
||||||
"""
|
"""
|
||||||
self.depends = [trim_package(package) for package in self.depends]
|
self.depends = [trim_package(package) for package in self.depends]
|
||||||
self.opt_depends = [trim_package(package) for package in self.opt_depends]
|
|
||||||
self.make_depends = [trim_package(package) for package in self.make_depends]
|
self.make_depends = [trim_package(package) for package in self.make_depends]
|
||||||
|
self.opt_depends = [trim_package(package) for package in self.opt_depends]
|
||||||
self.check_depends = [trim_package(package) for package in self.check_depends]
|
self.check_depends = [trim_package(package) for package in self.check_depends]
|
||||||
|
self.provides = [trim_package(package) for package in self.provides]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filepath(self) -> Path | None:
|
def filepath(self) -> Path | None:
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import contextlib
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
@ -221,22 +222,14 @@ class RepositoryPaths(LazyLogging):
|
|||||||
stat = path.stat()
|
stat = path.stat()
|
||||||
return stat.st_uid, stat.st_gid
|
return stat.st_uid, stat.st_gid
|
||||||
|
|
||||||
def cache_for(self, package_base: str) -> Path:
|
def _chown(self, path: Path) -> None:
|
||||||
"""
|
|
||||||
get path to cached PKGBUILD and package sources for the package base
|
|
||||||
|
|
||||||
Args:
|
|
||||||
package_base(str): package base name
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path: full path to directory for specified package base cache
|
|
||||||
"""
|
|
||||||
return self.cache / package_base
|
|
||||||
|
|
||||||
def chown(self, path: Path) -> None:
|
|
||||||
"""
|
"""
|
||||||
set owner of path recursively (from root) to root owner
|
set owner of path recursively (from root) to root owner
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
More likely you don't want to call this method explicitly, consider using :func:`preserve_owner`
|
||||||
|
as context manager instead
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path(Path): path to be chown
|
path(Path): path to be chown
|
||||||
|
|
||||||
@ -256,6 +249,56 @@ class RepositoryPaths(LazyLogging):
|
|||||||
set_owner(path)
|
set_owner(path)
|
||||||
path = path.parent
|
path = path.parent
|
||||||
|
|
||||||
|
def cache_for(self, package_base: str) -> Path:
|
||||||
|
"""
|
||||||
|
get path to cached PKGBUILD and package sources for the package base
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path: full path to directory for specified package base cache
|
||||||
|
"""
|
||||||
|
return self.cache / package_base
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def preserve_owner(self, path: Path | None = None) -> Generator[None, None, None]:
|
||||||
|
"""
|
||||||
|
perform any action preserving owner for any newly created file or directory
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path(Path | None, optional): use this path as root instead of repository root (Default value = None)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
This method is designed to use as context manager when you are going to perform operations which might
|
||||||
|
change filesystem, especially if you are doing it under unsafe flag, e.g.::
|
||||||
|
|
||||||
|
>>> with paths.preserve_owner():
|
||||||
|
>>> paths.tree_create()
|
||||||
|
|
||||||
|
Note, however, that this method doesn't handle any exceptions and will eventually interrupt
|
||||||
|
if there will be any.
|
||||||
|
"""
|
||||||
|
path = path or self.root
|
||||||
|
|
||||||
|
def walk(root: Path) -> Generator[Path, None, None]:
|
||||||
|
# basically walk, but skipping some content
|
||||||
|
for child in root.iterdir():
|
||||||
|
yield child
|
||||||
|
if child in (self.chroot.parent,):
|
||||||
|
yield from child.iterdir() # we only yield top-level in chroot directory
|
||||||
|
elif child.is_dir():
|
||||||
|
yield from walk(child)
|
||||||
|
|
||||||
|
# get current filesystem and run action
|
||||||
|
previous_snapshot = set(walk(path))
|
||||||
|
yield
|
||||||
|
|
||||||
|
# get newly created files and directories and chown them
|
||||||
|
new_entries = set(walk(path)).difference(previous_snapshot)
|
||||||
|
for entry in new_entries:
|
||||||
|
self._chown(entry)
|
||||||
|
|
||||||
def tree_clear(self, package_base: str) -> None:
|
def tree_clear(self, package_base: str) -> None:
|
||||||
"""
|
"""
|
||||||
clear package specific files
|
clear package specific files
|
||||||
@ -274,12 +317,13 @@ class RepositoryPaths(LazyLogging):
|
|||||||
"""
|
"""
|
||||||
if self.repository_id.is_empty:
|
if self.repository_id.is_empty:
|
||||||
return # do not even try to create tree in case if no repository id set
|
return # do not even try to create tree in case if no repository id set
|
||||||
for directory in (
|
|
||||||
self.cache,
|
with self.preserve_owner():
|
||||||
self.chroot,
|
for directory in (
|
||||||
self.packages,
|
self.cache,
|
||||||
self.pacman,
|
self.chroot,
|
||||||
self.repository,
|
self.packages,
|
||||||
):
|
self.pacman,
|
||||||
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
|
self.repository,
|
||||||
self.chown(directory)
|
):
|
||||||
|
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||||
|
@ -34,6 +34,7 @@ from ahriman.web.schemas.log_schema import LogSchema
|
|||||||
from ahriman.web.schemas.login_schema import LoginSchema
|
from ahriman.web.schemas.login_schema import LoginSchema
|
||||||
from ahriman.web.schemas.logs_rotate_schema import LogsRotateSchema
|
from ahriman.web.schemas.logs_rotate_schema import LogsRotateSchema
|
||||||
from ahriman.web.schemas.logs_schema import LogsSchema
|
from ahriman.web.schemas.logs_schema import LogsSchema
|
||||||
|
from ahriman.web.schemas.logs_search_schema import LogsSearchSchema
|
||||||
from ahriman.web.schemas.oauth2_schema import OAuth2Schema
|
from ahriman.web.schemas.oauth2_schema import OAuth2Schema
|
||||||
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
||||||
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
||||||
|
39
src/ahriman/web/schemas/logs_search_schema.py
Normal file
39
src/ahriman/web/schemas/logs_search_schema.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2025 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/>.
|
||||||
|
#
|
||||||
|
from ahriman import __version__
|
||||||
|
from ahriman.web.apispec import fields
|
||||||
|
from ahriman.web.schemas.pagination_schema import PaginationSchema
|
||||||
|
|
||||||
|
|
||||||
|
class LogsSearchSchema(PaginationSchema):
|
||||||
|
"""
|
||||||
|
request log search schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
head = fields.Boolean(metadata={
|
||||||
|
"description": "Return versions only without fetching logs themselves",
|
||||||
|
})
|
||||||
|
version = fields.String(metadata={
|
||||||
|
"description": "Package version to search",
|
||||||
|
"example": __version__,
|
||||||
|
})
|
||||||
|
process_id = fields.String(metadata={
|
||||||
|
"description": "Process unique identifier to search",
|
||||||
|
})
|
@ -22,6 +22,7 @@ import aiohttp_jinja2
|
|||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
from ahriman.core.auth.helpers import authorized_userid
|
from ahriman.core.auth.helpers import authorized_userid
|
||||||
|
from ahriman.core.utils import pretty_interval
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.apispec import aiohttp_apispec
|
from ahriman.web.apispec import aiohttp_apispec
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
@ -37,6 +38,10 @@ class IndexView(BaseView):
|
|||||||
* control - HTML to insert for login control, HTML string, required
|
* control - HTML to insert for login control, HTML string, required
|
||||||
* enabled - whether authorization is enabled by configuration or not, boolean, required
|
* enabled - whether authorization is enabled by configuration or not, boolean, required
|
||||||
* username - authenticated username if any, string, null means not authenticated
|
* username - authenticated username if any, string, null means not authenticated
|
||||||
|
* autorefresh_intervals - auto refresh intervals, optional
|
||||||
|
* interval - auto refresh interval in milliseconds, integer, required
|
||||||
|
* is_active - is current interval active or not, boolean, required
|
||||||
|
* text - text representation of the interval (e.g. "30 seconds"), string, required
|
||||||
* docs_enabled - indicates if api docs is enabled, boolean, required
|
* docs_enabled - indicates if api docs is enabled, boolean, required
|
||||||
* index_url - url to the repository index, string, optional
|
* index_url - url to the repository index, string, optional
|
||||||
* repositories - list of repositories unique identifiers, required
|
* repositories - list of repositories unique identifiers, required
|
||||||
@ -66,8 +71,19 @@ class IndexView(BaseView):
|
|||||||
"username": auth_username,
|
"username": auth_username,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autorefresh_intervals = [
|
||||||
|
{
|
||||||
|
"interval": interval * 1000, # milliseconds
|
||||||
|
"is_active": index == 0, # first element is always default
|
||||||
|
"text": pretty_interval(interval),
|
||||||
|
}
|
||||||
|
for index, interval in enumerate(self.configuration.getintlist("web", "autorefresh_intervals", fallback=[]))
|
||||||
|
if interval > 0 # special case if 0 exists and first, refresh will not be turned on by default
|
||||||
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"auth": auth,
|
"auth": auth,
|
||||||
|
"autorefresh_intervals": sorted(autorefresh_intervals, key=lambda interval: interval["interval"]),
|
||||||
"docs_enabled": aiohttp_apispec is not None,
|
"docs_enabled": aiohttp_apispec is not None,
|
||||||
"index_url": self.configuration.get("web", "index_url", fallback=None),
|
"index_url": self.configuration.get("web", "index_url", fallback=None),
|
||||||
"repositories": [
|
"repositories": [
|
||||||
|
@ -90,7 +90,7 @@ class LogsView(StatusViewGuard, BaseView):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_, status = self.service().package_get(package_base)
|
_, status = self.service().package_get(package_base)
|
||||||
logs = self.service(package_base=package_base).package_logs_get(package_base, -1, 0)
|
logs = self.service(package_base=package_base).package_logs_get(package_base, None, None, -1, 0)
|
||||||
except UnknownPackageError:
|
except UnknownPackageError:
|
||||||
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
|
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ class SearchView(BaseView):
|
|||||||
if not packages:
|
if not packages:
|
||||||
raise HTTPNotFound(reason=f"No packages found for terms: {search}")
|
raise HTTPNotFound(reason=f"No packages found for terms: {search}")
|
||||||
|
|
||||||
comparator: Callable[[AURPackage], str] = lambda item: str(item.package_base)
|
comparator: Callable[[AURPackage], str] = lambda item: item.package_base
|
||||||
response = [
|
response = [
|
||||||
{
|
{
|
||||||
"package": package.package_base,
|
"package": package.package_base,
|
||||||
|
@ -17,18 +17,22 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import itertools
|
||||||
|
|
||||||
from aiohttp.web import Response, json_response
|
from aiohttp.web import Response, json_response
|
||||||
|
from dataclasses import replace
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.apispec.decorators import apidocs
|
from ahriman.web.apispec.decorators import apidocs
|
||||||
from ahriman.web.schemas import LogSchema, PackageNameSchema, PaginationSchema
|
from ahriman.web.schemas import LogSchema, LogsSearchSchema, PackageNameSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
from ahriman.web.views.status_view_guard import StatusViewGuard
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
|
|
||||||
class LogsView(StatusViewGuard, BaseView):
|
class LogsView(StatusViewGuard, BaseView):
|
||||||
"""
|
""" else:
|
||||||
|
|
||||||
package logs web view
|
package logs web view
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
@ -47,7 +51,7 @@ class LogsView(StatusViewGuard, BaseView):
|
|||||||
error_404_description="Package base and/or repository are unknown",
|
error_404_description="Package base and/or repository are unknown",
|
||||||
schema=LogSchema(many=True),
|
schema=LogSchema(many=True),
|
||||||
match_schema=PackageNameSchema,
|
match_schema=PackageNameSchema,
|
||||||
query_schema=PaginationSchema,
|
query_schema=LogsSearchSchema,
|
||||||
)
|
)
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
"""
|
"""
|
||||||
@ -61,8 +65,19 @@ class LogsView(StatusViewGuard, BaseView):
|
|||||||
"""
|
"""
|
||||||
package_base = self.request.match_info["package"]
|
package_base = self.request.match_info["package"]
|
||||||
limit, offset = self.page()
|
limit, offset = self.page()
|
||||||
|
version = self.request.query.get("version", None)
|
||||||
|
process = self.request.query.get("process_id", None)
|
||||||
|
|
||||||
logs = self.service(package_base=package_base).package_logs_get(package_base, limit, offset)
|
logs = self.service(package_base=package_base).package_logs_get(package_base, version, process, limit, offset)
|
||||||
|
|
||||||
|
head = self.request.query.get("head", "false")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
if self.configuration._convert_to_boolean(head): # type: ignore[attr-defined]
|
||||||
|
# logs should be sorted already
|
||||||
|
logs = [
|
||||||
|
replace(next(log_records), message="") # remove messages
|
||||||
|
for _, log_records in itertools.groupby(logs, lambda log_record: log_record.log_record_id)
|
||||||
|
]
|
||||||
|
|
||||||
response = [log_record.view() for log_record in logs]
|
response = [log_record.view() for log_record in logs]
|
||||||
return json_response(response)
|
return json_response(response)
|
||||||
|
@ -166,11 +166,16 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis
|
|||||||
# package cache
|
# package cache
|
||||||
if not repositories:
|
if not repositories:
|
||||||
raise InitializeError("No repositories configured, exiting")
|
raise InitializeError("No repositories configured, exiting")
|
||||||
database = SQLite.load(configuration)
|
|
||||||
watchers: dict[RepositoryId, Watcher] = {}
|
watchers: dict[RepositoryId, Watcher] = {}
|
||||||
|
configuration_path, _ = configuration.check_loaded()
|
||||||
for repository_id in repositories:
|
for repository_id in repositories:
|
||||||
application.logger.info("load repository %s", repository_id)
|
application.logger.info("load repository %s", repository_id)
|
||||||
client = Client.load(repository_id, configuration, database, report=False) # explicitly load local client
|
# load settings explicitly for architecture if any
|
||||||
|
repository_configuration = Configuration.from_path(configuration_path, repository_id)
|
||||||
|
# load database instance, because it holds identifier
|
||||||
|
database = SQLite.load(repository_configuration)
|
||||||
|
# explicitly load local client
|
||||||
|
client = Client.load(repository_id, repository_configuration, database, report=False)
|
||||||
watchers[repository_id] = Watcher(client)
|
watchers[repository_id] = Watcher(client)
|
||||||
application[WatcherKey] = watchers
|
application[WatcherKey] = watchers
|
||||||
# workers cache
|
# workers cache
|
||||||
@ -179,6 +184,7 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis
|
|||||||
application[SpawnKey] = spawner
|
application[SpawnKey] = spawner
|
||||||
|
|
||||||
application.logger.info("setup authorization")
|
application.logger.info("setup authorization")
|
||||||
|
database = SQLite.load(configuration)
|
||||||
validator = application[AuthKey] = Auth.load(configuration, database)
|
validator = application[AuthKey] = Auth.load(configuration, database)
|
||||||
if validator.enabled:
|
if validator.enabled:
|
||||||
from ahriman.web.middlewares.auth_handler import setup_auth
|
from ahriman.web.middlewares.auth_handler import setup_auth
|
||||||
|
@ -58,9 +58,11 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
|
|||||||
sudo_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_sudo")
|
sudo_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_sudo")
|
||||||
executable_mock = mocker.patch("ahriman.application.handlers.setup.Setup.executable_create")
|
executable_mock = mocker.patch("ahriman.application.handlers.setup.Setup.executable_create")
|
||||||
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
|
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
|
||||||
|
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
_, repository_id = configuration.check_loaded()
|
||||||
Setup.run(args, repository_id, configuration, report=False)
|
Setup.run(args, repository_id, configuration, report=False)
|
||||||
|
owner_guard_mock.assert_called_once_with()
|
||||||
ahriman_configuration_mock.assert_called_once_with(args, repository_id, configuration)
|
ahriman_configuration_mock.assert_called_once_with(args, repository_id, configuration)
|
||||||
devtools_configuration_mock.assert_called_once_with(
|
devtools_configuration_mock.assert_called_once_with(
|
||||||
repository_id, args.from_configuration, args.mirror, args.multilib, f"file://{repository_paths.repository}")
|
repository_id, args.from_configuration, args.mirror, args.multilib, f"file://{repository_paths.repository}")
|
||||||
@ -268,13 +270,11 @@ def test_executable_create(configuration: Configuration, repository_paths: Repos
|
|||||||
"""
|
"""
|
||||||
must create executable
|
must create executable
|
||||||
"""
|
"""
|
||||||
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown")
|
|
||||||
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
|
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
|
||||||
unlink_mock = mocker.patch("pathlib.Path.unlink")
|
unlink_mock = mocker.patch("pathlib.Path.unlink")
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
_, repository_id = configuration.check_loaded()
|
||||||
Setup.executable_create(repository_paths, repository_id)
|
Setup.executable_create(repository_paths, repository_id)
|
||||||
chown_mock.assert_called_once_with(Setup.build_command(repository_paths.root, repository_id))
|
|
||||||
symlink_mock.assert_called_once_with(Setup.ARCHBUILD_COMMAND_PATH)
|
symlink_mock.assert_called_once_with(Setup.ARCHBUILD_COMMAND_PATH)
|
||||||
unlink_mock.assert_called_once_with(missing_ok=True)
|
unlink_mock.assert_called_once_with(missing_ok=True)
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from unittest.mock import call as MockCall
|
from unittest.mock import call as MockCall
|
||||||
|
|
||||||
@ -88,6 +89,17 @@ def test_multisearch_single(aur_package_ahriman: AURPackage, pacman: Pacman, moc
|
|||||||
search_mock.assert_called_once_with("ahriman", pacman=pacman, search_by=None)
|
search_mock.assert_called_once_with("ahriman", pacman=pacman, search_by=None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_multisearch_remove_duplicates(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must remove duplicates from search result
|
||||||
|
"""
|
||||||
|
package1 = replace(aur_package_ahriman)
|
||||||
|
package2 = replace(aur_package_ahriman, name="ahriman-triggers")
|
||||||
|
mocker.patch("ahriman.core.alpm.remote.Remote.package_search", return_value=[package1, package2])
|
||||||
|
|
||||||
|
assert Remote.multisearch("ahriman", pacman=pacman) == [package1]
|
||||||
|
|
||||||
|
|
||||||
def test_remote_git_url(remote: Remote) -> None:
|
def test_remote_git_url(remote: Remote) -> None:
|
||||||
"""
|
"""
|
||||||
must raise NotImplemented for missing remote git url
|
must raise NotImplemented for missing remote git url
|
||||||
|
@ -62,12 +62,12 @@ def test_database_copy(pacman: Pacman, mocker: MockerFixture) -> None:
|
|||||||
mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path))
|
mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path))
|
||||||
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
|
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
|
||||||
copy_mock = mocker.patch("shutil.copy")
|
copy_mock = mocker.patch("shutil.copy")
|
||||||
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown")
|
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
|
||||||
|
|
||||||
pacman.database_copy(pacman.handle, database, path, use_ahriman_cache=True)
|
pacman.database_copy(pacman.handle, database, path, use_ahriman_cache=True)
|
||||||
mkdir_mock.assert_called_once_with(mode=0o755, exist_ok=True)
|
mkdir_mock.assert_called_once_with(mode=0o755, exist_ok=True)
|
||||||
copy_mock.assert_called_once_with(path / "sync" / "core.db", dst_path)
|
copy_mock.assert_called_once_with(path / "sync" / "core.db", dst_path)
|
||||||
chown_mock.assert_called_once_with(dst_path)
|
owner_guard_mock.assert_called_once_with(dst_path.parent)
|
||||||
|
|
||||||
|
|
||||||
def test_database_copy_skip(pacman: Pacman, mocker: MockerFixture) -> None:
|
def test_database_copy_skip(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||||
@ -289,3 +289,4 @@ def test_package_provided_by(pacman: Pacman) -> None:
|
|||||||
must search through the provides lists
|
must search through the provides lists
|
||||||
"""
|
"""
|
||||||
assert list(pacman.provided_by("sh"))
|
assert list(pacman.provided_by("sh"))
|
||||||
|
assert list(pacman.provided_by("libacl.so")) # case with exact version
|
||||||
|
@ -133,6 +133,14 @@ def test_dump_architecture_specific(configuration: Configuration) -> None:
|
|||||||
assert dump["build"]["archbuild_flags"] == "hello flag"
|
assert dump["build"]["archbuild_flags"] == "hello flag"
|
||||||
|
|
||||||
|
|
||||||
|
def test_getintlist(configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
must extract list of integers
|
||||||
|
"""
|
||||||
|
configuration.set_option("build", "test_int_list", "1 42 3")
|
||||||
|
assert configuration.getintlist("build", "test_int_list") == [1, 42, 3]
|
||||||
|
|
||||||
|
|
||||||
def test_getlist(configuration: Configuration) -> None:
|
def test_getlist(configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
must return list of string correctly
|
must return list of string correctly
|
||||||
|
@ -71,11 +71,35 @@ def test_logs_insert_get_pagination(database: SQLite, package_ahriman: Package)
|
|||||||
"""
|
"""
|
||||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"))
|
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"))
|
||||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2"))
|
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2"))
|
||||||
assert database.logs_get(package_ahriman.base, 1, 1) == [
|
assert database.logs_get(package_ahriman.base, None, None, 1, 1) == [
|
||||||
LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"),
|
LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_logs_insert_get_filter_by_version(database: SQLite, package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must insert and get package logs with pagination
|
||||||
|
"""
|
||||||
|
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1", "p1"), 42.0, "message 1"))
|
||||||
|
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1", "p2"), 43.0, "message 2"))
|
||||||
|
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "2", "p1"), 44.0, "message 3"))
|
||||||
|
|
||||||
|
assert database.logs_get(package_ahriman.base, "1", None) == [
|
||||||
|
LogRecord(LogRecordId(package_ahriman.base, "1", "p1"), 42.0, "message 1"),
|
||||||
|
LogRecord(LogRecordId(package_ahriman.base, "1", "p2"), 43.0, "message 2"),
|
||||||
|
]
|
||||||
|
assert database.logs_get(package_ahriman.base, "2", None) == [
|
||||||
|
LogRecord(LogRecordId(package_ahriman.base, "2", "p1"), 44.0, "message 3"),
|
||||||
|
]
|
||||||
|
assert database.logs_get(package_ahriman.base, None, "p1") == [
|
||||||
|
LogRecord(LogRecordId(package_ahriman.base, "1", "p1"), 42.0, "message 1"),
|
||||||
|
LogRecord(LogRecordId(package_ahriman.base, "2", "p1"), 44.0, "message 3"),
|
||||||
|
]
|
||||||
|
assert database.logs_get(package_ahriman.base, "1", "p1") == [
|
||||||
|
LogRecord(LogRecordId(package_ahriman.base, "1", "p1"), 42.0, "message 1"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_logs_insert_get_multi(database: SQLite, package_ahriman: Package) -> None:
|
def test_logs_insert_get_multi(database: SQLite, package_ahriman: Package) -> None:
|
||||||
"""
|
"""
|
||||||
must insert and get package logs for multiple repositories
|
must insert and get package logs for multiple repositories
|
||||||
|
@ -36,6 +36,17 @@ def test_init_skip_migration(database: SQLite, mocker: MockerFixture) -> None:
|
|||||||
migrate_schema_mock.assert_not_called()
|
migrate_schema_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_skip_empty_repository(database: SQLite, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must skip migrations if repository identifier is not set
|
||||||
|
"""
|
||||||
|
database._repository_id = RepositoryId("", "")
|
||||||
|
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
|
||||||
|
|
||||||
|
database.init()
|
||||||
|
migrate_schema_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_package_clear(database: SQLite, repository_id: RepositoryId, mocker: MockerFixture) -> None:
|
def test_package_clear(database: SQLite, repository_id: RepositoryId, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must clear package data
|
must clear package data
|
||||||
|
@ -124,8 +124,9 @@ def test_package_logs_get(local_client: LocalClient, package_ahriman: Package, m
|
|||||||
must retrieve package logs
|
must retrieve package logs
|
||||||
"""
|
"""
|
||||||
logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_get")
|
logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_get")
|
||||||
local_client.package_logs_get(package_ahriman.base, 1, 2)
|
local_client.package_logs_get(package_ahriman.base, package_ahriman.version, "process", 1, 2)
|
||||||
logs_mock.assert_called_once_with(package_ahriman.base, 1, 2, local_client.repository_id)
|
logs_mock.assert_called_once_with(package_ahriman.base, package_ahriman.version, "process", 1, 2,
|
||||||
|
local_client.repository_id)
|
||||||
|
|
||||||
|
|
||||||
def test_package_logs_remove(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_package_logs_remove(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
@ -658,7 +658,7 @@ def test_package_logs_get(web_client: WebClient, package_ahriman: Package, mocke
|
|||||||
|
|
||||||
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=response_obj)
|
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=response_obj)
|
||||||
|
|
||||||
result = web_client.package_logs_get(package_ahriman.base, 1, 2)
|
result = web_client.package_logs_get(package_ahriman.base, None, None, 1, 2)
|
||||||
requests_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True),
|
requests_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True),
|
||||||
params=web_client.repository_id.query() + [("limit", "1"), ("offset", "2")])
|
params=web_client.repository_id.query() + [("limit", "1"), ("offset", "2")])
|
||||||
assert result == [
|
assert result == [
|
||||||
@ -666,6 +666,21 @@ def test_package_logs_get(web_client: WebClient, package_ahriman: Package, mocke
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_package_logs_get_filter(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must get logs with version and process id filter
|
||||||
|
"""
|
||||||
|
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
|
||||||
|
web_client.package_logs_get(package_ahriman.base, package_ahriman.version, LogRecordId.DEFAULT_PROCESS_ID, 1, 2)
|
||||||
|
requests_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True),
|
||||||
|
params=web_client.repository_id.query() + [
|
||||||
|
("limit", "1"),
|
||||||
|
("offset", "2"),
|
||||||
|
("version", package_ahriman.version),
|
||||||
|
("process_id", LogRecordId.DEFAULT_PROCESS_ID),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
def test_package_logs_get_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_package_logs_get_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must suppress any exception happened during logs fetch
|
must suppress any exception happened during logs fetch
|
||||||
|
@ -10,8 +10,8 @@ from unittest.mock import call as MockCall
|
|||||||
|
|
||||||
from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError
|
from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError
|
||||||
from ahriman.core.utils import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \
|
from ahriman.core.utils import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \
|
||||||
full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \
|
full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_interval, pretty_size, \
|
||||||
srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk
|
safe_filename, srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
@ -341,6 +341,18 @@ def test_pretty_datetime_empty() -> None:
|
|||||||
assert pretty_datetime(None) == ""
|
assert pretty_datetime(None) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_pretty_interval() -> None:
|
||||||
|
"""
|
||||||
|
must generate string from interval
|
||||||
|
"""
|
||||||
|
assert pretty_interval(1) == "1 second"
|
||||||
|
assert pretty_interval(42) == "42 seconds"
|
||||||
|
assert pretty_interval(62) == "1 minute 2 seconds"
|
||||||
|
assert pretty_interval(121) == "2 minutes 1 second"
|
||||||
|
assert pretty_interval(3600) == "1 hour"
|
||||||
|
assert pretty_interval(7242) == "2 hours 42 seconds"
|
||||||
|
|
||||||
|
|
||||||
def test_pretty_size_bytes() -> None:
|
def test_pretty_size_bytes() -> None:
|
||||||
"""
|
"""
|
||||||
must generate bytes string for bytes value
|
must generate bytes string for bytes value
|
||||||
|
@ -2,7 +2,7 @@ import datetime
|
|||||||
import json
|
import json
|
||||||
import pyalpm # typing: ignore
|
import pyalpm # typing: ignore
|
||||||
|
|
||||||
from dataclasses import asdict, fields
|
from dataclasses import asdict, fields, replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -38,6 +38,25 @@ def _get_official_data(resource_path_root: Path) -> dict[str, Any]:
|
|||||||
return json.loads(response)["results"][0]
|
return json.loads(response)["results"][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_init(aur_package_ahriman: AURPackage) -> None:
|
||||||
|
"""
|
||||||
|
must trim versions and descriptions from packages list
|
||||||
|
"""
|
||||||
|
package = replace(
|
||||||
|
aur_package_ahriman,
|
||||||
|
depends=["a=1"],
|
||||||
|
make_depends=["b>=3"],
|
||||||
|
opt_depends=["c: a description"],
|
||||||
|
check_depends=["d=4"],
|
||||||
|
provides=["e=5"],
|
||||||
|
)
|
||||||
|
assert package.depends == ["a"]
|
||||||
|
assert package.make_depends == ["b"]
|
||||||
|
assert package.opt_depends == ["c"]
|
||||||
|
assert package.check_depends == ["d"]
|
||||||
|
assert package.provides == ["e"]
|
||||||
|
|
||||||
|
|
||||||
def test_from_json(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None:
|
def test_from_json(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None:
|
||||||
"""
|
"""
|
||||||
must load package from json
|
must load package from json
|
||||||
|
@ -6,10 +6,15 @@ from ahriman.models.package_description import PackageDescription
|
|||||||
|
|
||||||
def test_post_init() -> None:
|
def test_post_init() -> None:
|
||||||
"""
|
"""
|
||||||
must trim versions and descriptions from dependencies list
|
must trim versions and descriptions from packages list
|
||||||
"""
|
"""
|
||||||
assert PackageDescription(depends=["a=1"], make_depends=["b>=3"], opt_depends=["c: a description"]) == \
|
assert PackageDescription(
|
||||||
PackageDescription(depends=["a"], make_depends=["b"], opt_depends=["c"])
|
depends=["a=1"],
|
||||||
|
make_depends=["b>=3"],
|
||||||
|
opt_depends=["c: a description"],
|
||||||
|
check_depends=["d=4"],
|
||||||
|
provides=["e=5"]
|
||||||
|
) == PackageDescription(depends=["a"], make_depends=["b"], opt_depends=["c"], check_depends=["d"], provides=["e"])
|
||||||
|
|
||||||
|
|
||||||
def test_filepath(package_description_ahriman: PackageDescription) -> None:
|
def test_filepath(package_description_ahriman: PackageDescription) -> None:
|
||||||
|
@ -198,15 +198,6 @@ def test_owner(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None
|
|||||||
assert RepositoryPaths.owner(repository_paths.root) == (42, 142)
|
assert RepositoryPaths.owner(repository_paths.root) == (42, 142)
|
||||||
|
|
||||||
|
|
||||||
def test_cache_for(repository_paths: RepositoryPaths, package_ahriman: Package) -> None:
|
|
||||||
"""
|
|
||||||
must return correct path for cache directory
|
|
||||||
"""
|
|
||||||
path = repository_paths.cache_for(package_ahriman.base)
|
|
||||||
assert path.name == package_ahriman.base
|
|
||||||
assert path.parent == repository_paths.cache
|
|
||||||
|
|
||||||
|
|
||||||
def test_chown(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
def test_chown(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must correctly set owner for the directory
|
must correctly set owner for the directory
|
||||||
@ -216,7 +207,7 @@ def test_chown(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None
|
|||||||
chown_mock = mocker.patch("os.chown")
|
chown_mock = mocker.patch("os.chown")
|
||||||
|
|
||||||
path = repository_paths.root / "path"
|
path = repository_paths.root / "path"
|
||||||
repository_paths.chown(path)
|
repository_paths._chown(path)
|
||||||
chown_mock.assert_called_once_with(path, 42, 42, follow_symlinks=False)
|
chown_mock.assert_called_once_with(path, 42, 42, follow_symlinks=False)
|
||||||
|
|
||||||
|
|
||||||
@ -229,7 +220,7 @@ def test_chown_parent(repository_paths: RepositoryPaths, mocker: MockerFixture)
|
|||||||
chown_mock = mocker.patch("os.chown")
|
chown_mock = mocker.patch("os.chown")
|
||||||
|
|
||||||
path = repository_paths.root / "parent" / "path"
|
path = repository_paths.root / "parent" / "path"
|
||||||
repository_paths.chown(path)
|
repository_paths._chown(path)
|
||||||
chown_mock.assert_has_calls([
|
chown_mock.assert_has_calls([
|
||||||
MockCall(path, 42, 42, follow_symlinks=False),
|
MockCall(path, 42, 42, follow_symlinks=False),
|
||||||
MockCall(path.parent, 42, 42, follow_symlinks=False)
|
MockCall(path.parent, 42, 42, follow_symlinks=False)
|
||||||
@ -245,7 +236,7 @@ def test_chown_skip(repository_paths: RepositoryPaths, mocker: MockerFixture) ->
|
|||||||
chown_mock = mocker.patch("os.chown")
|
chown_mock = mocker.patch("os.chown")
|
||||||
|
|
||||||
path = repository_paths.root / "path"
|
path = repository_paths.root / "path"
|
||||||
repository_paths.chown(path)
|
repository_paths._chown(path)
|
||||||
chown_mock.assert_not_called()
|
chown_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@ -254,7 +245,46 @@ def test_chown_invalid_path(repository_paths: RepositoryPaths) -> None:
|
|||||||
must raise invalid path exception in case if directory outside the root supplied
|
must raise invalid path exception in case if directory outside the root supplied
|
||||||
"""
|
"""
|
||||||
with pytest.raises(PathError):
|
with pytest.raises(PathError):
|
||||||
repository_paths.chown(repository_paths.root.parent)
|
repository_paths._chown(repository_paths.root.parent)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cache_for(repository_paths: RepositoryPaths, package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must return correct path for cache directory
|
||||||
|
"""
|
||||||
|
path = repository_paths.cache_for(package_ahriman.base)
|
||||||
|
assert path.name == package_ahriman.base
|
||||||
|
assert path.parent == repository_paths.cache
|
||||||
|
|
||||||
|
|
||||||
|
def test_preserve_owner(tmp_path: Path, repository_id: RepositoryId, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must preserve file owner during operations
|
||||||
|
"""
|
||||||
|
repository_paths = RepositoryPaths(tmp_path, repository_id)
|
||||||
|
repository_paths.tree_create()
|
||||||
|
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths._chown")
|
||||||
|
|
||||||
|
with repository_paths.preserve_owner():
|
||||||
|
(repository_paths.root / "created1").touch()
|
||||||
|
(repository_paths.chroot / "created2").touch()
|
||||||
|
chown_mock.assert_has_calls([MockCall(repository_paths.root / "created1")])
|
||||||
|
|
||||||
|
|
||||||
|
def test_preserve_owner_specific(tmp_path: Path, repository_id: RepositoryId, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must preserve file owner during operations only in specific directory
|
||||||
|
"""
|
||||||
|
repository_paths = RepositoryPaths(tmp_path, repository_id)
|
||||||
|
repository_paths.tree_create()
|
||||||
|
(repository_paths.root / "content").mkdir()
|
||||||
|
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths._chown")
|
||||||
|
|
||||||
|
with repository_paths.preserve_owner(repository_paths.root / "content"):
|
||||||
|
(repository_paths.root / "created1").touch()
|
||||||
|
(repository_paths.root / "content" / "created2").touch()
|
||||||
|
(repository_paths.chroot / "created3").touch()
|
||||||
|
chown_mock.assert_has_calls([MockCall(repository_paths.root / "content" / "created2")])
|
||||||
|
|
||||||
|
|
||||||
def test_tree_clear(repository_paths: RepositoryPaths, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_tree_clear(repository_paths: RepositoryPaths, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
@ -293,11 +323,11 @@ def test_tree_create(repository_paths: RepositoryPaths, mocker: MockerFixture) -
|
|||||||
and not callable(getattr(repository_paths, prop))
|
and not callable(getattr(repository_paths, prop))
|
||||||
}
|
}
|
||||||
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
|
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
|
||||||
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown")
|
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
|
||||||
|
|
||||||
repository_paths.tree_create()
|
repository_paths.tree_create()
|
||||||
mkdir_mock.assert_has_calls([MockCall(mode=0o755, parents=True, exist_ok=True) for _ in paths], any_order=True)
|
mkdir_mock.assert_has_calls([MockCall(mode=0o755, parents=True, exist_ok=True) for _ in paths], any_order=True)
|
||||||
chown_mock.assert_has_calls([MockCall(pytest.helpers.anyvar(int)) for _ in paths], any_order=True)
|
owner_guard_mock.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
def test_tree_create_skip(mocker: MockerFixture) -> None:
|
def test_tree_create_skip(mocker: MockerFixture) -> None:
|
||||||
|
1
tests/ahriman/web/schemas/test_logs_search_schema.py
Normal file
1
tests/ahriman/web/schemas/test_logs_search_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
@ -86,6 +86,31 @@ async def test_get_with_pagination(client: TestClient, package_ahriman: Package)
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_with_filter(client: TestClient, package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must get logs with filter by version and process identifier
|
||||||
|
"""
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
||||||
|
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
||||||
|
json={"created": 42.0, "message": "message 1", "version": "42"})
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
||||||
|
json={"created": 43.0, "message": "message 2", "version": "43"})
|
||||||
|
request_schema = pytest.helpers.schema_request(LogsView.get, location="querystring")
|
||||||
|
response_schema = pytest.helpers.schema_response(LogsView.get)
|
||||||
|
|
||||||
|
payload = {"version": "42", "process_id": LogRecordId.DEFAULT_PROCESS_ID}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs", params=payload)
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
logs = await response.json()
|
||||||
|
assert not response_schema.validate(logs)
|
||||||
|
assert logs == [
|
||||||
|
{"created": 42.0, "message": "message 1", "version": "42", "process_id": LogRecordId.DEFAULT_PROCESS_ID},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def test_get_bad_request(client: TestClient, package_ahriman: Package) -> None:
|
async def test_get_bad_request(client: TestClient, package_ahriman: Package) -> None:
|
||||||
"""
|
"""
|
||||||
must return bad request for invalid query parameters
|
must return bad request for invalid query parameters
|
||||||
@ -114,3 +139,41 @@ async def test_get_not_found(client: TestClient, package_ahriman: Package) -> No
|
|||||||
response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs")
|
response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs")
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
assert not response_schema.validate(await response.json())
|
assert not response_schema.validate(await response.json())
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_head(client: TestClient, package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must return only versions if head parameter is set
|
||||||
|
"""
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
||||||
|
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
||||||
|
json={"created": 42.0, "message": "message 1", "version": "42"})
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
||||||
|
json={"created": 43.0, "message": "message 2", "version": "42"})
|
||||||
|
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
||||||
|
json={"created": 44.0, "message": "message 3", "version": "43"})
|
||||||
|
request_schema = pytest.helpers.schema_request(LogsView.get, location="querystring")
|
||||||
|
response_schema = pytest.helpers.schema_response(LogsView.get)
|
||||||
|
|
||||||
|
payload = {"head": "true"}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs", params=payload)
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
logs = await response.json()
|
||||||
|
assert not response_schema.validate(logs)
|
||||||
|
assert logs == [
|
||||||
|
{
|
||||||
|
"created": 42.0,
|
||||||
|
"message": "",
|
||||||
|
"version": "42",
|
||||||
|
"process_id": LogRecordId.DEFAULT_PROCESS_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": 44.0,
|
||||||
|
"message": "",
|
||||||
|
"version": "43",
|
||||||
|
"process_id": LogRecordId.DEFAULT_PROCESS_ID,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
Reference in New Issue
Block a user