Graphical PDS migrator for AT Protocol

split up data

Changed files
+447 -401
islands
routes
api
+52 -34
islands/MigrationProgress.tsx
···
console.log("Starting data migration...");
try {
-
console.log("Data migration: Sending request to /api/migrate/data");
-
const dataRes = await fetch("/api/migrate/data", {
+
// Step 2.1: Migrate Repo
+
console.log("Data migration: Starting repo migration");
+
const repoRes = await fetch("/api/migrate/data/repo", {
method: "POST",
headers: { "Content-Type": "application/json" },
});
-
console.log("Data migration: Response status:", dataRes.status);
-
const dataText = await dataRes.text();
-
console.log("Data migration: Raw response:", dataText);
+
console.log("Repo migration: Response status:", repoRes.status);
+
const repoText = await repoRes.text();
+
console.log("Repo migration: Raw response:", repoText);
-
if (!dataRes.ok) {
+
if (!repoRes.ok) {
try {
-
const json = JSON.parse(dataText);
-
console.error("Data migration: Error response:", json);
-
throw new Error(json.message || "Failed to migrate data");
+
const json = JSON.parse(repoText);
+
console.error("Repo migration: Error response:", json);
+
throw new Error(json.message || "Failed to migrate repo");
} catch {
-
console.error("Data migration: Non-JSON error response:", dataText);
-
throw new Error(dataText || "Failed to migrate data");
+
console.error("Repo migration: Non-JSON error response:", repoText);
+
throw new Error(repoText || "Failed to migrate repo");
}
}
-
try {
-
const jsonData = JSON.parse(dataText);
-
console.log("Data migration: Parsed response:", jsonData);
-
if (!jsonData.success) {
-
console.error("Data migration: Unsuccessful response:", jsonData);
-
throw new Error(jsonData.message || "Data migration failed");
+
// Step 2.2: Migrate Blobs
+
console.log("Data migration: Starting blob migration");
+
const blobsRes = await fetch("/api/migrate/data/blobs", {
+
method: "POST",
+
headers: { "Content-Type": "application/json" },
+
});
+
+
console.log("Blob migration: Response status:", blobsRes.status);
+
const blobsText = await blobsRes.text();
+
console.log("Blob migration: Raw response:", blobsText);
+
+
if (!blobsRes.ok) {
+
try {
+
const json = JSON.parse(blobsText);
+
console.error("Blob migration: Error response:", json);
+
throw new Error(json.message || "Failed to migrate blobs");
+
} catch {
+
console.error("Blob migration: Non-JSON error response:", blobsText);
+
throw new Error(blobsText || "Failed to migrate blobs");
}
-
console.log("Data migration: Success response:", jsonData);
-
-
// Display migration logs
-
if (jsonData.logs && Array.isArray(jsonData.logs)) {
-
console.log("Data migration: Logs:", jsonData.logs);
-
jsonData.logs.forEach((log: string) => {
-
if (log.includes("Successfully migrated blob:")) {
-
console.log("Data migration: Blob success:", log);
-
} else if (log.includes("Failed to migrate blob")) {
-
console.error("Data migration: Blob failure:", log);
-
} else {
-
console.log("Data migration:", log);
-
}
-
});
+
}
+
+
// Step 2.3: Migrate Preferences
+
console.log("Data migration: Starting preferences migration");
+
const prefsRes = await fetch("/api/migrate/data/prefs", {
+
method: "POST",
+
headers: { "Content-Type": "application/json" },
+
});
+
+
console.log("Preferences migration: Response status:", prefsRes.status);
+
const prefsText = await prefsRes.text();
+
console.log("Preferences migration: Raw response:", prefsText);
+
+
if (!prefsRes.ok) {
+
try {
+
const json = JSON.parse(prefsText);
+
console.error("Preferences migration: Error response:", json);
+
throw new Error(json.message || "Failed to migrate preferences");
+
} catch {
+
console.error("Preferences migration: Non-JSON error response:", prefsText);
+
throw new Error(prefsText || "Failed to migrate preferences");
}
-
} catch (e) {
-
console.error("Data migration: Failed to parse response:", e);
-
throw new Error("Invalid response from server during data migration");
}
console.log("Data migration: Starting verification");
-367
routes/api/migrate/data.ts
···
-
import { define } from "../../../utils.ts";
-
import {
-
getSessionAgent,
-
} from "../../../lib/sessions.ts";
-
import { Agent, ComAtprotoSyncGetBlob } from "npm:@atproto/api";
-
-
// Retry configuration
-
const MAX_RETRIES = 3;
-
const INITIAL_RETRY_DELAY = 1000; // 1 second
-
-
/**
-
* Retry options
-
* @param maxRetries - The maximum number of retries
-
* @param initialDelay - The initial delay between retries
-
* @param onRetry - The function to call on retry
-
*/
-
interface RetryOptions {
-
maxRetries?: number;
-
initialDelay?: number;
-
onRetry?: (attempt: number, error: Error) => void;
-
}
-
-
/**
-
* Retry function with exponential backoff
-
* @param operation - The operation to retry
-
* @param options - The retry options
-
* @returns The result of the operation
-
*/
-
async function withRetry<T>(
-
operation: () => Promise<T>,
-
options: RetryOptions = {},
-
): Promise<T> {
-
const maxRetries = options.maxRetries ?? MAX_RETRIES;
-
const initialDelay = options.initialDelay ?? INITIAL_RETRY_DELAY;
-
-
let lastError: Error | null = null;
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
-
try {
-
return await operation();
-
} catch (error) {
-
lastError = error instanceof Error ? error : new Error(String(error));
-
-
// Don't retry on certain errors
-
if (error instanceof Error) {
-
// Don't retry on permanent errors like authentication
-
if (error.message.includes("Unauthorized") || error.message.includes("Invalid token")) {
-
throw error;
-
}
-
}
-
-
if (attempt < maxRetries - 1) {
-
const delay = initialDelay * Math.pow(2, attempt);
-
console.log(`Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms:`, lastError.message);
-
if (options.onRetry) {
-
options.onRetry(attempt + 1, lastError);
-
}
-
await new Promise(resolve => setTimeout(resolve, delay));
-
}
-
}
-
}
-
throw lastError ?? new Error("Operation failed after retries");
-
}
-
-
/**
-
* Handle blob upload to new PDS
-
* Retries on errors
-
* @param newAgent - The new agent
-
* @param blobRes - The blob response
-
* @param cid - The CID of the blob
-
*/
-
async function handleBlobUpload(
-
newAgent: Agent,
-
blobRes: ComAtprotoSyncGetBlob.Response,
-
cid: string
-
) {
-
try {
-
const contentLength = parseInt(blobRes.headers["content-length"] || "0", 10);
-
const contentType = blobRes.headers["content-type"];
-
-
// Check file size before attempting upload
-
const MAX_SIZE = 95 * 1024 * 1024; // 95MB to be safe
-
if (contentLength > MAX_SIZE) {
-
throw new Error(`Blob ${cid} exceeds maximum size limit (${contentLength} bytes)`);
-
}
-
-
await withRetry(
-
() => newAgent.com.atproto.repo.uploadBlob(blobRes.data, {
-
encoding: contentType,
-
}),
-
{
-
maxRetries: 5,
-
onRetry: (attempt, error) => {
-
console.log(`Retrying blob upload for ${cid} (attempt ${attempt}):`, error.message);
-
},
-
}
-
);
-
} catch (error) {
-
console.error(`Failed to upload blob ${cid}:`, error);
-
throw error;
-
}
-
}
-
-
/**
-
* Handle data migration
-
* @param ctx - The context object containing the request and response
-
* @returns A response object with the migration result
-
*/
-
export const handler = define.handlers({
-
async POST(ctx) {
-
const res = new Response();
-
try {
-
console.log("Data migration: Starting session retrieval");
-
const oldAgent = await getSessionAgent(ctx.req);
-
console.log("Data migration: Got old agent:", !!oldAgent);
-
-
// Log cookie information
-
const cookies = ctx.req.headers.get("cookie");
-
console.log("Data migration: Cookies present:", !!cookies);
-
console.log("Data migration: Cookie header:", cookies);
-
-
const newAgent = await getSessionAgent(ctx.req, res, true);
-
console.log("Data migration: Got new agent:", !!newAgent);
-
-
if (!oldAgent) {
-
return new Response(
-
JSON.stringify({
-
success: false,
-
message: "Unauthorized",
-
}),
-
{
-
status: 401,
-
headers: { "Content-Type": "application/json" },
-
},
-
);
-
}
-
if (!newAgent) {
-
return new Response(
-
JSON.stringify({
-
success: false,
-
message: "Migration session not found or invalid",
-
}),
-
{
-
status: 400,
-
headers: { "Content-Type": "application/json" },
-
},
-
);
-
}
-
-
const session = await oldAgent.com.atproto.server.getSession();
-
const accountDid = session.data.did;
-
-
// Migrate repo data with retries
-
const repoRes = await withRetry(
-
() => oldAgent.com.atproto.sync.getRepo({
-
did: accountDid,
-
}),
-
{
-
maxRetries: 5,
-
onRetry: (attempt, error) => {
-
console.log(`Retrying repo fetch (attempt ${attempt}):`, error.message);
-
},
-
}
-
);
-
-
await withRetry(
-
() => newAgent.com.atproto.repo.importRepo(repoRes.data, {
-
encoding: "application/vnd.ipld.car",
-
}),
-
{
-
maxRetries: 5,
-
onRetry: (attempt, error) => {
-
console.log(`Retrying repo import (attempt ${attempt}):`, error.message);
-
},
-
}
-
);
-
-
// Migrate blobs with enhanced error handling
-
let blobCursor: string | undefined = undefined;
-
const migratedBlobs: string[] = [];
-
const failedBlobs: Array<{ cid: string; error: string }> = [];
-
const migrationLogs: string[] = [];
-
let totalBlobs = 0;
-
let pageCount = 0;
-
-
// First count total blobs
-
console.log("Starting blob count...");
-
do {
-
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
-
did: accountDid,
-
cursor: blobCursor,
-
});
-
const newBlobs = listedBlobs.data.cids.length;
-
totalBlobs += newBlobs;
-
pageCount++;
-
console.log(`Blob count page ${pageCount}: found ${newBlobs} blobs, total so far: ${totalBlobs}`);
-
migrationLogs.push(`Blob count page ${pageCount}: found ${newBlobs} blobs, total so far: ${totalBlobs}`);
-
-
if (!listedBlobs.data.cursor) {
-
console.log("No more cursor, finished counting blobs");
-
break;
-
}
-
blobCursor = listedBlobs.data.cursor;
-
} while (blobCursor);
-
-
// Reset cursor for actual migration
-
blobCursor = undefined;
-
let processedBlobs = 0;
-
pageCount = 0;
-
-
do {
-
try {
-
const listedBlobs = await withRetry(
-
() => oldAgent.com.atproto.sync.listBlobs({
-
did: accountDid,
-
cursor: blobCursor,
-
}),
-
{
-
maxRetries: 5,
-
onRetry: (attempt, error) => {
-
const log = `Retrying blob list fetch (attempt ${attempt}): ${error.message}`;
-
console.log(log);
-
migrationLogs.push(log);
-
},
-
}
-
);
-
-
pageCount++;
-
console.log(`Processing blob page ${pageCount}: ${listedBlobs.data.cids.length} blobs`);
-
migrationLogs.push(`Processing blob page ${pageCount}: ${listedBlobs.data.cids.length} blobs`);
-
-
for (const cid of listedBlobs.data.cids) {
-
try {
-
const blobRes = await withRetry(
-
() => oldAgent.com.atproto.sync.getBlob({
-
did: accountDid,
-
cid,
-
}),
-
{
-
maxRetries: 5,
-
onRetry: (attempt, error) => {
-
const log = `Retrying blob download for ${cid} (attempt ${attempt}): ${error.message}`;
-
console.log(log);
-
migrationLogs.push(log);
-
},
-
}
-
);
-
-
await handleBlobUpload(newAgent, blobRes, cid);
-
migratedBlobs.push(cid);
-
processedBlobs++;
-
const progressLog = `Migrating blob ${processedBlobs} of ${totalBlobs}: ${cid}`;
-
console.log(progressLog);
-
migrationLogs.push(progressLog);
-
} catch (error) {
-
const errorMsg = error instanceof Error ? error.message : String(error);
-
console.error(`Failed to migrate blob ${cid}:`, error);
-
failedBlobs.push({
-
cid,
-
error: errorMsg,
-
});
-
migrationLogs.push(`Failed to migrate blob ${cid}: ${errorMsg}`);
-
}
-
}
-
-
if (!listedBlobs.data.cursor) {
-
console.log("No more cursor, finished processing blobs");
-
migrationLogs.push("No more cursor, finished processing blobs");
-
break;
-
}
-
blobCursor = listedBlobs.data.cursor;
-
} catch (error) {
-
const errorMsg = error instanceof Error ? error.message : String(error);
-
console.error("Error during blob migration batch:", error);
-
migrationLogs.push(`Error during blob migration batch: ${errorMsg}`);
-
if (error instanceof Error &&
-
(error.message.includes("Unauthorized") ||
-
error.message.includes("Invalid token"))) {
-
throw error;
-
}
-
break;
-
}
-
} while (blobCursor);
-
-
const completionMessage = `Data migration completed: ${migratedBlobs.length} blobs migrated${failedBlobs.length > 0 ? `, ${failedBlobs.length} failed` : ''} (${pageCount} pages processed)`;
-
console.log(completionMessage);
-
migrationLogs.push(completionMessage);
-
-
// Migrate preferences with retry
-
console.log("Starting preferences migration...");
-
migrationLogs.push("Starting preferences migration...");
-
-
const prefs = await withRetry(
-
() => oldAgent.app.bsky.actor.getPreferences(),
-
{
-
maxRetries: 3,
-
onRetry: (attempt, error) => {
-
const log = `Retrying preferences fetch (attempt ${attempt}): ${error.message}`;
-
console.log(log);
-
migrationLogs.push(log);
-
},
-
}
-
);
-
-
console.log("Preferences fetched, updating on new account...");
-
migrationLogs.push("Preferences fetched, updating on new account...");
-
-
await withRetry(
-
() => newAgent.app.bsky.actor.putPreferences(prefs.data),
-
{
-
maxRetries: 3,
-
onRetry: (attempt, error) => {
-
const log = `Retrying preferences update (attempt ${attempt}): ${error.message}`;
-
console.log(log);
-
migrationLogs.push(log);
-
},
-
}
-
);
-
-
console.log("Preferences migration completed");
-
migrationLogs.push("Preferences migration completed");
-
-
const finalMessage = `Data migration fully completed: ${migratedBlobs.length} blobs migrated${failedBlobs.length > 0 ? `, ${failedBlobs.length} failed` : ''} (${pageCount} pages processed), preferences migrated`;
-
console.log(finalMessage);
-
migrationLogs.push(finalMessage);
-
-
return new Response(
-
JSON.stringify({
-
success: true,
-
message: failedBlobs.length > 0
-
? `Data migration completed with ${failedBlobs.length} failed blobs`
-
: "Data migration completed successfully",
-
migratedBlobs,
-
failedBlobs,
-
totalMigrated: migratedBlobs.length,
-
totalFailed: failedBlobs.length,
-
logs: migrationLogs,
-
}),
-
{
-
status: failedBlobs.length > 0 ? 207 : 200,
-
headers: {
-
"Content-Type": "application/json",
-
...Object.fromEntries(res.headers),
-
},
-
},
-
);
-
} catch (error) {
-
console.error("Data migration error:", error);
-
return new Response(
-
JSON.stringify({
-
success: false,
-
message: error instanceof Error
-
? error.message
-
: "Failed to migrate data",
-
error: error instanceof Error ? {
-
name: error.name,
-
message: error.message,
-
stack: error.stack,
-
} : String(error),
-
}),
-
{
-
status: 400,
-
headers: { "Content-Type": "application/json" },
-
},
-
);
-
}
-
},
-
});
+197
routes/api/migrate/data/blobs.ts
···
+
import { getSessionAgent } from "../../../../lib/sessions.ts";
+
+
export const handler = {
+
async POST(req: Request) {
+
try {
+
console.log("Blob migration: Starting session retrieval");
+
const oldAgent = await getSessionAgent(req);
+
console.log("Blob migration: Got old agent:", !!oldAgent);
+
+
// Log cookie information
+
const cookies = req.headers.get("cookie");
+
console.log("Blob migration: Cookies present:", !!cookies);
+
console.log("Blob migration: Cookie header:", cookies);
+
+
const newAgent = await getSessionAgent(req, new Response(), true);
+
console.log("Blob migration: Got new agent:", !!newAgent);
+
+
if (!oldAgent || !newAgent || !oldAgent.did) {
+
return new Response(JSON.stringify({
+
success: false,
+
message: "Not authenticated"
+
}), {
+
status: 401,
+
headers: { "Content-Type": "application/json" }
+
});
+
}
+
+
// Migrate blobs
+
const migrationLogs: string[] = [];
+
const migratedBlobs: string[] = [];
+
const failedBlobs: string[] = [];
+
let pageCount = 0;
+
let blobCursor: string | undefined = undefined;
+
let totalBlobs = 0;
+
let processedBlobs = 0;
+
+
const startTime = Date.now();
+
console.log(`[${new Date().toISOString()}] Starting blob migration...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Starting blob migration...`);
+
+
// First count total blobs
+
console.log(`[${new Date().toISOString()}] Starting blob count...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Starting blob count...`);
+
+
do {
+
const pageStartTime = Date.now();
+
console.log(`[${new Date().toISOString()}] Counting blobs on page ${pageCount + 1}...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Counting blobs on page ${pageCount + 1}...`);
+
+
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
+
did: oldAgent.did,
+
cursor: blobCursor,
+
});
+
+
const newBlobs = listedBlobs.data.cids.length;
+
totalBlobs += newBlobs;
+
const pageTime = Date.now() - pageStartTime;
+
+
console.log(`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds, total so far: ${totalBlobs}`);
+
migrationLogs.push(`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds, total so far: ${totalBlobs}`);
+
+
pageCount++;
+
blobCursor = listedBlobs.data.cursor;
+
} while (blobCursor);
+
+
console.log(`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`);
+
migrationLogs.push(`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`);
+
+
// Reset cursor for actual migration
+
blobCursor = undefined;
+
pageCount = 0;
+
processedBlobs = 0;
+
+
do {
+
const pageStartTime = Date.now();
+
console.log(`[${new Date().toISOString()}] Fetching blob list page ${pageCount + 1}...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Fetching blob list page ${pageCount + 1}...`);
+
+
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
+
did: oldAgent.did,
+
cursor: blobCursor,
+
});
+
+
const pageTime = Date.now() - pageStartTime;
+
console.log(`[${new Date().toISOString()}] Found ${listedBlobs.data.cids.length} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds`);
+
migrationLogs.push(`[${new Date().toISOString()}] Found ${listedBlobs.data.cids.length} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds`);
+
+
blobCursor = listedBlobs.data.cursor;
+
+
for (const cid of listedBlobs.data.cids) {
+
try {
+
const blobStartTime = Date.now();
+
console.log(`[${new Date().toISOString()}] Starting migration for blob ${cid} (${processedBlobs + 1} of ${totalBlobs})...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Starting migration for blob ${cid} (${processedBlobs + 1} of ${totalBlobs})...`);
+
+
const blobRes = await oldAgent.com.atproto.sync.getBlob({
+
did: oldAgent.did,
+
cid,
+
});
+
+
const contentLength = blobRes.headers["content-length"];
+
if (!contentLength) {
+
throw new Error(`Blob ${cid} has no content length`);
+
}
+
+
const size = parseInt(contentLength, 10);
+
if (isNaN(size)) {
+
throw new Error(`Blob ${cid} has invalid content length: ${contentLength}`);
+
}
+
+
const MAX_SIZE = 200 * 1024 * 1024; // 200MB
+
if (size > MAX_SIZE) {
+
throw new Error(`Blob ${cid} exceeds maximum size limit (${size} bytes)`);
+
}
+
+
console.log(`[${new Date().toISOString()}] Downloading blob ${cid} (${size} bytes)...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Downloading blob ${cid} (${size} bytes)...`);
+
+
if (!blobRes.data) {
+
throw new Error(`Failed to download blob ${cid}: No data received`);
+
}
+
+
console.log(`[${new Date().toISOString()}] Uploading blob ${cid} to new account...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Uploading blob ${cid} to new account...`);
+
+
await newAgent.com.atproto.repo.uploadBlob(blobRes.data);
+
+
const blobTime = Date.now() - blobStartTime;
+
console.log(`[${new Date().toISOString()}] Successfully migrated blob ${cid} in ${blobTime/1000} seconds`);
+
migrationLogs.push(`[${new Date().toISOString()}] Successfully migrated blob ${cid} in ${blobTime/1000} seconds`);
+
migratedBlobs.push(cid);
+
} catch (error) {
+
const errorMessage = error instanceof Error ? error.message : String(error);
+
const detailedError = `[${new Date().toISOString()}] Failed to migrate blob ${cid}: ${errorMessage}`;
+
console.error(detailedError);
+
console.error('Full error details:', error);
+
migrationLogs.push(detailedError);
+
failedBlobs.push(cid);
+
}
+
+
processedBlobs++;
+
const progressLog = `[${new Date().toISOString()}] Progress: ${processedBlobs}/${totalBlobs} blobs processed (${Math.round((processedBlobs/totalBlobs)*100)}%)`;
+
console.log(progressLog);
+
migrationLogs.push(progressLog);
+
}
+
pageCount++;
+
} while (blobCursor);
+
+
const totalTime = Date.now() - startTime;
+
const completionMessage = `[${new Date().toISOString()}] Blob migration completed in ${totalTime/1000} seconds: ${migratedBlobs.length} blobs migrated${failedBlobs.length > 0 ? `, ${failedBlobs.length} failed` : ''} (${pageCount} pages processed)`;
+
console.log(completionMessage);
+
migrationLogs.push(completionMessage);
+
+
return new Response(
+
JSON.stringify({
+
success: true,
+
message: failedBlobs.length > 0
+
? `Blob migration completed with ${failedBlobs.length} failed blobs`
+
: "Blob migration completed successfully",
+
migratedBlobs,
+
failedBlobs,
+
totalMigrated: migratedBlobs.length,
+
totalFailed: failedBlobs.length,
+
totalProcessed: processedBlobs,
+
totalBlobs,
+
logs: migrationLogs,
+
timing: {
+
totalTime: totalTime/1000
+
}
+
}),
+
{
+
status: 200,
+
headers: { "Content-Type": "application/json" }
+
}
+
);
+
} catch (error) {
+
const message = error instanceof Error ? error.message : String(error);
+
console.error(`[${new Date().toISOString()}] Blob migration error:`, message);
+
console.error('Full error details:', error);
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: `Blob migration failed: ${message}`,
+
error: error instanceof Error ? {
+
name: error.name,
+
message: error.message,
+
stack: error.stack,
+
} : String(error)
+
}),
+
{
+
status: 500,
+
headers: { "Content-Type": "application/json" }
+
}
+
);
+
}
+
}
+
};
+98
routes/api/migrate/data/prefs.ts
···
+
import { getSessionAgent } from "../../../../lib/sessions.ts";
+
+
export const handler = {
+
async POST(req: Request) {
+
try {
+
console.log("Preferences migration: Starting session retrieval");
+
const oldAgent = await getSessionAgent(req);
+
console.log("Preferences migration: Got old agent:", !!oldAgent);
+
+
// Log cookie information
+
const cookies = req.headers.get("cookie");
+
console.log("Preferences migration: Cookies present:", !!cookies);
+
console.log("Preferences migration: Cookie header:", cookies);
+
+
const newAgent = await getSessionAgent(req, new Response(), true);
+
console.log("Preferences migration: Got new agent:", !!newAgent);
+
+
if (!oldAgent || !newAgent) {
+
return new Response(JSON.stringify({
+
success: false,
+
message: "Not authenticated"
+
}), {
+
status: 401,
+
headers: { "Content-Type": "application/json" }
+
});
+
}
+
+
// Migrate preferences
+
const migrationLogs: string[] = [];
+
const startTime = Date.now();
+
console.log(`[${new Date().toISOString()}] Starting preferences migration...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Starting preferences migration...`);
+
+
// Fetch preferences
+
console.log(`[${new Date().toISOString()}] Fetching preferences from old account...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Fetching preferences from old account...`);
+
+
const fetchStartTime = Date.now();
+
const prefs = await oldAgent.app.bsky.actor.getPreferences();
+
const fetchTime = Date.now() - fetchStartTime;
+
+
console.log(`[${new Date().toISOString()}] Preferences fetched in ${fetchTime/1000} seconds`);
+
migrationLogs.push(`[${new Date().toISOString()}] Preferences fetched in ${fetchTime/1000} seconds`);
+
+
// Update preferences
+
console.log(`[${new Date().toISOString()}] Updating preferences on new account...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Updating preferences on new account...`);
+
+
const updateStartTime = Date.now();
+
await newAgent.app.bsky.actor.putPreferences(prefs.data);
+
const updateTime = Date.now() - updateStartTime;
+
+
console.log(`[${new Date().toISOString()}] Preferences updated in ${updateTime/1000} seconds`);
+
migrationLogs.push(`[${new Date().toISOString()}] Preferences updated in ${updateTime/1000} seconds`);
+
+
const totalTime = Date.now() - startTime;
+
const completionMessage = `[${new Date().toISOString()}] Preferences migration completed in ${totalTime/1000} seconds total`;
+
console.log(completionMessage);
+
migrationLogs.push(completionMessage);
+
+
return new Response(
+
JSON.stringify({
+
success: true,
+
message: "Preferences migration completed successfully",
+
logs: migrationLogs,
+
timing: {
+
fetchTime: fetchTime/1000,
+
updateTime: updateTime/1000,
+
totalTime: totalTime/1000
+
}
+
}),
+
{
+
status: 200,
+
headers: { "Content-Type": "application/json" }
+
}
+
);
+
} catch (error) {
+
const message = error instanceof Error ? error.message : String(error);
+
console.error(`[${new Date().toISOString()}] Preferences migration error:`, message);
+
console.error('Full error details:', error);
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: `Preferences migration failed: ${message}`,
+
error: error instanceof Error ? {
+
name: error.name,
+
message: error.message,
+
stack: error.stack,
+
} : String(error)
+
}),
+
{
+
status: 500,
+
headers: { "Content-Type": "application/json" }
+
}
+
);
+
}
+
}
+
};
+100
routes/api/migrate/data/repo.ts
···
+
import { getSessionAgent } from "../../../../lib/sessions.ts";
+
+
export const handler = {
+
async POST(req: Request) {
+
try {
+
console.log("Repo migration: Starting session retrieval");
+
const oldAgent = await getSessionAgent(req);
+
console.log("Repo migration: Got old agent:", !!oldAgent);
+
+
// Log cookie information
+
const cookies = req.headers.get("cookie");
+
console.log("Repo migration: Cookies present:", !!cookies);
+
console.log("Repo migration: Cookie header:", cookies);
+
+
const newAgent = await getSessionAgent(req, new Response(), true);
+
console.log("Repo migration: Got new agent:", !!newAgent);
+
+
if (!oldAgent || !newAgent || !oldAgent.did) {
+
return new Response(JSON.stringify({
+
success: false,
+
message: "Not authenticated"
+
}), {
+
status: 401,
+
headers: { "Content-Type": "application/json" }
+
});
+
}
+
+
// Migrate repo data
+
const migrationLogs: string[] = [];
+
const startTime = Date.now();
+
console.log(`[${new Date().toISOString()}] Starting repo migration...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Starting repo migration...`);
+
+
// Get repo data from old account
+
console.log(`[${new Date().toISOString()}] Fetching repo data from old account...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Fetching repo data from old account...`);
+
+
const fetchStartTime = Date.now();
+
const repoData = await oldAgent.com.atproto.sync.getRepo({
+
did: oldAgent.did,
+
});
+
const fetchTime = Date.now() - fetchStartTime;
+
+
console.log(`[${new Date().toISOString()}] Repo data fetched in ${fetchTime/1000} seconds`);
+
migrationLogs.push(`[${new Date().toISOString()}] Repo data fetched in ${fetchTime/1000} seconds`);
+
+
console.log(`[${new Date().toISOString()}] Importing repo data to new account...`);
+
migrationLogs.push(`[${new Date().toISOString()}] Importing repo data to new account...`);
+
+
// Import repo data to new account
+
const importStartTime = Date.now();
+
await newAgent.com.atproto.repo.importRepo(repoData.data);
+
const importTime = Date.now() - importStartTime;
+
+
console.log(`[${new Date().toISOString()}] Repo data imported in ${importTime/1000} seconds`);
+
migrationLogs.push(`[${new Date().toISOString()}] Repo data imported in ${importTime/1000} seconds`);
+
+
const totalTime = Date.now() - startTime;
+
const completionMessage = `[${new Date().toISOString()}] Repo migration completed in ${totalTime/1000} seconds total`;
+
console.log(completionMessage);
+
migrationLogs.push(completionMessage);
+
+
return new Response(
+
JSON.stringify({
+
success: true,
+
message: "Repo migration completed successfully",
+
logs: migrationLogs,
+
timing: {
+
fetchTime: fetchTime/1000,
+
importTime: importTime/1000,
+
totalTime: totalTime/1000
+
}
+
}),
+
{
+
status: 200,
+
headers: { "Content-Type": "application/json" }
+
}
+
);
+
} catch (error) {
+
const message = error instanceof Error ? error.message : String(error);
+
console.error(`[${new Date().toISOString()}] Repo migration error:`, message);
+
console.error('Full error details:', error);
+
return new Response(
+
JSON.stringify({
+
success: false,
+
message: `Repo migration failed: ${message}`,
+
error: error instanceof Error ? {
+
name: error.name,
+
message: error.message,
+
stack: error.stack,
+
} : String(error)
+
}),
+
{
+
status: 500,
+
headers: { "Content-Type": "application/json" }
+
}
+
);
+
}
+
}
+
};