this repo has no description
at main 5.4 kB view raw
1import type { AtprotoClient } from "./atproto-client.ts"; 2import type { MigrationProgress } from "./types.ts"; 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 (baseUrl: ${sourceClient.getBaseUrl()})` : "NOT AVAILABLE", 24 ); 25 console.log( 26 "[blob-migration] Local client baseUrl:", 27 localClient.getBaseUrl(), 28 ); 29 console.log( 30 "[blob-migration] Local client has access token:", 31 localClient.getAccessToken() ? "yes" : "NO", 32 ); 33 34 onProgress({ currentOperation: "Checking for missing blobs..." }); 35 36 do { 37 const { blobs, cursor: nextCursor } = await localClient.listMissingBlobs( 38 cursor, 39 100, 40 ); 41 console.log( 42 "[blob-migration] listMissingBlobs returned", 43 blobs.length, 44 "blobs, cursor:", 45 nextCursor, 46 ); 47 missingBlobs.push(...blobs.map((blob) => blob.cid)); 48 cursor = nextCursor; 49 } while (cursor); 50 51 console.log("[blob-migration] Total missing blobs:", missingBlobs.length); 52 onProgress({ blobsTotal: missingBlobs.length }); 53 54 if (missingBlobs.length === 0) { 55 console.log("[blob-migration] No blobs to migrate"); 56 onProgress({ currentOperation: "No blobs to migrate" }); 57 return { migrated: 0, failed: [], total: 0, sourceUnreachable: false }; 58 } 59 60 if (!sourceClient) { 61 console.warn( 62 "[blob-migration] No source client available, cannot fetch blobs", 63 ); 64 onProgress({ 65 currentOperation: 66 `${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.`, 67 }); 68 return { 69 migrated: 0, 70 failed: missingBlobs, 71 total: missingBlobs.length, 72 sourceUnreachable: true, 73 }; 74 } 75 76 onProgress({ currentOperation: `Migrating ${missingBlobs.length} blobs...` }); 77 78 let migrated = 0; 79 const failed: string[] = []; 80 let sourceUnreachable = false; 81 82 for (const cid of missingBlobs) { 83 if (sourceUnreachable) { 84 failed.push(cid); 85 continue; 86 } 87 88 try { 89 onProgress({ 90 currentOperation: `Migrating blob ${ 91 migrated + 1 92 }/${missingBlobs.length}...`, 93 }); 94 95 console.log("[blob-migration] Fetching blob", cid, "from source"); 96 const { data: blobData, contentType } = await sourceClient 97 .getBlobWithContentType(userDid, cid); 98 console.log( 99 "[blob-migration] Got blob", 100 cid, 101 "size:", 102 blobData.byteLength, 103 "contentType:", 104 contentType, 105 ); 106 console.log("[blob-migration] Uploading blob", cid, "to local PDS..."); 107 const uploadResult = await localClient.uploadBlob(blobData, contentType); 108 console.log( 109 "[blob-migration] Upload response for", 110 cid, 111 ":", 112 JSON.stringify(uploadResult), 113 ); 114 migrated++; 115 onProgress({ blobsMigrated: migrated }); 116 } catch (e) { 117 const errorMessage = (e as Error).message || String(e); 118 console.error( 119 "[blob-migration] Failed to migrate blob", 120 cid, 121 ":", 122 errorMessage, 123 ); 124 125 const isNetworkError = errorMessage.includes("fetch") || 126 errorMessage.includes("network") || 127 errorMessage.includes("CORS") || 128 errorMessage.includes("Failed to fetch") || 129 errorMessage.includes("NetworkError") || 130 errorMessage.includes("blocked by CORS"); 131 132 if (isNetworkError) { 133 sourceUnreachable = true; 134 console.warn( 135 "[blob-migration] Source appears unreachable (likely CORS or network issue), skipping remaining blobs", 136 ); 137 const remaining = missingBlobs.length - migrated - 1; 138 if (migrated > 0) { 139 onProgress({ 140 currentOperation: 141 `Source PDS unreachable (browser security restriction). ${migrated} media files migrated successfully. ${ 142 remaining + 1 143 } could not be fetched - these may need to be re-uploaded.`, 144 }); 145 } else { 146 onProgress({ 147 currentOperation: 148 `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.`, 149 }); 150 } 151 } 152 failed.push(cid); 153 } 154 } 155 156 if (migrated === missingBlobs.length) { 157 onProgress({ 158 currentOperation: `All ${migrated} blobs migrated successfully`, 159 }); 160 } else if (migrated > 0) { 161 onProgress({ 162 currentOperation: 163 `${migrated}/${missingBlobs.length} blobs migrated. ${failed.length} failed.`, 164 }); 165 } else { 166 onProgress({ 167 currentOperation: `Could not migrate blobs (${failed.length} missing)`, 168 }); 169 } 170 171 return { migrated, failed, total: missingBlobs.length, sourceUnreachable }; 172}