Commit Graph
278 Commits
Author SHA1 Message Date
kolaente 8f9b9aba68 feat(migration): index the columns that reference stored files
Adds indexes on task_attachments.file_id, users.avatar_file_id,
users.export_file_id, projects.background_file_id and
migration_status.upload_file_id.
2026-11-05 15:42:45 +01:00
kolaente e3acf407fb fix(export): export related tasks as references
The user data export embedded every related task in full, repeating its
description once per relation. Related tasks are now exported as id and
project id only, since every related task is part of the export anyway.

The importer resolves such references across all imported projects and
creates relations once every project exists, so relations between
projects survive the round trip instead of duplicating the related task.
Exports with the full related task shape import as before.
2026-11-05 15:42:45 +01:00
kolaente 1f79a8e01a fix(migration): reject repeated csv column mappings, cap labels per row
- Reject a config that maps a column more than once, already when the
  import is started or previewed.
- Import repeated labels of a row once and reject rows with more than
  MaxLabelsPerTask (100) labels.
- Count items while converting rows so a file over the import item limit
  fails before the whole structure is built.
- Walk mappings sorted by column so a row only visits the cells it has.
2026-11-05 15:42:45 +01:00
kolaente 8725e89725 refactor(utils): share rune-safe string truncation 2026-11-05 15:42:45 +01:00
kolaente 462a76a466 fix(migration): cap planka names copied into every task and comment
Custom field, group and user names are repeated per value or comment, so one long name multiplied the description size.
2026-11-05 15:42:45 +01:00
kolaente deeee79834 fix(migration): import repeated planka lists and cards only once
Lists, cards, checklists and attachments are looked up by id while
converting a board, so a repeated id imported the whole group once per
copy. Keep the first entry of each id and stop the conversion as soon as
the import item limit is exceeded instead of after building everything.
2026-11-05 15:42:45 +01:00
kolaente d73b5e4963 fix(migration): mask credentials in urls of failed migration errors
Request urls are masked before a failed migration is logged or reported.
2026-11-05 15:42:45 +01:00
kolaente 886ce004c2 feat(migration): add a helper to mask credentials in error urls 2026-11-05 15:42:45 +01:00
kolaente 3830d2277c fix(migration): cap views per project on import 2026-11-03 23:41:40 +01:00
kolaente 1dca7b52e8 fix(migration): count views and per-view task positions in import item limit 2026-11-03 23:41:40 +01:00
kolaente 02ed6b293f fix(tasks): only create import relations between tasks of the imported batch
Review finding: CreateTaskRelationsForImport relied on an unenforced
invariant that every related task came from the same import. It now takes
the imported batch, builds the relations itself and rejects related tasks
outside the batch.
2026-11-03 21:04:05 +01:00
kolaente 64f542c1a1 perf(migration): check the imported task hierarchy for cycles once instead of per relation 2026-11-03 21:04:05 +01:00
kolaente 852e6d6d40 fix(migration): clean up stored import blobs when the import panics 2026-11-03 19:33:30 +01:00
kolaente ae77326e17 fix(migration): skip null positions and task buckets on import 2026-11-03 19:33:30 +01:00
kolaente 43da8bc1c6 fix(migration): check filters.json item limit before decoding saved filters 2026-11-03 18:28:14 +01:00
kolaente e6c3b9a943 fix(migration): explain blocked private planka hosts
Planka instances on a private network were rejected by the SSRF guard with
a generic "could not reach" error. Return a dedicated error pointing at
outgoingrequests.allownonroutableips instead.

Refs #4234
2026-10-23 14:50:36 +02:00
kolaente e7a2daa3ff fix(background): decode backgrounds before opening the write session
Address review decision: a background upload queued for decode capacity
held its transaction for up to 30s. The permission check now runs in a
short read session, the image is decoded and encoded without a session,
and a fresh write session re-checks permission and stores the result.
2026-10-20 23:05:30 +02:00
kolaente dc2b7ae37d fix(files): drop the file row when storing its content fails
Address pr-swarm finding: avatar sync on login no longer rolls back on a
store error, so a blobless files row would be committed.
2026-10-20 23:05:30 +02:00
kolaente 1ce3df2d60 refactor(imageutils): queue decodes on the caller's context
Address pr-swarm finding: avatar, background and OIDC/LDAP decodes passed
context.Background(), so a disconnected client stayed queued for up to 30s.
2026-10-20 23:05:30 +02:00
kolaente 01b0b10a73 fix(migration): count vikunja export items before decoding data.json
Address pr-swarm finding: the item cap ran only after data.json was fully
decoded into tasks, so a file of millions of bare objects still exhausted
memory.
2026-10-20 15:13:02 +02:00
kolaente a89f7dd596 fix(migration): bind oauth migration callbacks to the user who requested the auth url 2026-10-20 15:13:02 +02:00
kolaente 22374d73a5 fix(migration): bound todoist and microsoft todo api responses and item counts 2026-10-20 15:13:02 +02:00
kolaente bf881194ca fix(migration): cap trello and todoist downloads at files.maxsize 2026-10-20 15:13:02 +02:00
kolaente 1852e3b873 fix(migration): reject wekan boards with duplicate ids or too many items 2026-10-20 15:13:02 +02:00
kolaente bf8859bab3 fix(migration): cap the number of items one import may create 2026-10-20 15:13:02 +02:00
kolaente 757f00910e fix(migration): cap the entry count of vikunja export archives 2026-10-20 15:13:02 +02:00
kolaente dc3853feef fix(migration): dispatch events after importing saved filters 2026-10-20 13:34:51 +02:00
kolaente f5e6632b93 fix(migration): escape planka names in generated markdown
Checklist, custom field and link attachment names were concatenated raw
into the description markdown, so brackets, parens or HTML in names broke
links or were dropped.
2026-10-19 21:06:10 +02:00
kolaente 78288f9cbd fix(migration): escape trello link attachment url and name
Link attachments were concatenated raw into the description HTML, so a
quote in the URL or < / & in the name mangled the description.
2026-10-19 21:06:10 +02:00
kolaente d617a7a807 fix(migration): escape checklist titles in trello and wekan imports
Checklist and item titles were concatenated raw into the task description
HTML, so titles containing < or & were mangled.
2026-10-19 17:33:57 +02:00
kolaente f2d84ca6fc fix(migration): decode utf-16 and drop nul bytes in csv imports
A CSV saved as UTF-16 (Excel's "Unicode Text") was read as UTF-8, leaving
a NUL between every character. MySQL and SQLite store that, Postgres text
columns reject it with SQLSTATE 22021 and the whole import fails.

The file is now decoded to UTF-8 when read: a UTF-16 BOM switches to
UTF-16 decoding, remaining NUL bytes are dropped and invalid UTF-8 becomes
U+FFFD, matching what encoding/json does for the JSON importers.

Fixes API-CLOUD-4G
2026-10-13 21:45:53 +02:00
kolaente c7ff79c99c refactor(migration): read csv imports through one helper
Detect, preview and migrate each repeated the size checks and file read.
Extracting readCSVFile gives the raw bytes a single entry point, and
projectsFromCSV makes the migrate conversion testable without a database.
2026-10-13 21:45:53 +02:00
kolaenteandTink 0909a337d3 fix(migration): route Trello API requests through the outgoing client 2026-09-30 18:26:56 +02:00
kolaenteandTink 80a9f31cb3 fix(migration): remove the partial blob of a failed import upload write
The files row is inserted before the blob is written, and the local backend creates the file before copying into it, so a failed copy left a partial blob behind once the row was rolled back.
2026-09-15 17:57:00 +02:00
kolaenteandTink c5685e6130 fix(migration): keep a queued import upload out of the storage quota
The vikunja-file import checks the user's stored files against
migration.vikunjafile.maxuserstorage. The upload is now one of those
files, so an import counted its own zip against the quota. Exclude files
referenced as a migration upload.
2026-09-15 17:57:00 +02:00
kolaenteandTink fa9fc4e06f feat(migration): keep queued import uploads in the file storage
The upload was spooled to the receiving instance's temp dir and only its
name travelled with the event. That holds while events stay in-process,
but it ties the job to the instance that accepted the upload.

Store it through the files package on the configured backend and record
its id on the migration status, so the event carries only the status id
and any instance can run the import. The job copies it to a local temp
file first: the zip importer needs random access, which an S3 object body
cannot give.

The upload is removed once the import is over. An hourly cron deletes
leftovers from events lost on restart or instances that died mid-import.
2026-09-15 17:57:00 +02:00
kolaenteandTink 9c374843af refactor(migration): tidy the ticktick, wekan and csv read paths
Drop comments restating the code and inline single-use variables. No
behaviour change.
2026-09-15 16:59:37 +02:00
kolaenteandTink 3dc5c72904 refactor(migration): parse the csv import config once per request
The config was unmarshalled up front purely to produce a 400, then parsed
again by SetOptions, and a third time in the background job. Drop the
throwaway parse and give SetOptions a domain error that maps to 400 on
its own.
2026-09-15 16:59:37 +02:00
kolaenteandTink e06345e2d3 fix(migration): run file imports in the background
A file import ran inside the upload request. A big export takes minutes,
far longer than a reverse proxy holds a request open, so the client got a
502 while the import kept running and kept the user's migration slot, and
there was no way to cancel or retry it.

The upload request now only validates what is cheap to validate, claims
the slot, spools the upload into a per-process temp directory and queues a
FileMigrationRequestedEvent. FileMigrationListener runs the import through
the lifecycle it now shares with the OAuth migrations: stale event skip,
panic recovery, heartbeat, outcome and notifications. The vikunja-file
migrator rejects a non-zip or structurally broken export before queueing;
ticktick, wekan and csv would have to parse the whole upload, so they fail
in the job and report through the status and failure mail. The csv
migrator receives its column mapping as options carried with the event.
2026-09-15 16:59:37 +02:00
kolaenteandTink c7b37b0af8 refactor(migration): scan the vikunja-file archive once before importing
Split opening the zip and indexing its entries out of Migrate, so every
structural check that doesn't need entry contents runs in one place before
anything is imported. The missing VERSION check moves into that scan.
2026-09-15 16:59:37 +02:00
kolaenteandTink 84804e71f5 feat(migration): record whether a migration succeeded
A finished migration_status row looked the same whether the import worked
or failed, so the client could only guess. Rows now carry error_kind, a
closed set the client translates (reported, interrupted, credentials,
queue, upload, detail), and error_message, set only for "detail": the
error caused by the user's own data, the same text the failure mail sends.
A failure we report to Sentry stores just the kind, so server internals
never reach the status.
2026-09-15 16:59:37 +02:00
kolaenteandTink b5a235b0fd fix(migration): don't report a failed success mail as a failed migration
The migration was already finished and its claim released when the done
notification failed, yet the error was returned and the listener then sent
a failure mail and overwrote the finished row. Log it instead. The failure
mail's own log line also printed the migration error instead of the
notification error.
2026-09-15 16:59:37 +02:00
kolaenteandTink 7f9f3e091f fix(migration): log migration start and completion at info level
A background import that never finished left no trace in a default log
setup, since start and completion were only logged at debug.
2026-09-15 16:59:37 +02:00
kolaenteandTink 68ac9eaa99 fix(migration): judge an abandoned claim by a heartbeat, not by start time
The claim timeout was measured from started_at, so a long import either
kept a dead claim for a day (the old 24h default) or had its slot taken
over while still running. A running migration now refreshes heartbeat_at
at a tenth of the timeout, clamped to 1s..30s, and a claim only counts as
stale once its last beat is older than the timeout. Rows that never beat
fall back to started_at. The default timeout drops to 5m accordingly.

Also bind the stale cutoff in the configured timezone: xorm writes times in
the engine's zone but binds query args as-is, which broke the comparison on
SQLite.
2026-09-15 16:59:37 +02:00
kolaenteandTink 86289e36e3 fix(migration): answer 412 instead of 500 when a migration claim collides
A second import sent while the first claim was still being written could
fail the insert with whatever the driver reports under contention, which
surfaced as a 500. The claim now reads the running row before writing so a
losing request stays read-only, re-reads the live claim whenever the insert
fails, retries lock collisions a few times, and probes for a stale claim
before taking a write lock to release it.
2026-09-15 16:59:37 +02:00
kolaente b263affe93 fix(import): create imported tasks in one batch with preserved indexes
Top-level and related tasks are collected first and created in one batch
so preset indexes survive, related references resolve to the created
struct instead of a mutable id lookup, untitled relation stubs are skipped,
and positions and buckets remap through one old-id map.
2026-09-11 20:47:11 +02:00
Wouter BlancquaertandGitHub 3f079e7d6d fix(migration): preserve todoist task descriptions (#3819) 2026-09-08 08:23:34 +00:00
kolaente 7fa1c7db97 fix(sentry): group reported errors by the error, not the reporting frame
The api error handler, the poison queue logger and the migration listener all
captured through the same frame, so API-OSS-2P collected six unrelated MySQL
errors under one title. Set a derived fingerprint and an error.type tag on the
scope before capturing.
2026-09-06 10:51:30 +02:00
kolaente 93fa646485 fix(migration): return 400 instead of 500 for broken vikunja exports
Unsafe paths, unusable file ids, a missing or unparseable VERSION and
unreadable data/filter json all describe a broken upload, not a server
fault, but they were raw fmt.Errorf values and surfaced as a 500. They now
return ErrInvalidImportFile; an export from a version we can no longer
read gets its own ErrImportFromUnsupportedVersion (14013) so clients can
tell the two apart. The http messages stay generic and never echo a path
from the archive.
2026-09-06 00:12:30 +02:00
kolaente 4090a3de2a fix(migration)!: move zip/empty file error codes off the api token codes
ErrCodeNotAZipFile and ErrCodeFileIsEmpty shared 14001 and 14002 with
ErrCodeAPITokenInvalid and ErrCodeInvalidAPITokenPermission, so clients
mapping codes to messages rendered the api token text for a failed import.
The api token codes are the older ones, so the migration errors move to
14011 and 14012.

BREAKING CHANGE: a failed import now returns 14011 instead of 14001 and
14012 instead of 14002.
2026-09-06 00:12:30 +02:00