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 for (const blob of blobs) {
40 missingBlobs.push(blob.cid);
41 }
42 cursor = nextCursor;
43 } while (cursor);
44
45 console.log("[blob-migration] Total missing blobs:", missingBlobs.length);
46 onProgress({ blobsTotal: missingBlobs.length });
47
48 if (missingBlobs.length === 0) {
49 console.log("[blob-migration] No blobs to migrate");
50 onProgress({ currentOperation: "No blobs to migrate" });
51 return { migrated: 0, failed: [], total: 0, sourceUnreachable: false };
52 }
53
54 if (!sourceClient) {
55 console.warn(
56 "[blob-migration] No source client available, cannot fetch blobs",
57 );
58 onProgress({
59 currentOperation:
60 `${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.`,
61 });
62 return {
63 migrated: 0,
64 failed: missingBlobs,
65 total: missingBlobs.length,
66 sourceUnreachable: true,
67 };
68 }
69
70 onProgress({ currentOperation: `Migrating ${missingBlobs.length} blobs...` });
71
72 let migrated = 0;
73 const failed: string[] = [];
74 let sourceUnreachable = false;
75
76 for (const cid of missingBlobs) {
77 if (sourceUnreachable) {
78 failed.push(cid);
79 continue;
80 }
81
82 try {
83 onProgress({
84 currentOperation: `Migrating blob ${
85 migrated + 1
86 }/${missingBlobs.length}...`,
87 });
88
89 console.log("[blob-migration] Fetching blob", cid, "from source");
90 const blobData = await sourceClient.getBlob(userDid, cid);
91 console.log(
92 "[blob-migration] Got blob",
93 cid,
94 "size:",
95 blobData.byteLength,
96 );
97 await localClient.uploadBlob(blobData, "application/octet-stream");
98 console.log("[blob-migration] Uploaded blob", cid);
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}