this repo has no description
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}