mirror of
https://github.com/whyour/qinglong.git
synced 2025-12-13 07:25:05 +08:00
Add data migration script and comprehensive migration guide
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
5c798a0e93
commit
d42074f76a
252
MIGRATION_GUIDE.md
Normal file
252
MIGRATION_GUIDE.md
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
# Multi-User Data Migration Guide
|
||||
|
||||
This document explains how to migrate existing data to the multi-user system.
|
||||
|
||||
## Overview
|
||||
|
||||
When upgrading to the multi-user version of Qinglong, all existing data (cron tasks, environment variables, subscriptions, and dependencies) will be treated as "legacy data" that is accessible to all users.
|
||||
|
||||
To properly isolate data between users, you need to migrate existing data to specific user accounts.
|
||||
|
||||
## Migration Script
|
||||
|
||||
The `migrate-to-multiuser.js` script helps you assign existing legacy data to specific users.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js installed
|
||||
- Sequelize and dotenv packages (already included in the project)
|
||||
- At least one user created in the User Management interface
|
||||
|
||||
### Usage
|
||||
|
||||
#### 1. List All Users
|
||||
|
||||
First, see all available users in your system:
|
||||
|
||||
```bash
|
||||
node migrate-to-multiuser.js --list-users
|
||||
```
|
||||
|
||||
Output example:
|
||||
```
|
||||
Users in the system:
|
||||
ID Username Role Status
|
||||
-- -------- ---- ------
|
||||
1 admin Admin Enabled
|
||||
2 user1 User Enabled
|
||||
3 user2 User Enabled
|
||||
```
|
||||
|
||||
#### 2. Preview Migration (Dry Run)
|
||||
|
||||
Before making changes, preview what will be migrated:
|
||||
|
||||
```bash
|
||||
node migrate-to-multiuser.js --userId=1 --dry-run
|
||||
```
|
||||
|
||||
Or by username:
|
||||
|
||||
```bash
|
||||
node migrate-to-multiuser.js --username=admin --dry-run
|
||||
```
|
||||
|
||||
Output example:
|
||||
```
|
||||
Legacy Data Statistics:
|
||||
Cron tasks: 15
|
||||
Environment variables: 8
|
||||
Subscriptions: 3
|
||||
Dependencies: 5
|
||||
|
||||
DRY RUN: No changes will be made.
|
||||
|
||||
Would assign all legacy data to user ID 1
|
||||
```
|
||||
|
||||
#### 3. Perform Migration
|
||||
|
||||
Once you're ready, run the migration without `--dry-run`:
|
||||
|
||||
**By User ID:**
|
||||
```bash
|
||||
node migrate-to-multiuser.js --userId=1
|
||||
```
|
||||
|
||||
**By Username:**
|
||||
```bash
|
||||
node migrate-to-multiuser.js --username=admin
|
||||
```
|
||||
|
||||
Output example:
|
||||
```
|
||||
Found user 'admin' with ID 1
|
||||
|
||||
Legacy Data Statistics:
|
||||
Cron tasks: 15
|
||||
Environment variables: 8
|
||||
Subscriptions: 3
|
||||
Dependencies: 5
|
||||
|
||||
Migrating data to user ID 1...
|
||||
✓ Migrated 15 cron tasks
|
||||
✓ Migrated 8 environment variables
|
||||
✓ Migrated 3 subscriptions
|
||||
✓ Migrated 5 dependencies
|
||||
|
||||
✓ Migration completed successfully!
|
||||
```
|
||||
|
||||
### Command Line Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--userId=<id>` | Assign all legacy data to user with this ID |
|
||||
| `--username=<name>` | Assign all legacy data to user with this username |
|
||||
| `--list-users` | List all users in the system |
|
||||
| `--dry-run` | Show what would be changed without making changes |
|
||||
| `--help` | Show help message |
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Scenario 1: Single User to Multi-User
|
||||
|
||||
If you're upgrading from single-user to multi-user and want to keep all existing data under one admin account:
|
||||
|
||||
1. Create an admin user in the User Management interface
|
||||
2. Run migration: `node migrate-to-multiuser.js --username=admin`
|
||||
|
||||
### Scenario 2: Distribute Data to Multiple Users
|
||||
|
||||
If you want to distribute existing data to different users:
|
||||
|
||||
1. Create all necessary user accounts first
|
||||
2. Identify which data belongs to which user (you may need to do this manually by checking the database)
|
||||
3. For each user, manually update the `userId` field in the database tables (`Crontabs`, `Envs`, `Subscriptions`, `Dependences`)
|
||||
|
||||
**SQL Example:**
|
||||
```sql
|
||||
-- Assign specific cron tasks to user ID 2
|
||||
UPDATE Crontabs
|
||||
SET userId = 2
|
||||
WHERE name LIKE '%user2%' AND userId IS NULL;
|
||||
|
||||
-- Assign specific environment variables to user ID 2
|
||||
UPDATE Envs
|
||||
SET userId = 2
|
||||
WHERE name LIKE '%USER2%' AND userId IS NULL;
|
||||
```
|
||||
|
||||
### Scenario 3: Keep as Shared Data
|
||||
|
||||
If you want certain data to remain accessible to all users:
|
||||
|
||||
- Simply don't run the migration script
|
||||
- Data with `userId = NULL` remains as "legacy data" accessible to everyone
|
||||
- This is useful for shared cron tasks or environment variables
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Backup First**: Always backup your database before running migration scripts
|
||||
```bash
|
||||
cp data/database.sqlite data/database.sqlite.backup
|
||||
```
|
||||
|
||||
2. **Test in Dry Run**: Always use `--dry-run` first to see what will change
|
||||
|
||||
3. **One-Time Operation**: The script only migrates data where `userId` is NULL
|
||||
- Already migrated data won't be changed
|
||||
- You can run it multiple times safely
|
||||
|
||||
4. **Transaction Safety**: The migration uses database transactions
|
||||
- If any error occurs, all changes are rolled back
|
||||
- Your data remains safe
|
||||
|
||||
5. **User Must Exist**: The target user must exist before migration
|
||||
- Create users in the User Management interface first
|
||||
- Use `--list-users` to verify users exist
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "User not found"
|
||||
|
||||
**Problem:** The specified user doesn't exist in the database.
|
||||
|
||||
**Solution:**
|
||||
1. Run `node migrate-to-multiuser.js --list-users` to see available users
|
||||
2. Create the user in User Management interface if needed
|
||||
3. Use correct user ID or username
|
||||
|
||||
### Error: "Database connection failed"
|
||||
|
||||
**Problem:** Cannot connect to the database.
|
||||
|
||||
**Solution:**
|
||||
1. Check that `data/database.sqlite` exists
|
||||
2. Verify database file permissions
|
||||
3. Check `QL_DATA_DIR` environment variable if using custom path
|
||||
|
||||
### Error: "Migration failed"
|
||||
|
||||
**Problem:** An error occurred during migration.
|
||||
|
||||
**Solution:**
|
||||
1. Check the error message for details
|
||||
2. Verify database is not corrupted
|
||||
3. Restore from backup if needed
|
||||
4. Check database file permissions
|
||||
|
||||
## Manual Migration
|
||||
|
||||
If you prefer to migrate data manually using SQL:
|
||||
|
||||
### Connect to Database
|
||||
```bash
|
||||
sqlite3 data/database.sqlite
|
||||
```
|
||||
|
||||
### Check Legacy Data
|
||||
```sql
|
||||
-- Count legacy cron tasks
|
||||
SELECT COUNT(*) FROM Crontabs WHERE userId IS NULL;
|
||||
|
||||
-- View legacy cron tasks
|
||||
SELECT id, name, command FROM Crontabs WHERE userId IS NULL;
|
||||
```
|
||||
|
||||
### Migrate Data
|
||||
```sql
|
||||
-- Migrate all legacy data to user ID 1
|
||||
UPDATE Crontabs SET userId = 1 WHERE userId IS NULL;
|
||||
UPDATE Envs SET userId = 1 WHERE userId IS NULL;
|
||||
UPDATE Subscriptions SET userId = 1 WHERE userId IS NULL;
|
||||
UPDATE Dependences SET userId = 1 WHERE userId IS NULL;
|
||||
```
|
||||
|
||||
### Verify Migration
|
||||
```sql
|
||||
-- Check if any legacy data remains
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM Crontabs WHERE userId IS NULL) as legacy_crons,
|
||||
(SELECT COUNT(*) FROM Envs WHERE userId IS NULL) as legacy_envs,
|
||||
(SELECT COUNT(*) FROM Subscriptions WHERE userId IS NULL) as legacy_subs,
|
||||
(SELECT COUNT(*) FROM Dependences WHERE userId IS NULL) as legacy_deps;
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues with data migration:
|
||||
|
||||
1. Check this guide for solutions
|
||||
2. Review the error messages carefully
|
||||
3. Ensure you have a recent backup
|
||||
4. Open an issue on GitHub with:
|
||||
- Error messages
|
||||
- Migration command used
|
||||
- Database statistics (from dry run)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [MULTI_USER_GUIDE.md](./MULTI_USER_GUIDE.md) - Complete multi-user feature guide
|
||||
- [README.md](./README.md) - Main project documentation
|
||||
|
|
@ -90,6 +90,48 @@ DELETE /api/user-management
|
|||
- Minimum password length is 6 characters
|
||||
- Strong passwords are recommended
|
||||
|
||||
## 数据迁移 (Data Migration)
|
||||
|
||||
### 迁移工具 (Migration Tool)
|
||||
|
||||
项目提供了数据迁移脚本,可以将现有数据分配给特定用户。
|
||||
|
||||
A migration script is provided to assign existing data to specific users.
|
||||
|
||||
#### 使用方法 (Usage)
|
||||
|
||||
1. **列出所有用户 (List all users)**
|
||||
```bash
|
||||
node migrate-to-multiuser.js --list-users
|
||||
```
|
||||
|
||||
2. **预览迁移(不实际执行)(Dry run)**
|
||||
```bash
|
||||
node migrate-to-multiuser.js --userId=1 --dry-run
|
||||
```
|
||||
|
||||
3. **将数据迁移到指定用户ID (Migrate to user ID)**
|
||||
```bash
|
||||
node migrate-to-multiuser.js --userId=1
|
||||
```
|
||||
|
||||
4. **将数据迁移到指定用户名 (Migrate to username)**
|
||||
```bash
|
||||
node migrate-to-multiuser.js --username=admin
|
||||
```
|
||||
|
||||
#### 注意事项 (Important Notes)
|
||||
|
||||
- 迁移脚本只会处理 `userId` 为空的数据(遗留数据)
|
||||
- 已分配给用户的数据不会被修改
|
||||
- 建议先使用 `--dry-run` 预览变更
|
||||
- 迁移过程中如果出错会自动回滚
|
||||
|
||||
- The script only migrates data where `userId` is NULL (legacy data)
|
||||
- Data already assigned to users will not be changed
|
||||
- It's recommended to use `--dry-run` first to preview changes
|
||||
- Changes are automatically rolled back if an error occurs
|
||||
|
||||
## 向后兼容 (Backward Compatibility)
|
||||
|
||||
- 原有的单用户系统管理员账号继续有效
|
||||
|
|
@ -103,10 +145,10 @@ DELETE /api/user-management
|
|||
|
||||
1. **首次使用**:首次使用多用户功能时,建议先创建一个管理员账号作为备份
|
||||
2. **密码管理**:请妥善保管用户密码,忘记密码需要管理员重置
|
||||
3. **数据迁移**:如需将现有数据分配给特定用户,请联系管理员手动更新数据库
|
||||
3. **数据迁移**:使用提供的 `migrate-to-multiuser.js` 脚本将现有数据分配给特定用户
|
||||
4. **权限控制**:删除用户不会删除该用户的数据,数据会变为遗留数据
|
||||
|
||||
1. **First Use**: When first using multi-user functionality, it's recommended to create an admin account as a backup
|
||||
2. **Password Management**: Please keep user passwords safe; forgotten passwords need admin reset
|
||||
3. **Data Migration**: To assign existing data to specific users, contact admin for manual database update
|
||||
3. **Data Migration**: Use the provided `migrate-to-multiuser.js` script to assign existing data to specific users
|
||||
4. **Permission Control**: Deleting a user doesn't delete their data; the data becomes legacy data
|
||||
|
|
|
|||
317
migrate-to-multiuser.js
Executable file
317
migrate-to-multiuser.js
Executable file
|
|
@ -0,0 +1,317 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Multi-User Data Migration Script
|
||||
*
|
||||
* This script migrates existing data (Cron, Env, Subscription, Dependence)
|
||||
* to be associated with specific users.
|
||||
*
|
||||
* Usage:
|
||||
* node migrate-to-multiuser.js --userId=1
|
||||
* node migrate-to-multiuser.js --username=admin
|
||||
* node migrate-to-multiuser.js --list-users
|
||||
*
|
||||
* Options:
|
||||
* --userId=<id> Assign all legacy data to user with this ID
|
||||
* --username=<name> Assign all legacy data to user with this username
|
||||
* --list-users List all users in the system
|
||||
* --dry-run Show what would be changed without making changes
|
||||
* --help Show this help message
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
// Load environment variables
|
||||
require('dotenv').config();
|
||||
|
||||
// Configuration
|
||||
const config = {
|
||||
dbPath: process.env.QL_DATA_DIR || path.join(__dirname, '../data'),
|
||||
rootPath: __dirname,
|
||||
};
|
||||
|
||||
// Initialize Sequelize
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: path.join(config.dbPath, 'database.sqlite'),
|
||||
logging: false,
|
||||
});
|
||||
|
||||
// Define models
|
||||
const UserModel = sequelize.define('User', {
|
||||
username: Sequelize.STRING,
|
||||
password: Sequelize.STRING,
|
||||
role: Sequelize.NUMBER,
|
||||
status: Sequelize.NUMBER,
|
||||
});
|
||||
|
||||
const CrontabModel = sequelize.define('Crontab', {
|
||||
name: Sequelize.STRING,
|
||||
command: Sequelize.STRING,
|
||||
schedule: Sequelize.STRING,
|
||||
userId: Sequelize.NUMBER,
|
||||
});
|
||||
|
||||
const EnvModel = sequelize.define('Env', {
|
||||
name: Sequelize.STRING,
|
||||
value: Sequelize.STRING,
|
||||
userId: Sequelize.NUMBER,
|
||||
});
|
||||
|
||||
const SubscriptionModel = sequelize.define('Subscription', {
|
||||
name: Sequelize.STRING,
|
||||
url: Sequelize.STRING,
|
||||
userId: Sequelize.NUMBER,
|
||||
});
|
||||
|
||||
const DependenceModel = sequelize.define('Dependence', {
|
||||
name: Sequelize.STRING,
|
||||
type: Sequelize.NUMBER,
|
||||
userId: Sequelize.NUMBER,
|
||||
});
|
||||
|
||||
// Parse command line arguments
|
||||
function parseArgs() {
|
||||
const args = {
|
||||
userId: null,
|
||||
username: null,
|
||||
listUsers: false,
|
||||
dryRun: false,
|
||||
help: false,
|
||||
};
|
||||
|
||||
process.argv.slice(2).forEach(arg => {
|
||||
if (arg.startsWith('--userId=')) {
|
||||
args.userId = parseInt(arg.split('=')[1]);
|
||||
} else if (arg.startsWith('--username=')) {
|
||||
args.username = arg.split('=')[1];
|
||||
} else if (arg === '--list-users') {
|
||||
args.listUsers = true;
|
||||
} else if (arg === '--dry-run') {
|
||||
args.dryRun = true;
|
||||
} else if (arg === '--help' || arg === '-h') {
|
||||
args.help = true;
|
||||
}
|
||||
});
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
// Show help
|
||||
function showHelp() {
|
||||
console.log(`
|
||||
Multi-User Data Migration Script
|
||||
|
||||
This script migrates existing data (Cron, Env, Subscription, Dependence)
|
||||
to be associated with specific users.
|
||||
|
||||
Usage:
|
||||
node migrate-to-multiuser.js --userId=1
|
||||
node migrate-to-multiuser.js --username=admin
|
||||
node migrate-to-multiuser.js --list-users
|
||||
|
||||
Options:
|
||||
--userId=<id> Assign all legacy data to user with this ID
|
||||
--username=<name> Assign all legacy data to user with this username
|
||||
--list-users List all users in the system
|
||||
--dry-run Show what would be changed without making changes
|
||||
--help Show this help message
|
||||
|
||||
Examples:
|
||||
# List all users
|
||||
node migrate-to-multiuser.js --list-users
|
||||
|
||||
# Migrate all data to user ID 1 (dry run)
|
||||
node migrate-to-multiuser.js --userId=1 --dry-run
|
||||
|
||||
# Migrate all data to user 'admin'
|
||||
node migrate-to-multiuser.js --username=admin
|
||||
|
||||
Note: This script will only migrate data where userId is NULL or undefined.
|
||||
Data already assigned to users will not be changed.
|
||||
`);
|
||||
}
|
||||
|
||||
// List all users
|
||||
async function listUsers() {
|
||||
const users = await UserModel.findAll();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.log('\nNo users found in the database.');
|
||||
console.log('Please create users first using the User Management interface.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\nUsers in the system:');
|
||||
console.log('ID\tUsername\tRole\t\tStatus');
|
||||
console.log('--\t--------\t----\t\t------');
|
||||
|
||||
users.forEach(user => {
|
||||
const role = user.role === 0 ? 'Admin' : 'User';
|
||||
const status = user.status === 0 ? 'Enabled' : 'Disabled';
|
||||
console.log(`${user.id}\t${user.username}\t\t${role}\t\t${status}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Get statistics of legacy data
|
||||
async function getStatistics() {
|
||||
const stats = {
|
||||
crons: await CrontabModel.count({ where: { userId: null } }),
|
||||
envs: await EnvModel.count({ where: { userId: null } }),
|
||||
subscriptions: await SubscriptionModel.count({ where: { userId: null } }),
|
||||
dependences: await DependenceModel.count({ where: { userId: null } }),
|
||||
};
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Migrate data to a specific user
|
||||
async function migrateData(userId, dryRun = false) {
|
||||
const stats = await getStatistics();
|
||||
|
||||
console.log('\nLegacy Data Statistics:');
|
||||
console.log(` Cron tasks: ${stats.crons}`);
|
||||
console.log(` Environment variables: ${stats.envs}`);
|
||||
console.log(` Subscriptions: ${stats.subscriptions}`);
|
||||
console.log(` Dependencies: ${stats.dependences}`);
|
||||
console.log('');
|
||||
|
||||
if (stats.crons + stats.envs + stats.subscriptions + stats.dependences === 0) {
|
||||
console.log('No legacy data found. All data is already assigned to users.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log('DRY RUN: No changes will be made.\n');
|
||||
console.log(`Would assign all legacy data to user ID ${userId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Migrating data to user ID ${userId}...`);
|
||||
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
// Migrate crons
|
||||
if (stats.crons > 0) {
|
||||
await CrontabModel.update(
|
||||
{ userId },
|
||||
{ where: { userId: null }, transaction }
|
||||
);
|
||||
console.log(`✓ Migrated ${stats.crons} cron tasks`);
|
||||
}
|
||||
|
||||
// Migrate envs
|
||||
if (stats.envs > 0) {
|
||||
await EnvModel.update(
|
||||
{ userId },
|
||||
{ where: { userId: null }, transaction }
|
||||
);
|
||||
console.log(`✓ Migrated ${stats.envs} environment variables`);
|
||||
}
|
||||
|
||||
// Migrate subscriptions
|
||||
if (stats.subscriptions > 0) {
|
||||
await SubscriptionModel.update(
|
||||
{ userId },
|
||||
{ where: { userId: null }, transaction }
|
||||
);
|
||||
console.log(`✓ Migrated ${stats.subscriptions} subscriptions`);
|
||||
}
|
||||
|
||||
// Migrate dependences
|
||||
if (stats.dependences > 0) {
|
||||
await DependenceModel.update(
|
||||
{ userId },
|
||||
{ where: { userId: null }, transaction }
|
||||
);
|
||||
console.log(`✓ Migrated ${stats.dependences} dependencies`);
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
console.log('\n✓ Migration completed successfully!');
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
console.error('\n✗ Migration failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Main function
|
||||
async function main() {
|
||||
const args = parseArgs();
|
||||
|
||||
if (args.help) {
|
||||
showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Test database connection
|
||||
await sequelize.authenticate();
|
||||
console.log('Database connection established.');
|
||||
|
||||
if (args.listUsers) {
|
||||
await listUsers();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate arguments
|
||||
if (!args.userId && !args.username) {
|
||||
console.error('\nError: You must specify either --userId or --username');
|
||||
console.log('Use --help for usage information.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get user ID
|
||||
let userId = args.userId;
|
||||
|
||||
if (args.username) {
|
||||
const user = await UserModel.findOne({
|
||||
where: { username: args.username }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
console.error(`\nError: User '${args.username}' not found.`);
|
||||
console.log('Use --list-users to see available users.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
userId = user.id;
|
||||
console.log(`Found user '${args.username}' with ID ${userId}`);
|
||||
} else {
|
||||
// Verify user exists
|
||||
const user = await UserModel.findByPk(userId);
|
||||
|
||||
if (!user) {
|
||||
console.error(`\nError: User with ID ${userId} not found.`);
|
||||
console.log('Use --list-users to see available users.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Found user '${user.username}' with ID ${userId}`);
|
||||
}
|
||||
|
||||
// Perform migration
|
||||
await migrateData(userId, args.dryRun);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\nError:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
if (require.main === module) {
|
||||
main().catch(error => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { main, listUsers, migrateData, getStatistics };
|
||||
Loading…
Reference in New Issue
Block a user