Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations. pdsmoover.com
pds atproto migrations moo cow
at main 189 lines 6.1 kB view raw
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 }