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}