Files
sexy/packages/backend/src/scripts/data-migration.ts

566 lines
17 KiB
TypeScript
Raw Normal View History

/**
* Data Migration: Directus Custom Backend
*
* Migrates data from Directus tables to the new schema.
* Run with: tsx src/scripts/data-migration.ts
*
* Environment variables:
* DATABASE_URL - PostgreSQL connection (same DB)
* OLD_UPLOAD_DIR - Path to Directus uploads (e.g. /old-uploads)
* NEW_UPLOAD_DIR - Path to new upload dir (e.g. /data/uploads)
*/
import { Pool } from "pg";
import fs from "fs";
import path from "path";
const DATABASE_URL = process.env.DATABASE_URL || "postgresql://sexy:sexy@localhost:5432/sexy";
const OLD_UPLOAD_DIR = process.env.OLD_UPLOAD_DIR || "/old-uploads";
const NEW_UPLOAD_DIR = process.env.NEW_UPLOAD_DIR || "/data/uploads";
const pool = new Pool({ connectionString: DATABASE_URL });
async function query(sql: string, params: unknown[] = []) {
const client = await pool.connect();
try {
return await client.query(sql, params);
} finally {
client.release();
}
}
function copyFile(src: string, dest: string) {
const dir = path.dirname(dest);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (fs.existsSync(src)) {
fs.copyFileSync(src, dest);
return true;
}
return false;
}
async function migrateFiles() {
console.log("📁 Migrating files...");
const { rows } = await query(
`SELECT id, title, description, filename_disk, type, filesize, duration, uploaded_by, uploaded_on as date_created
FROM directus_files`,
);
let migrated = 0;
let skipped = 0;
for (const file of rows) {
// Check if already migrated
const existing = await query("SELECT id FROM files WHERE id = $1", [file.id]);
if (existing.rows.length > 0) {
skipped++;
continue;
}
await query(
`INSERT INTO files (id, title, description, filename, mime_type, filesize, duration, uploaded_by, date_created)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (id) DO NOTHING`,
[
file.id,
file.title,
file.description,
file.filename_disk || `${file.id}`,
file.type,
file.filesize,
file.duration,
file.uploaded_by,
file.date_created,
],
);
// Copy file to new location
const srcPath = path.join(OLD_UPLOAD_DIR, file.filename_disk || "");
const destPath = path.join(NEW_UPLOAD_DIR, file.id, file.filename_disk || `${file.id}`);
const copied = copyFile(srcPath, destPath);
if (!copied) {
console.warn(` ⚠️ File not found on disk: ${file.filename_disk}`);
}
migrated++;
}
console.log(` ✅ Files: ${migrated} migrated, ${skipped} already existed`);
}
async function migrateUsers() {
console.log("👥 Migrating users...");
const { rows } = await query(
`SELECT u.id, u.email, u.password, u.first_name, u.last_name,
u.description, u.avatar, u.join_date as date_created,
u.artist_name, u.slug,
r.name as role_name
FROM directus_users u
LEFT JOIN directus_roles r ON u.role = r.id
WHERE u.status = 'active'`,
);
let migrated = 0;
for (const user of rows) {
const existing = await query("SELECT id FROM users WHERE id = $1", [user.id]);
if (existing.rows.length > 0) {
migrated++;
continue;
}
const role =
user.role_name === "Model"
? "model"
: user.role_name === "Administrator"
? "admin"
: "viewer";
// Fetch tags from custom user fields if they exist
let tags: string[] = [];
try {
const tagsRes = await query("SELECT tags FROM directus_users WHERE id = $1", [user.id]);
if (tagsRes.rows[0]?.tags) {
tags = Array.isArray(tagsRes.rows[0].tags)
? tagsRes.rows[0].tags
: JSON.parse(tagsRes.rows[0].tags || "[]");
}
} catch {}
await query(
`INSERT INTO users (id, email, password_hash, first_name, last_name, artist_name, slug,
description, tags, role, avatar, email_verified, date_created)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
ON CONFLICT (id) DO NOTHING`,
[
user.id,
user.email,
user.password || "MIGRATED_NO_PASSWORD",
user.first_name,
user.last_name,
user.artist_name,
user.slug,
user.description,
JSON.stringify(tags),
role,
user.avatar,
true,
user.date_created,
],
);
migrated++;
}
console.log(` ✅ Users: ${migrated} migrated`);
}
async function migrateUserPhotos() {
console.log("🖼️ Migrating user photos...");
const { rows } = await query(
`SELECT directus_users_id as user_id, directus_files_id as file_id
FROM junction_directus_users_files`,
);
let migrated = 0;
for (const row of rows) {
const userExists = await query("SELECT id FROM users WHERE id = $1", [row.user_id]);
const fileExists = await query("SELECT id FROM files WHERE id = $1", [row.file_id]);
if (!userExists.rows.length || !fileExists.rows.length) continue;
await query(
`INSERT INTO user_photos (user_id, file_id, sort) VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING`,
[row.user_id, row.file_id, 0],
);
migrated++;
}
console.log(` ✅ User photos: ${migrated} migrated`);
}
async function migrateArticles() {
console.log("📰 Migrating articles...");
const { rows } = await query(
`SELECT id, slug, title, excerpt, content, image, tags, publish_date,
author, category, featured, date_created, date_updated
FROM sexy_articles`,
);
let migrated = 0;
for (const article of rows) {
await query(
`INSERT INTO articles (id, slug, title, excerpt, content, image, tags, publish_date,
author, category, featured, date_created, date_updated)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
ON CONFLICT (id) DO NOTHING`,
[
article.id,
article.slug,
article.title,
article.excerpt,
article.content,
article.image,
Array.isArray(article.tags) ? JSON.stringify(article.tags) : article.tags,
article.publish_date,
article.author,
article.category,
article.featured,
article.date_created,
article.date_updated,
],
);
migrated++;
}
console.log(` ✅ Articles: ${migrated} migrated`);
}
async function migrateVideos() {
console.log("🎬 Migrating videos...");
const { rows } = await query(
`SELECT id, slug, title, description, image, movie, tags, upload_date,
premium, featured
FROM sexy_videos`,
);
let migrated = 0;
for (const video of rows) {
await query(
`INSERT INTO videos (id, slug, title, description, image, movie, tags, upload_date,
premium, featured, likes_count, plays_count)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (id) DO NOTHING`,
[
video.id,
video.slug,
video.title,
video.description,
video.image,
video.movie,
Array.isArray(video.tags) ? JSON.stringify(video.tags) : video.tags,
video.upload_date,
video.premium,
video.featured,
0,
0,
],
);
migrated++;
}
console.log(` ✅ Videos: ${migrated} migrated`);
}
async function migrateVideoModels() {
console.log("🔗 Migrating video models...");
const { rows } = await query(
`SELECT sexy_videos_id as video_id, directus_users_id as user_id
FROM sexy_videos_models`,
);
let migrated = 0;
for (const row of rows) {
const videoExists = await query("SELECT id FROM videos WHERE id = $1", [row.video_id]);
const userExists = await query("SELECT id FROM users WHERE id = $1", [row.user_id]);
if (!videoExists.rows.length || !userExists.rows.length) continue;
await query(
`INSERT INTO video_models (video_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
[row.video_id, row.user_id],
);
migrated++;
}
console.log(` ✅ Video models: ${migrated} migrated`);
}
async function migrateVideoLikes() {
console.log("❤️ Migrating video likes...");
const { rows } = await query(
`SELECT id, video_id, user_id, date_created FROM sexy_video_likes`,
);
let migrated = 0;
for (const row of rows) {
await query(
`INSERT INTO video_likes (id, video_id, user_id, date_created) VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO NOTHING`,
[row.id, row.video_id, row.user_id, row.date_created],
);
migrated++;
}
console.log(` ✅ Video likes: ${migrated} migrated`);
}
async function migrateVideoPlays() {
console.log("▶️ Migrating video plays...");
const { rows } = await query(
`SELECT id, video_id, user_id, session_id, duration_watched, completed, date_created
FROM sexy_video_plays`,
);
let migrated = 0;
for (const row of rows) {
await query(
`INSERT INTO video_plays (id, video_id, user_id, session_id, duration_watched, completed, date_created)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO NOTHING`,
[
row.id,
row.video_id,
row.user_id,
row.session_id,
row.duration_watched,
row.completed,
row.date_created,
],
);
migrated++;
}
console.log(` ✅ Video plays: ${migrated} migrated`);
}
async function migrateRecordings() {
console.log("🎙️ Migrating recordings...");
const { rows } = await query(
`SELECT id, title, description, slug, duration, events, device_info,
user_created as user_id, status, tags, linked_video, public,
original_recording_id, date_created, date_updated
FROM sexy_recordings`,
);
let migrated = 0;
for (const recording of rows) {
await query(
`INSERT INTO recordings (id, title, description, slug, duration, events, device_info,
user_id, status, tags, linked_video, public,
original_recording_id, date_created, date_updated)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
ON CONFLICT (id) DO NOTHING`,
[
recording.id,
recording.title,
recording.description,
recording.slug,
recording.duration,
typeof recording.events === "string" ? recording.events : JSON.stringify(recording.events),
typeof recording.device_info === "string"
? recording.device_info
: JSON.stringify(recording.device_info),
recording.user_id,
recording.status,
Array.isArray(recording.tags) ? JSON.stringify(recording.tags) : recording.tags,
recording.linked_video,
recording.public,
recording.original_recording_id,
recording.date_created,
recording.date_updated,
],
);
migrated++;
}
console.log(` ✅ Recordings: ${migrated} migrated`);
}
async function migrateRecordingPlays() {
console.log("▶️ Migrating recording plays...");
const { rows } = await query(
`SELECT id, user_id, recording_id, duration_played, completed, date_created
FROM sexy_recording_plays`,
);
let migrated = 0;
for (const row of rows) {
await query(
`INSERT INTO recording_plays (id, recording_id, user_id, duration_played, completed, date_created)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (id) DO NOTHING`,
[row.id, row.recording_id, row.user_id, row.duration_played, row.completed, row.date_created],
);
migrated++;
}
console.log(` ✅ Recording plays: ${migrated} migrated`);
}
async function migrateComments() {
console.log("💬 Migrating comments...");
const { rows } = await query(
`SELECT id, collection, item, comment, user_created as user_id, date_created
FROM directus_comments
WHERE collection IN ('sexy_videos', 'sexy_recordings')`,
);
let migrated = 0;
for (const row of rows) {
// Map collection names
const collection = row.collection === "sexy_videos" ? "videos" : "recordings";
await query(
`INSERT INTO comments (collection, item_id, comment, user_id, date_created)
VALUES ($1, $2, $3, $4, $5)`,
[collection, row.item, row.comment, row.user_id, row.date_created],
);
migrated++;
}
console.log(` ✅ Comments: ${migrated} migrated`);
}
async function migrateAchievements() {
console.log("🏆 Migrating achievements...");
const { rows } = await query(
`SELECT id, code, name, description, icon, category, required_count, points_reward, status, sort
FROM sexy_achievements`,
);
let migrated = 0;
for (const row of rows) {
await query(
`INSERT INTO achievements (id, code, name, description, icon, category, required_count, points_reward, status, sort)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT (id) DO NOTHING`,
[
row.id,
row.code,
row.name,
row.description,
row.icon,
row.category,
row.required_count,
row.points_reward,
row.status,
row.sort,
],
);
migrated++;
}
console.log(` ✅ Achievements: ${migrated} migrated`);
}
async function migrateUserAchievements() {
console.log("🎖️ Migrating user achievements...");
const { rows } = await query(
`SELECT user_id, achievement_id, progress, date_unlocked FROM sexy_user_achievements`,
);
let migrated = 0;
for (const row of rows) {
const userExists = await query("SELECT id FROM users WHERE id = $1", [row.user_id]);
const achievementExists = await query("SELECT id FROM achievements WHERE id = $1", [
row.achievement_id,
]);
if (!userExists.rows.length || !achievementExists.rows.length) continue;
await query(
`INSERT INTO user_achievements (user_id, achievement_id, progress, date_unlocked)
VALUES ($1, $2, $3, $4)
ON CONFLICT (user_id, achievement_id) DO NOTHING`,
[row.user_id, row.achievement_id, row.progress, row.date_unlocked],
);
migrated++;
}
console.log(` ✅ User achievements: ${migrated} migrated`);
}
async function migrateUserPoints() {
console.log("💎 Migrating user points...");
const { rows } = await query(
`SELECT user_id, action, points, recording_id, date_created FROM sexy_user_points`,
);
let migrated = 0;
for (const row of rows) {
const userExists = await query("SELECT id FROM users WHERE id = $1", [row.user_id]);
if (!userExists.rows.length) continue;
await query(
`INSERT INTO user_points (user_id, action, points, recording_id, date_created)
VALUES ($1, $2, $3, $4, $5)`,
[row.user_id, row.action, row.points, row.recording_id, row.date_created],
);
migrated++;
}
console.log(` ✅ User points: ${migrated} migrated`);
}
async function migrateUserStats() {
console.log("📊 Migrating user stats...");
const { rows } = await query(
`SELECT user_id, total_raw_points, total_weighted_points, recordings_count,
playbacks_count, comments_count, achievements_count, last_updated
FROM sexy_user_stats`,
);
let migrated = 0;
for (const row of rows) {
const userExists = await query("SELECT id FROM users WHERE id = $1", [row.user_id]);
if (!userExists.rows.length) continue;
await query(
`INSERT INTO user_stats (user_id, total_raw_points, total_weighted_points, recordings_count,
playbacks_count, comments_count, achievements_count, last_updated)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (user_id) DO NOTHING`,
[
row.user_id,
row.total_raw_points,
row.total_weighted_points,
row.recordings_count,
row.playbacks_count,
row.comments_count,
row.achievements_count,
row.last_updated,
],
);
migrated++;
}
console.log(` ✅ User stats: ${migrated} migrated`);
}
async function main() {
console.log("🚀 Starting data migration from Directus to custom backend...\n");
try {
// Verify connection
await query("SELECT 1");
console.log("✅ Database connected\n");
// Migration order respects FK dependencies
await migrateFiles();
await migrateUsers();
await migrateUserPhotos();
await migrateArticles();
await migrateVideos();
await migrateVideoModels();
await migrateVideoLikes();
await migrateVideoPlays();
await migrateRecordings();
await migrateRecordingPlays();
await migrateComments();
await migrateAchievements();
await migrateUserAchievements();
await migrateUserPoints();
await migrateUserStats();
console.log("\n🎉 Migration complete!");
} catch (error) {
console.error("❌ Migration failed:", error);
process.exit(1);
} finally {
await pool.end();
}
}
main();