@atproto/pds 0.4.176 → 0.4.177
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/dist/account-manager/db/migrations/007-lexicon-failures-index.d.ts +4 -0
- package/dist/account-manager/db/migrations/007-lexicon-failures-index.d.ts.map +1 -0
- package/dist/account-manager/db/migrations/007-lexicon-failures-index.js +17 -0
- package/dist/account-manager/db/migrations/007-lexicon-failures-index.js.map +1 -0
- package/dist/account-manager/db/migrations/index.d.ts +2 -0
- package/dist/account-manager/db/migrations/index.d.ts.map +1 -1
- package/dist/account-manager/db/migrations/index.js +2 -0
- package/dist/account-manager/db/migrations/index.js.map +1 -1
- package/dist/account-manager/helpers/lexicon.d.ts.map +1 -1
- package/dist/account-manager/helpers/lexicon.js +7 -0
- package/dist/account-manager/helpers/lexicon.js.map +1 -1
- package/dist/account-manager/helpers/token.d.ts +32 -32
- package/dist/account-manager/scope-reference-getter.d.ts +14 -0
- package/dist/account-manager/scope-reference-getter.d.ts.map +1 -0
- package/dist/account-manager/scope-reference-getter.js +69 -0
- package/dist/account-manager/scope-reference-getter.js.map +1 -0
- package/dist/actor-store/actor-store.d.ts.map +1 -1
- package/dist/actor-store/actor-store.js +4 -1
- package/dist/actor-store/actor-store.js.map +1 -1
- package/dist/actor-store/blob/transactor.d.ts +2 -2
- package/dist/actor-store/blob/transactor.d.ts.map +1 -1
- package/dist/actor-store/blob/transactor.js +73 -24
- package/dist/actor-store/blob/transactor.js.map +1 -1
- package/dist/actor-store/record/reader.d.ts.map +1 -1
- package/dist/actor-store/record/reader.js +12 -9
- package/dist/actor-store/record/reader.js.map +1 -1
- package/dist/actor-store/repo/sql-repo-reader.d.ts.map +1 -1
- package/dist/actor-store/repo/sql-repo-reader.js +2 -2
- package/dist/actor-store/repo/sql-repo-reader.js.map +1 -1
- package/dist/actor-store/repo/sql-repo-transactor.d.ts.map +1 -1
- package/dist/actor-store/repo/sql-repo-transactor.js +16 -19
- package/dist/actor-store/repo/sql-repo-transactor.js.map +1 -1
- package/dist/actor-store/repo/transactor.d.ts.map +1 -1
- package/dist/actor-store/repo/transactor.js +11 -15
- package/dist/actor-store/repo/transactor.js.map +1 -1
- package/dist/api/com/atproto/admin/updateSubjectStatus.js +6 -2
- package/dist/api/com/atproto/admin/updateSubjectStatus.js.map +1 -1
- package/dist/api/com/atproto/repo/importRepo.d.ts.map +1 -1
- package/dist/api/com/atproto/repo/importRepo.js +43 -51
- package/dist/api/com/atproto/repo/importRepo.js.map +1 -1
- package/dist/auth-verifier.d.ts.map +1 -1
- package/dist/auth-verifier.js +2 -12
- package/dist/auth-verifier.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +20 -4
- package/dist/context.js.map +1 -1
- package/dist/disk-blobstore.d.ts.map +1 -1
- package/dist/disk-blobstore.js +10 -2
- package/dist/disk-blobstore.js.map +1 -1
- package/dist/lexicon/index.d.ts +49 -0
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +52 -1
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +470 -16
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +329 -7
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/com/atproto/moderation/defs.d.ts +8 -8
- package/dist/lexicon/types/com/atproto/moderation/defs.d.ts.map +1 -1
- package/dist/lexicon/types/com/atproto/moderation/defs.js +7 -7
- package/dist/lexicon/types/com/atproto/moderation/defs.js.map +1 -1
- package/dist/lexicon/types/com/atproto/temp/dereferenceScope.d.ts +24 -0
- package/dist/lexicon/types/com/atproto/temp/dereferenceScope.d.ts.map +1 -0
- package/dist/lexicon/types/com/atproto/temp/dereferenceScope.js +7 -0
- package/dist/lexicon/types/com/atproto/temp/dereferenceScope.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/defs.d.ts +92 -0
- package/dist/lexicon/types/tools/ozone/report/defs.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/report/defs.js +98 -0
- package/dist/lexicon/types/tools/ozone/report/defs.js.map +1 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +2 -1
- package/dist/logger.js.map +1 -1
- package/dist/scripts/rebuild-repo.d.ts.map +1 -1
- package/dist/scripts/rebuild-repo.js +3 -5
- package/dist/scripts/rebuild-repo.js.map +1 -1
- package/dist/scripts/sequencer-recovery/recoverer.js +8 -10
- package/dist/scripts/sequencer-recovery/recoverer.js.map +1 -1
- package/dist/sequencer/sequencer.js +2 -2
- package/dist/sequencer/sequencer.js.map +1 -1
- package/package.json +19 -16
- package/src/account-manager/db/migrations/007-lexicon-failures-index.ts +14 -0
- package/src/account-manager/db/migrations/index.ts +2 -0
- package/src/account-manager/helpers/lexicon.ts +14 -1
- package/src/account-manager/scope-reference-getter.ts +92 -0
- package/src/actor-store/actor-store.ts +5 -9
- package/src/actor-store/blob/transactor.ts +115 -42
- package/src/actor-store/record/reader.ts +14 -12
- package/src/actor-store/repo/sql-repo-reader.ts +12 -14
- package/src/actor-store/repo/sql-repo-transactor.ts +17 -23
- package/src/actor-store/repo/transactor.ts +29 -32
- package/src/api/com/atproto/admin/updateSubjectStatus.ts +7 -7
- package/src/api/com/atproto/repo/importRepo.ts +41 -55
- package/src/auth-verifier.ts +4 -20
- package/src/context.ts +26 -5
- package/src/disk-blobstore.ts +20 -3
- package/src/lexicon/index.ts +82 -0
- package/src/lexicon/lexicons.ts +341 -7
- package/src/lexicon/types/com/atproto/moderation/defs.ts +52 -7
- package/src/lexicon/types/com/atproto/temp/dereferenceScope.ts +42 -0
- package/src/lexicon/types/tools/ozone/report/defs.ts +154 -0
- package/src/logger.ts +1 -0
- package/src/scripts/rebuild-repo.ts +4 -5
- package/src/scripts/sequencer-recovery/recoverer.ts +8 -12
- package/src/sequencer/sequencer.ts +3 -3
- package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,14 @@
|
|
1
|
+
import { Kysely, sql } from 'kysely'
|
2
|
+
|
3
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
4
|
+
await db.schema
|
5
|
+
.createIndex('lexicon_failures_idx')
|
6
|
+
.on('lexicon')
|
7
|
+
// https://github.com/kysely-org/kysely/issues/302
|
8
|
+
.expression(sql`"updatedAt" DESC) WHERE ("lexicon" is NULL`)
|
9
|
+
.execute()
|
10
|
+
}
|
11
|
+
|
12
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
13
|
+
await db.schema.dropIndex('lexicon_failures_idx').execute()
|
14
|
+
}
|
@@ -4,6 +4,7 @@ import * as mig003 from './003-privileged-app-passwords'
|
|
4
4
|
import * as mig004 from './004-oauth'
|
5
5
|
import * as mig005 from './005-oauth-account-management'
|
6
6
|
import * as mig006 from './006-oauth-permission-sets'
|
7
|
+
import * as mig007 from './007-lexicon-failures-index'
|
7
8
|
|
8
9
|
export default {
|
9
10
|
'001': mig001,
|
@@ -12,4 +13,5 @@ export default {
|
|
12
13
|
'004': mig004,
|
13
14
|
'005': mig005,
|
14
15
|
'006': mig006,
|
16
|
+
'007': mig007,
|
15
17
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { Insertable } from 'kysely'
|
2
|
-
import { LexiconData } from '@atproto/oauth-provider'
|
2
|
+
import { LEXICON_REFRESH_FREQUENCY, LexiconData } from '@atproto/oauth-provider'
|
3
3
|
import { fromDateISO, fromJson, toDateISO, toJson } from '../../db'
|
4
4
|
import { AccountDb, Lexicon } from '../db'
|
5
5
|
|
@@ -20,6 +20,19 @@ export async function upsert(db: AccountDb, nsid: string, data: LexiconData) {
|
|
20
20
|
.values({ ...updates, nsid })
|
21
21
|
.onConflict((oc) => oc.column('nsid').doUpdateSet(updates)),
|
22
22
|
)
|
23
|
+
|
24
|
+
// Garbage collection: remove old, never resolved, lexicons.
|
25
|
+
// Uses "lexicon_failures_idx"
|
26
|
+
await db.executeWithRetry(
|
27
|
+
db.db
|
28
|
+
.deleteFrom('lexicon')
|
29
|
+
.where('lexicon', 'is', null)
|
30
|
+
.where(
|
31
|
+
'updatedAt',
|
32
|
+
'<',
|
33
|
+
toDateISO(new Date(Date.now() - LEXICON_REFRESH_FREQUENCY)),
|
34
|
+
),
|
35
|
+
)
|
23
36
|
}
|
24
37
|
|
25
38
|
export async function find(
|
@@ -0,0 +1,92 @@
|
|
1
|
+
import Redis from 'ioredis'
|
2
|
+
import { Agent, ComAtprotoTempDereferenceScope } from '@atproto/api'
|
3
|
+
import { DAY, backoffMs, retry } from '@atproto/common'
|
4
|
+
import { InvalidTokenError, OAuthScope } from '@atproto/oauth-provider'
|
5
|
+
import { UpstreamFailureError } from '@atproto/xrpc-server'
|
6
|
+
import { CachedGetter, GetterOptions } from '@atproto-labs/simple-store'
|
7
|
+
import { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'
|
8
|
+
import { SimpleStoreRedis } from '@atproto-labs/simple-store-redis'
|
9
|
+
|
10
|
+
const { InvalidScopeReferenceError } = ComAtprotoTempDereferenceScope
|
11
|
+
const PREFIX = 'ref:'
|
12
|
+
|
13
|
+
type ScopeReference = `${typeof PREFIX}${string}`
|
14
|
+
const isScopeReference = (scope?: OAuthScope): scope is ScopeReference =>
|
15
|
+
scope != null && scope.startsWith(PREFIX) && !scope.includes(' ')
|
16
|
+
|
17
|
+
const identity = <T>(value: T): T => value
|
18
|
+
|
19
|
+
export class ScopeReferenceGetter extends CachedGetter<
|
20
|
+
ScopeReference,
|
21
|
+
OAuthScope
|
22
|
+
> {
|
23
|
+
constructor(
|
24
|
+
protected readonly entryway: Agent,
|
25
|
+
redis?: Redis,
|
26
|
+
) {
|
27
|
+
super(
|
28
|
+
async (scope, options) => {
|
29
|
+
return retry(async () => this.fetchDereferencedScope(scope, options), {
|
30
|
+
maxRetries: 3,
|
31
|
+
getWaitMs: (n) => backoffMs(n, 250, 2000),
|
32
|
+
retryable: (err) =>
|
33
|
+
!options?.signal?.aborted &&
|
34
|
+
!(err instanceof InvalidScopeReferenceError),
|
35
|
+
})
|
36
|
+
},
|
37
|
+
redis
|
38
|
+
? new SimpleStoreRedis(redis, {
|
39
|
+
// tradeoff between wasted memory usage (by no longer used scopes)
|
40
|
+
// and amount of requests to entryway:
|
41
|
+
ttl: 1 * DAY,
|
42
|
+
|
43
|
+
keyPrefix: `auth-scope-${PREFIX}`,
|
44
|
+
encode: identity,
|
45
|
+
decode: identity,
|
46
|
+
})
|
47
|
+
: new SimpleStoreMemory({ max: 1000 }),
|
48
|
+
)
|
49
|
+
}
|
50
|
+
|
51
|
+
protected async fetchDereferencedScope(
|
52
|
+
scope: ScopeReference,
|
53
|
+
options?: GetterOptions,
|
54
|
+
): Promise<OAuthScope> {
|
55
|
+
const response = await this.entryway.com.atproto.temp.dereferenceScope(
|
56
|
+
{ scope },
|
57
|
+
{
|
58
|
+
signal: options?.signal,
|
59
|
+
headers: options?.noCache ? { 'Cache-Control': 'no-cache' } : undefined,
|
60
|
+
},
|
61
|
+
)
|
62
|
+
|
63
|
+
// @NOTE the part after `PREFIX` (in the input scope) is the CID of the
|
64
|
+
// scope string returned by entryway. Since there is a trust
|
65
|
+
// relationship with the entryway, we don't need to verify or enforce
|
66
|
+
// that here.
|
67
|
+
|
68
|
+
return response.data.scope
|
69
|
+
}
|
70
|
+
|
71
|
+
async dereference(scope?: OAuthScope): Promise<undefined | OAuthScope> {
|
72
|
+
if (!isScopeReference(scope)) return scope
|
73
|
+
|
74
|
+
return this.get(scope).catch(handleDereferenceError)
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
function handleDereferenceError(cause: unknown): never {
|
79
|
+
if (cause instanceof InvalidScopeReferenceError) {
|
80
|
+
// The scope reference cannot be found on the server.
|
81
|
+
// Consider the session as invalid, allowing entryway to
|
82
|
+
// re-build the scope as the user re-authenticates. This
|
83
|
+
// should never happen though.
|
84
|
+
throw InvalidTokenError.from(cause, 'DPoP')
|
85
|
+
}
|
86
|
+
|
87
|
+
throw new UpstreamFailureError(
|
88
|
+
'Failed to fetch token permissions',
|
89
|
+
undefined,
|
90
|
+
{ cause },
|
91
|
+
)
|
92
|
+
}
|
@@ -1,18 +1,14 @@
|
|
1
1
|
import assert from 'node:assert'
|
2
2
|
import fs, { mkdir } from 'node:fs/promises'
|
3
3
|
import path from 'node:path'
|
4
|
-
import {
|
5
|
-
chunkArray,
|
6
|
-
fileExists,
|
7
|
-
readIfExists,
|
8
|
-
rmIfExists,
|
9
|
-
} from '@atproto/common'
|
4
|
+
import { fileExists, readIfExists, rmIfExists } from '@atproto/common'
|
10
5
|
import * as crypto from '@atproto/crypto'
|
11
6
|
import { ExportableKeypair, Keypair } from '@atproto/crypto'
|
12
7
|
import { InvalidRequestError } from '@atproto/xrpc-server'
|
13
8
|
import { ActorStoreConfig } from '../config'
|
14
9
|
import { retrySqlite } from '../db'
|
15
10
|
import { DiskBlobStore } from '../disk-blobstore'
|
11
|
+
import { blobStoreLogger } from '../logger'
|
16
12
|
import { ActorStoreReader } from './actor-store-reader'
|
17
13
|
import { ActorStoreResources } from './actor-store-resources'
|
18
14
|
import { ActorStoreTransactor } from './actor-store-transactor'
|
@@ -137,9 +133,9 @@ export class ActorStore {
|
|
137
133
|
const cids = await this.read(did, async (store) =>
|
138
134
|
store.repo.blob.getBlobCids(),
|
139
135
|
)
|
140
|
-
await
|
141
|
-
|
142
|
-
)
|
136
|
+
await blobstore.deleteMany(cids).catch((err) => {
|
137
|
+
blobStoreLogger.error('Failed to delete blobs', { did, cids, err })
|
138
|
+
})
|
143
139
|
}
|
144
140
|
|
145
141
|
const { directory } = await this.getLocation(did)
|
@@ -3,7 +3,13 @@ import stream from 'node:stream'
|
|
3
3
|
import bytes from 'bytes'
|
4
4
|
import { fromStream as fileTypeFromStream } from 'file-type'
|
5
5
|
import { CID } from 'multiformats/cid'
|
6
|
-
import
|
6
|
+
import PQueue from 'p-queue'
|
7
|
+
import {
|
8
|
+
SECOND,
|
9
|
+
cloneStream,
|
10
|
+
sha256RawToCid,
|
11
|
+
streamSize,
|
12
|
+
} from '@atproto/common'
|
7
13
|
import { BlobRef } from '@atproto/lexicon'
|
8
14
|
import { BlobNotFoundError, BlobStore, WriteOpAction } from '@atproto/repo'
|
9
15
|
import { AtUri } from '@atproto/syntax'
|
@@ -11,12 +17,8 @@ import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
11
17
|
import { BackgroundQueue } from '../../background'
|
12
18
|
import * as img from '../../image'
|
13
19
|
import { StatusAttr } from '../../lexicon/types/com/atproto/admin/defs'
|
14
|
-
import {
|
15
|
-
|
16
|
-
PreparedDelete,
|
17
|
-
PreparedUpdate,
|
18
|
-
PreparedWrite,
|
19
|
-
} from '../../repo/types'
|
20
|
+
import { blobStoreLogger as log } from '../../logger'
|
21
|
+
import { PreparedBlobRef, PreparedWrite } from '../../repo/types'
|
20
22
|
import { ActorDb, Blob as BlobTable } from '../db'
|
21
23
|
import { BlobReader } from './reader'
|
22
24
|
|
@@ -113,38 +115,68 @@ export class BlobTransactor extends BlobReader {
|
|
113
115
|
async processWriteBlobs(rev: string, writes: PreparedWrite[]) {
|
114
116
|
await this.deleteDereferencedBlobs(writes)
|
115
117
|
|
116
|
-
const
|
118
|
+
const ac = new AbortController()
|
119
|
+
|
120
|
+
// Limit the number of parallel requests made to the BlobStore by using a
|
121
|
+
// a queue with concurrency management.
|
122
|
+
type Task = () => Promise<void>
|
123
|
+
const tasks: Task[] = []
|
124
|
+
|
117
125
|
for (const write of writes) {
|
118
|
-
if (
|
119
|
-
write.action === WriteOpAction.Create ||
|
120
|
-
write.action === WriteOpAction.Update
|
121
|
-
) {
|
126
|
+
if (isCreate(write) || isUpdate(write)) {
|
122
127
|
for (const blob of write.blobs) {
|
123
|
-
|
124
|
-
|
128
|
+
tasks.push(async () => {
|
129
|
+
if (ac.signal.aborted) return
|
130
|
+
await this.associateBlob(blob, write.uri)
|
131
|
+
await this.verifyBlobAndMakePermanent(blob, ac.signal)
|
132
|
+
})
|
125
133
|
}
|
126
134
|
}
|
127
135
|
}
|
128
|
-
|
136
|
+
|
137
|
+
try {
|
138
|
+
const queue = new PQueue({
|
139
|
+
concurrency: 20,
|
140
|
+
// The blob store should already limit the time of every operation. We
|
141
|
+
// add a timeout here as an extra precaution.
|
142
|
+
timeout: 60 * SECOND,
|
143
|
+
throwOnTimeout: true,
|
144
|
+
})
|
145
|
+
|
146
|
+
// Will reject as soon as any task fails, causing the "finally" block
|
147
|
+
// below to run, aborting every other pending tasks.
|
148
|
+
await queue.addAll(tasks)
|
149
|
+
} finally {
|
150
|
+
ac.abort()
|
151
|
+
}
|
129
152
|
}
|
130
153
|
|
131
|
-
async updateBlobTakedownStatus(
|
154
|
+
async updateBlobTakedownStatus(cid: CID, takedown: StatusAttr) {
|
132
155
|
const takedownRef = takedown.applied
|
133
156
|
? takedown.ref ?? new Date().toISOString()
|
134
157
|
: null
|
135
158
|
await this.db.db
|
136
159
|
.updateTable('blob')
|
137
160
|
.set({ takedownRef })
|
138
|
-
.where('cid', '=',
|
161
|
+
.where('cid', '=', cid.toString())
|
139
162
|
.executeTakeFirst()
|
163
|
+
|
140
164
|
try {
|
165
|
+
// @NOTE find a way to not perform i/o operations during the transaction
|
166
|
+
// (typically by using a state in the "blob" table, and another process to
|
167
|
+
// handle the actual i/o)
|
141
168
|
if (takedown.applied) {
|
142
|
-
await this.blobstore.quarantine(
|
169
|
+
await this.blobstore.quarantine(cid)
|
143
170
|
} else {
|
144
|
-
await this.blobstore.unquarantine(
|
171
|
+
await this.blobstore.unquarantine(cid)
|
145
172
|
}
|
146
173
|
} catch (err) {
|
147
174
|
if (!(err instanceof BlobNotFoundError)) {
|
175
|
+
log.error(
|
176
|
+
{ err, cid: cid.toString() },
|
177
|
+
'could not update blob takedown status',
|
178
|
+
)
|
179
|
+
|
148
180
|
throw err
|
149
181
|
}
|
150
182
|
}
|
@@ -154,21 +186,17 @@ export class BlobTransactor extends BlobReader {
|
|
154
186
|
writes: PreparedWrite[],
|
155
187
|
skipBlobStore?: boolean,
|
156
188
|
) {
|
157
|
-
const deletes = writes.filter(
|
158
|
-
|
159
|
-
) as PreparedDelete[]
|
160
|
-
const updates = writes.filter(
|
161
|
-
(w) => w.action === WriteOpAction.Update,
|
162
|
-
) as PreparedUpdate[]
|
189
|
+
const deletes = writes.filter(isDelete)
|
190
|
+
const updates = writes.filter(isUpdate)
|
163
191
|
const uris = [...deletes, ...updates].map((w) => w.uri.toString())
|
164
192
|
if (uris.length === 0) return
|
165
193
|
|
166
194
|
const deletedRepoBlobs = await this.db.db
|
167
195
|
.deleteFrom('record_blob')
|
168
196
|
.where('recordUri', 'in', uris)
|
169
|
-
.
|
197
|
+
.returning('blobCid')
|
170
198
|
.execute()
|
171
|
-
if (deletedRepoBlobs.length
|
199
|
+
if (deletedRepoBlobs.length === 0) return
|
172
200
|
|
173
201
|
const deletedRepoBlobCids = deletedRepoBlobs.map((row) => row.blobCid)
|
174
202
|
const duplicateCids = await this.db.db
|
@@ -178,53 +206,85 @@ export class BlobTransactor extends BlobReader {
|
|
178
206
|
.execute()
|
179
207
|
|
180
208
|
const newBlobCids = writes
|
181
|
-
.
|
182
|
-
|
183
|
-
|
184
|
-
: [],
|
185
|
-
)
|
186
|
-
.flat()
|
187
|
-
.map((b) => b.cid.toString())
|
209
|
+
.filter((w) => isUpdate(w) || isCreate(w))
|
210
|
+
.flatMap((w) => w.blobs.map((b) => b.cid.toString()))
|
211
|
+
|
188
212
|
const cidsToKeep = [
|
189
213
|
...newBlobCids,
|
190
214
|
...duplicateCids.map((row) => row.blobCid),
|
191
215
|
]
|
216
|
+
|
192
217
|
const cidsToDelete = deletedRepoBlobCids.filter(
|
193
218
|
(cid) => !cidsToKeep.includes(cid),
|
194
219
|
)
|
195
|
-
if (cidsToDelete.length
|
220
|
+
if (cidsToDelete.length === 0) return
|
196
221
|
|
197
222
|
await this.db.db
|
198
223
|
.deleteFrom('blob')
|
199
224
|
.where('cid', 'in', cidsToDelete)
|
200
225
|
.execute()
|
226
|
+
|
201
227
|
if (!skipBlobStore) {
|
202
228
|
this.db.onCommit(() => {
|
203
229
|
this.backgroundQueue.add(async () => {
|
204
|
-
|
205
|
-
cidsToDelete.map((cid) =>
|
206
|
-
|
230
|
+
try {
|
231
|
+
const cids = cidsToDelete.map((cid) => CID.parse(cid))
|
232
|
+
await this.blobstore.deleteMany(cids)
|
233
|
+
} catch (err) {
|
234
|
+
log.error(
|
235
|
+
{ err, cids: cidsToDelete },
|
236
|
+
'could not delete blobs from blobstore',
|
237
|
+
)
|
238
|
+
}
|
207
239
|
})
|
208
240
|
})
|
209
241
|
}
|
210
242
|
}
|
211
243
|
|
212
|
-
async verifyBlobAndMakePermanent(
|
244
|
+
async verifyBlobAndMakePermanent(
|
245
|
+
blob: PreparedBlobRef,
|
246
|
+
signal?: AbortSignal,
|
247
|
+
): Promise<void> {
|
213
248
|
const found = await this.db.db
|
214
249
|
.selectFrom('blob')
|
215
|
-
.
|
250
|
+
.select(['tempKey', 'size', 'mimeType'])
|
216
251
|
.where('cid', '=', blob.cid.toString())
|
217
252
|
.where('takedownRef', 'is', null)
|
218
253
|
.executeTakeFirst()
|
254
|
+
|
255
|
+
signal?.throwIfAborted()
|
256
|
+
|
219
257
|
if (!found) {
|
220
258
|
throw new InvalidRequestError(
|
221
259
|
`Could not find blob: ${blob.cid.toString()}`,
|
222
260
|
'BlobNotFound',
|
223
261
|
)
|
224
262
|
}
|
263
|
+
|
225
264
|
if (found.tempKey) {
|
226
265
|
verifyBlob(blob, found)
|
227
|
-
|
266
|
+
|
267
|
+
// @NOTE it is less than ideal to perform async (i/o) operations during a
|
268
|
+
// transaction. Especially since there have been instances of the actor-db
|
269
|
+
// being locked, requiring to kick the processes.
|
270
|
+
|
271
|
+
// The better solution would be to update the blob state in the database
|
272
|
+
// (e.g. "makeItPermanent") and to process those updates outside of the
|
273
|
+
// transaction.
|
274
|
+
|
275
|
+
await this.blobstore
|
276
|
+
.makePermanent(found.tempKey, blob.cid)
|
277
|
+
.catch((err) => {
|
278
|
+
log.error(
|
279
|
+
{ err, cid: blob.cid.toString() },
|
280
|
+
'could not make blob permanent',
|
281
|
+
)
|
282
|
+
|
283
|
+
throw err
|
284
|
+
})
|
285
|
+
|
286
|
+
signal?.throwIfAborted()
|
287
|
+
|
228
288
|
await this.db.db
|
229
289
|
.updateTable('blob')
|
230
290
|
.set({ tempKey: null })
|
@@ -300,7 +360,10 @@ function acceptedMime(mime: string, accepted: string[]): boolean {
|
|
300
360
|
return accepted.includes(mime)
|
301
361
|
}
|
302
362
|
|
303
|
-
function verifyBlob(
|
363
|
+
function verifyBlob(
|
364
|
+
blob: PreparedBlobRef,
|
365
|
+
found: Pick<BlobTable, 'size' | 'mimeType'>,
|
366
|
+
) {
|
304
367
|
const throwInvalid = (msg: string, errName = 'InvalidBlob') => {
|
305
368
|
throw new InvalidRequestError(msg, errName)
|
306
369
|
}
|
@@ -328,3 +391,13 @@ function verifyBlob(blob: PreparedBlobRef, found: BlobTable) {
|
|
328
391
|
)
|
329
392
|
}
|
330
393
|
}
|
394
|
+
|
395
|
+
function isCreate(write: PreparedWrite) {
|
396
|
+
return write.action === WriteOpAction.Create
|
397
|
+
}
|
398
|
+
function isUpdate(write: PreparedWrite) {
|
399
|
+
return write.action === WriteOpAction.Update
|
400
|
+
}
|
401
|
+
function isDelete(write: PreparedWrite) {
|
402
|
+
return write.action === WriteOpAction.Delete
|
403
|
+
}
|
@@ -213,19 +213,21 @@ export class RecordReader {
|
|
213
213
|
// Ensures that we don't end-up with duplicate likes, reposts, and follows from race conditions.
|
214
214
|
|
215
215
|
async getBacklinkConflicts(uri: AtUri, record: RepoRecord): Promise<AtUri[]> {
|
216
|
-
const
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
216
|
+
const conflicts: AtUri[] = []
|
217
|
+
|
218
|
+
for (const backlink of getBacklinks(uri, record)) {
|
219
|
+
const backlinks = await this.getRecordBacklinks({
|
220
|
+
collection: uri.collection,
|
221
|
+
path: backlink.path,
|
222
|
+
linkTo: backlink.linkTo,
|
223
|
+
})
|
224
|
+
|
225
|
+
for (const { rkey } of backlinks) {
|
226
|
+
conflicts.push(AtUri.make(uri.hostname, uri.collection, rkey))
|
227
|
+
}
|
228
|
+
}
|
229
|
+
|
226
230
|
return conflicts
|
227
|
-
.flat()
|
228
|
-
.map(({ rkey }) => AtUri.make(uri.hostname, uri.collection, rkey))
|
229
231
|
}
|
230
232
|
|
231
233
|
async listExistingBlocks(): Promise<CidSet> {
|
@@ -59,20 +59,18 @@ export class SqlRepoReader extends ReadableBlockstore {
|
|
59
59
|
const missing = new CidSet(cached.missing)
|
60
60
|
const missingStr = cached.missing.map((c) => c.toString())
|
61
61
|
const blocks = new BlockMap()
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
}),
|
75
|
-
)
|
62
|
+
for (const batch of chunkArray(missingStr, 500)) {
|
63
|
+
const res = await this.db.db
|
64
|
+
.selectFrom('repo_block')
|
65
|
+
.where('repo_block.cid', 'in', batch)
|
66
|
+
.select(['repo_block.cid as cid', 'repo_block.content as content'])
|
67
|
+
.execute()
|
68
|
+
for (const row of res) {
|
69
|
+
const cid = CID.parse(row.cid)
|
70
|
+
blocks.set(cid, row.content)
|
71
|
+
missing.delete(cid)
|
72
|
+
}
|
73
|
+
}
|
76
74
|
this.cache.addMap(blocks)
|
77
75
|
blocks.addMap(cached.blocks)
|
78
76
|
return { blocks, missing: missing.toList() }
|
@@ -45,24 +45,20 @@ export class SqlRepoTransactor extends SqlRepoReader implements RepoStorage {
|
|
45
45
|
}
|
46
46
|
|
47
47
|
async putMany(toPut: BlockMap, rev: string): Promise<void> {
|
48
|
-
const blocks: RepoBlock[] = []
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
.onConflict((oc) => oc.doNothing())
|
63
|
-
.execute(),
|
64
|
-
),
|
65
|
-
)
|
48
|
+
const blocks: RepoBlock[] = Array.from(toPut, ([cid, bytes]) => ({
|
49
|
+
cid: cid.toString(),
|
50
|
+
repoRev: rev,
|
51
|
+
size: bytes.length,
|
52
|
+
content: bytes,
|
53
|
+
}))
|
54
|
+
|
55
|
+
for (const batch of chunkArray(blocks, 50)) {
|
56
|
+
await this.db.db
|
57
|
+
.insertInto('repo_block')
|
58
|
+
.values(batch)
|
59
|
+
.onConflict((oc) => oc.doNothing())
|
60
|
+
.execute()
|
61
|
+
}
|
66
62
|
}
|
67
63
|
|
68
64
|
async deleteMany(cids: CID[]) {
|
@@ -75,11 +71,9 @@ export class SqlRepoTransactor extends SqlRepoReader implements RepoStorage {
|
|
75
71
|
}
|
76
72
|
|
77
73
|
async applyCommit(commit: CommitData, isCreate?: boolean) {
|
78
|
-
await
|
79
|
-
|
80
|
-
|
81
|
-
this.deleteMany(commit.removedCids.toList()),
|
82
|
-
])
|
74
|
+
await this.updateRoot(commit.cid, commit.rev, isCreate)
|
75
|
+
await this.putMany(commit.newBlocks, commit.rev)
|
76
|
+
await this.deleteMany(commit.removedCids.toList())
|
83
77
|
}
|
84
78
|
|
85
79
|
async updateRoot(cid: CID, rev: string, isCreate = false): Promise<void> {
|
@@ -55,11 +55,10 @@ export class RepoTransactor extends RepoReader {
|
|
55
55
|
this.signingKey,
|
56
56
|
writes.map(createWriteToOp),
|
57
57
|
)
|
58
|
-
await
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
])
|
58
|
+
await this.storage.applyCommit(commit, true)
|
59
|
+
await this.indexWrites(writes, commit.rev)
|
60
|
+
await this.blob.processWriteBlobs(commit.rev, writes)
|
61
|
+
|
63
62
|
const ops = writes.map((w) => ({
|
64
63
|
action: 'create' as const,
|
65
64
|
path: formatDataKey(w.uri.collection, w.uri.rkey),
|
@@ -87,14 +86,13 @@ export class RepoTransactor extends RepoReader {
|
|
87
86
|
throw new InvalidRequestError('Too many writes. Max event size: 2MB')
|
88
87
|
}
|
89
88
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
])
|
89
|
+
// persist the commit to repo storage
|
90
|
+
await this.storage.applyCommit(commit)
|
91
|
+
// & send to indexing
|
92
|
+
await this.indexWrites(writes, commit.rev)
|
93
|
+
// process blobs
|
94
|
+
await this.blob.processWriteBlobs(commit.rev, writes)
|
95
|
+
|
98
96
|
return commit
|
99
97
|
}
|
100
98
|
|
@@ -184,25 +182,24 @@ export class RepoTransactor extends RepoReader {
|
|
184
182
|
|
185
183
|
async indexWrites(writes: PreparedWrite[], rev: string) {
|
186
184
|
this.db.assertTransaction()
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
)
|
185
|
+
|
186
|
+
for (const write of writes) {
|
187
|
+
if (
|
188
|
+
write.action === WriteOpAction.Create ||
|
189
|
+
write.action === WriteOpAction.Update
|
190
|
+
) {
|
191
|
+
await this.record.indexRecord(
|
192
|
+
write.uri,
|
193
|
+
write.cid,
|
194
|
+
write.record,
|
195
|
+
write.action,
|
196
|
+
rev,
|
197
|
+
this.now,
|
198
|
+
)
|
199
|
+
} else if (write.action === WriteOpAction.Delete) {
|
200
|
+
await this.record.deleteRecord(write.uri)
|
201
|
+
}
|
202
|
+
}
|
206
203
|
}
|
207
204
|
|
208
205
|
async getDuplicateRecordCids(
|
@@ -19,16 +19,16 @@ export default function (server: Server, ctx: AppContext) {
|
|
19
19
|
await ctx.accountManager.takedownAccount(subject.did, takedown)
|
20
20
|
} else if (isStrongRef(subject)) {
|
21
21
|
const uri = new AtUri(subject.uri)
|
22
|
-
await ctx.actorStore.transact(uri.hostname, (store) =>
|
23
|
-
store.record.updateRecordTakedownStatus(uri, takedown)
|
24
|
-
)
|
22
|
+
await ctx.actorStore.transact(uri.hostname, async (store) => {
|
23
|
+
await store.record.updateRecordTakedownStatus(uri, takedown)
|
24
|
+
})
|
25
25
|
} else if (isRepoBlobRef(subject)) {
|
26
|
-
await ctx.actorStore.transact(subject.did, (store) =>
|
27
|
-
store.repo.blob.updateBlobTakedownStatus(
|
26
|
+
await ctx.actorStore.transact(subject.did, async (store) => {
|
27
|
+
await store.repo.blob.updateBlobTakedownStatus(
|
28
28
|
CID.parse(subject.cid),
|
29
29
|
takedown,
|
30
|
-
)
|
31
|
-
)
|
30
|
+
)
|
31
|
+
})
|
32
32
|
} else {
|
33
33
|
throw new InvalidRequestError('Invalid subject')
|
34
34
|
}
|