Merge pull request #11574 from appwrite/feat-installer

This commit is contained in:
Jake Barnby
2026-03-19 12:19:15 +00:00
committed by GitHub
16 changed files with 272 additions and 16 deletions
+2
View File
@@ -9,6 +9,7 @@ $defaultAppDomain = ($defaultAppDomain === 'traefik') ? 'localhost' : $defaultAp
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
$defaultDatabase = $vars['_APP_DB_ADAPTER']['default'] ?? 'mongodb';
$lockedDatabase = $isUpgrade && empty($lockedDatabase) ? $defaultDatabase : $lockedDatabase;
$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql'];
$isLocalInstall = $isLocalInstall ?? false;
@@ -64,6 +65,7 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time()
data-default-secret-key=""
data-default-assistant-openai-key=""
data-default-database="<?php echo htmlspecialchars((string) $defaultDatabase, ENT_QUOTES, 'UTF-8'); ?>"
data-enabled-databases="<?php echo htmlspecialchars(json_encode(array_values($enabledDatabases)), ENT_QUOTES, 'UTF-8'); ?>"
<?php if ($isLocalInstall) { ?>
data-dev-mode="true"
<?php } ?>
@@ -11,9 +11,13 @@
--neutral-300: rgba(173, 173, 176, 1);
--neutral-400: rgba(151, 151, 155, 1);
--neutral-500: rgba(129, 129, 134, 1);
--neutral-600: rgba(108, 108, 113, 1);
--neutral-700: rgba(86, 86, 92, 1);
--neutral-750: rgba(65, 65, 70, 1);
--neutral-800: rgba(45, 45, 49, 1);
--neutral-850: rgba(29, 29, 33, 1);
--neutral-900: rgba(25, 25, 28, 1);
--neutral-250: rgba(195, 195, 198, 1);
/* Warning colors */
--web-orange-200: rgba(255, 213, 194, 1);
@@ -172,6 +176,44 @@
--fgColor-neutral-weak: var(--fgcolor-neutral-weak);
--fgColor-accent: var(--fgcolor-accent);
--fgColor-on-accent: var(--fgcolor-on-accent);
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
:root {
--bgcolor-neutral-default: var(--neutral-900);
--bgcolor-neutral-primary: var(--neutral-850);
--bgcolor-neutral-secondary: var(--neutral-800);
--bgcolor-neutral-tertiary: var(--neutral-800);
--bgcolor-neutral-invert-weaker: var(--neutral-400);
--bgcolor-neutral-invert-weak: var(--neutral-300);
--bgcolor-success-weak: rgba(16, 185, 129, 0.12);
--bgcolor-warning-weaker: rgba(254, 124, 67, 0.08);
--bgcolor-warning-weak: rgba(254, 124, 67, 0.12);
--bgcolor-error-weaker: rgba(255, 69, 58, 0.08);
--fgcolor-neutral-primary: var(--neutral-50);
--fgcolor-neutral-secondary: var(--neutral-250);
--fgcolor-neutral-tertiary: var(--neutral-500);
--fgcolor-neutral-weak: var(--neutral-600);
--fgcolor-on-accent: var(--neutral-0);
--fgcolor-on-invert: var(--neutral-900);
--fgcolor-on-success-weak: rgba(52, 211, 153, 1);
--fgcolor-warning: var(--web-orange-500);
--fgcolor-on-warning-weak: var(--web-orange-500);
--fgcolor-error: var(--web-red-500);
--fgcolor-on-error: var(--neutral-0);
--border-neutral: var(--neutral-800);
--border-neutral-strong: var(--neutral-750);
--border-neutral-stronger: var(--neutral-600);
--border-focus: var(--neutral-600);
--overlay-neutral-hover: rgba(255, 255, 255, 0.04);
--overlay-neutral-pressed: rgba(255, 255, 255, 0.08);
--overlay-scrim: rgba(0, 0, 0, 0.48);
}
}
.installer-toast-stack {
@@ -1354,6 +1396,7 @@ body {
align-items: center;
justify-content: center;
line-height: 0;
color: var(--fgcolor-neutral-weak);
}
.step-indicator svg {
@@ -1,13 +1,13 @@
<svg width="131" height="25" viewBox="0 0 131 25" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M38.2643 19.5087C40.4069 19.5087 41.49 18.3947 41.9609 17.6362H42.1728C42.267 18.4421 42.832 19.2006 43.9386 19.2006H46.0341V16.8304H45.4926C45.1159 16.8304 44.9275 16.617 44.9275 16.2852V6.78055H42.1492V8.29751H41.9373C41.3958 7.53903 40.2656 6.47242 38.1937 6.47242C34.8974 6.47242 32.4487 9.2219 32.4487 12.9906C32.4487 16.7593 34.9445 19.5087 38.2643 19.5087ZM38.7588 16.8067C36.8045 16.8067 35.2741 15.3608 35.2741 13.0143C35.2741 10.7151 36.7574 9.15079 38.7352 9.15079C40.6188 9.15079 42.1963 10.5492 42.1963 13.0143C42.1963 15.1238 40.8543 16.8067 38.7588 16.8067Z" fill="#19191D"/>
<path d="M47.6745 24.0166H50.4528V17.6362H50.6647C51.1826 18.3947 52.2893 19.5087 54.4789 19.5087C57.7752 19.5087 60.1768 16.7118 60.1768 12.9906C60.1768 9.2456 57.6104 6.47242 54.2906 6.47242C52.1715 6.47242 51.1356 7.63384 50.6411 8.2738H50.4292V6.78055H47.6745V24.0166ZM53.8903 16.8778C51.9832 16.8778 50.4057 15.4556 50.4057 12.9906C50.4057 10.8811 51.7477 9.10339 53.8432 9.10339C55.7974 9.10339 57.3279 10.644 57.3279 12.9906C57.3279 15.2897 55.8445 16.8778 53.8903 16.8778Z" fill="#19191D"/>
<path d="M61.6104 24.0166H64.3887V17.6362H64.6006C65.1186 18.3947 66.2252 19.5087 68.4149 19.5087C71.7112 19.5087 73.8839 16.7118 73.8839 12.9906C73.8839 9.2456 71.5464 6.47242 68.2265 6.47242C66.1075 6.47242 65.0715 7.63384 64.5771 8.2738H64.3652V6.78055H61.6104V24.0166ZM67.8263 16.8778C65.9191 16.8778 64.3416 15.4556 64.3416 12.9906C64.3416 10.8811 65.6837 9.10339 67.7792 9.10339C69.7334 9.10339 71.2638 10.644 71.2638 12.9906C71.2638 15.2897 69.7805 16.8778 67.8263 16.8778Z" fill="#19191D"/>
<path d="M77.5565 19.489H81.4885L83.7252 9.74733H83.8665L86.1033 19.489H90.0117L93.1415 7.06896H90.3414L88.1046 16.8343H87.8927L85.6559 7.06896H81.9594L79.6991 16.8343H79.4872L77.2739 7.06896H74.3073L77.5565 19.489Z" fill="#19191D"/>
<path d="M94.549 19.489H97.3273V13.3501C97.3273 11.0036 98.4104 9.55771 100.435 9.55771H101.66V6.76083H100.741C99.1638 6.76083 97.963 7.85114 97.4921 8.89405H97.3038V7.06896H94.549V19.489Z" fill="#19191D"/>
<path d="M115.447 19.489H117.613V17.0003H115.47C114.623 17.0003 114.27 16.621 114.27 15.744V9.53401H117.754V7.06896H114.27V3.58472H111.633V7.06896H109.325V9.53401H111.468V15.7677C111.468 18.3987 113.045 19.489 115.447 19.489Z" fill="#19191D"/>
<path d="M125.067 19.5087C127.633 19.5087 129.893 18.2288 130.694 15.6452L128.151 15.029C127.704 16.4037 126.409 17.1148 125.043 17.1148C123.018 17.1148 121.676 15.7875 121.653 13.7016H131V12.9195C131 9.2219 128.716 6.47242 124.949 6.47242C121.629 6.47242 118.78 9.10339 118.78 13.0143C118.78 16.8067 121.3 19.5087 125.067 19.5087ZM121.676 11.6632C121.841 10.17 123.183 8.91377 124.949 8.91377C126.644 8.91377 128.033 9.98037 128.175 11.6632H121.676Z" fill="#19191D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.09 19.489H105.312V9.53401H103.145V7.06896H108.09V19.489Z" fill="#19191D"/>
<path d="M106.494 5.34533C107.507 5.34533 108.26 4.58686 108.26 3.59136C108.26 2.61956 107.507 1.86108 106.494 1.86108C105.482 1.86108 104.729 2.61956 104.729 3.59136C104.729 4.58686 105.482 5.34533 106.494 5.34533Z" fill="#19191D"/>
<path d="M38.2643 19.5087C40.4069 19.5087 41.49 18.3947 41.9609 17.6362H42.1728C42.267 18.4421 42.832 19.2006 43.9386 19.2006H46.0341V16.8304H45.4926C45.1159 16.8304 44.9275 16.617 44.9275 16.2852V6.78055H42.1492V8.29751H41.9373C41.3958 7.53903 40.2656 6.47242 38.1937 6.47242C34.8974 6.47242 32.4487 9.2219 32.4487 12.9906C32.4487 16.7593 34.9445 19.5087 38.2643 19.5087ZM38.7588 16.8067C36.8045 16.8067 35.2741 15.3608 35.2741 13.0143C35.2741 10.7151 36.7574 9.15079 38.7352 9.15079C40.6188 9.15079 42.1963 10.5492 42.1963 13.0143C42.1963 15.1238 40.8543 16.8067 38.7588 16.8067Z" fill="currentColor"/>
<path d="M47.6745 24.0166H50.4528V17.6362H50.6647C51.1826 18.3947 52.2893 19.5087 54.4789 19.5087C57.7752 19.5087 60.1768 16.7118 60.1768 12.9906C60.1768 9.2456 57.6104 6.47242 54.2906 6.47242C52.1715 6.47242 51.1356 7.63384 50.6411 8.2738H50.4292V6.78055H47.6745V24.0166ZM53.8903 16.8778C51.9832 16.8778 50.4057 15.4556 50.4057 12.9906C50.4057 10.8811 51.7477 9.10339 53.8432 9.10339C55.7974 9.10339 57.3279 10.644 57.3279 12.9906C57.3279 15.2897 55.8445 16.8778 53.8903 16.8778Z" fill="currentColor"/>
<path d="M61.6104 24.0166H64.3887V17.6362H64.6006C65.1186 18.3947 66.2252 19.5087 68.4149 19.5087C71.7112 19.5087 73.8839 16.7118 73.8839 12.9906C73.8839 9.2456 71.5464 6.47242 68.2265 6.47242C66.1075 6.47242 65.0715 7.63384 64.5771 8.2738H64.3652V6.78055H61.6104V24.0166ZM67.8263 16.8778C65.9191 16.8778 64.3416 15.4556 64.3416 12.9906C64.3416 10.8811 65.6837 9.10339 67.7792 9.10339C69.7334 9.10339 71.2638 10.644 71.2638 12.9906C71.2638 15.2897 69.7805 16.8778 67.8263 16.8778Z" fill="currentColor"/>
<path d="M77.5565 19.489H81.4885L83.7252 9.74733H83.8665L86.1033 19.489H90.0117L93.1415 7.06896H90.3414L88.1046 16.8343H87.8927L85.6559 7.06896H81.9594L79.6991 16.8343H79.4872L77.2739 7.06896H74.3073L77.5565 19.489Z" fill="currentColor"/>
<path d="M94.549 19.489H97.3273V13.3501C97.3273 11.0036 98.4104 9.55771 100.435 9.55771H101.66V6.76083H100.741C99.1638 6.76083 97.963 7.85114 97.4921 8.89405H97.3038V7.06896H94.549V19.489Z" fill="currentColor"/>
<path d="M115.447 19.489H117.613V17.0003H115.47C114.623 17.0003 114.27 16.621 114.27 15.744V9.53401H117.754V7.06896H114.27V3.58472H111.633V7.06896H109.325V9.53401H111.468V15.7677C111.468 18.3987 113.045 19.489 115.447 19.489Z" fill="currentColor"/>
<path d="M125.067 19.5087C127.633 19.5087 129.893 18.2288 130.694 15.6452L128.151 15.029C127.704 16.4037 126.409 17.1148 125.043 17.1148C123.018 17.1148 121.676 15.7875 121.653 13.7016H131V12.9195C131 9.2219 128.716 6.47242 124.949 6.47242C121.629 6.47242 118.78 9.10339 118.78 13.0143C118.78 16.8067 121.3 19.5087 125.067 19.5087ZM121.676 11.6632C121.841 10.17 123.183 8.91377 124.949 8.91377C126.644 8.91377 128.033 9.98037 128.175 11.6632H121.676Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.09 19.489H105.312V9.53401H103.145V7.06896H108.09V19.489Z" fill="currentColor"/>
<path d="M106.494 5.34533C107.507 5.34533 108.26 4.58686 108.26 3.59136C108.26 2.61956 107.507 1.86108 106.494 1.86108C105.482 1.86108 104.729 2.61956 104.729 3.59136C104.729 4.58686 105.482 5.34533 106.494 5.34533Z" fill="currentColor"/>
<path d="M24.2577 16.4436V21.9248H10.6705C6.71194 21.9248 3.25559 19.7204 1.40636 16.4436C1.13754 15.9672 0.90225 15.4674 0.704883 14.9487C0.31744 13.9322 0.0738912 12.8415 0 11.7034V10.2214C0.0160422 9.96781 0.0413207 9.71617 0.0743773 9.46752C0.141949 8.95727 0.244035 8.45799 0.378206 7.97265C1.64748 3.37143 5.77469 0 10.6705 0C15.5662 0 19.693 3.37143 20.9622 7.97265H15.1526C14.1988 6.47279 12.5479 5.4812 10.6705 5.4812C8.79305 5.4812 7.14216 6.47279 6.18839 7.97265C5.89768 8.42859 5.67212 8.93136 5.52434 9.46752C5.39308 9.94289 5.32308 10.4442 5.32308 10.9624C5.32308 12.5335 5.96768 13.9497 7.00119 14.9487C7.95886 15.876 9.25 16.4436 10.6705 16.4436H24.2577Z" fill="#FD366E"/>
<path d="M24.2578 9.46753V14.9487H14.3398C15.3733 13.9497 16.018 12.5335 16.018 10.9624C16.018 10.4442 15.9479 9.9429 15.8167 9.46753H24.2578Z" fill="#FD366E"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8Z" fill="#D8D8DB"/>
<path d="M5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 261 B

After

Width:  |  Height:  |  Size: 266 B

@@ -2,6 +2,11 @@
const getBodyDataset = () => document.body?.dataset ?? {};
const isUpgradeMode = () => getBodyDataset().upgrade === 'true';
const getLockedDatabase = () => getBodyDataset().lockedDatabase || '';
const getEnabledDatabases = () => {
const raw = getBodyDataset().enabledDatabases;
if (!raw) return ['mongodb', 'mariadb', 'postgresql'];
try { return JSON.parse(raw); } catch (e) { return ['mongodb', 'mariadb', 'postgresql']; }
};
const STEP_IDS = Object.freeze({
CONFIG_FILES: 'config-files',
@@ -95,6 +100,7 @@
getBodyDataset,
isUpgradeMode,
getLockedDatabase,
getEnabledDatabases,
STEP_IDS,
STATUS,
SSE_EVENTS,
@@ -27,6 +27,7 @@
let activeInstall = null;
let unloadGuard = null;
let sseSessionDetails = null;
const csrfToken = document.querySelector('meta[name="appwrite-installer-csrf"]')?.getAttribute('content') || '';
const withCsrfHeader = (headers = {}) => {
@@ -524,6 +525,9 @@
inProgress: payload.message || payload.step,
done: payload.message || payload.step
};
if (step.id === STEP_IDS.ACCOUNT_SETUP && payload.details?.sessionSecret) {
sseSessionDetails = payload.details;
}
progressState.set(step.id, {
status: payload.status || STATUS.IN_PROGRESS,
message: payload.message,
@@ -599,7 +603,7 @@
});
if (!allDone) return;
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
const sessionDetails = accountState?.details;
const sessionDetails = sseSessionDetails || accountState?.details;
finalizeInstall();
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
@@ -740,7 +744,7 @@
}
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
const sessionDetails = accountState?.details;
const sessionDetails = sseSessionDetails || accountState?.details;
finalizeInstall();
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
+17 -1
View File
@@ -9,7 +9,8 @@
const {
INSTALLATION_STEPS,
clampStep,
isUpgradeMode
isUpgradeMode,
getEnabledDatabases
} = Context;
const {
@@ -79,6 +80,19 @@
}
};
const applyEnabledDatabases = (root) => {
const enabled = getEnabledDatabases?.() || [];
const radios = root.querySelectorAll('input[name="database"]');
radios.forEach((radio) => {
if (!enabled.includes(radio.value)) {
const card = radio.closest('.selector-card');
if (card) {
card.remove();
}
}
});
};
const bindDatabaseSelection = (root) => {
const radios = root.querySelectorAll('input[name="database"]');
radios.forEach((radio) => {
@@ -139,6 +153,8 @@
return;
}
applyEnabledDatabases(root);
const lockedDatabase = getLockedDatabase?.() || '';
if (lockedDatabase) {
lockDatabaseSelection(root, lockedDatabase);
@@ -7,8 +7,12 @@ $defaultHttpsPort = $defaultHttpsPort ?? '443';
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
$defaultAssistantOpenAIKey = $defaultAssistantOpenAIKey ?? '';
$defaultDatabase = $defaultDatabase ?? 'mongodb';
$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql'];
$selectedDatabase = $lockedDatabase ?: $defaultDatabase;
$isDatabaseLocked = !empty($lockedDatabase);
$mongoEnabled = in_array('mongodb', $enabledDatabases, true);
$mariaEnabled = in_array('mariadb', $enabledDatabases, true);
$postgresEnabled = in_array('postgresql', $enabledDatabases, true);
$mongoDisabled = $isDatabaseLocked && $selectedDatabase !== 'mongodb';
$mariaDisabled = $isDatabaseLocked && $selectedDatabase !== 'mariadb';
$postgresDisabled = $isDatabaseLocked && $selectedDatabase !== 'postgresql';
@@ -43,6 +47,7 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
<div class="input-group stack-xs">
<label class="label-text typography-text-m-500 text-neutral-secondary">Database</label>
<div class="selector-group<?php echo $isDatabaseLocked ? ' is-locked' : ''; ?>">
<?php if ($mongoEnabled) { ?>
<label class="selector-card <?php echo ($selectedDatabase === 'mongodb') ? 'selected' : ''; ?><?php echo $mongoDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mongoDisabled ? ' aria-disabled="true"' : ''; ?>>
<input type="radio" name="database" value="mongodb" <?php echo ($selectedDatabase === 'mongodb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mongoDisabled ? 'disabled' : ''; ?>>
<div class="selector-content">
@@ -54,7 +59,9 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
<?php } ?>
</label>
<?php } ?>
<?php if ($mariaEnabled) { ?>
<label class="selector-card <?php echo ($selectedDatabase === 'mariadb') ? 'selected' : ''; ?><?php echo $mariaDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mariaDisabled ? ' aria-disabled="true"' : ''; ?>>
<input type="radio" name="database" value="mariadb" <?php echo ($selectedDatabase === 'mariadb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mariaDisabled ? 'disabled' : ''; ?>>
<div class="selector-content">
@@ -66,7 +73,9 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
<?php } ?>
</label>
<?php } ?>
<?php if ($postgresEnabled) { ?>
<label class="selector-card <?php echo ($selectedDatabase === 'postgresql') ? 'selected' : ''; ?><?php echo $postgresDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $postgresDisabled ? ' aria-disabled="true"' : ''; ?>>
<input type="radio" name="database" value="postgresql" <?php echo ($selectedDatabase === 'postgresql') ? 'checked' : ''; ?> class="sr-only" <?php echo $postgresDisabled ? 'disabled' : ''; ?>>
<div class="selector-content">
@@ -78,6 +87,7 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
<?php } ?>
</label>
<?php } ?>
</div>
</div>
@@ -48,6 +48,16 @@ class Complete extends Action
@touch(Server::INSTALLER_COMPLETE_FILE);
if (!$sessionSecret && $installId !== '') {
$data = $state->readProgressFile($installId);
$details = $data['details'][Server::STEP_ACCOUNT_SETUP] ?? [];
if (!empty($details['sessionSecret'])) {
$sessionSecret = $details['sessionSecret'];
$sessionId = $sessionId ?: ($details['sessionId'] ?? '');
$sessionExpire = $sessionExpire ?: ($details['sessionExpire'] ?? '');
}
}
if ($sessionSecret) {
$isHttps = $request->getProtocol() === 'https';
$sameSite = $isHttps ? Response::COOKIE_SAMESITE_NONE : Response::COOKIE_SAMESITE_LAX;
@@ -127,6 +127,10 @@ class Install extends Action
$this->sendBadRequest($response, $swooleResponse, $wantsStream, 'Please select a supported database');
return;
}
if (!$config->isDatabaseEnabled($database)) {
$this->sendBadRequest($response, $swooleResponse, $wantsStream, 'The selected database is not available');
return;
}
}
$installId = $state->sanitizeInstallId($installId);
@@ -44,6 +44,7 @@ class View extends Action
$defaultHttpsPort = $config->getDefaultHttpsPort();
$isUpgrade = $config->isUpgrade();
$lockedDatabase = $config->getLockedDatabase();
$enabledDatabases = $config->getEnabledDatabases();
$isLocalInstall = $config->isLocal();
$defaultEmailCertificates = $vars['_APP_EMAIL_CERTIFICATES']['default'] ?? '';
@@ -14,6 +14,7 @@ final class Config
'isLocal',
'hostPath',
'lockedDatabase',
'enabledDatabases',
'vars',
];
@@ -26,6 +27,7 @@ final class Config
private bool $isLocal = false;
private ?string $hostPath = null;
private ?string $lockedDatabase = null;
private array $enabledDatabases = ['mongodb', 'mariadb'];
private array $vars = [];
public function __construct(array $values = [])
@@ -67,6 +69,9 @@ final class Config
if ($this->hasValidStringValue($values, 'lockedDatabase')) {
$this->setLockedDatabase((string) $values['lockedDatabase']);
}
if (array_key_exists('enabledDatabases', $values) && is_array($values['enabledDatabases'])) {
$this->setEnabledDatabases($values['enabledDatabases']);
}
if (array_key_exists('vars', $values) && is_array($values['vars'])) {
$this->setVars($values['vars']);
}
@@ -100,6 +105,7 @@ final class Config
'isLocal' => $this->isLocal,
'hostPath' => $this->hostPath,
'lockedDatabase' => $this->lockedDatabase,
'enabledDatabases' => $this->enabledDatabases,
];
}
@@ -202,4 +208,28 @@ final class Config
{
$this->lockedDatabase = $value;
}
/**
* @return string[]
*/
public function getEnabledDatabases(): array
{
return $this->enabledDatabases;
}
/**
* @param string[] $value
*/
public function setEnabledDatabases(array $value): void
{
$filtered = array_values(array_filter($value, fn ($v) => is_string($v) && $v !== ''));
if (!empty($filtered)) {
$this->enabledDatabases = $filtered;
}
}
public function isDatabaseEnabled(string $adapter): bool
{
return in_array($adapter, $this->enabledDatabases, true);
}
}
@@ -207,6 +207,53 @@ class State
return sys_get_temp_dir() . '/appwrite-install-' . $installId . '.json';
}
public function clearStaleLock(): void
{
$this->withGlobalLock(function ($handle, $lock) {
if (!$handle) {
return;
}
if ($lock === null) {
return;
}
if ($this->isGlobalLockActive($lock)) {
return;
}
ftruncate($handle, 0);
rewind($handle);
});
$tempDir = sys_get_temp_dir();
foreach ((array) glob($tempDir . '/appwrite-install-*.json') as $file) {
$contents = @file_get_contents($file);
if ($contents === false) {
continue;
}
$data = json_decode($contents, true);
if (!is_array($data)) {
@unlink($file);
continue;
}
$updatedAt = $data['updatedAt'] ?? 0;
$age = time() - (int) $updatedAt;
$isTerminal = isset($data['error']);
if (!$isTerminal && !empty($data['steps'])) {
$isTerminal = true;
foreach ($data['steps'] as $step) {
if (($step['status'] ?? '') !== Server::STATUS_COMPLETED) {
$isTerminal = false;
break;
}
}
}
if ($age > self::GLOBAL_LOCK_TIMEOUT_SECONDS) {
@unlink($file);
} elseif ($isTerminal && $age > 60) {
@unlink($file);
}
}
}
public function reserveGlobalLock(string $installId): string
{
return (string) $this->withGlobalLock(function ($handle, $lock) use ($installId) {
@@ -127,6 +127,8 @@ class Server
private function startSwooleServer(string $host, int $port, ?string $readyFile = null): void
{
$this->state->clearStaleLock();
// Preload static files into memory
$files = new Files();
$files->load($this->paths['views']);
+18 -3
View File
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Tasks;
use Appwrite\Docker\Compose;
use Appwrite\Docker\Env;
use Appwrite\Platform\Installer\Runtime\State;
use Appwrite\Platform\Installer\Server as InstallerServer;
use Appwrite\Utopia\View;
use Utopia\Auth\Proofs\Password;
@@ -23,7 +24,7 @@ class Install extends Action
private const int WEB_SERVER_CHECK_DELAY_SECONDS = 1;
private const int HEALTH_CHECK_ATTEMPTS = 30;
private const int HEALTH_CHECK_DELAY_SECONDS = 3;
private const int HEALTH_CHECK_DELAY_SECONDS = 1;
private const string PATTERN_ENV_VAR_NAME = '/^[A-Z0-9_]+$/';
private const string PATTERN_DB_PASSWORD_VAR = '/^_APP_DB_.*_PASS$/';
@@ -176,6 +177,13 @@ class Install extends Action
}
}
$installerConfig = $this->readInstallerConfig();
$enabledDatabases = $installerConfig['enabledDatabases'] ?? ['mongodb', 'mariadb'];
if (!in_array($database, $enabledDatabases, true)) {
Console::error("Database '{$database}' is not available. Available options: " . implode(', ', $enabledDatabases));
Console::exit(1);
}
// If interactive and web mode enabled, start web server
if ($interactive === 'Y' && Console::isInteractive()) {
Console::success('Starting web installer...');
@@ -295,6 +303,12 @@ class Install extends Action
@unlink(InstallerServer::INSTALLER_COMPLETE_FILE);
$state = new State([]);
$state->clearStaleLock();
$installerConfig = $this->readInstallerConfig();
$enabledDatabases = $installerConfig['enabledDatabases'] ?? ['mongodb', 'mariadb'];
$this->setInstallerConfig([
'defaultHttpPort' => $defaultHttpPort,
'defaultHttpsPort' => $defaultHttpsPort,
@@ -304,6 +318,7 @@ class Install extends Action
'vars' => $vars,
'isUpgrade' => $isUpgrade,
'lockedDatabase' => $lockedDatabase,
'enabledDatabases' => $enabledDatabases,
'isLocal' => $this->isLocalInstall(),
'hostPath' => $this->hostPath ?: null,
]);
@@ -734,8 +749,8 @@ class Install extends Action
{
$client = new Client();
$client
->setTimeout(5000)
->setConnectTimeout(5000)
->setTimeout(2000)
->setConnectTimeout(2000)
->addHeader('Host', $domain);
$healthPath = '/v1/health/version';
@@ -20,6 +20,7 @@ class ConfigTest extends TestCase
$this->assertFalse($config->isLocal());
$this->assertNull($config->getHostPath());
$this->assertNull($config->getLockedDatabase());
$this->assertEquals(['mongodb', 'mariadb'], $config->getEnabledDatabases());
$this->assertEmpty($config->getVars());
}
@@ -412,6 +413,7 @@ class ConfigTest extends TestCase
'isLocal',
'hostPath',
'lockedDatabase',
'enabledDatabases',
];
foreach ($expectedKeys as $key) {
@@ -435,6 +437,7 @@ class ConfigTest extends TestCase
$this->assertFalse($array['isLocal']);
$this->assertNull($array['hostPath']);
$this->assertNull($array['lockedDatabase']);
$this->assertEquals(['mongodb', 'mariadb'], $array['enabledDatabases']);
}
public function testMultipleApplyCallsAccumulate(): void
@@ -485,4 +488,67 @@ class ConfigTest extends TestCase
$this->assertCount(1, $vars);
$this->assertEquals('B', $vars[0]['name']);
}
public function testDefaultEnabledDatabases(): void
{
$config = new Config();
$this->assertEquals(['mongodb', 'mariadb'], $config->getEnabledDatabases());
$this->assertTrue($config->isDatabaseEnabled('mongodb'));
$this->assertTrue($config->isDatabaseEnabled('mariadb'));
$this->assertFalse($config->isDatabaseEnabled('postgresql'));
}
public function testSetEnabledDatabases(): void
{
$config = new Config();
$config->setEnabledDatabases(['mongodb', 'mariadb', 'postgresql']);
$this->assertEquals(['mongodb', 'mariadb', 'postgresql'], $config->getEnabledDatabases());
$this->assertTrue($config->isDatabaseEnabled('postgresql'));
}
public function testSetEnabledDatabasesFiltersInvalid(): void
{
$config = new Config();
$config->setEnabledDatabases(['mongodb', '', null, 42, 'mariadb']);
$this->assertEquals(['mongodb', 'mariadb'], $config->getEnabledDatabases());
}
public function testSetEnabledDatabasesEmptyArrayPreservesExisting(): void
{
$config = new Config();
$config->setEnabledDatabases(['mongodb', 'postgresql']);
$config->setEnabledDatabases([]);
$this->assertEquals(['mongodb', 'postgresql'], $config->getEnabledDatabases());
}
public function testApplyEnabledDatabases(): void
{
$config = new Config();
$config->apply(['enabledDatabases' => ['mongodb', 'mariadb', 'postgresql']]);
$this->assertEquals(['mongodb', 'mariadb', 'postgresql'], $config->getEnabledDatabases());
$this->assertTrue($config->isDatabaseEnabled('postgresql'));
}
public function testApplyEnabledDatabasesNonArrayIgnored(): void
{
$config = new Config();
$config->apply(['enabledDatabases' => 'mongodb']);
$this->assertEquals(['mongodb', 'mariadb'], $config->getEnabledDatabases());
}
public function testEnabledDatabasesInToArray(): void
{
$config = new Config();
$config->setEnabledDatabases(['mongodb']);
$array = $config->toArray();
$this->assertEquals(['mongodb'], $array['enabledDatabases']);
}
public function testEnabledDatabasesRoundTrip(): void
{
$config = new Config();
$config->setEnabledDatabases(['mongodb', 'postgresql']);
$rebuilt = new Config($config->toArray());
$this->assertEquals(['mongodb', 'postgresql'], $rebuilt->getEnabledDatabases());
}
}