- missing blobs
- retry flags
- info
+124
-55
src/pdsmoover.js
+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
+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
+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
README.md
···
2
2
3
3

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)
History
1 round
0 comments
expand 0 comments
pull request successfully merged