-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement Cloudreve to AsterDrive migration #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
967c39e
8110984
6cc083c
d7623ea
8116384
661b2b4
c1a495b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,129 @@ | ||
| # AsterDriveMigration | ||
|
|
||
| Design documents: | ||
|
|
||
| - [Cloudreve and AsterDrive field mapping](docs/cloudreve-to-asterdrive-field-mapping.md) | ||
| - [Migration architecture](docs/migration-architecture.md) | ||
|
|
||
| Database migration tool for moving a Cloudreve v4 installation to AsterDrive (AD). | ||
|
|
||
| ## Supported data | ||
|
|
||
| - Cloudreve groups to AD storage policy groups | ||
| - users and profiles, including quotas and disabled status | ||
| - local, S3, OSS, KS3, OBS and Tencent COS storage policies | ||
| - folders, files, physical entities/blobs and version history | ||
| - file/folder metadata, including Cloudreve v4 `tag:*` metadata as AD tags | ||
| - public shares and regenerated AD v2 direct-link mappings | ||
| - Cloudreve background tasks as non-executable terminal history | ||
|
|
||
| Cloudreve password hashes are not compatible with AD. The migration assigns the supplied temporary password to every migrated user and sets `must_change_password = true`. Passkeys, WebDAV credentials, two-factor secrets, OAuth grants, sessions and filesystem events are not migrated. | ||
|
|
||
| Cloudreve tasks are preserved only as terminal AD `system_runtime` history. Completed/error/canceled statuses are mapped to terminal AD statuses; queued/processing/suspending tasks are archived as canceled and are never resumed. | ||
|
|
||
| The tool reuses existing storage objects. For local storage, pass the directory from which Cloudreve entity paths should be resolved. Object-storage policies keep their existing bucket, endpoint, credentials and object keys. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| 1. Stop writes to Cloudreve and back up both databases. | ||
| 2. Create an empty AD database and run the AD database migrations against it. | ||
| 3. Do not start the AD application against the target database until this migration completes. | ||
|
|
||
| ## Check compatibility | ||
|
|
||
| ```powershell | ||
| cargo run -- check ` | ||
| --source-url "sqlite://C:/cloudreve/cloudreve.db?mode=ro" ` | ||
| --target-url "sqlite://C:/asterdrive/asterdrive.db" ` | ||
| --report-path "C:/migration/cloudreve-preflight.json" | ||
| ``` | ||
|
|
||
| The URLs can also be provided through `CLOUDREVE_DATABASE_URL` and `ASTERDRIVE_DATABASE_URL`. SQLite, MySQL and PostgreSQL URLs supported by SeaORM can be used. | ||
|
|
||
| ## Run migration | ||
|
|
||
| ```powershell | ||
| $env:ASTER_MIGRATION_DEFAULT_PASSWORD = "replace-with-a-strong-temporary-password" | ||
| $env:ASTER_DIRECT_LINK_SECRET = "use-the-same-auth.direct_link_secret-as-ad" | ||
|
|
||
| cargo run -- migrate ` | ||
| --source-url "sqlite://C:/cloudreve/cloudreve.db?mode=ro" ` | ||
| --target-url "sqlite://C:/asterdrive/asterdrive.db" ` | ||
| --local-base-path "C:/cloudreve" ` | ||
| --run-id "cloudreve-cutover-2026-07-13" ` | ||
| --report-path "C:/migration/cloudreve-to-ad.json" | ||
| ``` | ||
|
|
||
| `ASTER_DIRECT_LINK_SECRET` is optional only when there are no direct links to regenerate. When supplied, each active Cloudreve `direct_links` row is mapped to a newly signed AD `/d/v2...` URL and stored under the target file's `cloudreve.direct_links` properties. Existing Cloudreve `/f/...` URLs cannot remain valid because the URL path, ID and signing algorithms differ. | ||
|
|
||
| Use `--dry-run` to perform preflight checks without writing the target. The target core tables must be empty by default. `--allow-non-empty-target` disables that guard but may still fail on unique values or conflicting data. | ||
|
|
||
| Cloudreve Qiniu, Upyun, remote-node, OneDrive and encrypted storage policies cannot be reused safely by AD. The migration stops when they are present. `--skip-unsupported-policies` explicitly omits those policies and all dependent files. | ||
|
|
||
| ## Reuse local storage | ||
|
|
||
| For compatible Cloudreve local storage, migration keeps each `entities.source` path unchanged and configures AD to read the same files through the target policy base path. This avoids copying object bytes and therefore does not require a second full data volume. | ||
|
|
||
| `--local-base-path` is the fallback root for every local policy. When policies use different volumes, pass one or more explicit source-policy mappings: | ||
|
|
||
| ```powershell | ||
| cargo run -- migrate ` | ||
| --source-url "sqlite://C:/cloudreve/cloudreve.db?mode=ro" ` | ||
| --target-url "sqlite://C:/asterdrive/asterdrive.db" ` | ||
| --default-password "change-this-password" ` | ||
| --local-base-path "D:/cloudreve-default" ` | ||
| --local-policy-root "1=D:/cloudreve-default" ` | ||
| --local-policy-root "2=E:/cloudreve-archive" ` | ||
| --verify-local-storage | ||
| ``` | ||
|
|
||
| `--verify-local-storage` checks each compatible local policy root and every migrated local blob's resolved path, regular-file status, open permission and byte length. With `--dry-run`, it scans all eligible local blobs before any target writes. It does not verify S3-compatible buckets or prove that a running AD instance can access a Docker mount; validate those separately before cutover. | ||
|
|
||
| ## Limited resume | ||
|
|
||
| Migration stages run in this order: policies, policy groups, users, folders, blobs, files, metadata, shares, direct links and tasks. Most stages are committed as one transaction. Blobs use keyset pages ordered by Cloudreve `entities.id`, while files use keyset pages ordered by Cloudreve `files.id`; each page writes target records, object mappings, its cursor and report progress in one target transaction. | ||
|
|
||
| Use a stable run ID for an operational migration. If a stage fails, fix the external cause and rerun the same command with `--resume`: | ||
|
|
||
| ```powershell | ||
| cargo run -- migrate ` | ||
| --source-url "sqlite://C:/cloudreve/cloudreve.db?mode=ro" ` | ||
| --target-url "sqlite://C:/asterdrive/asterdrive.db" ` | ||
| --local-base-path "C:/cloudreve" ` | ||
| --run-id "cloudreve-cutover-2026-07-13" ` | ||
| --blob-batch-size 500 ` | ||
| --file-batch-size 500 ` | ||
| --resume ` | ||
| --report-path "C:/migration/cloudreve-to-ad.json" | ||
| ``` | ||
|
|
||
| Resume verifies the source URL/count fingerprint, target URL fingerprint and migration-plan fingerprint. Repeat the same password, direct-link secret and migration flags used by the original run. Completed stages are skipped and their source-to-target mappings are restored from the checkpoint. Blob batch size is operational only and may be adjusted when resuming. | ||
|
|
||
| `--blob-batch-size` and `--file-batch-size` default to 500 and accept 1-10000. A failed blob or file page rolls back only that page; resume continues after the last committed entity or file ID. Blob and normal-file source rows are no longer loaded into memory as complete collections, and their mappings are stored row-by-row in `aster_external_migration_object_map`. | ||
|
|
||
| Folders, metadata, shares, direct links and tasks are still stage-level only: a failure restarts that entire stage. A file page loads only its related entity/version rows into memory. Physical object bytes are still reused in place rather than copied or uploaded. | ||
|
|
||
| ## JSON migration report | ||
|
|
||
| `--report-path` writes a pretty-printed JSON report for both `check` and `migrate`. A completed migration report contains: | ||
|
|
||
| - source and migrated counts for every supported object type | ||
| - skipped counts grouped by type, plus source ID and reason for every skipped object | ||
| - sorted source-to-target ID mappings for policies, groups, users, folders, blobs, files, shares and archived tasks | ||
| - every Cloudreve tag metadata assignment and its AD tag/entity IDs | ||
| - every regenerated direct-link URL and its Cloudreve/AD file IDs | ||
| - post-commit database count checks, imported-task terminal-state checks, tag-binding checks and direct-link property checks | ||
| - run ID, whether the execution resumed, and the list of completed stages | ||
|
|
||
| The CLI writes the JSON report before returning a validation error. A failed post-migration check therefore produces a usable report and exits with a non-zero status. Direct-link URLs are bearer-style public capabilities, so the report file must be stored with restricted access. | ||
|
|
||
| ## Verification before push | ||
|
|
||
| Every migration feature must include focused tests plus end-to-end coverage where it writes database state. Before pushing, run and require all of these commands to pass: | ||
|
|
||
| ```powershell | ||
| cargo fmt -- --check | ||
| cargo check --offline | ||
| cargo clippy --offline --all-targets --all-features -- -D warnings | ||
| cargo test --offline --all-targets --all-features | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,13 +4,12 @@ use sea_orm::entity::prelude::*; | |
|
|
||
| #[sea_orm::model] | ||
| #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] | ||
| #[sea_orm(schema_name = "public", table_name = "files")] | ||
| #[sea_orm(table_name = "files")] | ||
| pub struct Model { | ||
|
Comment on lines
+7
to
8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 不要移除
🤖 Prompt for AI Agents |
||
| #[sea_orm(primary_key)] | ||
| pub id: i64, | ||
| pub created_at: DateTimeWithTimeZone, | ||
| pub updated_at: DateTimeWithTimeZone, | ||
| pub deleted_at: Option<DateTimeWithTimeZone>, | ||
| pub r#type: i64, | ||
| #[sea_orm(unique_key = "file_file_children_name")] | ||
| pub name: String, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: AsterCommunity/AsterDriveMigration
Length of output: 236
🏁 Script executed:
Repository: AsterCommunity/AsterDriveMigration
Length of output: 35564
🌐 Web query:
SeaORM #[sea_orm(schema_name = "...")] PostgreSQL default schema behavior table_name search_path💡 Result:
In SeaORM, the
#[sea_orm(schema_name = "...", table_name = "...")]attribute explicitly binds an entity to a specific PostgreSQL schema [1][2]. Whenschema_nameis provided, SeaORM generates SQL queries that qualify the table name with the specified schema (e.g.,"schema_name"."table_name") [3][4]. The interaction betweenschema_nameand the PostgreSQLsearch_pathis as follows: 1. Explicit Schema Binding: By using theschema_nameattribute, you are telling SeaORM to always use that specific schema for the entity, regardless of the activesearch_path[3][4]. 2. PostgreSQL search_path: Thesearch_path(configured viaConnectOptions::set_schema_search_path[5] or the database connection string [6]) is a PostgreSQL-level mechanism that tells the database which schemas to look into when an identifier is unqualified [6][7]. 3. Behavior: If you define an entity with aschema_name, SeaORM treats it as a fully qualified identifier. Consequently, thesearch_pathhas no effect on these specific entities because the query already includes the schema prefix [3]. If you do not specify aschema_name, SeaORM will not prefix the table, and PostgreSQL will rely on thesearch_pathto resolve the table location [6]. Note that there are known edge cases, particularly with schema synchronization tools, where some generated SQL (such as for indexes or constraints) may not always consistently use the qualified schema name unless explicitly handled or configured [3]. For managing PostgreSQL schema configuration in SeaORM, you can use: - ConnectOptions:opt.set_schema_search_path("my_schema".into())[5] - Connection String:postgres://user:pass@host/db?options[search_path]=my_schema[6]Citations:
search_path=schema_namesuffix in PG connection string SeaQL/sea-orm#1272🌐 Web query:
SeaORM schema_name PostgreSQL default search_path public unqualified table name entity model💡 Result:
In SeaORM, managing PostgreSQL schemas and unqualified table names is handled primarily through the
schema_nameattribute in the entity model and connection configuration [1][2][3]. To map an entity to a specific PostgreSQL schema, you use theschema_nameattribute within the#[sea_orm(...)]derive macro [1][2]. This ensures the generated SQL appropriately qualifies the table name with the schema (e.g., "schema_name"."table_name") [4][2]. For connection-level handling, specifically when you want to avoid manual schema qualification or rely on the PostgreSQLsearch_path, you have two main options: 1. Connection Options: You can set the default PostgreSQL schema search path programmatically when establishing the database connection [5][6]. let mut opt = ConnectOptions::new("postgres://user:pass@localhost/db".to_owned); opt.set_schema_search_path("my_schema".into); let db = Database::connect(opt).await?; 2. Connection String: You can append theoptions[search_path]parameter to your connection string, which allows SQLx and SeaORM to honor the search path without requiring explicit schema qualification in your code [6]. postgres://user:pass@localhost/db?options[search_path]=my_schema Note that whileschema_nameexplicitly defines the schema for an entity, relying onsearch_pathcan sometimes lead to issues with certain automated tools or generated queries (such as schema-sync creating indexes with unqualified table names if not handled correctly) [7][4]. If you explicitly defineschema_namein your entity model, SeaORM typically generates schema-qualified identifiers [4][2]. If you require specific migration behaviors for non-public schemas, you can pass the-s <schema_name>flag tosea-orm-clior set theDATABASE_SCHEMAenvironment variable [5][8].Citations:
search_path=schema_namesuffix in PG connection string SeaQL/sea-orm#1272补上 PostgreSQL schema 绑定
crates/cloudreve-entities/src/entities.rs:7现在只有table_name = "entities";src/migration/mod.rs里又是直接Database::connect(url)后跑cr::entities::Entity::find().all(db),没有任何schema_name/search_path配置。这样源表解析全靠连接默认 schema,非public部署会读错表或直接失败。要么显式固定 Cloudreve schema,要么把search_path配进连接串,再补 PostgreSQL 集成测试。🤖 Prompt for AI Agents