1 parent
15a598525f
commit
39f3c89b0a
30 files changed
+556
-137
No files matched your search
+90
-52
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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
@@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -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 } } },
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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/',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user