this repo has no description
1import type { AtprotoClient } from "./atproto-client"; 2import type { MigrationProgress } from "./types"; 3 4export interface BlobMigrationResult { 5 migrated: number; 6 failed: string[]; 7 total: number; 8 sourceUnreachable: boolean; 9} 10 11export async function migrateBlobs( 12 localClient: AtprotoClient, 13 sourceClient: AtprotoClient | null, 14 userDid: string, 15 onProgress: (update: Partial<MigrationProgress>) => void, 16): Promise<BlobMigrationResult> { 17 const missingBlobs: string[] = []; 18 let cursor: string | undefined; 19 20 console.log("[blob-migration] Starting blob migration for", userDid); 21 console.log( 22 "[blob-migration] Source client:", 23 sourceClient ? "available" : "NOT AVAILABLE", 24 ); 25 26 onProgress({ currentOperation: "Checking for missing blobs..." }); 27 28 do { 29 const { blobs, cursor: nextCursor } = await localClient.listMissingBlobs( 30 cursor, 31 100, 32 ); 33 console.log( 34 "[blob-migration] listMissingBlobs returned", 35 blobs.length, 36 "blobs, cursor:", 37 nextCursor, 38 ); 39 missingBlobs.push(...blobs.map((blob) => blob.cid)); 40 cursor = nextCursor; 41 } while (cursor); 42 43 console.log("[blob-migration] Total missing blobs:", missingBlobs.length); 44 onProgress({ blobsTotal: missingBlobs.length }); 45 46 if (missingBlobs.length === 0) { 47 console.log("[blob-migration] No blobs to migrate"); 48 onProgress({ currentOperation: "No blobs to migrate" }); 49 return { migrated: 0, failed: [], total: 0, sourceUnreachable: false }; 50 } 51 52 if (!sourceClient) { 53 console.warn( 54 "[blob-migration] No source client available, cannot fetch blobs", 55 ); 56 onProgress({ 57 currentOperation: 58 `${missingBlobs.length} media files missing. No source PDS URL available - your old server may have shut down. Posts will work, but some images/media may be unavailable.`, 59 }); 60 return { 61 migrated: 0, 62 failed: missingBlobs, 63 total: missingBlobs.length, 64 sourceUnreachable: true, 65 }; 66 } 67 68 onProgress({ currentOperation: `Migrating ${missingBlobs.length} blobs...` }); 69 70 let migrated = 0; 71 const failed: string[] = []; 72 let sourceUnreachable = false; 73 74 for (const cid of missingBlobs) { 75 if (sourceUnreachable) { 76 failed.push(cid); 77 continue; 78 } 79 80 try { 81 onProgress({ 82 currentOperation: `Migrating blob ${ 83 migrated + 1 84 }/${missingBlobs.length}...`, 85 }); 86 87 console.log("[blob-migration] Fetching blob", cid, "from source"); 88 const { data: blobData, contentType } = await sourceClient.getBlobWithContentType(userDid, cid); 89 console.log( 90 "[blob-migration] Got blob", 91 cid, 92 "size:", 93 blobData.byteLength, 94 "contentType:", 95 contentType, 96 ); 97 await localClient.uploadBlob(blobData, contentType); 98 console.log("[blob-migration] Uploaded blob", cid, "with contentType:", contentType); 99 migrated++; 100 onProgress({ blobsMigrated: migrated }); 101 } catch (e) { 102 const errorMessage = (e as Error).message || String(e); 103 console.error( 104 "[blob-migration] Failed to migrate blob", 105 cid, 106 ":", 107 errorMessage, 108 ); 109 110 const isNetworkError = errorMessage.includes("fetch") || 111 errorMessage.includes("network") || 112 errorMessage.includes("CORS") || 113 errorMessage.includes("Failed to fetch") || 114 errorMessage.includes("NetworkError") || 115 errorMessage.includes("blocked by CORS"); 116 117 if (isNetworkError) { 118 sourceUnreachable = true; 119 console.warn( 120 "[blob-migration] Source appears unreachable (likely CORS or network issue), skipping remaining blobs", 121 ); 122 const remaining = missingBlobs.length - migrated - 1; 123 if (migrated > 0) { 124 onProgress({ 125 currentOperation: 126 `Source PDS unreachable (browser security restriction). ${migrated} media files migrated successfully. ${ 127 remaining + 1 128 } could not be fetched - these may need to be re-uploaded.`, 129 }); 130 } else { 131 onProgress({ 132 currentOperation: 133 `Cannot reach source PDS (browser security restriction). This commonly happens when the old server has shut down or doesn't allow cross-origin requests. Your posts will work, but ${missingBlobs.length} media files couldn't be recovered.`, 134 }); 135 } 136 } 137 failed.push(cid); 138 } 139 } 140 141 if (migrated === missingBlobs.length) { 142 onProgress({ 143 currentOperation: `All ${migrated} blobs migrated successfully`, 144 }); 145 } else if (migrated > 0) { 146 onProgress({ 147 currentOperation: 148 `${migrated}/${missingBlobs.length} blobs migrated. ${failed.length} failed.`, 149 }); 150 } else { 151 onProgress({ 152 currentOperation: `Could not migrate blobs (${failed.length} missing)`, 153 }); 154 } 155 156 return { migrated, failed, total: missingBlobs.length, sourceUnreachable }; 157}