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" : "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
89 .getBlobWithContentType(userDid, cid);
90 console.log(
91 "[blob-migration] Got blob",
92 cid,
93 "size:",
94 blobData.byteLength,
95 "contentType:",
96 contentType,
97 );
98 await localClient.uploadBlob(blobData, contentType);
99 console.log(
100 "[blob-migration] Uploaded blob",
101 cid,
102 "with contentType:",
103 contentType,
104 );
105 migrated++;
106 onProgress({ blobsMigrated: migrated });
107 } catch (e) {
108 const errorMessage = (e as Error).message || String(e);
109 console.error(
110 "[blob-migration] Failed to migrate blob",
111 cid,
112 ":",
113 errorMessage,
114 );
115
116 const isNetworkError = errorMessage.includes("fetch") ||
117 errorMessage.includes("network") ||
118 errorMessage.includes("CORS") ||
119 errorMessage.includes("Failed to fetch") ||
120 errorMessage.includes("NetworkError") ||
121 errorMessage.includes("blocked by CORS");
122
123 if (isNetworkError) {
124 sourceUnreachable = true;
125 console.warn(
126 "[blob-migration] Source appears unreachable (likely CORS or network issue), skipping remaining blobs",
127 );
128 const remaining = missingBlobs.length - migrated - 1;
129 if (migrated > 0) {
130 onProgress({
131 currentOperation:
132 `Source PDS unreachable (browser security restriction). ${migrated} media files migrated successfully. ${
133 remaining + 1
134 } could not be fetched - these may need to be re-uploaded.`,
135 });
136 } else {
137 onProgress({
138 currentOperation:
139 `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.`,
140 });
141 }
142 }
143 failed.push(cid);
144 }
145 }
146
147 if (migrated === missingBlobs.length) {
148 onProgress({
149 currentOperation: `All ${migrated} blobs migrated successfully`,
150 });
151 } else if (migrated > 0) {
152 onProgress({
153 currentOperation:
154 `${migrated}/${missingBlobs.length} blobs migrated. ${failed.length} failed.`,
155 });
156 } else {
157 onProgress({
158 currentOperation: `Could not migrate blobs (${failed.length} missing)`,
159 });
160 }
161
162 return { migrated, failed, total: missingBlobs.length, sourceUnreachable };
163}