diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml
index 7e3f9ab604ce3..20b47367c09cb 100644
--- a/build/psalm-baseline.xml
+++ b/build/psalm-baseline.xml
@@ -3522,10 +3522,6 @@
delete($key)]]>
set($key, $value)]]>
-
-
-
-
diff --git a/config/config.sample.php b/config/config.sample.php
index 411e46c46c409..f2e5fa2be1091 100644
--- a/config/config.sample.php
+++ b/config/config.sample.php
@@ -45,6 +45,17 @@
*/
'instanceid' => '',
+ /**
+ * This is a unique identifier for your server.
+ * It is useful when your Nextcloud instance is spread between different servers.
+ * Once it's set it shouldn't be changed.
+ *
+ * Value must be an integer, comprised between 0 and 1023.
+ *
+ * This value should be overriden by hostname in $CONFIG_HOSTNAME
+ */
+ 'serverid' => -1,
+
/**
* The salt used to hash all passwords, auto-generated by the Nextcloud
* installer. (There are also per-user salts.) If you lose this salt, you lose
@@ -2813,3 +2824,18 @@
*/
'enable_lazy_objects' => true,
];
+
+/**
+ * CONFIG_HOSTNAME allows to set specific configuration value on specific hosts.
+ * It should be used when configuration is stored on a shared filesystem between several servers.
+ *
+ * Only options listed in \OC\Config::HOST_OVERRIDE_CONFIG can be defined here.
+ */
+$CONFIG_HOSTNAME = [
+ 'hostname_a' => [
+ 'serverid' => 42,
+ ],
+ 'hostname_b' => [
+ 'serverid' => 43,
+ ],
+];
diff --git a/lib/private/Config.php b/lib/private/Config.php
index a9eb58a186646..4f1fe4941be12 100644
--- a/lib/private/Config.php
+++ b/lib/private/Config.php
@@ -15,6 +15,10 @@
*/
class Config {
public const ENV_PREFIX = 'NC_';
+ // List configurations that can be overriden based on server hostname
+ private const HOST_OVERRIDE_CONFIG = [
+ 'serverid',
+ ];
/** @var array Associative array ($key => $value) */
protected $cache = [];
@@ -199,7 +203,7 @@ private function readData() {
// Include file and merge config
foreach ($configFiles as $file) {
- unset($CONFIG);
+ $CONFIG = $CONFIG_HOSTNAME = null;
// Invalidate opcache (only if the timestamp changed)
if (function_exists('opcache_invalidate')) {
@@ -226,6 +230,10 @@ private function readData() {
}
try {
+ /**
+ * @var ?array $CONFIG
+ * @var ?array $CONFIG_HOSTNAME
+ */
include $file;
} finally {
// Close the file pointer and release the lock
@@ -241,9 +249,20 @@ private function readData() {
}
throw new \Exception($errorMessage);
}
- if (isset($CONFIG) && is_array($CONFIG)) {
+ if (is_array($CONFIG)) {
$this->cache = array_merge($this->cache, $CONFIG);
}
+ if (is_array($CONFIG_HOSTNAME) && !empty($CONFIG_HOSTNAME)) {
+ $hostname = gethostname();
+ if (isset($CONFIG_HOSTNAME[$hostname]) && is_array($CONFIG_HOSTNAME[$hostname])) {
+ $filteredConfig = array_filter(
+ $CONFIG_HOSTNAME[$hostname],
+ fn ($key) => in_array($key, self::HOST_OVERRIDE_CONFIG),
+ ARRAY_FILTER_USE_KEY,
+ );
+ $this->cache = array_merge($this->cache, $filteredConfig);
+ }
+ }
}
// grab any "NC_" environment variables
diff --git a/lib/private/Snowflake/Generator.php b/lib/private/Snowflake/Generator.php
index f378482a315dd..b3fcaeee7dbe5 100644
--- a/lib/private/Snowflake/Generator.php
+++ b/lib/private/Snowflake/Generator.php
@@ -10,6 +10,7 @@
namespace OC\Snowflake;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
use OCP\Snowflake\IGenerator;
use Override;
@@ -23,6 +24,7 @@
final class Generator implements IGenerator {
public function __construct(
private readonly ITimeFactory $timeFactory,
+ private readonly IConfig $config,
) {
}
@@ -100,7 +102,10 @@ private function getCurrentTime(): array {
}
private function getServerId(): int {
- return crc32(gethostname() ?: random_bytes(8));
+ $serverid = $this->config->getSystemValueInt('serverid', -1);
+ return $serverid > 0
+ ? $serverid
+ : crc32(gethostname() ?: random_bytes(8));
}
private function isCli(): bool {
diff --git a/tests/lib/Snowflake/GeneratorTest.php b/tests/lib/Snowflake/GeneratorTest.php
index 748d0f190422a..382c1bfbd4bb9 100644
--- a/tests/lib/Snowflake/GeneratorTest.php
+++ b/tests/lib/Snowflake/GeneratorTest.php
@@ -13,6 +13,7 @@
use OC\Snowflake\Decoder;
use OC\Snowflake\Generator;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
use OCP\Snowflake\IGenerator;
use PHPUnit\Framework\Attributes\DataProvider;
use Test\TestCase;
@@ -22,12 +23,17 @@
*/
class GeneratorTest extends TestCase {
private Decoder $decoder;
+ private IConfig|MockObject $config;
public function setUp():void {
$this->decoder = new Decoder();
+ $this->config = $this->createMock(IConfig::class);
+ $this->config->method('getSystemValueInt')
+ ->with('serverid')
+ ->willReturn(42);
}
public function testGenerator(): void {
- $generator = new Generator(new TimeFactory());
+ $generator = new Generator(new TimeFactory(), $this->config);
$snowflakeId = $generator->nextId();
$data = $this->decoder->decode($generator->nextId());
@@ -45,6 +51,9 @@ public function testGenerator(): void {
// Check CLI
$this->assertTrue($data['isCli']);
+
+ // Check serverId
+ $this->assertEquals(42, $data['serverId']);
}
#[DataProvider('provideSnowflakeData')]
@@ -53,11 +62,12 @@ public function testGeneratorWithFixedTime(string $date, int $expectedSeconds, i
$timeFactory = $this->createMock(ITimeFactory::class);
$timeFactory->method('now')->willReturn($dt);
- $generator = new Generator($timeFactory);
+ $generator = new Generator($timeFactory, $this->config);
$data = $this->decoder->decode($generator->nextId());
$this->assertEquals($expectedSeconds, ($data['createdAt']->format('U') - IGenerator::TS_OFFSET));
$this->assertEquals($expectedMilliseconds, (int)$data['createdAt']->format('v'));
+ $this->assertEquals(42, $data['serverId']);
}
public static function provideSnowflakeData(): array {