Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations. pdsmoover.com
pds atproto migrations moo cow

MOOre features #1

merged opened by baileytownsend.dev targeting main from feature/RetryLogicAndInfo
  • missing blobs
  • retry flags
  • info
Labels

None yet.

Participants 1
AT URI
at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/sh.tangled.repo.pull/3lvw55jriiu22
+282 -66
Diff #0
+124 -55
src/pdsmoover.js
··· 35 35 constructor() { 36 36 this.oldAgent = null; 37 37 this.newAgent = null; 38 + this.missingBlobs = []; 39 + //State for reruns 40 + this.createNewAccount = true; 41 + this.migrateRepo = true; 42 + this.migrateBlobs = true; 43 + this.migrateMissingBlobs = true; 44 + this.migratePrefs = true; 45 + this.migratePlcRecord = true; 38 46 } 39 47 40 48 /** ··· 77 85 await oldAgent.login({identifier: oldHandle, password: password, authFactorToken: twoFactorCode}); 78 86 } 79 87 80 - safeStatusUpdate(statusUpdateHandler, 'Checking that the new PDS is an actual PDS'); 88 + safeStatusUpdate(statusUpdateHandler, 'Checking that the new PDS is an actual PDS (if the url is wrong this takes a while to error out)'); 81 89 const newAgent = new AtpAgent({service: newPdsUrl}); 82 90 const newHostDesc = await newAgent.com.atproto.server.describeServer(); 83 - const newHostWebDid = newHostDesc.data.did; 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 - 100 - 101 - 102 - 103 - 104 - 105 - 91 + if (this.createNewAccount) { 92 + const newHostWebDid = newHostDesc.data.did; 106 93 94 + safeStatusUpdate(statusUpdateHandler, 'Creating a new account on the new PDS'); 107 95 96 + const createAuthResp = await oldAgent.com.atproto.server.getServiceAuth({ 97 + aud: newHostWebDid, 98 + lxm: 'com.atproto.server.createAccount', 99 + }); 100 + const serviceJwt = createAuthResp.data.token; 108 101 102 + const createNewAccount = await newAgent.com.atproto.server.createAccount({ 103 + did: usersDid, 104 + handle: newHandle, 105 + email: newEmail, 106 + password: password, 107 + inviteCode: inviteCode, 108 + }, 109 + { 110 + headers: {authorization: `Bearer ${serviceJwt}`}, 111 + encoding: 'application/json', 112 + }); 109 113 114 + if (createNewAccount.data.did !== usersDid.toString()) { 115 + throw new Error('Did not create the new account with the same did as the old account'); 116 + } 117 + } 118 + safeStatusUpdate(statusUpdateHandler, 'Logging in with the new account'); 110 119 111 120 await newAgent.login({ 112 121 identifier: usersDid, 113 - password, 122 + password: password, 114 123 }); 115 124 116 - safeStatusUpdate(statusUpdateHandler, 'Migrating your repo'); 117 - 118 - 119 - encoding: 'application/vnd.ipld.car', 120 - }); 121 - 122 - safeStatusUpdate(statusUpdateHandler, 'Migrating your blobs'); 123 - 124 - let blobCursor = undefined; 125 - let uploadedBlobs = 0; 126 - do { 127 - safeStatusUpdate(statusUpdateHandler, `Migrating blobs, ${uploadedBlobs}/${uploadedBlobs + 100}`); 128 - uploadedBlobs += 100; 129 - const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({ 130 - did: usersDid, 131 - cursor: blobCursor, 132 - limit: 100, 125 + if (this.migrateRepo) { 126 + safeStatusUpdate(statusUpdateHandler, 'Migrating your repo'); 127 + const repoRes = await oldAgent.com.atproto.sync.getRepo({did: usersDid}); 128 + await newAgent.com.atproto.repo.importRepo(repoRes.data, { 129 + encoding: 'application/vnd.ipld.car', 133 130 }); 134 - for (const cid of listedBlobs.data.cids) { 135 - try { 136 - //TODO may move the status update here but would have it only update like every 10 131 + } 137 132 133 + let newAccountStatus = await newAgent.com.atproto.server.checkAccountStatus(); 138 134 135 + if (this.migrateBlobs) { 136 + safeStatusUpdate(statusUpdateHandler, 'Migrating your blobs'); 139 137 138 + let blobCursor = undefined; 139 + let uploadedBlobs = 0; 140 + do { 141 + safeStatusUpdate(statusUpdateHandler, `Migrating blobs: ${uploadedBlobs}/${newAccountStatus.data.expectedBlobs}`); 142 + 143 + const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({ 144 + did: usersDid, 145 + cursor: blobCursor, 146 + limit: 100, 147 + }); 148 + 149 + for (const cid of listedBlobs.data.cids) { 150 + try { 151 + const blobRes = await oldAgent.com.atproto.sync.getBlob({ 152 + did: usersDid, 153 + cid, 154 + }); 155 + await newAgent.com.atproto.repo.uploadBlob(blobRes.data, { 156 + encoding: blobRes.headers['content-type'], 157 + }); 158 + uploadedBlobs++; 159 + if (uploadedBlobs % 10 === 0) { 160 + safeStatusUpdate(statusUpdateHandler, `Migrating blobs: ${uploadedBlobs}/${newAccountStatus.data.expectedBlobs}`); 161 + } 162 + } catch (error) { 163 + console.error(error); 164 + } 165 + } 166 + blobCursor = listedBlobs.data.cursor; 167 + } while (blobCursor); 168 + } 140 169 141 - await newAgent.com.atproto.repo.uploadBlob(blobRes.data, { 142 - encoding: blobRes.headers['content-type'], 170 + if (this.migrateMissingBlobs) { 171 + newAccountStatus = await newAgent.com.atproto.server.checkAccountStatus(); 172 + if (newAccountStatus.data.expectedBlobs !== newAccountStatus.data.importedBlobs) { 173 + let totalMissingBlobs = newAccountStatus.data.expectedBlobs - newAccountStatus.data.importedBlobs; 174 + safeStatusUpdate(statusUpdateHandler, 'Looks like there are some missing blobs. Going to try and upload them now.'); 175 + //Probably should be shared between main blob uploader, but eh 176 + let missingBlobCursor = undefined; 177 + let missingUploadedBlobs = 0; 178 + do { 179 + safeStatusUpdate(statusUpdateHandler, `Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs}`); 180 + 181 + const missingBlobs = await newAgent.com.atproto.repo.listMissingBlobs({ 182 + cursor: missingBlobCursor, 183 + limit: 100, 143 184 }); 144 - } catch (error) { 145 - //TODO silently logging for now will do a missing blobs later 146 - console.error(error); 147 185 186 + for (const recordBlob of missingBlobs.data.blobs) { 187 + try { 188 + //TODO may move the status update here but would have it only update like every 10 189 + const blobRes = await oldAgent.com.atproto.sync.getBlob({ 190 + did: usersDid, 191 + cid: recordBlob.cid, 192 + }); 193 + await newAgent.com.atproto.repo.uploadBlob(blobRes.data, { 194 + encoding: blobRes.headers['content-type'], 195 + }); 196 + if (missingUploadedBlobs % 10 === 0) { 197 + safeStatusUpdate(statusUpdateHandler, `Migrating blobs: ${missingUploadedBlobs}/${totalMissingBlobs}`); 198 + } 199 + missingUploadedBlobs++; 200 + } catch (error) { 201 + //TODO silently logging for now will do a missing blobs later 202 + console.error(error); 203 + this.missingBlobs.push(cid); 204 + } 205 + } 206 + missingBlobCursor = missingBlobs.data.cursor; 207 + } while (missingBlobCursor); 148 208 149 - blobCursor = listedBlobs.data.cursor; 150 - } while (blobCursor); 209 + } 210 + } 211 + if (this.migratePrefs) { 212 + const prefs = await oldAgent.app.bsky.actor.getPreferences(); 213 + await newAgent.app.bsky.actor.putPreferences(prefs.data); 214 + this.oldAgent = oldAgent; 215 + this.newAgent = newAgent; 216 + } 151 217 152 - //TODO NEED to do some checking on the missing blobs here 218 + if (this.migratePlcRecord) { 219 + await oldAgent.com.atproto.identity.requestPlcOperationSignature(); 220 + safeStatusUpdate(statusUpdateHandler, 'Please check your email for a PLC token'); 221 + } 222 + } 153 223 154 - const prefs = await oldAgent.app.bsky.actor.getPreferences(); 155 - await newAgent.app.bsky.actor.putPreferences(prefs.data); 224 + async signPlcOperation(token) {
+25 -9
public/style.css
··· 14 14 15 15 16 16 17 + font-weight: 500; 18 + color: #646cff; 19 + text-decoration: inherit; 20 + text-decoration: underline; 21 + } 17 22 23 + a:hover { 18 24 19 25 20 26 ··· 94 100 95 101 96 102 103 + margin-top: 30px; 104 + } 97 105 106 + /* Left align the advance options section */ 107 + div[x-show="showAdvance"].section { 108 + text-align: left; 109 + } 98 110 99 - 100 - 101 - 102 - 103 - 104 - 105 - 106 - 107 - 111 + h1 { 112 + font-size: 3.2em; 113 + line-height: 1.1; 108 114 109 115 110 116 ··· 124 130 font-weight: bold; 125 131 text-align: center; 126 132 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 133 + } 134 + 135 + .form-checkbox { 136 + 137 + font-size: 2rem; 138 + font-weight: bold; 139 + line-height: 1.1; 140 + display: grid; 141 + grid-template-columns: 1em auto; 142 + gap: 0.5em; 127 143 }
+130
public/info.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"/> 5 + <link rel="icon" type="image/webp" href="/moo.webp"/> 6 + <meta property="og:description" content="ATProto account migration tool"/> 7 + <meta property="og:image" content="/moo.webp"> 8 + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/> 9 + <title>PDS MOOver Info</title> 10 + <link rel="stylesheet" href="/style.css"> 11 + <script src="https://unpkg.com/alpinejs" defer></script> 12 + 13 + </head> 14 + <body> 15 + <div class="container"> 16 + <h1>PDS MOOver</h1> 17 + <div class="cow-image"> 18 + <img src="/moo.webp" alt="Cartoon milk cow" style="max-width: 100%; max-height: 100%; object-fit: contain;"> 19 + </div> 20 + <div class="section" id="top"> 21 + <p> This page is to help you decide if you want to use PDS MOOver to move your ATProto(Bluesky) account to a new 22 + PDS. 23 + One way or the other. TLDR (You should still read the whole thing), at least read and follow the <a 24 + href="#precautions">precautions section</a>.</p> 25 + 26 + 27 + <section class="section" style="text-align:left"> 28 + <h2>Alpha</h2> 29 + <p>PDS MOOver is a no frills' PDS account migration tool. Meaning you will find few bells and whistles to 30 + simplify the process and hopefully allow more successful account migrations for most users. The creator 31 + or host of this tool will not be able to help you recover your account if something goes wrong. So be 32 + advised you and your PDS admin may be on your own besides helpful answers and understand the risk you 33 + take in doing an account movement. The tool has been tested, and it has worked well, but it is still 34 + important to understand this. If you follow <a 35 + href="#precautions">precautions section</a> this will also greatly increase the odds of 36 + recovering from any issues. You will still need to have some know how, but it is very possible with a 37 + backup rotation key and backup of your account. I also recommend trying out an alt before you do your 38 + main account so you know what to expect.</p> 39 + </section> 40 + 41 + 42 + <nav aria-label="Table of contents" class="section" style="text-align:left"> 43 + <h3>Table of contents</h3> 44 + <ol> 45 + <li><a href="#precautions">Precautions</a></li> 46 + <li><a href="#help">!!!!!HELP!!!!!</a></li> 47 + <li><a href="#why">Why doesn't PDS MOOver have xyz?</a></li> 48 + <li><a href="#done">Alright account migrated, now what?</a></li> 49 + <li><a href="#slow">Why is it so SLOW?</a></li> 50 + <li><a href="#open-source">Can I check out the code anywhere?</a></li> 51 + </ol> 52 + </nav> 53 + 54 + <section id="precautions" class="section" style="text-align:left"> 55 + <h2>Precautions</h2> 56 + <p>Before you even run PDS MOOver I STRONGLY suggest you add a rotation key and have a back up of your repo 57 + and blobs. With both of these you are not guarantee a full recovery if something goes wrong. But it will 58 + greatly increase your odds.</p> 59 + <h3>Rotation Key Backup</h3> 60 + <p>I recommend using <a href="https://atpairport.com/ticket-booth">atpairport.com</a> to add a new rotation 61 + key to your PLC. or you can follow <a href="https://whtwnd.com/bnewbold.net/3lj7jmt2ct72r">this blog 62 + post</a> to do it with goat. PDS MOOver will not use this rotation key at this time, but if 63 + something happens, it helps to have this in the meantime. 64 + <p> 65 + <h3>Repo & Blob backup</h3> 66 + 67 + <p>I also recommend taking a back up of your account with <a href="https://boat.kelinci.net/">boat</a>. You 68 + want to <a href="https://boat.kelinci.net/repo-export">"Export repository"</a> and <a 69 + href="https://boat.kelinci.net/blob-export">"Export 70 + blobs"</a>. Both just take your 71 + current handle and download those exports to your computer. These are your posts, pictures, and videos. 72 + </p> 73 + </section> 74 + 75 + <section id="help" class="section" style="text-align:left"> 76 + <h2>!!!!!HELP!!!!!</h2> 77 + <p>If you're having issues with PDS MOOver first of all, I'm very sorry. I have tested this to the best of 78 + my 79 + ability, but PDS migrations do come with risks. I would recommend getting with the owner of the PDS and 80 + seeing where the account stands with tools like <a href="https://pdsls.dev">pdsls</a>.</p> 81 + 82 + <p> The tool is designed to be able to be re ran IF you set the Advance Options flags.For example, lets say 83 + if it created the account, repo is there but some blobs are missing. You can uncheck everything but 84 + "Migrate Missing Blobs", "Migrate Prefs", and "Migrate PLC record" and it will pick up after the account 85 + repo migration. It is odd in the fact that all the fields are required. That's just to cut down on logic 86 + to hopefully cut down on bugs. If you don't ever see the "Please enter your PLC Token" you can just 87 + forget about it and call it a day if it's too much. Your old account is still active and working.</p> 88 + </section> 89 + 90 + <section id="why" class="section" style="text-align:left"> 91 + <h2>Why doesn't PDS MOOver have xyz?</h2> 92 + <p>PDS MOOver was designed to pretty much be the goat account migration with a UI. Like in this <a 93 + href="https://whtwnd.com/bnewbold.net/3l5ii332pf32u"> post</a>. Keeping it simple and hard fails if 94 + anything 95 + goes wrong 96 + to 97 + hopefully cover most use cases. Rule of thumb if reading the goat migration blog post makes you nervous, 98 + you may not want to move to a new PDS with PDS MOOver till it has been battle tested.</p> 99 + </section> 100 + 101 + <section id="done" class="section" style="text-align:left"> 102 + <h2>Alright account migrated, now what?</h2> 103 + <p>Welcome to your new PDS! I recommend now re running the steps in <a 104 + href="#precautions">precautions section</a> along with keeping regular backups of your account. With 105 + the rotation key and backups you can almost always recover even if your new PDS disappears overnight. It 106 + may take a bit of know how, but it is very possible.</p> 107 + </section> 108 + 109 + <section id="slow" class="section" style="text-align:left"> 110 + <h2>Why is it so SLOW?</h2> 111 + <p>Everything happens client side, and the blob uploads take a while. Nothing runs in parallel. Blob uploads 112 + happen one at a time; once one is done, the next goes. This is done just to keep it as simple as 113 + possible and to hopefully limit the chance of failures on uploads. My personal account takes about 114 + 20-30ish mins to move with 1,700ish blobs at 800mb on a 1gig internet connection.</p> 115 + </section> 116 + 117 + <section id="open-source" class="section" style="text-align:left"> 118 + <h2>Alright account migrated, now what?</h2> 119 + <p>Yep! PDS MOOver is 100% open source and can find the code on <a 120 + href="https://tangled.sh/@baileytownsend.dev/pds-moover">tangled.sh</a>. Also, if you're a 121 + developer, 122 + and you want to fork the code for a new UI. PDS MOOver's logic is all in one js file. Just take it and 123 + its dependencies and have at it.</p> 124 + </section> 125 + </div> 126 + 127 + </div> 128 + 129 + </body> 130 + </html>
+2 -2
README.md
··· 2 2 3 3 ![moo](./public/moo.webp) 4 4 5 - Use at your own risk, I will not host till I have a bit more validation and retry logic in place. 6 - AND it will still be very much so, use at your own Risk. 5 + A no frills client side PDS account migrator for ATProto. For more info check out 6 + [pdsmoover.com](https://pdsmoover.com)
+1
index.html
··· 110 110 this.plcStatus = 'PLC operation signed successfully! Your account has been MOOved to the new PDS.'; 111 111 // this.askForPlcToken = false; 112 112 } catch (error) { 113 + this.error = error.message; 113 114 console.error(error); 114 115 } 115 116 }

History

1 round 0 comments
sign up or login to add to the discussion
7 commits
expand
missing blobs and better upload status
new flags no logic
new flags styled a bit more
Looking solid. New flags logic
got to reboot
info
done?
expand 0 comments
pull request successfully merged