Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations.
pdsmoover.com
pds
atproto
migrations
moo
cow
1import { AtpAgent } from '@atproto/api'
2import { handleAndPDSResolver } from './atprotoUtils.js'
3
4/**
5 * Class to help find missing blobs from the did's previous PDS and import them into the current PDS
6 */
7class MissingBlobs {
8 constructor() {
9 /**
10 * The user's current PDS agent
11 * @type {AtpAgent}
12 */
13 this.currentPdsAgent = null
14 /**
15 * The user's old PDS agent
16 * @type {AtpAgent}
17 */
18 this.oldPdsAgent = null
19 /**
20 * the user's did
21 * @type {string|null}
22 */
23 this.did = null
24 /**
25 * The user's current PDS url
26 * @type {null}
27 */
28 this.currentPdsUrl = null
29 /**
30 * A list of the missing cids blobs from the old PDS. In this case if a retry upload fails it gets put in this array for the ui
31 * @type {string[]}
32 */
33 this.missingBlobs = []
34 }
35
36 /**
37 * Logs the user into the current PDS and gets the account status
38 * @param handle {string}
39 * @param password {string}
40 * @param twoFactorCode {string|null}
41 * @returns {Promise<{accountStatus: OutputSchema, missingBlobsCount: number}>}
42 */
43 async currentAgentLogin(handle, password, twoFactorCode = null) {
44 let { usersDid, pds } = await handleAndPDSResolver(handle)
45 this.did = usersDid
46 this.currentPdsUrl = pds
47 const agent = new AtpAgent({
48 service: pds,
49 })
50
51 if (twoFactorCode === null) {
52 await agent.login({ identifier: usersDid, password })
53 } else {
54 await agent.login({
55 identifier: usersDid,
56 password: password,
57 authFactorToken: twoFactorCode,
58 })
59 }
60
61 this.currentPdsAgent = agent
62
63 const result = await agent.com.atproto.server.checkAccountStatus()
64 const missingBlobs = await this.currentPdsAgent.com.atproto.repo.listMissingBlobs({
65 limit: 10,
66 })
67 return { accountStatus: result.data, missingBlobsCount: missingBlobs.data.blobs.length }
68 }
69
70 /**
71 * Logs into the old PDS and gets the account status.
72 * Does not need a handle
73 * since it is assumed the user has already logged in with the current PDS and we are using their did
74 * @param password {string}
75 * @param twoFactorCode {string|null}
76 * @param pdsUrl {string|null} - If you know the url of the old PDS you can pass it in here. If not it will be guessed at from plc ops
77 * @returns {Promise<void>}
78 */
79 async oldAgentLogin(password, twoFactorCode = null, pdsUrl = null) {
80 let oldPds = null
81
82 if (pdsUrl === null) {
83 const response = await fetch(`https://plc.directory/${this.did}/log`)
84 let auditLog = await response.json()
85 auditLog = auditLog.reverse()
86 let debugCount = 0
87 for (const entry of auditLog) {
88 console.log(`Loop: ${debugCount++}`)
89 console.log(entry)
90 if (entry.services) {
91 if (entry.services.atproto_pds) {
92 if (entry.services.atproto_pds.type === 'AtprotoPersonalDataServer') {
93 const pds = entry.services.atproto_pds.endpoint
94 console.log(`Found PDS: ${pds}`)
95 if (pds.toLowerCase() !== this.currentPdsUrl.toLowerCase()) {
96 oldPds = pds
97 break
98 }
99 }
100 }
101 }
102 }
103 if (oldPds === null) {
104 throw new Error('Could not find your old PDS')
105 }
106 } else {
107 oldPds = pdsUrl
108 }
109
110 const agent = new AtpAgent({
111 service: oldPds,
112 })
113
114 if (twoFactorCode === null) {
115 await agent.login({ identifier: this.did, password })
116 } else {
117 await agent.login({
118 identifier: this.did,
119 password: password,
120 authFactorToken: twoFactorCode,
121 })
122 }
123 this.oldPdsAgent = agent
124 }
125
126 /**
127 * Gets the missing blobs from the old PDS and uploads them to the current PDS
128 * @param statusUpdateHandler {function} - A function to update the status of the migration. This is useful for showing the user the progress of the migration
129 * @returns {Promise<{accountStatus: OutputSchema, missingBlobsCount: number}>}
130 */
131 async migrateMissingBlobs(statusUpdateHandler) {
132 if (this.currentPdsAgent === null) {
133 throw new Error('Current PDS agent is not set')
134 }
135 if (this.oldPdsAgent === null) {
136 throw new Error('Old PDS agent is not set')
137 }
138 statusUpdateHandler('Starting to import blobs...')
139
140 let totalMissingBlobs = 0
141 let missingBlobCursor = undefined
142 let missingUploadedBlobs = 0
143
144 do {
145 const missingBlobs = await this.currentPdsAgent.com.atproto.repo.listMissingBlobs({
146 cursor: missingBlobCursor,
147 limit: 1000,
148 })
149 totalMissingBlobs += missingBlobs.data.blobs.length
150
151 for (const recordBlob of missingBlobs.data.blobs) {
152 try {
153 const blobRes = await this.oldPdsAgent.com.atproto.sync.getBlob({
154 did: this.did,
155 cid: recordBlob.cid,
156 })
157 let result = await this.currentPdsAgent.com.atproto.repo.uploadBlob(blobRes.data, {
158 encoding: blobRes.headers['content-type'],
159 })
160
161 if (result.status === 429) {
162 statusUpdateHandler(
163 `You are being rate limited. Will need to try again later to get the rest of the blobs. Migrated blobs: ${missingUploadedBlobs}/${totalMissingBlobs}`,
164 )
165 }
166
167 if (missingUploadedBlobs % 2 === 0) {
168 statusUpdateHandler(
169 `Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs} (The total may increase as we find more)`,
170 )
171 }
172 missingUploadedBlobs++
173 } catch (error) {
174 console.error(error)
175 this.missingBlobs.push(recordBlob.cid)
176 }
177 }
178 missingBlobCursor = missingBlobs.data.cursor
179 } while (missingBlobCursor)
180
181 const accountStatus = await this.currentPdsAgent.com.atproto.server.checkAccountStatus()
182 const missingBlobs = await this.currentPdsAgent.com.atproto.repo.listMissingBlobs({
183 limit: 10,
184 })
185 return { accountStatus: accountStatus.data, missingBlobsCount: missingBlobs.data.blobs.length }
186 }
187}
188
189export { MissingBlobs }