Completely re-worked how backend:create command works
This commit is contained in:
41
FAQ.md
41
FAQ.md
@@ -239,42 +239,55 @@ The schema is simple, it's a list of users in the following format:
|
|||||||
version: "1.5"
|
version: "1.5"
|
||||||
map:
|
map:
|
||||||
# 1st user...
|
# 1st user...
|
||||||
- my_plex_server:
|
- my_plex_server: # your main user backend name
|
||||||
name: "mike_jones"
|
name: "mike_jones"
|
||||||
options: { } # optional key.
|
options: { } # optional key.
|
||||||
my_jellyfin_server:
|
my_jellyfin_server: # your main user backend name
|
||||||
name: "jones_mike"
|
name: "jones_mike"
|
||||||
my_emby_server:
|
my_emby_server: # your main user backend name
|
||||||
name: "mikeJones"
|
name: "mikeJones"
|
||||||
replace_with: "mike_jones" # optional action, to replace the username with the new one.
|
replace_with: "mike_jones" # optional action, to replace the username with the new one.
|
||||||
# 2nd user...
|
# 2nd user...
|
||||||
- my_emby_server:
|
- my_emby_server: # your main user backend name
|
||||||
name: "jiji_jones"
|
name: "jiji_jones"
|
||||||
options: { } # optional key.
|
options: { } # optional key.
|
||||||
my_plex_server:
|
my_plex_server: # your main user backend name
|
||||||
name: "jones_jiji"
|
name: "jones_jiji"
|
||||||
my_jellyfin_server:
|
my_jellyfin_server: # your main user backend name
|
||||||
name: "jijiJones"
|
name: "jiji.jones"
|
||||||
replace_with: "jiji_jones" # optional action, to replace the username with the new one.
|
replace_with: "jiji_jones" # optional action, to replace the username with the new one.
|
||||||
#.... more users
|
#.... more users
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!IMPORTANT]
|
If you create a map for a user, it SHOULD include all the backends you want to sync the user data with. while th matcher
|
||||||
> As we enforce specific naming convention for backend names and usernames they must follow the following format
|
might automatically detect the other backends even if not included in the map, it best to manually set them in group to
|
||||||
> `^[a-z_0-9]+$` which means, lowercase letters, numbers and `_` are allowed. No spaces, uppercase letters or special
|
prevent any issues that might arise. Each list item is a user, and each user has a list of backends. Each backend
|
||||||
> characters are allowed. you can use the `replace_with` key to replace the username with the new one. if it's not
|
|
||||||
> complying with the naming convention, or you want to force specific display name.
|
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> the backend names `my_plex_server`, `my_jellyfin_server`, `my_emby_server` are the names you have chosen for
|
> the backend names `my_plex_server`, `my_jellyfin_server`, `my_emby_server` are the names you have chosen for
|
||||||
> your> backends.
|
> your backends.
|
||||||
>
|
>
|
||||||
> The `name` field is whatever the backend is reporting the username as.
|
> The `name` field must match the name after normalization, so if you have a backend with the name `Mike Jones` as
|
||||||
|
> username, the `name:` in the `mapper.yaml` file should be `mike_jones` as the `backend:create` command will normalize
|
||||||
|
> the name before passing it to the mapper which the mapper will convert it to `mike_jones`.
|
||||||
|
|
||||||
|
## Important
|
||||||
|
|
||||||
|
We enforce strict naming convention for backend names and usernames, So they must follow the following format
|
||||||
|
`^[a-z_0-9]+$` which means, lowercase letters, numbers and `_` are allowed. No spaces, uppercase letters or special
|
||||||
|
characters or name entirely made of digits are allowed. If the username is not complying with the naming convention, we
|
||||||
|
forcibly normalize it to make it comply with the naming convention.
|
||||||
|
|
||||||
|
If you want another name, you can use `replace_with` key to replace the username with the new one. However, the name
|
||||||
|
also must comply with the naming convention.
|
||||||
|
|
||||||
This yaml file helps map your users usernames in the different backends, so the tool can sync the correct user data. If
|
This yaml file helps map your users usernames in the different backends, so the tool can sync the correct user data. If
|
||||||
you added or updated mapping, you should delete `users` directory and generate new data. by running the `backend:create`
|
you added or updated mapping, you should delete `users` directory and generate new data. by running the `backend:create`
|
||||||
command as described in the previous section.
|
command as described in the previous section.
|
||||||
|
|
||||||
|
You can run the `backend:create` command with `-v --dry-run` to see what it will do and if you need to create a mapping
|
||||||
|
file or not.
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
# How do i migrate invited friends i.e. (external user) data from from plex to emby/jellyfin?
|
# How do i migrate invited friends i.e. (external user) data from from plex to emby/jellyfin?
|
||||||
|
|||||||
13
NEWS.md
13
NEWS.md
@@ -1,5 +1,18 @@
|
|||||||
# Old Updates
|
# Old Updates
|
||||||
|
|
||||||
|
### 2025-02-11
|
||||||
|
|
||||||
|
We recently have added support to generate accesstoken for external `Plex` users, i.e. `not home users`. so the
|
||||||
|
`backends:create` command now supports generating the needed config files for external users. Beware the support for
|
||||||
|
this is still in early stages, and might not work as expected. report any issues you might face.
|
||||||
|
|
||||||
|
### 2025-02-05
|
||||||
|
|
||||||
|
We have added initial support to browse the WebUI as sub user, it's still in early stages, only few Endpoints support
|
||||||
|
it.
|
||||||
|
We have also added support to webhooks to allow sub users, you simply have to add new hooks using `user@backend`. Please
|
||||||
|
take look at [this FAQ](FAQ.md#how-to-add-webhooks) to learn how to use it for sub users.
|
||||||
|
|
||||||
### 2025-02-02
|
### 2025-02-02
|
||||||
|
|
||||||
We are happy to announce that we have merged in direct support for multi-user in `state:import` and `state:export`
|
We are happy to announce that we have merged in direct support for multi-user in `state:import` and `state:export`
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -9,9 +9,27 @@ out of the box, this tool support `Jellyfin`, `Plex` and `Emby` media servers.
|
|||||||
|
|
||||||
## Updates
|
## Updates
|
||||||
|
|
||||||
|
## 2025-04-06
|
||||||
|
|
||||||
|
We have recently re-worked how the `backend:create` command works, and we no longer generate random name for invalid
|
||||||
|
backends names or usernames. We do a normalization step to make sure the name is valid. This should help with the
|
||||||
|
confusion of having random names. This means if you re-run the `backend:create` you most likely will get a different
|
||||||
|
name then before. So, we suggest to re-run the command with `--re-create` flag. This flag will delete the current
|
||||||
|
sub-users, and regenerate updated config files.
|
||||||
|
|
||||||
|
We have also added new guard for the command, so if you already generated your sub-users, re-running the command will
|
||||||
|
show you a warning message and exit without doing anything. to run the command again either you need to use
|
||||||
|
`--re-create`or `--run` flag. The `--run` flag will run the command without deleting the current sub-users.
|
||||||
|
|
||||||
|
We have also updated `mapper.yaml` file format to v1.5, please take moment to read the
|
||||||
|
related the [FAQ](FAQ.md#whats-the-schema-for-the-mapperyaml-file)
|
||||||
|
about it. The new format has added support for renaming the usernames, and the new spec is more versatile which should
|
||||||
|
help us if we need to update something or add new features in the future.
|
||||||
|
|
||||||
### 2025-03-13
|
### 2025-03-13
|
||||||
|
|
||||||
We have recently added support for plex webhooks via tautulli which you can use if you don't have PlexPass. This should help
|
We have recently added support for plex webhooks via tautulli which you can use if you don't have PlexPass. This should
|
||||||
|
help
|
||||||
close the gap with other media servers.
|
close the gap with other media servers.
|
||||||
|
|
||||||
### 2025-02-19
|
### 2025-02-19
|
||||||
@@ -27,19 +45,6 @@ with seconds as value, the minimum value is `180` seconds. `0` seconds means it'
|
|||||||
We are still not keen on this feature, and it might be removed in future releases if we aren't able to deal with the
|
We are still not keen on this feature, and it might be removed in future releases if we aren't able to deal with the
|
||||||
issues we are facing.
|
issues we are facing.
|
||||||
|
|
||||||
### 2025-02-11
|
|
||||||
|
|
||||||
We recently have added support to generate accesstoken for external `Plex` users, i.e. `not home users`. so the
|
|
||||||
`backends:create` command now supports generating the needed config files for external users. Beware the support for
|
|
||||||
this is still in early stages, and might not work as expected. report any issues you might face.
|
|
||||||
|
|
||||||
### 2025-02-05
|
|
||||||
|
|
||||||
We have added initial support to browse the WebUI as sub user, it's still in early stages, only few Endpoints support
|
|
||||||
it.
|
|
||||||
We have also added support to webhooks to allow sub users, you simply have to add new hooks using `user@backend`. Please
|
|
||||||
take look at [this FAQ](FAQ.md#how-to-add-webhooks) to learn how to use it for sub users.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
Refer to [NEWS](NEWS.md) for old updates.
|
Refer to [NEWS](NEWS.md) for old updates.
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<p class="control" v-if="backends && backends.length>0">
|
<p class="control" v-if="backends && backends.length>0">
|
||||||
<button class="button is-purple" v-tooltip.bottom="'Create sub users backends.'"
|
<button class="button is-purple" v-tooltip.bottom="'Create sub users backends.'"
|
||||||
@click="navigateTo(makeConsoleCommand('backend:create -v', true))"
|
@click="navigateTo(makeConsoleCommand('backend:create -B -v', true))"
|
||||||
:disabled="'main' !== api_user">
|
:disabled="'main' !== api_user">
|
||||||
<span class="icon"><i class="fas fa-users"></i></span>
|
<span class="icon"><i class="fas fa-users"></i></span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace App\Commands\Backend;
|
|||||||
use App\Backends\Common\ClientInterface as iClient;
|
use App\Backends\Common\ClientInterface as iClient;
|
||||||
use App\Backends\Plex\PlexClient;
|
use App\Backends\Plex\PlexClient;
|
||||||
use App\Command;
|
use App\Command;
|
||||||
|
use App\Commands\State\BackupCommand;
|
||||||
|
use App\Commands\System\TasksCommand;
|
||||||
use App\Libs\Attributes\Route\Cli;
|
use App\Libs\Attributes\Route\Cli;
|
||||||
use App\Libs\Config;
|
use App\Libs\Config;
|
||||||
use App\Libs\ConfigFile;
|
use App\Libs\ConfigFile;
|
||||||
@@ -42,8 +44,22 @@ class CreateUsersCommand extends Command
|
|||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setName(self::ROUTE)
|
$this->setName(self::ROUTE)
|
||||||
|
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not commit any changes.')
|
||||||
|
->addOption(
|
||||||
|
're-create',
|
||||||
|
'r',
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Delete current users configuration files and re-create them.'
|
||||||
|
)
|
||||||
->addOption('regenerate-tokens', 'g', InputOption::VALUE_NONE, 'Generate new tokens for PLEX users.')
|
->addOption('regenerate-tokens', 'g', InputOption::VALUE_NONE, 'Generate new tokens for PLEX users.')
|
||||||
->addOption('--dry-run', null, InputOption::VALUE_NONE, 'Do not commit any changes.')
|
->addOption('run', null, InputOption::VALUE_NONE, 'Allow creating the users even if data already exists.')
|
||||||
|
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not commit any changes.')
|
||||||
|
->addOption(
|
||||||
|
'generate-backup',
|
||||||
|
'B',
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Generate initial backups for the remote user data.'
|
||||||
|
)
|
||||||
->addOption(
|
->addOption(
|
||||||
'update',
|
'update',
|
||||||
'u',
|
'u',
|
||||||
@@ -74,25 +90,28 @@ class CreateUsersCommand extends Command
|
|||||||
Mapping is done automatically based on the username, however, if your users have different usernames
|
Mapping is done automatically based on the username, however, if your users have different usernames
|
||||||
on each backend, you can create <value>{path}/config/mapper.yaml</value> file with the following format:
|
on each backend, you can create <value>{path}/config/mapper.yaml</value> file with the following format:
|
||||||
|
|
||||||
- my_plex_server:
|
version: "1.5"
|
||||||
|
map:
|
||||||
|
# first user
|
||||||
|
-
|
||||||
|
my_plex_server:
|
||||||
name: "mike_jones"
|
name: "mike_jones"
|
||||||
options: { }
|
|
||||||
my_jellyfin_server:
|
my_jellyfin_server:
|
||||||
name: "jones_mike"
|
name: "jones_mike"
|
||||||
options: { }
|
options: { }
|
||||||
my_emby_server:
|
my_emby_server:
|
||||||
name: "mikeJones"
|
name: "mikeJones"
|
||||||
options: { }
|
replace_with: "mike_jones"
|
||||||
# second user
|
# second user
|
||||||
- my_emby_server:
|
-
|
||||||
|
my_emby_server:
|
||||||
name: "jiji_jones"
|
name: "jiji_jones"
|
||||||
options: { }
|
options: { }
|
||||||
my_plex_server:
|
my_plex_server:
|
||||||
name: "jones_jiji"
|
name: "jones_jiji"
|
||||||
options: { }
|
|
||||||
my_jellyfin_server:
|
my_jellyfin_server:
|
||||||
name: "jijiJones"
|
name: "jijiJones"
|
||||||
options: { }
|
replace_with: "jiji_jones"
|
||||||
|
|
||||||
<question># How to regenerate tokens?</question>
|
<question># How to regenerate tokens?</question>
|
||||||
|
|
||||||
@@ -116,47 +135,31 @@ class CreateUsersCommand extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function purgeUsersConfig(string $path, bool $dryRun): void
|
||||||
* Executes the command.
|
|
||||||
*
|
|
||||||
* @param iInput $input The input interface.
|
|
||||||
* @param iOutput $output The output interface.
|
|
||||||
*
|
|
||||||
* @return int The exit code. 0 for success, 1 for failure.
|
|
||||||
*/
|
|
||||||
protected function runCommand(iInput $input, iOutput $output): int
|
|
||||||
{
|
{
|
||||||
$dryRun = $input->getOption('dry-run');
|
if (false === is_dir($path)) {
|
||||||
|
return;
|
||||||
if ($dryRun) {
|
|
||||||
$this->logger->notice('SYSTEM: Running in dry-run mode. No changes will be made.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->logger->notice("SYSTEM: Deleting users directory '{path}' contents.", [
|
||||||
|
'path' => $path
|
||||||
|
]);
|
||||||
|
|
||||||
|
deletePath(path: $path, logger: $this->logger, dryRun: $dryRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load current user backends.
|
||||||
|
*
|
||||||
|
* @return array The list of backends.
|
||||||
|
*/
|
||||||
|
private function loadBackends(): array
|
||||||
|
{
|
||||||
|
$backends = [];
|
||||||
$supported = Config::get('supported', []);
|
$supported = Config::get('supported', []);
|
||||||
$configFile = ConfigFile::open(Config::get('backends_file'), 'yaml');
|
$configFile = ConfigFile::open(Config::get('backends_file'), 'yaml');
|
||||||
$configFile->setLogger($this->logger);
|
$configFile->setLogger($this->logger);
|
||||||
|
|
||||||
$mapFile = Config::get('mapper_file');
|
|
||||||
$mapping = [];
|
|
||||||
|
|
||||||
if (file_exists($mapFile) && filesize($mapFile) > 10) {
|
|
||||||
$map = ConfigFile::open(Config::get('mapper_file'), 'yaml');
|
|
||||||
$mapping = $map->get('map', $map->getAll());
|
|
||||||
if (!empty($mapping)) {
|
|
||||||
if (false === $map->has('version') || false === $map->has('map')) {
|
|
||||||
$this->logger->warning(
|
|
||||||
"SYSTEM: Please upgrade your mapper.yaml file to v1.5 format spec for better compatibility and features, check the FAQ.md for the updated format.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logger->info("SYSTEM: Mapper file found, using it to map users.", [
|
|
||||||
'map' => arrayToString($mapping)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$backends = [];
|
|
||||||
|
|
||||||
foreach ($configFile->getAll() as $backendName => $backend) {
|
foreach ($configFile->getAll() as $backendName => $backend) {
|
||||||
$type = strtolower(ag($backend, 'type', 'unknown'));
|
$type = strtolower(ag($backend, 'type', 'unknown'));
|
||||||
|
|
||||||
@@ -182,21 +185,62 @@ class CreateUsersCommand extends Command
|
|||||||
$backends[$backendName] = $backend;
|
$backends[$backendName] = $backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($backends)) {
|
return $backends;
|
||||||
$this->logger->error('SYSTEM: No valid backends were found.');
|
|
||||||
return self::FAILURE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->notice("SYSTEM: Getting users list from '{backends}'.", [
|
/**
|
||||||
'backends' => join(', ', array_keys($backends))
|
* Load user mappings from the mapper file.
|
||||||
|
*
|
||||||
|
* @return array The list of user mappings. or empty array.
|
||||||
|
*/
|
||||||
|
private function loadMappings(): array
|
||||||
|
{
|
||||||
|
$mapFile = Config::get('mapper_file');
|
||||||
|
|
||||||
|
if (false === file_exists($mapFile) || filesize($mapFile) < 10) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = ConfigFile::open(Config::get('mapper_file'), 'yaml');
|
||||||
|
|
||||||
|
$mapping = $map->get('map', $map->getAll());
|
||||||
|
|
||||||
|
if (empty($mapping)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $map->has('version')) {
|
||||||
|
$this->logger->warning("SYSTEM: Starting with mapper.yaml v1.5, the version key is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $map->has('map')) {
|
||||||
|
$this->logger->warning("SYSTEM: Please upgrade your mapper.yaml file to v1.5 format spec.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info("SYSTEM: Mapper file found, using it to map users.", [
|
||||||
|
'map' => arrayToString($mapping)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return $mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get backends users.
|
||||||
|
*
|
||||||
|
* @param array $backends The list of backends.
|
||||||
|
* @param array $map The user mappings.
|
||||||
|
*
|
||||||
|
* @return array The list of backends users.
|
||||||
|
*/
|
||||||
|
private function get_backends_users(array $backends, array &$map): array
|
||||||
|
{
|
||||||
$users = [];
|
$users = [];
|
||||||
|
|
||||||
foreach ($backends as $backend) {
|
foreach ($backends as $backend) {
|
||||||
/** @var iClient $client */
|
/** @var iClient $client */
|
||||||
$client = ag($backend, 'class');
|
$client = ag($backend, 'class');
|
||||||
assert($backend instanceof iClient);
|
assert($backend instanceof iClient);
|
||||||
|
|
||||||
$this->logger->info("SYSTEM: Getting users from '{backend}'.", [
|
$this->logger->info("SYSTEM: Getting users from '{backend}'.", [
|
||||||
'backend' => $client->getContext()->backendName
|
'backend' => $client->getContext()->backendName
|
||||||
]);
|
]);
|
||||||
@@ -206,41 +250,41 @@ class CreateUsersCommand extends Command
|
|||||||
/** @var array $info */
|
/** @var array $info */
|
||||||
$info = $backend;
|
$info = $backend;
|
||||||
|
|
||||||
$user = $this->map_actions($user, ag($backend, 'name'), $mapping);
|
$backedName = ag($backend, 'name');
|
||||||
|
|
||||||
$_name = (string)ag($user, 'name');
|
// -- this was source of lots of bugs and confusion for users,
|
||||||
|
// -- we decided to normalize the user-names early in the process.
|
||||||
|
$user['name'] = normalizeName((string)$user['name']);
|
||||||
|
|
||||||
if (false === isValidName($_name)) {
|
// -- run map actions.
|
||||||
$rename = substr(md5($_name), 0, 8);
|
$this->map_actions($backedName, $user, $map);
|
||||||
|
|
||||||
|
// -- If normalization fails, ignore the user.
|
||||||
|
if (false === isValidName($user['name'])) {
|
||||||
$this->logger->error(
|
$this->logger->error(
|
||||||
message: "SYSTEM: Renaming invalid user name '{backend}: {name}' to '{backend}: {renamed}'. username must be in [a-z_0-9] format.",
|
message: "SYSTEM: Invalid user name '{backend}: {name}'. User names must be in [a-z_0-9] format. Skipping user.",
|
||||||
context: [
|
context: ['name' => $user['name'], 'backend' => $backedName]
|
||||||
'name' => $_name,
|
|
||||||
'backend' => ag($backend, 'name'),
|
|
||||||
'renamed' => $rename
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
$user = ag_set($user, 'name', $rename);
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- user here refers to user_id not the name.
|
// -- user here refers to user_id not the name.
|
||||||
$info['user'] = ag($user, 'id', ag($info, 'user'));
|
$info['user'] = ag($user, 'id', ag($info, 'user'));
|
||||||
|
|
||||||
// -- The display name is used to create user directory.
|
// -- The display name is used to create user directory.
|
||||||
$info['displayName'] = ag($user, 'name');
|
$info['displayName'] = $user['name'];
|
||||||
|
|
||||||
$info['backendName'] = strtolower(r("{backend}_{user}", [
|
$info['backendName'] = normalizeName(r("{backend}_{user}", [
|
||||||
'backend' => ag($backend, 'name'),
|
'backend' => $backedName,
|
||||||
'user' => ag($user, 'name'),
|
'user' => $user['name']
|
||||||
]));
|
]));
|
||||||
|
|
||||||
if (false === isValidName($info['backendName'])) {
|
if (false === isValidName($info['backendName'])) {
|
||||||
$rename = substr(md5($info['backendName']), 0, 8);
|
|
||||||
$this->logger->error(
|
$this->logger->error(
|
||||||
message: "SYSTEM: Renaming invalid backend name '{name}'. backend name must be in [a-z_0-9], renaming to '{renamed}'",
|
message: "SYSTEM: Invalid backend name '{name}'. Backend name must be in [a-z_0-9] format. skipping the associated users.",
|
||||||
context: ['name' => $info['backendName'], 'renamed' => $rename]
|
context: ['name' => $info['backendName']]
|
||||||
);
|
);
|
||||||
$info['backendName'] = $rename;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$info = ag_delete($info, 'options.' . Options::PLEX_USER_PIN);
|
$info = ag_delete($info, 'options.' . Options::PLEX_USER_PIN);
|
||||||
@@ -249,7 +293,7 @@ class CreateUsersCommand extends Command
|
|||||||
'options.' . Options::ALT_ID => ag($backend, 'user')
|
'options.' . Options::ALT_ID => ag($backend, 'user')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// -- of course, Plex has to be special.
|
// -- Of course, Plex has to be special.
|
||||||
if (PlexClient::CLIENT_NAME === ucfirst(ag($backend, 'type'))) {
|
if (PlexClient::CLIENT_NAME === ucfirst(ag($backend, 'type'))) {
|
||||||
$info = ag_sets($info, [
|
$info = ag_sets($info, [
|
||||||
'token' => 'reuse_or_generate_token',
|
'token' => 'reuse_or_generate_token',
|
||||||
@@ -265,8 +309,7 @@ class CreateUsersCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$user['backend'] = ag($backend, 'name');
|
$user = ag_sets($user, ['backend' => $backedName, 'client_data' => $info]);
|
||||||
$user['client_data'] = $info;
|
|
||||||
$users[] = $user;
|
$users[] = $user;
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
@@ -293,50 +336,67 @@ class CreateUsersCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$users = $this->generate_users_list($users);
|
return $users;
|
||||||
|
|
||||||
if (count($users) < 1) {
|
|
||||||
$this->logger->warning('No users were found.');
|
|
||||||
return self::FAILURE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->notice("SYSTEM: User matching results {results}.", [
|
/**
|
||||||
'results' => arrayToString($this->usersList($users)),
|
* Create user configuration files.
|
||||||
]);
|
*
|
||||||
|
* @param iInput $input The input interface.
|
||||||
|
* @param array $users The list of users to create.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function create_user(iInput $input, array $users): void
|
||||||
|
{
|
||||||
|
$dryRun = (bool)$input->getOption('dry-run');
|
||||||
|
$updateUsers = (bool)$input->getOption('update');
|
||||||
|
$regenerateTokens = (bool)$input->getOption('regenerate-tokens');
|
||||||
|
$generateBackups = (bool)$input->getOption('generate-backup');
|
||||||
|
$isReCreate = (bool)$input->getOption('re-create');
|
||||||
|
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
$userName = strtolower(ag($user, 'name', 'unknown'));
|
// -- User subdirectory name.
|
||||||
|
$userName = normalizeName(ag($user, 'name', 'unknown'));
|
||||||
|
|
||||||
if (false === isValidName($userName)) {
|
if (false === isValidName($userName)) {
|
||||||
$rename = substr(md5($userName), 0, 8);
|
|
||||||
$this->logger->error(
|
$this->logger->error(
|
||||||
message: "SYSTEM: Renaming invalid username '{user}'. Username must be in [a-z_0-9], renaming to '{renamed}'",
|
message: "SYSTEM: Invalid username '{user}'. User names must be in [a-z_0-9] format. skipping user.",
|
||||||
context: ['user' => $userName, 'renamed' => $rename]
|
context: ['user' => $userName]
|
||||||
);
|
);
|
||||||
$userName = $rename;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$subUserPath = r(fixPath(Config::get('path') . '/users/{user}'), ['user' => $userName]);
|
$subUserPath = r(fixPath(Config::get('path') . '/users/{user}'), ['user' => $userName]);
|
||||||
|
|
||||||
if (false === is_dir($subUserPath)) {
|
$this->logger->info(
|
||||||
$this->logger->info("SYSTEM: Creating '{user}' directory '{path}'.", [
|
false === is_dir(
|
||||||
|
$subUserPath
|
||||||
|
) ? "SYSTEM: Creating '{user}' directory '{path}'." : "SYSTEM: '{user}' directory '{path}' already exists.",
|
||||||
|
[
|
||||||
'user' => $userName,
|
'user' => $userName,
|
||||||
'path' => $subUserPath
|
'path' => $subUserPath,
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
|
|
||||||
if (false === $dryRun && false === mkdir($subUserPath, 0755, true)) {
|
if (false === $dryRun && false === is_dir($subUserPath) && false === mkdir($subUserPath, 0755, true)) {
|
||||||
$this->logger->error("SYSTEM: Failed to create '{user}' directory '{path}'.", [
|
$this->logger->error("SYSTEM: Failed to create '{user}' directory '{path}'.", [
|
||||||
'user' => $userName,
|
'user' => $userName,
|
||||||
'path' => $subUserPath
|
'path' => $subUserPath
|
||||||
]);
|
]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$config_file = "{$subUserPath}/servers.yaml";
|
$config_file = "{$subUserPath}/servers.yaml";
|
||||||
$this->logger->notice("SYSTEM: Creating '{user}' configuration file '{file}'.", [
|
$this->logger->notice(
|
||||||
|
file_exists(
|
||||||
|
$config_file
|
||||||
|
) ? "SYSTEM: '{user}' configuration file '{file}' already exists." : "SYSTEM: Creating '{user}' configuration file '{file}'.",
|
||||||
|
[
|
||||||
'user' => $userName,
|
'user' => $userName,
|
||||||
'file' => $config_file
|
'file' => $config_file
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
|
|
||||||
$perUser = ConfigFile::open(
|
$perUser = ConfigFile::open(
|
||||||
file: $dryRun ? "php://memory" : $config_file,
|
file: $dryRun ? "php://memory" : $config_file,
|
||||||
@@ -347,17 +407,16 @@ class CreateUsersCommand extends Command
|
|||||||
);
|
);
|
||||||
|
|
||||||
$perUser->setLogger($this->logger);
|
$perUser->setLogger($this->logger);
|
||||||
$regenerateTokens = $input->getOption('regenerate-tokens');
|
|
||||||
|
|
||||||
foreach (ag($user, 'backends', []) as $backend) {
|
foreach (ag($user, 'backends', []) as $backend) {
|
||||||
$name = ag($backend, 'client_data.backendName');
|
$name = ag($backend, 'client_data.backendName');
|
||||||
|
|
||||||
if (false === isValidName($name)) {
|
if (false === isValidName($name)) {
|
||||||
$rename = substr(md5($name), 0, 8);
|
|
||||||
$this->logger->error(
|
$this->logger->error(
|
||||||
message: "SYSTEM: Renaming invalid backend name '{name}'. backend name must be in [a-z_0-9], renaming to '{renamed}'",
|
message: "SYSTEM: Invalid backend name '{name}'. Backend name must be in [a-z_0-9] format. skipping backend.",
|
||||||
context: ['name' => $name, 'renamed' => $rename]
|
context: ['name' => $name]
|
||||||
);
|
);
|
||||||
$name = $rename;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$clientData = ag_delete(ag($backend, 'client_data'), 'class');
|
$clientData = ag_delete(ag($backend, 'client_data'), 'class');
|
||||||
@@ -371,7 +430,7 @@ class CreateUsersCommand extends Command
|
|||||||
} else {
|
} else {
|
||||||
$clientData = ag_delete($clientData, ['token', 'import.lastSync', 'export.lastSync']);
|
$clientData = ag_delete($clientData, ['token', 'import.lastSync', 'export.lastSync']);
|
||||||
$clientData = array_replace_recursive($perUser->get($name), $clientData);
|
$clientData = array_replace_recursive($perUser->get($name), $clientData);
|
||||||
if ($input->getOption('update')) {
|
if (true === $updateUsers) {
|
||||||
$update = [
|
$update = [
|
||||||
'url' => ag($backend, 'client_data.url'),
|
'url' => ag($backend, 'client_data.url'),
|
||||||
'options.ALT_NAME' => ag($backend, 'client_data.name'),
|
'options.ALT_NAME' => ag($backend, 'client_data.name'),
|
||||||
@@ -464,22 +523,119 @@ class CreateUsersCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$dbFile = r($subUserPath . "/{user}.db", ['user' => 'user']);
|
$dbFile = $subUserPath . "/user.db";
|
||||||
if (false === file_exists($dbFile)) {
|
$this->logger->notice(
|
||||||
$this->logger->notice("SYSTEM: Creating '{user}' database '{db}'.", [
|
file_exists(
|
||||||
|
$dbFile
|
||||||
|
) ? "SYSTEM: '{user}' database file '{db}' already exists." : "SYSTEM: Creating '{user}' database file '{db}'.",
|
||||||
|
[
|
||||||
'user' => $userName,
|
'user' => $userName,
|
||||||
'db' => $dbFile
|
'db' => $dbFile
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
|
|
||||||
if (false === $dryRun) {
|
if (false === $dryRun) {
|
||||||
|
if (false === file_exists($dbFile)) {
|
||||||
perUserDb($userName);
|
perUserDb($userName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$perUser->persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true === $generateBackups && false === $isReCreate) {
|
||||||
|
$this->logger->notice("SYSTEM: Queuing event to backup '{user}' remote watch state.", [
|
||||||
|
'user' => $userName
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (false === $dryRun) {
|
||||||
|
queueEvent(TasksCommand::CNAME, [
|
||||||
|
'command' => BackupCommand::ROUTE,
|
||||||
|
'args' => ['-v', '-u', $userName, '--file', '{user}.{backend}.{date}.initial_backup.json'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === $dryRun) {
|
/**
|
||||||
$perUser->persist();
|
* Executes the command.
|
||||||
|
*
|
||||||
|
* @param iInput $input The input interface.
|
||||||
|
* @param iOutput $output The output interface.
|
||||||
|
*
|
||||||
|
* @return int The exit code. 0 for success, 1 for failure.
|
||||||
|
*/
|
||||||
|
protected function runCommand(iInput $input, iOutput $output): int
|
||||||
|
{
|
||||||
|
if (true === ($dryRun = $input->getOption('dry-run'))) {
|
||||||
|
$this->logger->notice('SYSTEM: Running in dry-run mode. No changes will be made.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$usersPath = Config::get('path') . '/users';
|
||||||
|
$hasConfig = is_dir($usersPath) && count(glob($usersPath . '/*/*.yaml')) > 0;
|
||||||
|
|
||||||
|
if ($hasConfig && (false === $input->getOption('run') && false === $input->getOption('re-create'))) {
|
||||||
|
$output->writeln(
|
||||||
|
<<<Text
|
||||||
|
<error>ERROR:</error> Users configuration already exists.
|
||||||
|
|
||||||
|
If you want to re-create the users configuration, run the same command with [<flag>-r, --re-create</flag>] flag,
|
||||||
|
This will do the following:
|
||||||
|
|
||||||
|
1. Delete the current sub-users configuration and data.
|
||||||
|
2. Re-create the sub-users configuration.
|
||||||
|
|
||||||
|
Otherwise, you can use the [<flag>--run</flag>] to keep current configuration and update it with the new
|
||||||
|
users.
|
||||||
|
<value>
|
||||||
|
Beware, we have recently changed how we do matching, most likely if you run without re-creating the configuration.
|
||||||
|
it will result in double users for same user or more.
|
||||||
|
</value>
|
||||||
|
<notice>
|
||||||
|
We suggest to re-create the configuration. If you generated your users before date 2025-04-06.
|
||||||
|
</notice>
|
||||||
|
Text
|
||||||
|
);
|
||||||
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (true === $input->getOption('re-create')) {
|
||||||
|
$this->purgeUsersConfig($usersPath, $dryRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
$backends = $this->loadBackends();
|
||||||
|
|
||||||
|
if (empty($backends)) {
|
||||||
|
$this->logger->error('SYSTEM: No valid backends were found.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapping = $this->loadMappings();
|
||||||
|
|
||||||
|
$this->logger->notice("SYSTEM: Getting users list from '{backends}'.", [
|
||||||
|
'backends' => join(', ', array_keys($backends))
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backendsUser = $this->get_backends_users($backends, $mapping);
|
||||||
|
|
||||||
|
if (count($backendsUser) < 1) {
|
||||||
|
$this->logger->error('SYSTEM: No Backend users were found.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = $this->generate_users_list($backendsUser, $mapping);
|
||||||
|
|
||||||
|
if (count($users) < 1) {
|
||||||
|
$this->logger->warning("We weren't able to match any users across backends.");
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->notice("SYSTEM: User matching results {results}.", [
|
||||||
|
'results' => arrayToString($this->usersList($users)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->create_user(input: $input, users: $users);
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,10 +643,11 @@ class CreateUsersCommand extends Command
|
|||||||
* Generate a list of users that are matched across all backends.
|
* Generate a list of users that are matched across all backends.
|
||||||
*
|
*
|
||||||
* @param array $users The list of users from all backends.
|
* @param array $users The list of users from all backends.
|
||||||
|
* @param array{string: array{string: string, options: array}} $map The map of users to match.
|
||||||
*
|
*
|
||||||
* @return array{name: string, backends: array<string, array<string, mixed>>}[] The list of matched users.
|
* @return array{name: string, backends: array<string, array<string, mixed>>}[] The list of matched users.
|
||||||
*/
|
*/
|
||||||
private function generate_users_list(array $users): array
|
private function generate_users_list(array $users, array $map = []): array
|
||||||
{
|
{
|
||||||
$allBackends = [];
|
$allBackends = [];
|
||||||
foreach ($users as $u) {
|
foreach ($users as $u) {
|
||||||
@@ -501,20 +658,19 @@ class CreateUsersCommand extends Command
|
|||||||
|
|
||||||
// Build a lookup: $usersBy[backend][lowercased_name] = userObject
|
// Build a lookup: $usersBy[backend][lowercased_name] = userObject
|
||||||
$usersBy = [];
|
$usersBy = [];
|
||||||
|
$usersList = [];
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
$backend = $user['backend'];
|
$backend = $user['backend'];
|
||||||
$nameLower = (string)strtolower((string)$user['name']);
|
$nameLower = strtolower($user['name']);
|
||||||
if (ag($user, 'id') === ag($user, 'client_data.options.' . Options::ALT_ID)) {
|
if (ag($user, 'id') === ag($user, 'client_data.options.' . Options::ALT_ID)) {
|
||||||
$this->logger->debug('Skipping main user "{backend}: {name}".', [
|
$this->logger->debug('Skipping main user "{name}".', ['name' => $user['name']]);
|
||||||
'name' => $user['name'],
|
|
||||||
'backend' => $user['backend'],
|
|
||||||
]);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!isset($usersBy[$backend])) {
|
if (!isset($usersBy[$backend])) {
|
||||||
$usersBy[$backend] = [];
|
$usersBy[$backend] = [];
|
||||||
}
|
}
|
||||||
$usersBy[$backend][(string)$nameLower] = $user;
|
$usersBy[$backend][$nameLower] = $user;
|
||||||
|
$usersList[$backend][] = $nameLower;
|
||||||
}
|
}
|
||||||
|
|
||||||
$results = [];
|
$results = [];
|
||||||
@@ -547,7 +703,7 @@ class CreateUsersCommand extends Command
|
|||||||
$names = [];
|
$names = [];
|
||||||
foreach ($allBackends as $b) {
|
foreach ($allBackends as $b) {
|
||||||
if (isset($backendDict[$b])) {
|
if (isset($backendDict[$b])) {
|
||||||
$names[] = (string)$backendDict[$b]['name'];
|
$names[] = $backendDict[$b]['name'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,8 +740,6 @@ class CreateUsersCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$finalName = (string)$finalName;
|
|
||||||
|
|
||||||
// Build final row: "name" + sub-array "backends"
|
// Build final row: "name" + sub-array "backends"
|
||||||
$row = [
|
$row = [
|
||||||
'name' => strtolower($finalName),
|
'name' => strtolower($finalName),
|
||||||
@@ -611,13 +765,90 @@ class CreateUsersCommand extends Command
|
|||||||
|
|
||||||
// For each user in this backend
|
// For each user in this backend
|
||||||
foreach ($usersBy[$backend] as $nameLower => $userObj) {
|
foreach ($usersBy[$backend] as $nameLower => $userObj) {
|
||||||
$nameLower = (string)$nameLower;
|
|
||||||
|
|
||||||
// Skip if already used
|
// Skip if already used
|
||||||
if ($alreadyUsed($backend, $nameLower)) {
|
if ($alreadyUsed($backend, $nameLower)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map-based matching first
|
||||||
|
$matchedMapEntry = null;
|
||||||
|
foreach ($map as $mapRow) {
|
||||||
|
if (ag($mapRow, "{$backend}.name") === $nameLower) {
|
||||||
|
$this->logger->notice("Mapper: Found map entry for '{backend}: {user}'", [
|
||||||
|
'backend' => $backend,
|
||||||
|
'user' => $nameLower,
|
||||||
|
'map' => $mapRow,
|
||||||
|
]);
|
||||||
|
$matchedMapEntry = $mapRow;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($matchedMapEntry) {
|
||||||
|
// Build mapMatch from the map row.
|
||||||
|
$mapMatch = [$backend => $userObj];
|
||||||
|
|
||||||
|
// Gather all the other backends from the map
|
||||||
|
foreach ($allBackends as $otherBackend) {
|
||||||
|
if ($otherBackend === $backend) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isset($matchedMapEntry[$otherBackend]['name'])) {
|
||||||
|
$mappedNameLower = strtolower($matchedMapEntry[$otherBackend]['name']);
|
||||||
|
if (isset($usersBy[$otherBackend][$mappedNameLower])) {
|
||||||
|
$mapMatch[$otherBackend] = $usersBy[$otherBackend][$mappedNameLower];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we matched ≥ 2 backends, unify them
|
||||||
|
if (count($mapMatch) >= 2) {
|
||||||
|
// --- MERGE map-based "options" into client_data => options, if any ---
|
||||||
|
foreach ($mapMatch as $b => &$matchedUser) {
|
||||||
|
// If the map entry has an 'options' array for this backend,
|
||||||
|
// merge it into $matchedUser['client_data']['options'].
|
||||||
|
if (isset($matchedMapEntry[$b]['options']) && is_array($matchedMapEntry[$b]['options'])) {
|
||||||
|
$mapOptions = $matchedMapEntry[$b]['options'];
|
||||||
|
|
||||||
|
// Ensure $matchedUser['client_data'] is an array
|
||||||
|
if (!isset($matchedUser['client_data']) || !is_array($matchedUser['client_data'])) {
|
||||||
|
$matchedUser['client_data'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure $matchedUser['client_data']['options'] is an array
|
||||||
|
if (!isset($matchedUser['client_data']['options']) || !is_array(
|
||||||
|
$matchedUser['client_data']['options']
|
||||||
|
)) {
|
||||||
|
$matchedUser['client_data']['options'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the map's options
|
||||||
|
$matchedUser['client_data']['options'] = array_replace_recursive(
|
||||||
|
$matchedUser['client_data']['options'],
|
||||||
|
$mapOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($matchedUser); // break reference from the loop
|
||||||
|
|
||||||
|
// Build final row
|
||||||
|
$results[] = $buildUnifiedRow($mapMatch);
|
||||||
|
|
||||||
|
// Mark & remove from $usersBy
|
||||||
|
foreach ($mapMatch as $b => $mu) {
|
||||||
|
$nm = strtolower($mu['name']);
|
||||||
|
$used[] = [$b, $nm];
|
||||||
|
unset($usersBy[$b][$nm]);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
$this->logger->error("No partial fallback match via map for '{backend}: {user}'", [
|
||||||
|
'backend' => $userObj['backend'],
|
||||||
|
'user' => $userObj['name'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Direct-name matching if map fails
|
// Direct-name matching if map fails
|
||||||
$directMatch = [$backend => $userObj];
|
$directMatch = [$backend => $userObj];
|
||||||
foreach ($allBackends as $otherBackend) {
|
foreach ($allBackends as $otherBackend) {
|
||||||
@@ -645,9 +876,11 @@ class CreateUsersCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If neither map nor direct matched for ≥2
|
// If neither map nor direct matched for ≥2
|
||||||
$this->logger->error("Direct mapping failed for '{backend}: {user}' no match found.", [
|
$this->logger->error("No other users were found that match '{backend}: {user}'.", [
|
||||||
'backend' => $userObj['backend'],
|
'backend' => $userObj['backend'],
|
||||||
'user' => $userObj['name']
|
'user' => $userObj['name'],
|
||||||
|
'map' => arrayToString($map),
|
||||||
|
'list' => arrayToString($usersList),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -684,8 +917,6 @@ class CreateUsersCommand extends Command
|
|||||||
* @param string $backend The backend name.
|
* @param string $backend The backend name.
|
||||||
* @param array $mapping the mapper file data.
|
* @param array $mapping the mapper file data.
|
||||||
*
|
*
|
||||||
* @return array the modified user data if any.
|
|
||||||
*
|
|
||||||
* - my_plex_server:
|
* - my_plex_server:
|
||||||
* name: "mike_jones"
|
* name: "mike_jones"
|
||||||
* options: { }
|
* options: { }
|
||||||
@@ -698,67 +929,63 @@ class CreateUsersCommand extends Command
|
|||||||
* options: { }
|
* options: { }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
private function map_actions(array $user, string $backend, array $mapping): array
|
private function map_actions(string $backend, array &$user, array &$mapping): void
|
||||||
{
|
{
|
||||||
if (null === ($username = ag($user, 'name'))) {
|
if (null === ($username = ag($user, 'name'))) {
|
||||||
$this->logger->debug("SYSTEM: No username found for '{backend}' backend.", [
|
$this->logger->error("MAPPER: No username was given from one user of '{backend}' backend.", [
|
||||||
'backend' => $backend
|
'backend' => $backend
|
||||||
]);
|
]);
|
||||||
return $user;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- check if backend has mapping
|
|
||||||
$hasMapping = array_filter($mapping, fn($map) => array_key_exists($backend, $map));
|
$hasMapping = array_filter($mapping, fn($map) => array_key_exists($backend, $map));
|
||||||
if (empty($hasMapping)) {
|
if (count($hasMapping) < 1) {
|
||||||
$this->logger->debug("No mapping found for '{backend}' backend.", [
|
$this->logger->info("MAPPER: No mapping exists for '{backend}' backend.", [
|
||||||
'backend' => $backend
|
'backend' => $backend
|
||||||
]);
|
]);
|
||||||
return $user;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$found = false;
|
$found = false;
|
||||||
$user_map = [];
|
$user_map = [];
|
||||||
|
|
||||||
foreach ($mapping as $map) {
|
foreach ($mapping as &$map) {
|
||||||
$map_backend = array_keys($map)[0];
|
foreach ($map as $map_backend => &$loop_map) {
|
||||||
|
|
||||||
if ($backend !== $map_backend) {
|
if ($backend !== $map_backend) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (ag($loop_map, "name") !== $username) {
|
||||||
if (ag($map, "{$backend}.name") !== $username) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$found = true;
|
$found = true;
|
||||||
$user_map = ag($map, $backend, []);
|
$user_map = &$loop_map;
|
||||||
break;
|
break 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === $found) {
|
if (false === $found) {
|
||||||
$this->logger->debug("No mapping found for '{backend}: {username}'.", [
|
$this->logger->debug("MAPPER: No map exists for '{backend}: {username}'.", [
|
||||||
'backend' => $backend,
|
'backend' => $backend,
|
||||||
'username' => $username
|
'username' => $username
|
||||||
]);
|
]);
|
||||||
return $user;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- replace_with action.
|
|
||||||
if (null !== ($newUsername = ag($user_map, 'replace_with'))) {
|
if (null !== ($newUsername = ag($user_map, 'replace_with'))) {
|
||||||
if (!is_string($newUsername) || false === isValidName($newUsername)) {
|
if (false === is_string($newUsername) || false === isValidName($newUsername)) {
|
||||||
$this->logger->error(
|
$this->logger->error(
|
||||||
message: "SYSTEM: Mapper failed to rename '{backend}: {username}' to '{backend}: {new_username}' name must be in [a-z_0-9] format.",
|
message: "MAPPER: Failed to replace '{backend}: {username}' with '{backend}: {new_username}' name must be in [a-z_0-9] format.",
|
||||||
context: [
|
context: [
|
||||||
'backend' => $backend,
|
'backend' => $backend,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'new_username' => $newUsername
|
'new_username' => $newUsername
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
return $user;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->notice(
|
$this->logger->notice(
|
||||||
message: "SYSTEM: Mapper is renaming '{backend}: {username}' to '{backend}: {new_username}'.",
|
message: "MAPPER: Renaming '{backend}: {username}' to '{backend}: {new_username}'.",
|
||||||
context: [
|
context: [
|
||||||
'backend' => $backend,
|
'backend' => $backend,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
@@ -767,8 +994,7 @@ class CreateUsersCommand extends Command
|
|||||||
);
|
);
|
||||||
|
|
||||||
$user['name'] = $newUsername;
|
$user['name'] = $newUsername;
|
||||||
}
|
$user_map['name'] = $newUsername;
|
||||||
|
}
|
||||||
return $user;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,7 +256,6 @@ final class TasksCommand extends Command
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (self::CNAME === $eventName) {
|
if (self::CNAME === $eventName) {
|
||||||
$event->addLog(r("Task: Run '{name}'.", ['name' => $eventName]));
|
$event->addLog(r("Task: Run '{name}'.", ['name' => $eventName]));
|
||||||
$exitCode = $this->run_command(
|
$exitCode = $this->run_command(
|
||||||
|
|||||||
@@ -858,20 +858,23 @@ if (!function_exists('getAppVersion')) {
|
|||||||
if (is_dir($gitDir)) {
|
if (is_dir($gitDir)) {
|
||||||
try {
|
try {
|
||||||
// Get the current branch name.
|
// Get the current branch name.
|
||||||
$cmdBranch = 'git --git-dir={cwd} rev-parse --abbrev-ref HEAD';
|
$cmdBranch = 'git --git-path
|
||||||
|
={cwd} rev-parse --abbrev-ref HEAD';
|
||||||
$procBranch = Process::fromShellCommandline(r($cmdBranch, ['cwd' => escapeshellarg($gitDir)]));
|
$procBranch = Process::fromShellCommandline(r($cmdBranch, ['cwd' => escapeshellarg($gitDir)]));
|
||||||
$procBranch->run();
|
$procBranch->run();
|
||||||
$branch = $procBranch->isSuccessful() ? trim($procBranch->getOutput()) : 'unknown';
|
$branch = $procBranch->isSuccessful() ? trim($procBranch->getOutput()) : 'unknown';
|
||||||
|
|
||||||
// Get the short commit hash.
|
// Get the short commit hash.
|
||||||
$cmdCommit = 'git --git-dir={cwd} rev-parse --short HEAD';
|
$cmdCommit = 'git --git-path
|
||||||
|
={cwd} rev-parse --short HEAD';
|
||||||
$procCommit = Process::fromShellCommandline(r($cmdCommit, ['cwd' => escapeshellarg($gitDir)]));
|
$procCommit = Process::fromShellCommandline(r($cmdCommit, ['cwd' => escapeshellarg($gitDir)]));
|
||||||
$procCommit->run();
|
$procCommit->run();
|
||||||
$commit = $procCommit->isSuccessful() ? trim($procCommit->getOutput()) : 'unknown';
|
$commit = $procCommit->isSuccessful() ? trim($procCommit->getOutput()) : 'unknown';
|
||||||
|
|
||||||
// Get the commit date (from HEAD) in YYYYMMDD format.
|
// Get the commit date (from HEAD) in YYYYMMDD format.
|
||||||
// This uses "git show" with a custom date format.
|
// This uses "git show" with a custom date format.
|
||||||
$cmdDate = 'git --git-dir={cwd} show -s --format=%cd --date=format:%Y%m%d HEAD';
|
$cmdDate = 'git --git-path
|
||||||
|
={cwd} show -s --format=%cd --date=format:%Y%m%d HEAD';
|
||||||
$procDate = Process::fromShellCommandline(r($cmdDate, ['cwd' => escapeshellarg($gitDir)]));
|
$procDate = Process::fromShellCommandline(r($cmdDate, ['cwd' => escapeshellarg($gitDir)]));
|
||||||
$procDate->run();
|
$procDate->run();
|
||||||
$commitDate = $procDate->isSuccessful() ? trim($procDate->getOutput()) : date('Ymd');
|
$commitDate = $procDate->isSuccessful() ? trim($procDate->getOutput()) : date('Ymd');
|
||||||
@@ -1165,13 +1168,16 @@ if (false === function_exists('generateRoutes')) {
|
|||||||
try {
|
try {
|
||||||
$dirs = [__DIR__ . '/../Commands'];
|
$dirs = [__DIR__ . '/../Commands'];
|
||||||
foreach (array_keys(Config::get('supported', [])) as $backend) {
|
foreach (array_keys(Config::get('supported', [])) as $backend) {
|
||||||
$dir = r(__DIR__ . '/../Backends/{backend}/Commands', ['backend' => ucfirst($backend)]);
|
$path
|
||||||
|
= r(__DIR__ . '/../Backends/{backend}/Commands', ['backend' => ucfirst($backend)]);
|
||||||
|
|
||||||
if (!file_exists($dir)) {
|
if (!file_exists(
|
||||||
|
$path
|
||||||
|
)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$dirs[] = $dir;
|
$dirs[] = $path;
|
||||||
}
|
}
|
||||||
foreach (AttributesScanner::scan($dirs, allowNonInvokable: true)->for(Cli::class) as $item) {
|
foreach (AttributesScanner::scan($dirs, allowNonInvokable: true)->for(Cli::class) as $item) {
|
||||||
$routes_cli[] = [
|
$routes_cli[] = [
|
||||||
@@ -2592,17 +2598,33 @@ if (!function_exists('getUsersContext')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (false === is_readable($usersDir)) {
|
if (false === is_readable($usersDir)) {
|
||||||
throw new RuntimeException(r("Unable to read '{dir}' directory.", ['dir' => $usersDir]));
|
throw new RuntimeException(
|
||||||
|
r(
|
||||||
|
"Unable to read '{path
|
||||||
|
}' directory.",
|
||||||
|
[
|
||||||
|
'path
|
||||||
|
' => $usersDir
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (new DirectoryIterator(Config::get('path') . '/users') as $dir) {
|
foreach (new DirectoryIterator(Config::get('path') . '/users') as $path
|
||||||
if ($dir->isDot() || false === $dir->isDir()) {
|
) {
|
||||||
|
if ($path
|
||||||
|
->isDot() || false === $path
|
||||||
|
->isDir()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = perUserConfig($dir->getBasename());
|
$config = perUserConfig(
|
||||||
|
$path
|
||||||
|
->getBasename()
|
||||||
|
);
|
||||||
|
|
||||||
$userName = $dir->getBasename();
|
$userName = $path
|
||||||
|
->getBasename();
|
||||||
$perUserCache = perUserCacheAdapter($userName);
|
$perUserCache = perUserCacheAdapter($userName);
|
||||||
$db = perUserDb($userName);
|
$db = perUserDb($userName);
|
||||||
if (count($dbOpts) > 0) {
|
if (count($dbOpts) > 0) {
|
||||||
@@ -2653,3 +2675,69 @@ if (!function_exists('getUserContext')) {
|
|||||||
return $users[$user];
|
return $users[$user];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!function_exists('deletePath')) {
|
||||||
|
/**
|
||||||
|
* Delete the contents of given path.
|
||||||
|
*
|
||||||
|
* @param string $path The path to delete.
|
||||||
|
* @param iLogger|null $logger The logger instance.
|
||||||
|
* @param bool $dryRun (Optional) Whether to perform a dry run.
|
||||||
|
*
|
||||||
|
* @return bool Whether the operation was successful.
|
||||||
|
*/
|
||||||
|
function deletePath(string $path, iLogger|null $logger = null, bool $dryRun = false): bool
|
||||||
|
{
|
||||||
|
if (false === is_dir($path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
iterator: new RecursiveDirectoryIterator(
|
||||||
|
directory: $path,
|
||||||
|
flags: RecursiveDirectoryIterator::SKIP_DOTS
|
||||||
|
),
|
||||||
|
mode: RecursiveIteratorIterator::CHILD_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($iterator as $item) {
|
||||||
|
if (null !== $logger) {
|
||||||
|
$context = [
|
||||||
|
'path' => $item->getPathname(),
|
||||||
|
'type' => $item->isDir() ? 'directory' : 'file'
|
||||||
|
];
|
||||||
|
$logger->info("Removing {type} '{path}'.", $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true === $dryRun) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true === $item->isDir()) {
|
||||||
|
@rmdir($item->getPathname());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@unlink($item->getPathname());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('normalizeName')) {
|
||||||
|
/**
|
||||||
|
* Normalize the name to be in [a-z_0-9] format.
|
||||||
|
*
|
||||||
|
* @param string $name The name to normalize.
|
||||||
|
*
|
||||||
|
* @return string The normalized name.
|
||||||
|
*/
|
||||||
|
function normalizeName(string $name): string
|
||||||
|
{
|
||||||
|
$name = strtolower($name);
|
||||||
|
$name = preg_replace('/[^a-z0-9_]/', '_', $name);
|
||||||
|
return trim($name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user