Add rename-user SQL template; make batch transaction handling robust against non-Postgres backends
- New 'rename-user' template (templates / template rename-user <old> <new>): best-effort uid rename across core tables (oc_users, oc_preferences, oc_group_user, oc_group_admin, oc_ldap_user_mapping, oc_share, oc_mounts, oc_storages). Explicitly NOT a supported Nextcloud operation - the generated script carries an in-line warning (as leading SQL comments) that app-specific tables (Talk, Calendar, Contacts, Mail, 2FA/WebAuthn...) are not covered and that the data directory must be renamed on disk manually, followed by occ files:scan --all. Documented the same caveat in both READMEs. - Hardened the round-2 transaction wrapping: beginTransaction()/commit()/ rollBack() are now wrapped in try/catch. PostgreSQL (our backend) has fully transactional DDL so this wasn't actually broken here, but Nextcloud also supports MySQL/MariaDB via the same IDBConnection, where DDL implicitly commits - on that backend a DDL statement in the batch would make a later commit()/rollBack() throw 'no active transaction' and previously that exception was unhandled (HTTP 500 instead of a clean JSON response). Now: if rollBack() itself fails after an error, we report rollbackFailed+warning instead of falsely claiming rolledBack, since earlier statements in that batch may already be permanently applied. If commit() fails with nothing to commit (already auto-committed), that's logged only, since the effects are already durably persisted.
This commit is contained in:
@@ -68,6 +68,15 @@ you are about to run, ideally against a non-critical row/user first.
|
||||
- The application is not a real interactive terminal and does not support long running tasks.
|
||||
So if your instance is pretty big, commands like `occ files:scan` will time out and fail.
|
||||
- Do not use `occ maintenance:mode --on`, obvious...
|
||||
- The `rename-user` SQL template is **best-effort only**: renaming a Nextcloud
|
||||
username is not an officially supported operation. It updates the core
|
||||
tables it knows about (`oc_users`, `oc_preferences`, `oc_group_user`,
|
||||
`oc_group_admin`, `oc_ldap_user_mapping`, `oc_share`, `oc_mounts`,
|
||||
`oc_storages`), but does **not** touch app-specific tables (Talk, Calendar,
|
||||
Contacts, Mail, two-factor/WebAuthn, etc.). You must also manually rename
|
||||
the user's data directory on disk (`data/<old> -> data/<new>`) with the web
|
||||
server stopped or in maintenance mode, then run `occ files:scan --all`.
|
||||
Back up the database first and test on a non-critical account.
|
||||
|
||||
## Deploying updates
|
||||
|
||||
|
||||
@@ -79,6 +79,16 @@ occ-команд.
|
||||
Поэтому на крупных инсталляциях команды вроде `occ files:scan` могут
|
||||
завершаться по таймауту с ошибкой.
|
||||
- Не используйте `occ maintenance:mode --on`, это очевидно...
|
||||
- Шаблон `rename-user` — это **best-effort**, а не официально поддерживаемая
|
||||
Nextcloud операция. Он обновляет только известные таблицы ядра
|
||||
(`oc_users`, `oc_preferences`, `oc_group_user`, `oc_group_admin`,
|
||||
`oc_ldap_user_mapping`, `oc_share`, `oc_mounts`, `oc_storages`), но **не**
|
||||
трогает таблицы сторонних приложений (Talk, Calendar, Contacts, Mail,
|
||||
двухфакторная аутентификация/WebAuthn и т.д.). Дополнительно нужно вручную
|
||||
переименовать каталог данных пользователя на диске (`data/<старый> ->
|
||||
data/<новый>`) при остановленном веб-сервере или в режиме обслуживания, а
|
||||
затем выполнить `occ files:scan --all`. Сначала сделайте бэкап базы и
|
||||
протестируйте на некритичном аккаунте.
|
||||
|
||||
## Обновление кода на сервере
|
||||
|
||||
|
||||
+38
@@ -103,6 +103,42 @@
|
||||
"SELECT * FROM oc_ldap_user_mapping WHERE owncloud_name = '" + v + "'"
|
||||
].join(';\n') + ';';
|
||||
}
|
||||
},
|
||||
'rename-user': {
|
||||
args: ['old_uid', 'new_uid'],
|
||||
description: 'ВНИМАНИЕ: не официальная операция Nextcloud. Переименовывает uid только в основных таблицах ядра — покрывает не всё, требует ручных доп. шагов (см. предупреждение в самом скрипте)',
|
||||
build: function (oldUid, newUid) {
|
||||
var o = escapeSqlString(oldUid);
|
||||
var n = escapeSqlString(newUid);
|
||||
// Предупреждение — только однострочные "--"-комментарии без ";" внутри,
|
||||
// поэтому splitStatements() (парный на бэкенде и здесь) не разобьёт их
|
||||
// как отдельные запросы, а stripLeadingComments() на бэкенде уберёт
|
||||
// этот блок перед определением типа самого первого запроса (SELECT).
|
||||
var warning =
|
||||
"-- WARNING rename is NOT an officially supported Nextcloud operation\n" +
|
||||
"-- This only updates core tables below - it does NOT cover app-specific\n" +
|
||||
"-- tables (Talk, Calendar, Contacts, Mail, two-factor, WebAuthn, etc.)\n" +
|
||||
"-- After running this you must ALSO, with the web server stopped or in\n" +
|
||||
"-- maintenance mode, rename the data directory on disk (data/" + o + " -> data/" + n + ")\n" +
|
||||
"-- and then run: occ files:scan --all\n" +
|
||||
"-- Back up the database first and test on a non-critical account\n";
|
||||
var statements = [
|
||||
"SELECT uid FROM oc_users WHERE uid = '" + o + "'",
|
||||
"SELECT uid FROM oc_users WHERE uid = '" + n + "'",
|
||||
"UPDATE oc_users SET uid = '" + n + "' WHERE uid = '" + o + "'",
|
||||
"UPDATE oc_preferences SET userid = '" + n + "' WHERE userid = '" + o + "'",
|
||||
"UPDATE oc_group_user SET uid = '" + n + "' WHERE uid = '" + o + "'",
|
||||
"UPDATE oc_group_admin SET uid = '" + n + "' WHERE uid = '" + o + "'",
|
||||
"UPDATE oc_ldap_user_mapping SET owncloud_name = '" + n + "' WHERE owncloud_name = '" + o + "'",
|
||||
"UPDATE oc_share SET uid_owner = '" + n + "' WHERE uid_owner = '" + o + "'",
|
||||
"UPDATE oc_share SET uid_initiator = '" + n + "' WHERE uid_initiator = '" + o + "'",
|
||||
"UPDATE oc_share SET share_with = '" + n + "' WHERE share_with = '" + o + "' AND share_type = 0",
|
||||
"UPDATE oc_mounts SET user_id = '" + n + "' WHERE user_id = '" + o + "'",
|
||||
"UPDATE oc_storages SET id = 'home::" + n + "' WHERE id = 'home::" + o + "'",
|
||||
"SELECT uid FROM oc_users WHERE uid = '" + n + "'"
|
||||
];
|
||||
return warning + statements.join(';\n') + ';';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -218,6 +254,8 @@
|
||||
});
|
||||
if (response.rolledBack) {
|
||||
term.echo('[[;#ff5555;]Batch failed partway through — all statements in this batch were rolled back.]');
|
||||
} else if (response.rollbackFailed) {
|
||||
term.echo('[[;#ff0000;]' + $.terminal.escape_formatting(response.warning || 'Batch failed and the rollback itself failed — earlier statements may have been permanently applied. Check manually.') + ']');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -259,10 +259,30 @@ class DbController extends Controller
|
||||
// выполненные в этой же пачке изменения откатываются, а не
|
||||
// остаются частично применёнными. SET (без LOCAL) не транзакционен
|
||||
// в PostgreSQL, поэтому откат не затрагивает current_setting().
|
||||
//
|
||||
// Про DDL: в PostgreSQL (наша БД) DDL — CREATE/ALTER/DROP TABLE и
|
||||
// т.п. — полностью транзакционен и откатывается вместе с остальными
|
||||
// изменениями пачки, поэтому здесь для Postgres проблемы нет. Но
|
||||
// Nextcloud работает и с MySQL/MariaDB через тот же IDBConnection, а
|
||||
// там DDL делает неявный COMMIT — если эта пачка выполнится на
|
||||
// MySQL-инстансе, commit()/rollBack() после такого DDL получат
|
||||
// исключение "no active transaction". Оборачиваем begin/commit/
|
||||
// rollBack в try/catch, чтобы это не превращалось в необработанное
|
||||
// исключение и HTTP 500 вместо аккуратного JSON-ответа.
|
||||
$results = [];
|
||||
$rolledBack = false;
|
||||
$rollbackFailed = false;
|
||||
$transactionStarted = false;
|
||||
|
||||
try {
|
||||
$this->db->beginTransaction();
|
||||
$transactionStarted = true;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('[occweb] beginTransaction() failed, proceeding without an explicit transaction: {error}', [
|
||||
'app' => 'occweb',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($queries as $query) {
|
||||
$normalized = $this->stripLeadingComments($query);
|
||||
@@ -321,20 +341,54 @@ class DbController extends Controller
|
||||
// Откатываем всю пачку и останавливаемся: не продолжаем
|
||||
// выполнять оставшиеся запросы (например, серию DELETE),
|
||||
// если один из предыдущих шагов не выполнился.
|
||||
if ($transactionStarted) {
|
||||
try {
|
||||
$this->db->rollBack();
|
||||
$rolledBack = true;
|
||||
} catch (\Exception $rollbackError) {
|
||||
// Транзакция уже закрыта не нами — например, неявным
|
||||
// COMMIT-ом от DDL-запроса на MySQL/MariaDB. Всё, что
|
||||
// выполнилось ДО этой точки в пачке, могло остаться
|
||||
// применённым навсегда — намеренно НЕ ставим
|
||||
// rolledBack в true, это было бы неправдой.
|
||||
$rollbackFailed = true;
|
||||
$this->logger->warning('[occweb] rollBack() failed after an error — earlier statements in this batch may already be permanently applied: {error}', [
|
||||
'app' => 'occweb',
|
||||
'error' => $rollbackError->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$rolledBack) {
|
||||
if (!$rolledBack && !$rollbackFailed && $transactionStarted) {
|
||||
try {
|
||||
$this->db->commit();
|
||||
}
|
||||
|
||||
return new JSONResponse([
|
||||
'success' => true,
|
||||
'rolledBack' => $rolledBack,
|
||||
'results' => $results
|
||||
} catch (\Exception $e) {
|
||||
// Транзакция уже закоммичена неявно (например, DDL-запросом
|
||||
// на MySQL/MariaDB) — эффекты уже сохранены, это не ошибка
|
||||
// выполнения самой пачки.
|
||||
$this->logger->info('[occweb] commit() had nothing to commit (likely auto-committed by a DDL statement): {error}', [
|
||||
'app' => 'occweb',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$response = [
|
||||
'success' => true,
|
||||
'rolledBack' => $rolledBack,
|
||||
'results' => $results
|
||||
];
|
||||
|
||||
if ($rollbackFailed) {
|
||||
$response['rollbackFailed'] = true;
|
||||
$response['warning'] = 'The batch failed partway through and the rollback itself failed (this can happen if an earlier '
|
||||
. 'statement, e.g. a DDL statement, implicitly committed on this database backend). Statements executed before the '
|
||||
. 'failure may have been permanently applied — check the results above and verify manually.';
|
||||
}
|
||||
|
||||
return new JSONResponse($response);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user