Fixed upgrade crash.

Thanks to poc-sm and Claude !

Fixes #6367
This commit is contained in:
Lauri Ojansivu committed 2026-06-06 02:53:58 +03:00
1 parent 15a598525f
commit 39f3c89b0a
30 files changed
+556 -137

No files matched your search

+90 -52
View File
@@ -28,7 +28,7 @@ Versions:
# Upcoming WeKan ® release
This release [fixes](https://github.com/wekan/wekan/commit/357de728c03113b787065bac2c5832ad77f1a117) the following CRITICAL SECURITY ISSUES:
This release adds the following [CRITICAL SECURITY FIXES](https://github.com/wekan/wekan/commit/357de728c03113b787065bac2c5832ad77f1a117):
- [Fix GHSA-qfqv-42qw-vvwh: `cloneBoard` Meteor method has no authorization check — any user can clone (read) any private board by ID (CWE-639, CWE-862)](https://github.com/wekan/wekan/security/advisories/GHSA-qfqv-42qw-vvwh).
The `cloneBoard` Meteor method in `models/import.js` copied an entire board —
@@ -128,34 +128,8 @@ and, while auditing that client-side permission checks are also enforced server-
behind global admin. Now requires global admin.
Thanks to Claude.
and adds the following updates:
and, in the same access-control audit, the attachment write API was tightened:
- [Update Windows MongoDB](https://github.com/wekan/wekan/commit/beedf2fefa614018ce7ed499465092e814a050d9).
Thanks to xet7.
- [Update DDP transport and reactivity order for multi-tenant deployment](https://github.com/wekan/wekan/pull/6365).
Thanks to italojs.
- [Updated to Meteor 3.5-rc.1](https://github.com/wekan/wekan/commit/29b3f52e50ea2ee9bfbfb98d3842ce8a9b8e52e3).
Thanks to Meteor developers.
- [Fixed OpenAPI REST API documentation generation](https://github.com/wekan/wekan/commit/c71a97cba20247ce227a288fa74ef23cb3c4c83e),
which had been broken
since after WeKan v7.93 and only generated docs for the `login`/`register`
endpoints (2 operations) instead of the full API. The Meteor 3 migration moved
the REST routes from `models/*.js` (`JsonRoutes.add(...)`) into
`server/models/*.js` (`WebApp.handlers.get/post/put/delete(...)`) and
introduced optional chaining (`?.`) that the `esprima` Python parser cannot
read, so `openapi/generate_openapi.py` silently skipped every route file.
The generator now understands both routing styles, scans both `models/` and
`server/models/`, downlevels modern JS syntax so files parse, handles the
`type: Array` SimpleSchema idiom, and `releases/rebuild-docs.sh` works directly
with Python 3.12.x (PEP 668). The generated `public/api/wekan.yml` /
`wekan.html` now cover the full API again (89 operations / 61 paths).
Thanks to Claude.
- **Documented the attachment / file REST API in the OpenAPI docs** — file
upload, download, info, listing a board's files, copy, move and delete (the
`/api/attachment/*` endpoints registered via `WebApp.handlers.use()`, which the
generator cannot auto-discover) are now documented in `openapi/extra_paths.yml`
and injected into the generated docs, including their authentication and
permission requirements. Thanks to Claude.
- **Security: attachment write operations in the REST/DDP API now require board
write access, not just membership (CWE-862, CWE-639).** Both attachment API
implementations (`server/routes/attachmentApi.js` HTTP routes and
@@ -167,10 +141,9 @@ and adds the following updates:
admins still allowed); read operations (download / list / info) keep
membership-level access. Added security tests in
`server/lib/tests/attachmentApi.tests.js`. Thanks to Claude.
- **The attachment copy API now honours the admin "Admin Panel / Attachments"
API transfer limits.** Copying an attachment creates a new attachment but
skipped the `apiUploadBlocked` / `apiUploadMaxBytes` checks that upload
enforces; copy now respects them in both API implementations. Thanks to Claude.
and adds the following new features:
- **SVG image uploads are now sanitized instead of rejected.** Uploaded SVGs
(attachments and avatars) are cleaned in place in `onAfterUpload` via the new
`models/lib/sanitizeSvg.js`, which removes JavaScript (`<script>`, inline
@@ -178,26 +151,6 @@ and adds the following updates:
`<iframe>` / `<object>` / `<embed>` and similar active content) and XML loops
(`<!DOCTYPE>` / `<!ENTITY>` entity-expansion / XXE constructs and
`<?xml-stylesheet?>`), so SVG images can be uploaded safely. Thanks to Claude.
- [Fixed Admin Panel / Attachments / Move Attachment doing nothing / crashing](https://github.com/wekan/wekan/commit/d30803276d6d06db44f65ee1fb583cc11aac8b7b).
Several issues in the bulk move:
- The GridFS source matcher required `meta.gridFsFileId` to be *absent*, but
Meteor-Files always sets it, so selecting "MongoDB Meteor-Files" matched zero
files and "nothing happened". The matcher now recognizes real GridFS files
(`versions.*.storage === 'gridfs'` or a `meta.gridFsFileId` reference), and a
"nothing to move" message is shown when a source is empty instead of silently
doing nothing.
- Moving files crashed the whole server with
`FilesCollection#findOne() not available in server` — `ReactiveCache.getAttachment`
used a synchronous `findOne()`; it now uses `findOneAsync()`. The background
job is also hardened so a single failing file is skipped instead of crashing
the server via an unhandled rejection.
- The attachment **copy** API now honours the admin API upload limits (it
previously skipped them).
- **Fixed the "MongoDB Meteor-Files" file-count statistic** in Admin Panel /
Attachments, which counted *every* attachment metadata document (so files on
the Filesystem were wrongly reported as being in GridFS). It now counts only
attachments actually stored in GridFS, consistent with the move tool. Also
renamed the mislabeled "Mongo-Files" column to "Meteor-Files". Thanks to Claude.
- **Unified attachment/avatar storage migration: move any → any.** Admin Panel /
Attachments / Move Attachment can now move **Attachments, Avatars, or both**,
from **any source to any destination** across Filesystem, Meteor-Files GridFS,
@@ -218,6 +171,91 @@ and adds the following updates:
factory now carries its collection (`Attachments` or `Avatars`), so **avatars
can be stored in Meteor-Files GridFS and cloud**, not only on the filesystem.
Attachment behavior is unchanged. Thanks to Claude.
and adds the following updates:
- [Update Windows MongoDB](https://github.com/wekan/wekan/commit/beedf2fefa614018ce7ed499465092e814a050d9).
Thanks to xet7.
- [Update DDP transport and reactivity order for multi-tenant deployment](https://github.com/wekan/wekan/pull/6365).
Thanks to italojs.
- [Updated to Meteor 3.5-rc.1](https://github.com/wekan/wekan/commit/29b3f52e50ea2ee9bfbfb98d3842ce8a9b8e52e3).
Thanks to Meteor developers.
- **Documented the attachment / file REST API in the OpenAPI docs** — file
upload, download, info, listing a board's files, copy, move and delete (the
`/api/attachment/*` endpoints registered via `WebApp.handlers.use()`, which the
generator cannot auto-discover) are now documented in `openapi/extra_paths.yml`
and injected into the generated docs, including their authentication and
permission requirements. Thanks to Claude.
and adds the following fixes:
- **[Fixed upgrade crash]() `An error occurred when creating an index for collection
"users": Topology is closed` / `MongoServerSelectionError: Server selection
timed out after 30000 ms`.** When WeKan started before MongoDB was reachable
and had an elected replica-set primary (common right after an upgrade, while
MongoDB replays its WiredTiger journal, or when the app container starts at the
same instant as the database), the first index creation threw and Node exited.
Now:
- **WeKan waits until MongoDB is ready** (reachable, with an elected primary)
before it starts, in every launch path: `start-wekan.sh`, `start-wekan.bat`,
the Snap (`snap-src/bin/wekan-control`), and the app itself
(`server/00waitForMongo.js` / `server/lib/mongoStartup.js`, which blocks the
first `Meteor.startup` so it also protects the plain Docker image). The Docker
`docker-compose.yml` and `docker-compose-multitenancy.yml` now give `wekandb`
a `healthcheck` (primary elected) and the WeKan/tenant services
`depends_on: condition: service_healthy`.
- **If MongoDB stays unreachable for too long** (default 120s, configurable via
`WEKAN_DB_WAIT_TIMEOUT`), a clear English-only message is printed to the
Node.js console / `docker logs` / `snap logs` explaining that a database
upgrade with `mongodump` (old MongoDB) and `mongorestore --drop` (new
MongoDB) may be needed, and reminding that attachments and avatars live
on disk under `WRITABLE_PATH` (`files`, `attachments`, `avatars`; on Snap
`/var/snap/wekan/common/files`, on Docker the `wekan-files` volume at
`/data`) and must be copied too. WeKan keeps retrying after printing it.
- **Index creation is now idempotent and crash-safe.** A new `ensureIndex`
helper checks the existing indexes and only creates the ones that are
missing, and never throws — a single index problem is logged in English
instead of taking the whole server down. All startup index creation across
the model files was switched to it.
Thanks to Claude.
- [Fixed OpenAPI REST API documentation generation](https://github.com/wekan/wekan/commit/c71a97cba20247ce227a288fa74ef23cb3c4c83e),
which had been broken
since after WeKan v7.93 and only generated docs for the `login`/`register`
endpoints (2 operations) instead of the full API. The Meteor 3 migration moved
the REST routes from `models/*.js` (`JsonRoutes.add(...)`) into
`server/models/*.js` (`WebApp.handlers.get/post/put/delete(...)`) and
introduced optional chaining (`?.`) that the `esprima` Python parser cannot
read, so `openapi/generate_openapi.py` silently skipped every route file.
The generator now understands both routing styles, scans both `models/` and
`server/models/`, downlevels modern JS syntax so files parse, handles the
`type: Array` SimpleSchema idiom, and `releases/rebuild-docs.sh` works directly
with Python 3.12.x (PEP 668). The generated `public/api/wekan.yml` /
`wekan.html` now cover the full API again (89 operations / 61 paths).
Thanks to Claude.
- **The attachment copy API now honours the admin "Admin Panel / Attachments"
API transfer limits.** Copying an attachment creates a new attachment but
skipped the `apiUploadBlocked` / `apiUploadMaxBytes` checks that upload
enforces; copy now respects them in both API implementations. Thanks to Claude.
- [Fixed Admin Panel / Attachments / Move Attachment doing nothing / crashing](https://github.com/wekan/wekan/commit/d30803276d6d06db44f65ee1fb583cc11aac8b7b).
Several issues in the bulk move:
- The GridFS source matcher required `meta.gridFsFileId` to be *absent*, but
Meteor-Files always sets it, so selecting "MongoDB Meteor-Files" matched zero
files and "nothing happened". The matcher now recognizes real GridFS files
(`versions.*.storage === 'gridfs'` or a `meta.gridFsFileId` reference), and a
"nothing to move" message is shown when a source is empty instead of silently
doing nothing.
- Moving files crashed the whole server with
`FilesCollection#findOne() not available in server` — `ReactiveCache.getAttachment`
used a synchronous `findOne()`; it now uses `findOneAsync()`. The background
job is also hardened so a single failing file is skipped instead of crashing
the server via an unhandled rejection.
- The attachment **copy** API now honours the admin API upload limits (it
previously skipped them).
- **Fixed the "MongoDB Meteor-Files" file-count statistic** in Admin Panel /
Attachments, which counted *every* attachment metadata document (so files on
the Filesystem were wrongly reported as being in GridFS). It now counts only
attachments actually stored in GridFS, consistent with the move tool. Also
renamed the mislabeled "Mongo-Files" column to "Meteor-Files". Thanks to Claude.
- **Read legacy CollectionFS attachments and avatars in place** (without
migrating). The backward-compatibility layer
(`models/lib/attachmentBackwardCompatibility.js`) was broken — it looked up the
+14 -2
View File
@@ -58,6 +58,16 @@ services:
sleep 2
done
wait
# Report healthy only once the replica set has an elected primary, so the
# tenant apps (depends_on: condition: service_healthy) do not start before
# MongoDB is ready and crash with "Topology is closed". start_period gives a
# large database time to replay its journal after an upgrade.
healthcheck:
test: ["CMD", "mongosh", "--host", "127.0.0.1", "--port", "27017", "--quiet", "--eval", "try { quit(rs.status().ok === 1 && db.hello().isWritablePrimary ? 0 : 1) } catch (e) { quit(1) }"]
interval: 10s
timeout: 10s
retries: 30
start_period: 60s
volumes:
- wekan-db:/data/db
- wekan-db-dump:/dump
@@ -76,7 +86,8 @@ services:
restart: always
network_mode: "host"
depends_on:
- wekandb
wekandb:
condition: service_healthy
environment:
- PORT=8081
- ROOT_URL=https://tenant1.example.com
@@ -100,7 +111,8 @@ services:
restart: always
network_mode: "host"
depends_on:
- wekandb
wekandb:
condition: service_healthy
environment:
- PORT=8082
- ROOT_URL=https://tenant2.example.com
+18 -1
View File
@@ -258,6 +258,17 @@ services:
- wekan-tier
expose:
- 27017
# Healthcheck: report healthy only once the replica set has an elected
# primary, so the wekan app (depends_on: condition: service_healthy) does
# not start before MongoDB is ready and crash with "Topology is closed".
# start_period gives a large database time to replay its journal after an
# upgrade before failures are counted.
healthcheck:
test: ["CMD", "mongosh", "--host", "127.0.0.1", "--port", "27017", "--quiet", "--eval", "try { quit(rs.status().ok === 1 && db.hello().isWritablePrimary ? 0 : 1) } catch (e) { quit(1) }"]
interval: 10s
timeout: 10s
retries: 30
start_period: 60s
volumes:
- /etc/localtime:/etc/localtime:ro
- wekan-db:/data/db
@@ -902,7 +913,13 @@ services:
# Wait spinner to use
# - WAIT_SPINNER=Bounce
#---------------------------------------------------------------------
#depends_on:
# Wait for MongoDB to be healthy (replica-set primary elected) before
# starting WeKan, so index creation does not crash with "Topology is closed".
# If you use FerretDB instead, comment out the wekandb block below and
# uncomment the wekan-ferretdb block.
depends_on:
wekandb:
condition: service_healthy
# wekan-migrate:
# condition: service_completed_successfully
# wekan-ferretdb:
+2 -1
View File
@@ -15,6 +15,7 @@ import AttachmentStorageSettings from './attachmentStorageSettings';
import Attachments, { normalizeRemovedFiles } from './attachments';
import Boards from '/models/boards';
import { allowIsBoardMember } from '/server/lib/utils';
import { ensureIndex } from '/server/lib/mongoStartup';
// ---------------------------------------------------------------------------
// Server-only configuration
@@ -396,7 +397,7 @@ Meteor.methods({
// ---------------------------------------------------------------------------
Meteor.startup(async () => {
await Attachments.collection.createIndexAsync({ 'meta.cardId': 1 });
await ensureIndex(Attachments, { 'meta.cardId': 1 });
// Ensure standard GridFS index on attachments.chunks for efficient chunk lookups.
// Without this, queries like find({files_id: ObjectId}) do full collection scans.
+16
View File
@@ -0,0 +1,16 @@
import { Meteor } from 'meteor/meteor';
import { waitForMongoReady } from '/server/lib/mongoStartup';
// ============================================================================
// MUST be the first server import (see server/imports.js) so this Meteor.startup
// hook is registered first and therefore runs before any other startup hook
// that creates indexes (server/models/users.js, collectionBootstrap.js, ...).
//
// It blocks startup until MongoDB is reachable with an elected replica-set
// primary, preventing the "An error occurred when creating an index ...
// Topology is closed" crash when WeKan starts before MongoDB is ready (common
// right after an upgrade while MongoDB replays its journal).
// ============================================================================
Meteor.startup(async () => {
await waitForMongoReady();
});
+4 -3
View File
@@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import AttachmentMigrationStatus from '/models/attachmentMigrationStatus';
import { ensureIndex } from '/server/lib/mongoStartup';
// Allow/Deny rules
// This collection is server-only and should not be modified by clients
@@ -12,7 +13,7 @@ AttachmentMigrationStatus.allow({
// Create indexes for better query performance
Meteor.startup(() => {
AttachmentMigrationStatus._collection.createIndexAsync({ boardId: 1 });
AttachmentMigrationStatus._collection.createIndexAsync({ userId: 1, boardId: 1 });
AttachmentMigrationStatus._collection.createIndexAsync({ updatedAt: -1 });
ensureIndex(AttachmentMigrationStatus, { boardId: 1 });
ensureIndex(AttachmentMigrationStatus, { userId: 1, boardId: 1 });
ensureIndex(AttachmentMigrationStatus, { updatedAt: -1 });
});
+15 -14
View File
@@ -6,6 +6,7 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import CronJobStatus from '/models/cronJobStatus';
import { ensureIndex } from '/server/lib/mongoStartup';
// Collections for persistent storage
export { CronJobStatus };
@@ -49,26 +50,26 @@ if (Meteor.isServer) {
if (Meteor.isServer) {
Meteor.startup(async () => {
// Index for job status queries
await CronJobStatus._collection.createIndexAsync({ jobId: 1 });
await CronJobStatus._collection.createIndexAsync({ status: 1 });
await CronJobStatus._collection.createIndexAsync({ createdAt: 1 });
await CronJobStatus._collection.createIndexAsync({ updatedAt: 1 });
await ensureIndex(CronJobStatus, { jobId: 1 });
await ensureIndex(CronJobStatus, { status: 1 });
await ensureIndex(CronJobStatus, { createdAt: 1 });
await ensureIndex(CronJobStatus, { updatedAt: 1 });
// Index for job steps queries
await CronJobSteps._collection.createIndexAsync({ jobId: 1 });
await CronJobSteps._collection.createIndexAsync({ stepIndex: 1 });
await CronJobSteps._collection.createIndexAsync({ status: 1 });
await ensureIndex(CronJobSteps, { jobId: 1 });
await ensureIndex(CronJobSteps, { stepIndex: 1 });
await ensureIndex(CronJobSteps, { status: 1 });
// Index for job queue queries
await CronJobQueue._collection.createIndexAsync({ priority: 1, createdAt: 1 });
await CronJobQueue._collection.createIndexAsync({ status: 1 });
await CronJobQueue._collection.createIndexAsync({ jobType: 1 });
await ensureIndex(CronJobQueue, { priority: 1, createdAt: 1 });
await ensureIndex(CronJobQueue, { status: 1 });
await ensureIndex(CronJobQueue, { jobType: 1 });
// Index for job errors queries
await CronJobErrors._collection.createIndexAsync({ jobId: 1, createdAt: -1 });
await CronJobErrors._collection.createIndexAsync({ stepId: 1 });
await CronJobErrors._collection.createIndexAsync({ severity: 1 });
await CronJobErrors._collection.createIndexAsync({ createdAt: -1 });
await ensureIndex(CronJobErrors, { jobId: 1, createdAt: -1 });
await ensureIndex(CronJobErrors, { stepId: 1 });
await ensureIndex(CronJobErrors, { severity: 1 });
await ensureIndex(CronJobErrors, { createdAt: -1 });
});
}
+5
View File
@@ -3,6 +3,11 @@
// Loaded by server/main.js after bootstrap (collectionHelpers) completes.
// ============================================================================
// ****IMPORTANT**** Wait for MongoDB to be ready BEFORE anything else, so the
// first index creation never crashes the server with "Topology is closed".
// Registers the first Meteor.startup hook, so it runs before index-creating ones.
import '/server/00waitForMongo';
// ****IMPORTANT**** Initialize upload directories BEFORE models are loaded
// This ensures ostrio:files can create necessary directories without permission errors
import '/server/initializeDirs';
+172
View File
@@ -0,0 +1,172 @@
import { MongoInternals } from 'meteor/mongo';
// ============================================================================
// WeKan MongoDB startup helpers
//
// Two problems this module solves, both seen when upgrading WeKan/MongoDB
// (e.g. the "Topology is closed" / "Server selection timed out" crash on
// startup, https://github.com/wekan/wekan/issues ):
//
// 1. WeKan must WAIT until MongoDB is actually reachable and has an elected
// replica-set primary before it starts creating indexes. Otherwise the
// first createIndex() throws and Node exits.
//
// 2. Index creation must be IDEMPOTENT: check the existing indexes and only
// add the ones that are missing, so an upgrade never crashes on an index
// that already exists (or has different options).
//
// All log messages here are intentionally English only (no i18n) so they are
// readable in `docker logs`, `snap logs wekan` and the Node.js terminal.
// ============================================================================
// How long to wait for MongoDB before printing the "you may need to upgrade
// the database with mongodump/mongorestore" guidance. We keep retrying after
// this; we just stop staying silent. Override with WEKAN_DB_WAIT_TIMEOUT
// (seconds).
const WAIT_HINT_AFTER_SECONDS = parseInt(
process.env.WEKAN_DB_WAIT_TIMEOUT || '120',
10,
);
const RETRY_INTERVAL_MS = 5000;
// Printed once, in English only, when MongoDB stays unreachable long enough
// that a database/MongoDB-version upgrade is the likely cause.
const UPGRADE_HINT = `
========================================================================
WeKan: still cannot connect to MongoDB.
If you just upgraded WeKan or MongoDB, the existing database may have been
created by an OLDER MongoDB version that the new MongoDB cannot open, so
MongoDB never finishes starting and WeKan keeps waiting here.
How to upgrade the database (dump with the old version, restore with the new):
1. Start the OLD, previously-working MongoDB version.
2. Back up: mongodump --archive=/dump/wekan.archive --gzip
3. Stop the old MongoDB. Start the NEW MongoDB version on an EMPTY data dir.
4. Restore: mongorestore --archive=/dump/wekan.archive --gzip --drop
5. Start WeKan again.
Attachments and avatars are stored ON DISK under WRITABLE_PATH, NOT in
MongoDB, so when moving to a new server ALSO copy that whole directory
(it contains "files", "attachments" and "avatars"):
- Snap: /var/snap/wekan/common/files
- Docker: the "wekan-files" volume mounted at /data
- Source: the WRITABLE_PATH you configured in start-wekan.sh
Also check the MongoDB logs (docker logs wekan-db / snap logs wekan.mongodb)
for the real reason, for example:
- incompatible featureCompatibilityVersion (step major versions one by one),
- "wrong mongod version" on the data files,
- an unclean shutdown that needs mongod --repair.
WeKan will keep retrying every ${RETRY_INTERVAL_MS / 1000}s in case MongoDB is
just slow to replay its journal.
========================================================================
`;
function getRawDb() {
// Available once the `mongo` package has loaded, which is before any
// application code runs.
return MongoInternals.defaultRemoteCollectionDriver().mongo.db;
}
// Resolve a node-mongodb rawCollection from a Meteor Mongo.Collection, a
// Meteor-Files FilesCollection (its Mongo.Collection is at `.collection`), or
// anything that already exposes rawCollection().
function getRawCollection(collection) {
if (!collection) return null;
if (typeof collection.rawCollection === 'function') {
return collection.rawCollection();
}
if (collection._collection && typeof collection._collection.rawCollection === 'function') {
return collection._collection.rawCollection();
}
if (collection.collection && typeof collection.collection.rawCollection === 'function') {
return collection.collection.rawCollection();
}
return null;
}
// One connectivity probe: must answer AND be a writable primary, because we
// are about to create indexes (a write). Throws on any failure.
async function probeMongo() {
const hello = await getRawDb().command({ hello: 1 });
if (!hello.isWritablePrimary) {
throw new Error('MongoDB reachable but replica-set primary not elected yet');
}
}
/**
* Block until MongoDB is reachable and a writable primary is available.
* Never rejects: it keeps retrying, and after WAIT_HINT_AFTER_SECONDS it prints
* the (English) mongodump/mongorestore upgrade guidance once.
*/
export async function waitForMongoReady() {
let waited = 0;
let hintShown = false;
// Fast path: already connected.
try {
await probeMongo();
return;
} catch (e) {
console.log(`WeKan: waiting for MongoDB to become ready (${e.message})...`);
}
// eslint-disable-next-line no-constant-condition
while (true) {
await new Promise(resolve => setTimeout(resolve, RETRY_INTERVAL_MS));
waited += RETRY_INTERVAL_MS / 1000;
try {
await probeMongo();
console.log('WeKan: MongoDB is ready.');
return;
} catch (e) {
if (!hintShown && waited >= WAIT_HINT_AFTER_SECONDS) {
console.error(UPGRADE_HINT);
hintShown = true;
} else {
console.log(`WeKan: MongoDB not ready yet (${e.message}), retrying in ${RETRY_INTERVAL_MS / 1000}s...`);
}
}
}
}
/**
* Idempotent index creation: look at the indexes that already exist on the
* collection and only create the index when no index with the same key pattern
* is present. Never throws — a single index problem must not crash startup.
*
* @param {Mongo.Collection|{rawCollection:Function}} collection
* @param {Object} keys index key spec, e.g. { boardId: 1 }
* @param {Object} [options] passed straight through to createIndex (unique, etc.)
* @returns {Promise<boolean>} true if an index was created, false if skipped
*/
export async function ensureIndex(collection, keys, options = {}) {
const raw = getRawCollection(collection);
if (!raw) {
console.error('WeKan ensureIndex: could not resolve rawCollection, skipping', keys);
return false;
}
const wanted = JSON.stringify(keys);
try {
let existing = [];
try {
existing = await raw.indexes();
} catch (e) {
// Collection may not exist yet — that is fine, createIndex will make it.
existing = [];
}
const alreadyThere = existing.some(idx => JSON.stringify(idx.key) === wanted);
if (alreadyThere) {
return false;
}
await raw.createIndex(keys, options);
return true;
} catch (e) {
// e.g. transient connection error or an option conflict on an existing
// index. Log in English and continue; do not crash the whole server.
console.error(
`WeKan ensureIndex: failed to ensure index ${wanted} on "${raw.collectionName}": ${e.message}`,
);
return false;
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import Actions from '/models/actions';
import { ensureIndex } from '/server/lib/mongoStartup';
Meteor.startup(async () => {
await Actions._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(Actions, { modifiedAt: -1 });
});
+9 -8
View File
@@ -7,6 +7,7 @@ import Cards from '/models/cards';
import Integrations from '/models/integrations';
import { RulesHelper } from '/server/rulesHelper';
import { Notifications } from '/server/notifications/notifications';
import { ensureIndex } from '/server/lib/mongoStartup';
function normalizeActivityText(value, fallback = '') {
return typeof value === 'string' ? value : fallback;
@@ -32,20 +33,20 @@ Activities.after.insert(async (userId, doc) => {
});
Meteor.startup(async () => {
await Activities._collection.createIndexAsync({ createdAt: -1 });
await Activities._collection.createIndexAsync({ modifiedAt: -1 });
await Activities._collection.createIndexAsync({ updatedAt: 1, deleted: 1 });
await Activities._collection.createIndexAsync({ cardId: 1, createdAt: -1 });
await Activities._collection.createIndexAsync({ boardId: 1, createdAt: -1 });
await Activities._collection.createIndexAsync(
await ensureIndex(Activities, { createdAt: -1 });
await ensureIndex(Activities, { modifiedAt: -1 });
await ensureIndex(Activities, { updatedAt: 1, deleted: 1 });
await ensureIndex(Activities, { cardId: 1, createdAt: -1 });
await ensureIndex(Activities, { boardId: 1, createdAt: -1 });
await ensureIndex(Activities,
{ commentId: 1 },
{ partialFilterExpression: { commentId: { $exists: true } } },
);
await Activities._collection.createIndexAsync(
await ensureIndex(Activities,
{ attachmentId: 1 },
{ partialFilterExpression: { attachmentId: { $exists: true } } },
);
await Activities._collection.createIndexAsync(
await ensureIndex(Activities,
{ customFieldId: 1 },
{ partialFilterExpression: { customFieldId: { $exists: true } } },
);
+4 -3
View File
@@ -16,6 +16,7 @@ import Swimlanes from '/models/swimlanes';
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
import Triggers from '/models/triggers';
import Users from '/models/users';
import { ensureIndex } from '/server/lib/mongoStartup';
const getTAPi18n = () => require('/imports/i18n').TAPi18n;
@@ -327,15 +328,15 @@ Boards.before.insert(async (userId, doc) => {
});
Meteor.startup(async () => {
await Boards._collection.createIndexAsync({ modifiedAt: -1 });
await Boards._collection.createIndexAsync(
await ensureIndex(Boards, { modifiedAt: -1 });
await ensureIndex(Boards,
{
_id: 1,
'members.userId': 1,
},
{ unique: true },
);
await Boards._collection.createIndexAsync({ 'members.userId': 1 });
await ensureIndex(Boards, { 'members.userId': 1 });
});
Boards.after.insert(async (userId, doc) => {
+3 -2
View File
@@ -5,6 +5,7 @@ import { sendJsonResult } from '/server/apiMiddleware';
import { ReactiveCache } from '/imports/reactiveCache';
import Activities from '/models/activities';
import CardComments from '/models/cardComments';
import { ensureIndex } from '/server/lib/mongoStartup';
async function commentCreation(userId, doc) {
const card = await ReactiveCache.getCard(doc.cardId);
@@ -24,8 +25,8 @@ async function commentCreation(userId, doc) {
}
Meteor.startup(async () => {
await CardComments._collection.createIndexAsync({ modifiedAt: -1 });
await CardComments._collection.createIndexAsync({ cardId: 1, createdAt: -1 });
await ensureIndex(CardComments, { modifiedAt: -1 });
await ensureIndex(CardComments, { cardId: 1, createdAt: -1 });
});
CardComments.after.insert(async (userId, doc) => {
+6 -5
View File
@@ -21,6 +21,7 @@ import Cards, {
updateActivities,
} from '/models/cards';
import Lists from '/models/lists';
import { ensureIndex } from '/server/lib/mongoStartup';
Meteor.methods({
async createCardWithDueDate(boardId, listId, title, dueDate, swimlaneId) {
@@ -399,11 +400,11 @@ Meteor.methods({
});
Meteor.startup(async () => {
await Cards._collection.createIndexAsync({ modifiedAt: -1 });
await Cards._collection.createIndexAsync({ updatedAt: 1, deleted: 1 });
await Cards._collection.createIndexAsync({ boardId: 1, createdAt: -1 });
await Cards._collection.createIndexAsync({ boardId: 1, listId: 1 });
await Cards._collection.createIndexAsync({ parentId: 1 });
await ensureIndex(Cards, { modifiedAt: -1 });
await ensureIndex(Cards, { updatedAt: 1, deleted: 1 });
await ensureIndex(Cards, { boardId: 1, createdAt: -1 });
await ensureIndex(Cards, { boardId: 1, listId: 1 });
await ensureIndex(Cards, { parentId: 1 });
Meteor.defer(() => {
addCronJob();
});
+5 -4
View File
@@ -11,12 +11,13 @@ import ChecklistItems, {
publishChekListUncompleted,
} from '/models/checklistItems';
import Activities from '/models/activities';
import { ensureIndex } from '/server/lib/mongoStartup';
Meteor.startup(async () => {
await ChecklistItems._collection.createIndexAsync({ modifiedAt: -1 });
await ChecklistItems._collection.createIndexAsync({ updatedAt: 1, deleted: 1 });
await ChecklistItems._collection.createIndexAsync({ checklistId: 1 });
await ChecklistItems._collection.createIndexAsync({ cardId: 1 });
await ensureIndex(ChecklistItems, { modifiedAt: -1 });
await ensureIndex(ChecklistItems, { updatedAt: 1, deleted: 1 });
await ensureIndex(ChecklistItems, { checklistId: 1 });
await ensureIndex(ChecklistItems, { cardId: 1 });
});
ChecklistItems.after.update(async (userId, doc, fieldNames) => {
+3 -2
View File
@@ -7,6 +7,7 @@ import { allowIsBoardMemberByCard } from '/server/lib/utils';
import Checklists from '/models/checklists';
import ChecklistItems from '/models/checklistItems';
import Activities from '/models/activities';
import { ensureIndex } from '/server/lib/mongoStartup';
Meteor.methods({
async moveChecklist(checklistId, newCardId) {
@@ -66,8 +67,8 @@ Meteor.methods({
});
Meteor.startup(async () => {
await Checklists._collection.createIndexAsync({ modifiedAt: -1 });
await Checklists._collection.createIndexAsync({ cardId: 1, createdAt: 1 });
await ensureIndex(Checklists, { modifiedAt: -1 });
await ensureIndex(Checklists, { cardId: 1, createdAt: 1 });
});
Checklists.after.insert(async (userId, doc) => {
+20 -18
View File
@@ -16,14 +16,15 @@ import InviteToBoardRolesSettings, {
} from '/models/inviteToBoardRolesSettings';
import Triggers from '/models/triggers';
import UnsavedEditCollection from '/models/unsavedEdits';
import { ensureIndex } from '/server/lib/mongoStartup';
Meteor.startup(async () => {
await AccessibilitySettings._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(AccessibilitySettings, { modifiedAt: -1 });
if (!(await AccessibilitySettings.findOneAsync({}))) {
await AccessibilitySettings.insertAsync({ enabled: false, sort: 0 });
}
await AccountSettings._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(AccountSettings, { modifiedAt: -1 });
await AccountSettings.upsertAsync(
{ _id: 'accounts-allowEmailChange' },
{ $setOnInsert: { booleanValue: false, sort: 0 } },
@@ -37,18 +38,19 @@ Meteor.startup(async () => {
{ $setOnInsert: { booleanValue: false, sort: 0 } },
);
await Announcements._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(Announcements, { modifiedAt: -1 });
if (!(await Announcements.findOneAsync({}))) {
await Announcements.insertAsync({ enabled: false, sort: 0 });
}
await CardCommentReactions._collection.createIndexAsync(
await ensureIndex(
CardCommentReactions,
{ cardCommentId: 1 },
{ unique: true },
);
await InvitationCodes._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(InvitationCodes, { modifiedAt: -1 });
await LockoutSettings._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(LockoutSettings, { modifiedAt: -1 });
await LockoutSettings.upsertAsync(
{ _id: 'known-failuresBeforeLockout' },
{
@@ -117,31 +119,31 @@ Meteor.startup(async () => {
},
);
await OrgUser._collection.createIndexAsync({ orgId: -1 });
await OrgUser._collection.createIndexAsync({ orgId: -1, userId: -1 });
await ensureIndex(OrgUser, { orgId: -1 });
await ensureIndex(OrgUser, { orgId: -1, userId: -1 });
await Presences._collection.createIndexAsync({ serverId: -1 });
await ensureIndex(Presences, { serverId: -1 });
await PositionHistory._collection.createIndexAsync({ boardId: 1, entityType: 1, entityId: 1 });
await PositionHistory._collection.createIndexAsync({ boardId: 1, entityType: 1 });
await PositionHistory._collection.createIndexAsync({ createdAt: -1 });
await ensureIndex(PositionHistory, { boardId: 1, entityType: 1, entityId: 1 });
await ensureIndex(PositionHistory, { boardId: 1, entityType: 1 });
await ensureIndex(PositionHistory, { createdAt: -1 });
await Rules._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(Rules, { modifiedAt: -1 });
await TableVisibilityModeSettings._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(TableVisibilityModeSettings, { modifiedAt: -1 });
await TableVisibilityModeSettings.upsertAsync(
{ _id: 'tableVisibilityMode-allowPrivateOnly' },
{ $setOnInsert: { booleanValue: false, sort: 0 } },
);
await InviteToBoardRolesSettings._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(InviteToBoardRolesSettings, { modifiedAt: -1 });
await InviteToBoardRolesSettings.upsertAsync(
{ _id: INVITE_TO_BOARD_ROLES_ID },
{ $setOnInsert: { allowedRoles: INVITE_TO_BOARD_ROLES_DEFAULT, sort: 0 } },
);
await Triggers._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(Triggers, { modifiedAt: -1 });
await UnsavedEditCollection._collection.createIndexAsync({ modifiedAt: -1 });
await UnsavedEditCollection._collection.createIndexAsync({ userId: 1 });
await ensureIndex(UnsavedEditCollection, { modifiedAt: -1 });
await ensureIndex(UnsavedEditCollection, { userId: 1 });
});
+3 -2
View File
@@ -7,6 +7,7 @@ import { ReactiveCache } from '/imports/reactiveCache';
import Activities from '/models/activities';
import Cards from '/models/cards';
import CustomFields from '/models/customFields';
import { ensureIndex } from '/server/lib/mongoStartup';
async function customFieldCreation(userId, doc) {
await Activities.insertAsync({
@@ -40,8 +41,8 @@ async function customFieldEdit(userId, doc) {
}
Meteor.startup(async () => {
await CustomFields._collection.createIndexAsync({ modifiedAt: -1 });
await CustomFields._collection.createIndexAsync({ boardIds: 1 });
await ensureIndex(CustomFields, { modifiedAt: -1 });
await ensureIndex(CustomFields, { boardIds: 1 });
});
CustomFields.after.insert(async (userId, doc) => {
+3 -2
View File
@@ -5,10 +5,11 @@ import { sendJsonResult } from '/server/apiMiddleware';
import { ReactiveCache } from '/imports/reactiveCache';
import Integrations from '/models/integrations';
import { validateAttachmentUrl } from '/models/lib/attachmentUrlValidation';
import { ensureIndex } from '/server/lib/mongoStartup';
Meteor.startup(async () => {
await Integrations._collection.createIndexAsync({ modifiedAt: -1 });
await Integrations._collection.createIndexAsync({ boardId: 1 });
await ensureIndex(Integrations, { modifiedAt: -1 });
await ensureIndex(Integrations, { boardId: 1 });
});
WebApp.handlers.get('/api/boards/:boardId/integrations', async function(req, res) {
+5 -4
View File
@@ -8,6 +8,7 @@ import Activities from '/models/activities';
import Boards from '/models/boards';
import Cards from '/models/cards';
import Lists from '/models/lists';
import { ensureIndex } from '/server/lib/mongoStartup';
const hasBoardWriteAccess = (userId, board) => {
if (!userId || !board) {
@@ -432,10 +433,10 @@ Meteor.methods({
});
Meteor.startup(async () => {
await Lists._collection.rawCollection().createIndex({ modifiedAt: -1 });
await Lists._collection.rawCollection().createIndex({ updatedAt: 1, deleted: 1 });
await Lists._collection.rawCollection().createIndex({ boardId: 1 });
await Lists._collection.rawCollection().createIndex({ archivedAt: -1 });
await ensureIndex(Lists, { modifiedAt: -1 });
await ensureIndex(Lists, { updatedAt: 1, deleted: 1 });
await ensureIndex(Lists, { boardId: 1 });
await ensureIndex(Lists, { archivedAt: -1 });
});
Lists.after.insert(async (userId, doc) => {
+2 -1
View File
@@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveCache } from '/imports/reactiveCache';
import Org from '/models/org';
import { ensureIndex } from '/server/lib/mongoStartup';
Meteor.methods({
async setCreateOrg(
@@ -207,5 +208,5 @@ Meteor.methods({
});
Meteor.startup(async () => {
await Org._collection.createIndexAsync({ orgDisplayName: 1 });
await ensureIndex(Org, { orgDisplayName: 1 });
});
+2 -1
View File
@@ -7,6 +7,7 @@ import { ServiceConfiguration } from 'meteor/service-configuration';
import Settings from '/models/settings';
import InvitationCodes from '/models/invitationCodes';
import EmailLocalization from '/server/lib/emailLocalization';
import { ensureIndex } from '/server/lib/mongoStartup';
const getReactiveCache = () => require('/imports/reactiveCache').ReactiveCache;
const getTAPi18n = () => require('/imports/i18n').TAPi18n;
@@ -108,7 +109,7 @@ function isApiEnabled() {
}
Meteor.startup(async () => {
await Settings._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(Settings, { modifiedAt: -1 });
const setting = await getReactiveCache().getCurrentSetting();
if (!setting) {
const now = new Date();
+3 -2
View File
@@ -7,6 +7,7 @@ import { ReactiveCache } from '/imports/reactiveCache';
import Swimlanes from '/models/swimlanes';
import Activities from '/models/activities';
import Cards from '/models/cards';
import { ensureIndex } from '/server/lib/mongoStartup';
Meteor.methods({
async ensureDefaultSwimlane(boardId) {
@@ -37,8 +38,8 @@ Meteor.methods({
});
Meteor.startup(async () => {
await Swimlanes._collection.createIndexAsync({ modifiedAt: -1 });
await Swimlanes._collection.createIndexAsync({ boardId: 1 });
await ensureIndex(Swimlanes, { modifiedAt: -1 });
await ensureIndex(Swimlanes, { boardId: 1 });
});
Swimlanes.after.insert(async (userId, doc) => {
+2 -1
View File
@@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveCache } from '/imports/reactiveCache';
import Team from '/models/team';
import { ensureIndex } from '/server/lib/mongoStartup';
Meteor.methods({
async setCreateTeam(
@@ -185,5 +186,5 @@ Meteor.methods({
});
Meteor.startup(async () => {
await Team._collection.createIndexAsync({ teamDisplayName: 1 });
await ensureIndex(Team, { teamDisplayName: 1 });
});
+2 -1
View File
@@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import Translation from '/models/translation';
import { ensureIndex } from '/server/lib/mongoStartup';
const getReactiveCache = () => require('/imports/reactiveCache').ReactiveCache;
@@ -50,5 +51,5 @@ Meteor.methods({
});
Meteor.startup(async () => {
await Translation._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(Translation, { modifiedAt: -1 });
});
+6 -5
View File
@@ -3,6 +3,7 @@ import { check } from 'meteor/check';
import Boards from '/models/boards';
import UserPositionHistory from '/models/userPositionHistory';
import { ReactiveCache } from '/imports/reactiveCache';
import { ensureIndex } from '/server/lib/mongoStartup';
// Reject callers who are not allowed to see the board. Mirrors the guard used
// by the sibling positionHistory.track* methods (server/methods/positionHistory.js).
@@ -17,11 +18,11 @@ const requireBoardVisible = async (userId, boardId) => {
};
Meteor.startup(async () => {
await UserPositionHistory._collection.createIndexAsync({ userId: 1, boardId: 1, createdAt: -1 });
await UserPositionHistory._collection.createIndexAsync({ userId: 1, entityType: 1, entityId: 1 });
await UserPositionHistory._collection.createIndexAsync({ userId: 1, isCheckpoint: 1 });
await UserPositionHistory._collection.createIndexAsync({ batchId: 1 });
await UserPositionHistory._collection.createIndexAsync({ createdAt: 1 });
await ensureIndex(UserPositionHistory, { userId: 1, boardId: 1, createdAt: -1 });
await ensureIndex(UserPositionHistory, { userId: 1, entityType: 1, entityId: 1 });
await ensureIndex(UserPositionHistory, { userId: 1, isCheckpoint: 1 });
await ensureIndex(UserPositionHistory, { batchId: 1 });
await ensureIndex(UserPositionHistory, { createdAt: 1 });
});
UserPositionHistory.trackChange = async function(options) {
+3 -2
View File
@@ -11,6 +11,7 @@ import { debounce } from '/imports/lib/collectionHelpers';
import { Authentication } from '/server/authentication';
import { sendJsonResult } from '/server/apiMiddleware';
import EmailLocalization from '/server/lib/emailLocalization';
import { ensureIndex } from '/server/lib/mongoStartup';
import ImpersonatedUsers from '/models/impersonatedUsers';
import Boards from '/models/boards';
import InvitationCodes from '/models/invitationCodes';
@@ -993,9 +994,9 @@ const startNotificationCleanup = debounce(
Meteor.startup(async () => {
for (const value of allowedSortValues) {
await Lists._collection.createIndexAsync(value);
await ensureIndex(Lists, value);
}
await Users._collection.createIndexAsync({ modifiedAt: -1 });
await ensureIndex(Users, { modifiedAt: -1 });
Users.find({ 'profile.avatarUrl': { $regex: '/cfs/files/avatars/' } }).forEach(doc => {
doc.profile.avatarUrl = doc.profile.avatarUrl.replace(
'/cfs/files/avatars/',
+37
View File
@@ -45,11 +45,48 @@ if [ "true" != "${DISABLE_MONGODB}" ]; then
MONGO_BIN="$SNAP/bin/mongosh"
MONGO_PORT="${MONGODB_PORT:-27019}"
MONGO_HOST="${MONGODB_BIND_IP:-127.0.0.1}"
# After WEKAN_DB_WAIT_TIMEOUT seconds (default 120) print the English upgrade
# guidance once, then keep retrying (large databases can take minutes to
# replay the WiredTiger journal after a snap update).
WEKAN_DB_WAIT_TIMEOUT=${WEKAN_DB_WAIT_TIMEOUT:-120}
db_waited=0
db_hint_shown=false
until "$MONGO_BIN" --host "$MONGO_HOST" --port "$MONGO_PORT" --quiet \
--eval "try { quit(rs.status().ok === 1 && rs.isMaster().ismaster ? 0 : 1) } catch(e) { quit(1) }" \
>/dev/null 2>&1; do
if [ "$db_hint_shown" != "true" ] && [ "$db_waited" -ge "$WEKAN_DB_WAIT_TIMEOUT" ]; then
echo ""
echo "========================================================================"
echo "WeKan: still cannot connect to MongoDB."
echo ""
echo "If you just upgraded WeKan or MongoDB, the existing database may have"
echo "been created by an OLDER MongoDB version that the new MongoDB cannot"
echo "open, so MongoDB never finishes starting and WeKan keeps waiting here."
echo ""
echo "Upgrade the database - dump with the old version, restore with the new:"
echo " 1. Start the OLD, previously-working MongoDB version."
echo " 2. Back up: mongodump --archive=wekan.archive --gzip"
echo " 3. Stop old MongoDB. Start the NEW MongoDB on an EMPTY data dir."
echo " 4. Restore: mongorestore --archive=wekan.archive --gzip --drop"
echo " 5. Start WeKan again."
echo ""
echo "Attachments and avatars are stored ON DISK under WRITABLE_PATH, NOT"
echo "in MongoDB, so when moving to a new server ALSO copy that whole"
echo "directory (it contains 'files', 'attachments' and 'avatars'):"
echo " - Snap: /var/snap/wekan/common/files"
echo " - Docker: the 'wekan-files' volume mounted at /data"
echo " - Source: the WRITABLE_PATH you configured"
echo ""
echo "Also check the MongoDB log (snap logs ${SNAP_NAME:-wekan}.mongodb) for"
echo "the real reason: incompatible featureCompatibilityVersion, wrong mongod"
echo "version, or an unclean shutdown needing 'mongod --repair'."
echo "========================================================================"
echo ""
db_hint_shown=true
fi
echo "MongoDB not ready yet, retrying in 5 seconds..."
sleep 5
db_waited=$((db_waited + 5))
done
echo "MongoDB replica set primary is ready."
fi
+52
View File
@@ -749,6 +749,58 @@ REM #node --stack-size=65500 main.js
REM #-------------------- OPTIONAL SETTINGS END --------------------
REM #-------------------- WAIT FOR MONGODB TO BE READY --------------------
REM # Do not start WeKan until MongoDB is reachable and a primary is elected,
REM # otherwise the first index creation crashes with "Topology is closed".
REM # After WEKAN_DB_WAIT_TIMEOUT seconds (default 120) the English upgrade
REM # guidance is printed once; WeKan keeps retrying after that.
IF "%WEKAN_DB_WAIT_TIMEOUT%"=="" SET WEKAN_DB_WAIT_TIMEOUT=120
SET DB_WAITED=0
SET DB_HINT_SHOWN=false
ECHO Waiting for MongoDB to be ready at 127.0.0.1:27017...
:wait_for_mongodb
mongosh --port 27017 --quiet --eval "try { quit(db.hello().isWritablePrimary ? 0 : 1) } catch (e) { quit(1) }" >NUL 2>NUL
IF %ERRORLEVEL% EQU 0 (
ECHO MongoDB is ready.
GOTO mongodb_ready
)
IF "%DB_HINT_SHOWN%"=="false" IF %DB_WAITED% GEQ %WEKAN_DB_WAIT_TIMEOUT% (
ECHO.
ECHO ========================================================================
ECHO WeKan: still cannot connect to MongoDB.
ECHO.
ECHO If you just upgraded WeKan or MongoDB, the existing database may have
ECHO been created by an OLDER MongoDB version that the new MongoDB cannot
ECHO open, so MongoDB never finishes starting and WeKan keeps waiting here.
ECHO.
ECHO Upgrade the database - dump with the old version, restore with the new:
ECHO 1. Start the OLD, previously-working MongoDB version.
ECHO 2. Back up: mongodump --archive=wekan.archive --gzip
ECHO 3. Stop old MongoDB. Start the NEW MongoDB on an EMPTY data dir.
ECHO 4. Restore: mongorestore --archive=wekan.archive --gzip --drop
ECHO 5. Start WeKan again.
ECHO.
ECHO Attachments and avatars are stored ON DISK under WRITABLE_PATH, NOT
ECHO in MongoDB, so when moving to a new server ALSO copy that whole
ECHO directory - it contains 'files', 'attachments' and 'avatars':
ECHO - Snap: /var/snap/wekan/common/files
ECHO - Docker: the 'wekan-files' volume mounted at /data
ECHO - Source: the WRITABLE_PATH configured in start-wekan.bat
ECHO.
ECHO Also check the MongoDB log for the real reason: incompatible
ECHO featureCompatibilityVersion, wrong mongod version, or an unclean
ECHO shutdown needing 'mongod --repair'. WeKan keeps retrying every 5s.
ECHO ========================================================================
ECHO.
SET DB_HINT_SHOWN=true
)
ECHO MongoDB not ready yet, retrying in 5 seconds...
timeout /t 5 /nobreak >NUL
SET /A DB_WAITED=%DB_WAITED%+5
GOTO wait_for_mongodb
:mongodb_ready
REM #----------------------------------------------------------------------
ECHO Starting Wekan in a persistent cmd loop...
:start_wekan
+48
View File
@@ -30,6 +30,54 @@
USE_CHANGE_STREAMS=false
fi
#----------------------------------------------------------------------
#-------------------- WAIT FOR MONGODB TO BE READY --------------------
# Do not start WeKan until MongoDB is reachable and a primary is elected,
# otherwise the first index creation crashes with "Topology is closed".
# After WEKAN_DB_WAIT_TIMEOUT seconds (default 120) the upgrade guidance
# below is printed once (English only); WeKan keeps retrying after that.
if command -v mongosh > /dev/null 2>&1; then
WEKAN_DB_WAIT_TIMEOUT=${WEKAN_DB_WAIT_TIMEOUT:-120}
db_waited=0
db_hint_shown=false
echo "Waiting for MongoDB to be ready at 127.0.0.1:27017..."
until mongosh --port 27017 --quiet --eval 'try { quit(db.hello().isWritablePrimary ? 0 : 1) } catch (e) { quit(1) }' > /dev/null 2>&1; do
if [ "$db_hint_shown" != "true" ] && [ "$db_waited" -ge "$WEKAN_DB_WAIT_TIMEOUT" ]; then
echo ""
echo "========================================================================"
echo "WeKan: still cannot connect to MongoDB."
echo ""
echo "If you just upgraded WeKan or MongoDB, the existing database may have"
echo "been created by an OLDER MongoDB version that the new MongoDB cannot"
echo "open, so MongoDB never finishes starting and WeKan keeps waiting here."
echo ""
echo "Upgrade the database (dump with the old version, restore with the new):"
echo " 1. Start the OLD, previously-working MongoDB version."
echo " 2. Back up: mongodump --archive=wekan.archive --gzip"
echo " 3. Stop old MongoDB. Start the NEW MongoDB on an EMPTY data dir."
echo " 4. Restore: mongorestore --archive=wekan.archive --gzip --drop"
echo " 5. Start WeKan again."
echo ""
echo "Attachments and avatars are stored ON DISK under WRITABLE_PATH, NOT"
echo "in MongoDB, so when moving to a new server ALSO copy that whole"
echo "directory (it contains 'files', 'attachments' and 'avatars'):"
echo " - Snap: /var/snap/wekan/common/files"
echo " - Docker: the 'wekan-files' volume mounted at /data"
echo " - Source: the WRITABLE_PATH configured in start-wekan.sh"
echo ""
echo "Also check the MongoDB log for the real reason (incompatible"
echo "featureCompatibilityVersion, wrong mongod version, or an unclean"
echo "shutdown needing 'mongod --repair'). WeKan keeps retrying every 5s."
echo "========================================================================"
echo ""
db_hint_shown=true
fi
echo "MongoDB not ready yet, retrying in 5 seconds..."
sleep 5
db_waited=$((db_waited + 5))
done
echo "MongoDB is ready."
fi
#----------------------------------------------------------------------
cd .build/bundle
#-------------------- USING MONGODB CHANGE STREAMS WITH REPLICA SETS AT CURRENT DATABASE --------------------
# If you would not like to use Change Streams and replica set for improving speed, change to use polling: