Updated most of backend API endpoints to support sub users.

This commit is contained in:
ArabCoders
2025-02-05 14:02:05 +03:00
parent a4aec9ef99
commit c50a9a8b5e
18 changed files with 218 additions and 216 deletions

View File

@@ -10,7 +10,8 @@
<div class="field is-grouped">
<p class="control" v-if="backends && backends.length>0">
<button class="button is-purple" v-tooltip.bottom="'Create sub users backends.'"
@click="navigateTo(makeConsoleCommand('backend:create -v', true))">
@click="navigateTo(makeConsoleCommand('backend:create -v', true))"
:disabled="'main' !== api_user">
<span class="icon"><i class="fas fa-users"></i></span>
</button>
</p>
@@ -196,6 +197,7 @@ useHead({title: 'Backends'})
const backends = ref([])
const toggleForm = ref(false)
const api_url = useStorage('api_url', '')
const api_user = useStorage('api_user', 'main')
const show_page_tips = useStorage('show_page_tips', true)
const isLoading = ref(false)
const selectedCommand = ref('')

View File

@@ -8,9 +8,11 @@ use App\Libs\Attributes\Route\Post;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
use Throwable;
@@ -23,13 +25,11 @@ final class AccessToken
}
#[Post(Index::URL . '/{name:backend}/accesstoken[/]', name: 'backend.accesstoken')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __invoke(iRequest $request, string $name, iEImport $mapper, iLogger $logger): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $mapper, $logger);
if (null === $this->getBackend(name: $name)) {
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
@@ -40,7 +40,7 @@ final class AccessToken
}
try {
$client = $this->getClient(name: $name);
$client = $this->getClient(name: $name, userContext: $userContext);
$token = $client->getUserToken(
userId: $id,
username: $data->get('username', $client->getContext()->backendName . '_user'),

View File

@@ -4,29 +4,28 @@ declare(strict_types=1);
namespace App\API\Backend;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\Database\DBLayer;
use App\Libs\Enums\Http\Status;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
final class Delete
{
use APITraits;
#[\App\Libs\Attributes\Route\Delete(Index::URL . '/{name:backend}[/]', name: 'backend.delete')]
public function __invoke(DBLayer $db, iRequest $request, array $args = []): iResponse
public function __invoke(iRequest $request, string $name, iEImport $mapper, iLogger $logger): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $mapper, $logger);
if (null === ($data = $this->getBackend(name: $name))) {
if (null === ($data = $this->getBackend(name: $name, userContext: $userContext))) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
$db = $userContext->db->getDBLayer();
set_time_limit(0);
ignore_user_abort(true);
@@ -53,7 +52,7 @@ final class Delete
$deletedRecords = $stmt->rowCount();
ConfigFile::open(Config::get('backends_file'), 'yaml')->delete($name)->persist();
$userContext->config->delete($name)->persist();
return api_response(Status::OK, [
'deleted' => [

View File

@@ -8,10 +8,12 @@ use App\Backends\Plex\PlexClient;
use App\Libs\Attributes\Route\Get;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
use Throwable;
@@ -19,23 +21,17 @@ final class Discover
{
use APITraits;
public function __construct(private readonly iHttp $http)
{
}
#[Get(Index::URL . '/{name:backend}/discover[/]', name: 'backend.discover')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __invoke(iRequest $request, string $name, iEImport $mapper, iLogger $logger, iHttp $http): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $mapper, $logger);
if (null === $this->getBackend(name: $name)) {
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
try {
$client = $this->getClient(name: $name);
$client = $this->getClient(name: $name, userContext: $userContext);
if (PlexClient::CLIENT_NAME !== $client->getType()) {
return api_error('Discover is only available for Plex backends.', Status::BAD_REQUEST);
@@ -50,7 +46,7 @@ final class Discover
$opts[Options::ADMIN_TOKEN] = $adminToken;
}
$list = $client::discover($this->http, $context->backendToken, $opts);
$list = $client::discover($http, $context->backendToken, $opts);
return api_response(Status::OK, ag($list, 'list', []));
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);

View File

@@ -11,9 +11,11 @@ use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
final class Ignore
@@ -22,19 +24,17 @@ final class Ignore
private ConfigFile $file;
public function __construct()
public function __construct(private readonly iEImport $mapper, private readonly iLogger $logger)
{
$this->file = new ConfigFile(Config::get('path') . '/config/ignore.yaml', type: 'yaml', autoCreate: true);
}
#[Get(Index::URL . '/{name:backend}/ignore[/]', name: 'backend.ignoredIds')]
public function ignoredIds(string $name): iResponse
public function ignoredIds(iRequest $request, string $name): iResponse
{
if (empty($name)) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (null === $this->getBackend(name: $name)) {
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
@@ -70,19 +70,11 @@ final class Ignore
}
#[Delete(Index::URL . '/{name:backend}/ignore[/]', name: 'backend.ignoredIds.delete')]
public function deleteRule(iRequest $request, array $args = []): iResponse
public function deleteRule(iRequest $request, string $name): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (null === $this->getBackend(name: $name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
$data = $this->getBackends(name: $name);
if (empty($data)) {
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
@@ -93,7 +85,7 @@ final class Ignore
}
try {
checkIgnoreRule($rule);
checkIgnoreRule($rule, userContext: $userContext);
} catch (Throwable $e) {
return api_error($e->getMessage(), Status::BAD_REQUEST);
}
@@ -108,19 +100,11 @@ final class Ignore
}
#[Post(Index::URL . '/{name:backend}/ignore[/]', name: 'backend.ignoredIds.add')]
public function addRule(iRequest $request, array $args = []): iResponse
public function addRule(iRequest $request, string $name): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (null === $this->getBackend(name: $name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
$data = $this->getBackends(name: $name);
if (empty($data)) {
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
@@ -150,7 +134,7 @@ final class Ignore
}
try {
checkIgnoreRule($rule);
checkIgnoreRule($rule, userContext: $userContext);
$id = makeIgnoreId($rule);
} catch (Throwable $e) {
return api_error($e->getMessage(), Status::BAD_REQUEST);

View File

@@ -6,9 +6,11 @@ namespace App\API\Backend;
use App\Libs\Attributes\Route\Get;
use App\Libs\Enums\Http\Status;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
final class Index
{
@@ -17,13 +19,11 @@ final class Index
public const string URL = '%{api.prefix}/backend';
#[Get(self::URL . '/{name:backend}[/]', name: 'backend.view')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __invoke(iRequest $request, string $name, iEImport $mapper, iLogger $logger): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $mapper, $logger);
if (null === ($data = $this->getBackend(name: $name))) {
if (null === ($data = $this->getBackend(name: $name, userContext: $userContext))) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}

View File

@@ -8,10 +8,12 @@ use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
final class Info
@@ -19,18 +21,16 @@ final class Info
use APITraits;
#[Get(Index::URL . '/{name:backend}/info[/]', name: 'backend.info')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __invoke(iRequest $request, string $name, iEImport $mapper, iLogger $logger): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $mapper, $logger);
if (null === $this->getBackend(name: $name)) {
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
try {
$client = $this->getClient(name: $name);
$client = $this->getClient(name: $name, userContext: $userContext);
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
}

View File

@@ -7,33 +7,35 @@ namespace App\API\Backend;
use App\API\Backend\Index as BackendsIndex;
use App\Libs\Attributes\Route\Get;
use App\Libs\Attributes\Route\Route;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
final class Library
{
use APITraits;
#[Get(BackendsIndex::URL . '/{name:backend}/library[/]', name: 'backend.library')]
public function listLibraries(string $name): iResponse
public function __construct(private readonly iEImport $mapper, private readonly iLogger $logger)
{
if (empty($name)) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
}
if (null === $this->getBackend(name: $name)) {
#[Get(BackendsIndex::URL . '/{name:backend}/library[/]', name: 'backend.library')]
public function listLibraries(iRequest $request, string $name): iResponse
{
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
try {
$client = $this->getClient(name: $name);
$client = $this->getClient(name: $name, userContext: $userContext);
return api_response(Status::OK, $client->listLibraries());
} catch (RuntimeException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
@@ -43,31 +45,23 @@ final class Library
}
#[Route(['POST', 'DELETE'], BackendsIndex::URL . '/{name:backend}/library/{id}[/]', name: 'backend.library.ignore')]
public function ignoreLibrary(iRequest $request, array $args = []): iResponse
public function ignoreLibrary(iRequest $request, string $name, string|int $id): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (null === $this->getBackend(name: $name)) {
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
if (null === ($id = ag($args, 'id'))) {
return api_error('Invalid value for id path parameter.', Status::BAD_REQUEST);
}
$remove = 'DELETE' === $request->getMethod();
$config = ConfigFile::open(Config::get('backends_file'), 'yaml');
if (null === $config->get($name)) {
if (null === $userContext->config->get($name)) {
return api_error(r("Backend '{backend}' not found.", ['backend' => $name]), Status::NOT_FOUND);
}
$ignoreIds = array_map(
fn($v) => trim($v),
explode(',', (string)$config->get("{$name}.options." . Options::IGNORE, ''))
explode(',', (string)$userContext->config->get("{$name}.options." . Options::IGNORE, ''))
);
$mode = !(true === $remove);
@@ -80,7 +74,7 @@ final class Library
$found = false;
$libraries = $this->getClient(name: $name)->listLibraries();
$libraries = $this->getClient(name: $name, userContext: $userContext)->listLibraries();
foreach ($libraries as &$library) {
if ((string)ag($library, 'id') === (string)$id) {
@@ -101,7 +95,9 @@ final class Library
$ignoreIds = array_diff($ignoreIds, [$id]);
}
$config->set("{$name}.options." . Options::IGNORE, implode(',', array_values($ignoreIds)))->persist();
$userContext->config
->set("{$name}.options." . Options::IGNORE, implode(',', array_values($ignoreIds)))
->persist();
return api_response(Status::OK, $libraries);
}

View File

@@ -11,10 +11,12 @@ use App\Libs\DataUtil;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
final class Mismatched
@@ -50,14 +52,16 @@ final class Mismatched
'*',
];
#[Get(backendIndex::URL . '/{name:backend}/mismatched[/[{id}[/]]]', name: 'backend.mismatched')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __construct(private readonly iEImport $mapper, private readonly iLogger $logger)
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
}
if (null === $this->getBackend(name: $name)) {
#[Get(backendIndex::URL . '/{name:backend}/mismatched[/[{id}[/]]]', name: 'backend.mismatched')]
public function __invoke(iRequest $request, string $name, string|int|null $id = null): iResponse
{
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
@@ -88,13 +92,13 @@ final class Mismatched
}
try {
$client = $this->getClient(name: $name, config: $backendOpts);
$client = $this->getClient(name: $name, config: $backendOpts, userContext: $userContext);
} catch (RuntimeException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
}
$ids = [];
if (null !== ($id = ag($args, 'id'))) {
if (null !== $id) {
$ids[] = $id;
} else {
foreach ($client->listLibraries() as $library) {

View File

@@ -5,35 +5,35 @@ declare(strict_types=1);
namespace App\API\Backend;
use App\Libs\Attributes\Route\Route;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\ValidationException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
final class Option
{
use APITraits;
#[Route(['GET', 'POST', 'PATCH', 'DELETE'], Index::URL . '/{name:backend}/option[/{option}[/]]')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __construct(private readonly iEImport $mapper, private readonly iLogger $logger)
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
}
$list = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
#[Route(['GET', 'POST', 'PATCH', 'DELETE'], Index::URL . '/{name:backend}/option[/{option}[/]]')]
public function __invoke(iRequest $request, string $name, string|null $option = null): iResponse
{
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (false === $list->has($name)) {
if (false === $userContext->config->has($name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
$data = DataUtil::fromRequest($request);
if (null === ($option = ag($args, 'option', $data->get('key')))) {
if (null === ($option = $option ?? $data->get('key'))) {
return api_error('No option key was given.', Status::BAD_REQUEST);
}
@@ -52,17 +52,17 @@ final class Option
}
if ('GET' === $request->getMethod()) {
if (false === $list->has($name . '.' . $option)) {
if (false === $userContext->config->has($name . '.' . $option)) {
return api_error(r("Option '{option}' not found in backend '{name}' config.", [
'option' => $option,
'name' => $name
]), Status::NOT_FOUND);
}
return $this->viewOption($spec, $list->get("{$name}.{$option}"));
return $this->viewOption($spec, $userContext->config->get("{$name}.{$option}"));
}
if ('DELETE' === $request->getMethod() && false === $list->has("{$name}.{$option}")) {
if ('DELETE' === $request->getMethod() && false === $userContext->config->has("{$name}.{$option}")) {
return api_error(r("Option '{option}' not found in backend '{name}' config.", [
'option' => $option,
'name' => $name
@@ -70,10 +70,10 @@ final class Option
}
if ('DELETE' === $request->getMethod()) {
if (null !== ($value = $list->get($name . '.' . $option))) {
if (null !== ($value = $userContext->config->get($name . '.' . $option))) {
settype($value, ag($spec, 'type', 'string'));
}
$list->delete("{$name}.{$option}");
$userContext->config->delete("{$name}.{$option}");
} else {
if (null !== ($value = $data->get('value'))) {
if (ag($spec, 'type', 'string') === 'bool') {
@@ -94,10 +94,10 @@ final class Option
}
}
$list->set("{$name}.{$option}", $value);
$userContext->config->set("{$name}.{$option}", $value);
}
$list->persist();
$userContext->config->persist();
return api_response(Status::OK, [
'key' => $option,

View File

@@ -8,30 +8,34 @@ use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
final class Search
{
use APITraits;
#[Get(Index::URL . '/{name:backend}/search[/[{id}[/]]]', name: 'backend.search')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __construct(private readonly iEImport $mapper, private readonly iLogger $logger)
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
}
if (null === $this->getBackend(name: $name)) {
#[Get(Index::URL . '/{name:backend}/search[/[{id}[/]]]', name: 'backend.search')]
public function __invoke(iRequest $request, string $name, string|int|null $id = null): iResponse
{
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
$params = DataUtil::fromRequest($request, true);
$id = ag($args, 'id', $params->get('id', null));
$id = $id ?? $params->get('id') ?? null;
$query = $params->get('q', null);
if (null === $id && null === $query) {
@@ -39,7 +43,7 @@ final class Search
}
try {
$backend = $this->getClient(name: $name);
$backend = $this->getClient(name: $name, userContext: $userContext);
} catch (RuntimeException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
}
@@ -52,7 +56,7 @@ final class Search
if (null !== $id) {
$data = $backend->searchId($id, [Options::RAW_RESPONSE => $raw]);
if (!empty($data)) {
$item = $this->formatEntity($data);
$item = $this->formatEntity($data, userContext: $userContext);
if (true === $raw) {
$item[Options::RAW_RESPONSE] = ag($data, Options::RAW_RESPONSE, []);
}
@@ -65,7 +69,7 @@ final class Search
opts: [Options::RAW_RESPONSE => $raw]
);
foreach ($data as $entity) {
$item = $this->formatEntity($entity);
$item = $this->formatEntity($entity, userContext: $userContext);
if (true === $raw) {
$item[Options::RAW_RESPONSE] = ag($entity, Options::RAW_RESPONSE, []);
}

View File

@@ -8,10 +8,12 @@ use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
final class Sessions
@@ -19,18 +21,16 @@ final class Sessions
use APITraits;
#[Get(Index::URL . '/{name:backend}/sessions[/]', name: 'backend.sessions')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __invoke(iRequest $request, string $name, iEImport $mapper, iLogger $logger): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $mapper, $logger);
if (null === $this->getBackend(name: $name)) {
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
try {
$client = $this->getClient(name: $name);
$client = $this->getClient(name: $name, userContext: $userContext);
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
}

View File

@@ -7,23 +7,24 @@ namespace App\API\Backend;
use App\API\Backend\Index as backendIndex;
use App\Libs\Attributes\Route\Delete;
use App\Libs\Attributes\Route\Get;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\DataUtil;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Mappers\Import\DirectMapper;
use App\Libs\Mappers\Import\MemoryMapper;
use App\Libs\Mappers\Import\ReadOnlyMapper;
use App\Libs\Traits\APITraits;
use App\Libs\UserContext;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Psr\SimpleCache\CacheInterface as iCache;
final class Stale
{
use APITraits;
public function __construct(private readonly ReadOnlyMapper $mapper, private readonly MemoryMapper $local)
public function __construct(private readonly iEImport $mapper, private readonly iLogger $logger)
{
set_time_limit(0);
ini_set('memory_limit', '-1');
@@ -32,8 +33,10 @@ final class Stale
#[Get(backendIndex::URL . '/{name:backend}/stale/{id}[/]', name: 'backend.stale.list')]
public function listContent(iRequest $request, string $name, string|int $id): iResponse
{
if (empty($name)) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
if (empty($id)) {
@@ -44,6 +47,7 @@ final class Stale
try {
$data = $this->getContent(
userContext: $userContext,
name: $name,
id: $id,
ignore: (bool)$params->get('ignore', false),
@@ -58,15 +62,12 @@ final class Stale
}
#[Delete(backendIndex::URL . '/{name:backend}/stale/{id}[/]', name: 'backend.stale.delete')]
public function deleteContent(
iRequest $request,
DirectMapper $mapper,
iDB $db,
string $name,
string|int $id
): iResponse {
if (empty($name)) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
public function deleteContent(iRequest $request, string $name, string|int $id, DirectMapper $mapper): iResponse
{
$userContext = $this->getUserContext($request, $mapper, $this->logger);
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
if (empty($id)) {
@@ -79,14 +80,19 @@ final class Stale
return api_error('No items to delete.', Status::BAD_REQUEST);
}
$mapper->loadData();
$userContext->mapper->loadData();
return api_message('Removed stale references.', Status::OK);
}
private function getContent(string $name, string|int $id, bool $ignore = false, int|float $timeout = 0): array
{
if (null === $this->getBackend(name: $name)) {
private function getContent(
UserContext $userContext,
string $name,
string|int $id,
bool $ignore = false,
int|float $timeout = 0
): array {
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
throw new RuntimeException(r("Backend '{name}' not found.", ['name' => $name]));
}
@@ -96,20 +102,23 @@ final class Stale
$backendOpts = ag_set($backendOpts, 'client.timeout', (float)$timeout);
}
$client = $this->getClient(name: $name, config: $backendOpts);
$client = $this->getClient(name: $name, config: $backendOpts, userContext: $userContext);
$remote = cacheableItem(
"remote-data-{$id}-{$name}",
fn() => array_map(fn($item) => ag($item->getMetadata($item->via), iState::COLUMN_ID),
$client->getLibraryContent($id))
, ignoreCache: $ignore
key: "remote-data-{$id}-{$name}",
function: fn() => array_map(
callback: fn($item) => ag($item->getMetadata($item->via), iState::COLUMN_ID),
array: $client->getLibraryContent($id)
),
ignoreCache: $ignore,
opts: [
iCache::class => $userContext->cache
]
);
$this->local->loadData();
$localCount = 0;
foreach ($this->local->getObjects() as $entity) {
foreach ($userContext->mapper->loadData()->getObjects() as $entity) {
$backendData = $entity->getMetadata($name);
if (empty($backendData)) {
continue;

View File

@@ -10,24 +10,28 @@ use App\Libs\DataUtil;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
final class Unmatched
{
use APITraits;
#[Get(backendIndex::URL . '/{name:backend}/unmatched[/[{id}[/]]]', name: 'backend.unmatched')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __construct(private readonly iEImport $mapper, private readonly iLogger $logger)
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
}
if (null === $this->getBackend(name: $name)) {
#[Get(backendIndex::URL . '/{name:backend}/unmatched[/[{id}[/]]]', name: 'backend.unmatched')]
public function __invoke(iRequest $request, string $name, string|int|null $id = null): iResponse
{
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
@@ -44,13 +48,13 @@ final class Unmatched
$opts[Options::RAW_RESPONSE] = true;
try {
$client = $this->getClient(name: $name, config: $backendOpts);
$client = $this->getClient(name: $name, config: $backendOpts, userContext: $userContext);
} catch (RuntimeException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
}
$ids = [];
if (null !== ($id = ag($args, 'id'))) {
if (null !== $id) {
$ids[] = $id;
} else {
foreach ($client->listLibraries() as $library) {

View File

@@ -9,19 +9,19 @@ use App\Backends\Common\ClientInterface as iClient;
use App\Backends\Common\Context;
use App\Libs\Attributes\Route\Patch;
use App\Libs\Attributes\Route\Put;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\Container;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\Backends\InvalidContextException;
use App\Libs\Exceptions\ValidationException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use App\Libs\Uri;
use JsonException;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
final class Update
{
@@ -37,36 +37,31 @@ final class Update
'export',
];
private ConfigFile $backendFile;
public function __construct()
public function __construct(private readonly iEImport $mapper, private readonly iLogger $logger)
{
$this->backendFile = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
}
#[Put(Index::URL . '/{name:backend}[/]', name: 'backend.update')]
public function update(iRequest $request, array $args = []): iResponse
public function update(iRequest $request, string $name): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (false === $this->backendFile->has($name)) {
if (false === $userContext->config->has($name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
try {
$client = $this->getClient($name);
$client = $this->getClient($name, userContext: $userContext);
$config = DataUtil::fromArray($this->fromRequest($this->backendFile->get($name), $request, $client));
$config = DataUtil::fromArray($this->fromRequest($userContext->config->get($name), $request, $client));
$context = new Context(
clientName: $this->backendFile->get("{$name}.type"),
clientName: $userContext->config->get("{$name}.type"),
backendName: $name,
backendUrl: new Uri($config->get('url')),
cache: Container::get(BackendCache::class),
cache: Container::get(BackendCache::class)->with(adapter: $userContext->cache),
backendId: $config->get('uuid', null),
backendToken: $this->backendFile->get("{$name}.token", null),
backendToken: $userContext->config->get("{$name}.token", null),
backendUser: $config->get('user', null),
options: $config->get('options', []),
);
@@ -75,18 +70,18 @@ final class Update
return api_error('Context information validation failed.', Status::BAD_REQUEST);
}
$this->backendFile->set($name, $config->getAll());
$userContext->config->set($name, $config->getAll());
// -- sanity check.
if (true === (bool)$this->backendFile->get("{$name}.import.enabled", false)) {
if ($this->backendFile->has("{$name}.options." . Options::IMPORT_METADATA_ONLY)) {
$this->backendFile->delete("{$name}.options." . Options::IMPORT_METADATA_ONLY);
if (true === (bool)$userContext->config->get("{$name}.import.enabled", false)) {
if ($userContext->config->has("{$name}.options." . Options::IMPORT_METADATA_ONLY)) {
$userContext->config->delete("{$name}.options." . Options::IMPORT_METADATA_ONLY);
}
}
$this->backendFile->persist();
$userContext->config->persist();
$backend = $this->getBackends(name: $name);
$backend = $this->getBackends(name: $name, userContext: $userContext);
if (empty($backend)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
@@ -101,21 +96,18 @@ final class Update
}
#[Patch(Index::URL . '/{name:backend}[/]', name: 'backend.patch')]
public function patchUpdate(iRequest $request, array $args = []): iResponse
public function patchUpdate(iRequest $request, string $name): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for name path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $this->mapper, $this->logger);
if (false === $this->backendFile->has($name)) {
if (false === $userContext->config->has($name)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
try {
$data = json_decode((string)$request->getBody(), true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
return api_error(r('Invalid JSON data. {error}', ['error' => $e->getMessage()]),
Status::BAD_REQUEST);
return api_error(r('Invalid JSON data. {error}', ['error' => $e->getMessage()]), Status::BAD_REQUEST);
}
$updates = [];
@@ -154,12 +146,12 @@ final class Update
}
foreach ($updates as $key => $value) {
$this->backendFile->set($key, $value);
$userContext->config->set($key, $value);
}
$this->backendFile->persist();
$userContext->config->persist();
$backend = $this->getBackends(name: $name);
$backend = $this->getBackends(name: $name, userContext: $userContext);
if (empty($backend)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);

View File

@@ -8,10 +8,12 @@ use App\Libs\Attributes\Route\Get;
use App\Libs\DataUtil;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Options;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
final class Users
@@ -19,13 +21,11 @@ final class Users
use APITraits;
#[Get(Index::URL . '/{name:backend}/users[/]', name: 'backend.users')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __invoke(iRequest $request, string $name, iEImport $mapper, iLogger $logger): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for id path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $mapper, $logger);
if (null === $this->getBackend(name: $name)) {
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
@@ -41,7 +41,10 @@ final class Users
}
try {
return api_response(Status::OK, $this->getClient(name: $name)->getUsersList($opts));
return api_response(
Status::OK,
$this->getClient(name: $name, userContext: $userContext)->getUsersList($opts)
);
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
} catch (Throwable $e) {

View File

@@ -7,9 +7,11 @@ namespace App\API\Backend;
use App\Libs\Attributes\Route\Get;
use App\Libs\Enums\Http\Status;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Mappers\ExtendedImportInterface as iEImport;
use App\Libs\Traits\APITraits;
use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger;
use Throwable;
final class Version
@@ -17,18 +19,18 @@ final class Version
use APITraits;
#[Get(Index::URL . '/{name:backend}/version[/]', name: 'backend.version')]
public function __invoke(iRequest $request, array $args = []): iResponse
public function __invoke(iRequest $request, string $name, iEImport $mapper, iLogger $logger): iResponse
{
if (null === ($name = ag($args, 'name'))) {
return api_error('Invalid value for id path parameter.', Status::BAD_REQUEST);
}
$userContext = $this->getUserContext($request, $mapper, $logger);
if (null === $this->getBackend(name: $name)) {
if (null === $this->getBackend(name: $name, userContext: $userContext)) {
return api_error(r("Backend '{name}' not found.", ['name' => $name]), Status::NOT_FOUND);
}
try {
return api_response(Status::OK, ['version' => $this->getClient(name: $name)->getVersion()]);
return api_response(Status::OK, [
'version' => $this->getClient(name: $name, userContext: $userContext)->getVersion()
]);
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
} catch (Throwable $e) {

View File

@@ -1273,11 +1273,12 @@ if (!function_exists('checkIgnoreRule')) {
* Check if the given ignore rule is valid.
*
* @param string $guid The ignore rule to check.
* @param UserContext|null $userContext (Optional) The user context.
*
* @return bool True if the ignore rule is valid, false otherwise.
* @throws RuntimeException Throws an exception if the ignore rule is invalid.
*/
function checkIgnoreRule(string $guid): bool
function checkIgnoreRule(string $guid, UserContext|null $userContext = null): bool
{
$urlParts = parse_url($guid);
@@ -1319,7 +1320,11 @@ if (!function_exists('checkIgnoreRule')) {
throw new RuntimeException('No backend was given.');
}
$backends = array_keys(Config::get('servers', []));
if (null !== $userContext) {
$backends = array_keys($userContext->config->getAll());
} else {
$backends = array_keys(Config::get('servers', []));
}
if (false === in_array($backend, $backends)) {
throw new RuntimeException(r("Invalid backend name '{backend}' was given. Expected values are '{list}'.", [
@@ -1725,11 +1730,13 @@ if (!function_exists('isTaskWorkerRunning')) {
}
switch (PHP_OS) {
case 'Linux': {
case 'Linux':
{
$status = file_exists(r('/proc/{pid}/status', ['pid' => $pid]));
}
break;
case 'WINNT': {
case 'WINNT':
{
// -- Windows does not have a /proc directory so we need different way to get the status.
@exec("tasklist /FI \"PID eq {$pid}\" 2>NUL", $output);
// -- windows doesn't return 0 if the process is not found. we need to parse the output.