Skip to content

feat(nextcloud): trigger endpoint for bulk migrations#4728

Open
rezk2ll wants to merge 2 commits intomasterfrom
feat/nextcloud-migration
Open

feat(nextcloud): trigger endpoint for bulk migrations#4728
rezk2ll wants to merge 2 commits intomasterfrom
feat/nextcloud-migration

Conversation

@rezk2ll
Copy link
Copy Markdown
Contributor

@rezk2ll rezk2ll commented Apr 13, 2026

Summary

Adds POST /remote/nextcloud/migration to start a Nextcloud-to-Cozy bulk migration from the Settings UI.

The endpoint probes the supplied credentials against the Nextcloud OCS cloud/user endpoint, upserts an io.cozy.accounts document with the resolved WebDAV user ID, creates an io.cozy.nextcloud.migrations tracking document in pending state, and publishes a nextcloud.migration.requested command to the new migration RabbitMQ exchange.

A separate migration service consumes the command and drives the existing /remote/nextcloud/:account/* routes to perform the transfer, writing progress back to the tracking document.

The Settings UI monitors the document in real time to display progress. Only one migration can be in flight per instance; failed migrations do not block new attempts.

API

POST /remote/nextcloud/migration (requires POST io.cozy.nextcloud.migrations)

{
  "nextcloud_url": "https://nextcloud.example.com",
  "nextcloud_login": "alice",
  "nextcloud_app_password": "xxxxx-xxxxx-xxxxx-xxxxx-xxxxx",
  "source_path": "/"
}

source_path is optional and defaults to /. The password should be a Nextcloud app password.

201 Created

{
  "data": {
    "id": "d4e5f6a7b8c94d0ea1b2c3d4e5f6a7b8",
    "type": "io.cozy.nextcloud.migrations",
    "attributes": {
      "status": "pending",
      "target_dir": "/Nextcloud",
      "progress": {
        "files_imported": 0,
        "files_total": 0,
        "bytes_imported": 0,
        "bytes_total": 0
      },
      "errors": [],
      "skipped": [],
      "started_at": null,
      "finished_at": null
    }
  }
}

Error responses

Code When
401 Unauthorized The Nextcloud server rejected the supplied credentials
409 Conflict A pending or running migration already exists for this instance
500 Internal Server Error Conflict check, account upsert, or tracking document creation failed
502 Bad Gateway The Nextcloud instance is unreachable
503 Service Unavailable RabbitMQ publish failed; the tracking document is marked failed before returning

RabbitMQ contract

Exchange migration, routing key nextcloud.migration.requested. Payload (the consumer is responsible for declaring its queue and binding):

{
  "migrationId": "d4e5f6a7b8c94d0ea1b2c3d4e5f6a7b8",
  "workplaceFqdn": "alice.cozy.example.com",
  "accountId": "a1b2c3d4e5f6",
  "sourcePath": "/",
  "timestamp": 1712563200
}

Credentials are never in the payload; they live in the io.cozy.accounts document referenced by accountId. MessageID is set to the migration ID for cross-system tracing.

Credentials probe

The OCS probe itself is not new. It already existed as a lazy fallback behind (nc *NextCloud).fetchUserID(), called from fillUserID only when an existing Nextcloud account document was missing its cached webdav_user_id. In practice that path was almost never exercised, so a latent bug sat there undetected: the probe targeted /ocs/v2.php/apps/user_status/api/v1/user_status, and any non-200 response (including a 404 from a managed Nextcloud host that strips the optional user_status app) was turned into webdav.ErrInvalidAuth.

What this PR adds is the synchronous, user-facing path: the migration trigger endpoint calls FetchUserIDWithCredentials on the caller-supplied credentials before persisting anything, so the probe is now on the critical path of every trigger. That exposure turned the latent classification bug into a user-visible "credentials are invalid" 401 for anyone pointing the feature at a managed Nextcloud like thegood.cloud.

The follow-up commit on this branch fixes it by switching the probe to OCS Core (/ocs/v2.php/cloud/user, which cannot be disabled by an admin) and narrowing the auth-failure classification to 401/403 only, so other non-2xx statuses bubble up with their real cause instead of being silently relabelled. Both code paths (the old lazy fallback and the new synchronous call) benefit from the fix.

Validated end-to-end against a real Nextcloud instance: the probe, the encrypted-at-rest account persistence, the tracking document, and the RabbitMQ publish with no credentials in the payload all behave as documented.

@rezk2ll rezk2ll requested a review from a team as a code owner April 13, 2026 11:21
Twake users migrating from Nextcloud need a way to import their entire
file tree into their Cozy. Konnectors and stack workers are bounded by
job timeouts, so the heavy lifting has to live outside the stack. This
endpoint is the user-facing entry point: it validates the credentials
synchronously, persists the account, creates a tracking document, and
hands the work off to a separate migration service via RabbitMQ. The
service drives the existing /remote/nextcloud/:account/* routes and
updates the tracking document, which the Settings UI watches over
realtime.
@rezk2ll rezk2ll force-pushed the feat/nextcloud-migration branch from 641380f to e01ad4c Compare April 13, 2026 11:24
Managed Nextcloud providers (e.g. thegood.cloud) strip the optional
user_status app, so the probe returned 404 on otherwise-valid
instances. Combined with the coarse "non-200 = invalid auth"
classifier, every migration request against these hosts surfaced
as a misleading 401 "credentials invalid".

Probe /ocs/v2.php/cloud/user (OCS Core, cannot be disabled) and
narrow auth-failure classification to 401/403 only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant