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 =
111 errorMessage.includes("fetch") ||
112 errorMessage.includes("network") ||
113 errorMessage.includes("CORS") ||
114 errorMessage.includes("Failed to fetch") ||
115 errorMessage.includes("NetworkError") ||
116 errorMessage.includes("blocked by CORS");
117
118 if (isNetworkError) {
119 sourceUnreachable = true;
120 console.warn(
121 "[blob-migration] Source appears unreachable (likely CORS or network issue), skipping remaining blobs",
122 );
123 const remaining = missingBlobs.length - migrated - 1;
124 if (migrated > 0) {
125 onProgress({
126 currentOperation:
127 `Source PDS unreachable (browser security restriction). ${migrated} media files migrated successfully. ${remaining + 1} could not be fetched - these may need to be re-uploaded.`,
128 });
129 } else {
130 onProgress({
131 currentOperation:
132 `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.`,
133 });
134 }
135 }
136 failed.push(cid);
137 }
138 }
139
140 if (migrated === missingBlobs.length) {
141 onProgress({
142 currentOperation: `All ${migrated} blobs migrated successfully`,
143 });
144 } else if (migrated > 0) {
145 onProgress({
146 currentOperation:
147 `${migrated}/${missingBlobs.length} blobs migrated. ${failed.length} failed.`,
148 });
149 } else {
150 onProgress({
151 currentOperation: `Could not migrate blobs (${failed.length} missing)`,
152 });
153 }
154
155 return { migrated, failed, total: missingBlobs.length, sourceUnreachable };
156}