+65
src/Discovery/DiscoveryResult.php
+65
src/Discovery/DiscoveryResult.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpParity\Discovery;
4
+
5
+
/**
6
+
* Immutable value object representing the result of a discovery operation.
7
+
*/
8
+
readonly class DiscoveryResult
9
+
{
10
+
public function __construct(
11
+
public bool $success,
12
+
public array $dids = [],
13
+
public int $total = 0,
14
+
public ?string $error = null,
15
+
public bool $incomplete = false,
16
+
) {}
17
+
18
+
/**
19
+
* Check if the discovery operation succeeded.
20
+
*/
21
+
public function isSuccess(): bool
22
+
{
23
+
return $this->success;
24
+
}
25
+
26
+
/**
27
+
* Check if the discovery operation failed.
28
+
*/
29
+
public function isFailed(): bool
30
+
{
31
+
return ! $this->success;
32
+
}
33
+
34
+
/**
35
+
* Check if the discovery was stopped before completion (e.g., limit reached).
36
+
*/
37
+
public function isIncomplete(): bool
38
+
{
39
+
return $this->incomplete;
40
+
}
41
+
42
+
/**
43
+
* Create a successful result.
44
+
*/
45
+
public static function success(array $dids, bool $incomplete = false): self
46
+
{
47
+
return new self(
48
+
success: true,
49
+
dids: $dids,
50
+
total: count($dids),
51
+
incomplete: $incomplete,
52
+
);
53
+
}
54
+
55
+
/**
56
+
* Create a failed result.
57
+
*/
58
+
public static function failed(string $error): self
59
+
{
60
+
return new self(
61
+
success: false,
62
+
error: $error,
63
+
);
64
+
}
65
+
}
+119
src/Discovery/DiscoveryService.php
+119
src/Discovery/DiscoveryService.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpParity\Discovery;
4
+
5
+
use BackedEnum;
6
+
use Generator;
7
+
use SocialDept\AtpClient\Facades\Atp;
8
+
use SocialDept\AtpParity\Import\ImportService;
9
+
use Throwable;
10
+
11
+
/**
12
+
* Service for discovering DIDs with records in specific collections.
13
+
*/
14
+
class DiscoveryService
15
+
{
16
+
public function __construct(
17
+
protected ImportService $importService
18
+
) {}
19
+
20
+
/**
21
+
* Discover all DIDs with records in a collection.
22
+
*
23
+
* @return Generator<string> Yields DIDs
24
+
*/
25
+
public function discoverDids(string|BackedEnum $collection, ?int $limit = null): Generator
26
+
{
27
+
$collection = $collection instanceof BackedEnum ? $collection->value : $collection;
28
+
$cursor = null;
29
+
$count = 0;
30
+
31
+
do {
32
+
$response = Atp::atproto->sync->listReposByCollection(
33
+
collection: $collection,
34
+
limit: min(500, $limit ? $limit - $count : 500),
35
+
cursor: $cursor,
36
+
);
37
+
38
+
foreach ($response->repos as $repo) {
39
+
$did = $repo['did'] ?? null;
40
+
41
+
if ($did) {
42
+
yield $did;
43
+
$count++;
44
+
45
+
if ($limit !== null && $count >= $limit) {
46
+
return;
47
+
}
48
+
}
49
+
}
50
+
51
+
$cursor = $response->cursor;
52
+
} while ($cursor !== null);
53
+
}
54
+
55
+
/**
56
+
* Discover DIDs and return as an array.
57
+
*/
58
+
public function discover(string|BackedEnum $collection, ?int $limit = null): DiscoveryResult
59
+
{
60
+
try {
61
+
$dids = iterator_to_array($this->discoverDids($collection, $limit));
62
+
$incomplete = $limit !== null && count($dids) >= $limit;
63
+
64
+
return DiscoveryResult::success($dids, $incomplete);
65
+
} catch (Throwable $e) {
66
+
return DiscoveryResult::failed($e->getMessage());
67
+
}
68
+
}
69
+
70
+
/**
71
+
* Discover and import all users for a collection.
72
+
*/
73
+
public function discoverAndImport(
74
+
string|BackedEnum $collection,
75
+
?int $limit = null,
76
+
?callable $onProgress = null
77
+
): DiscoveryResult {
78
+
$collection = $collection instanceof BackedEnum ? $collection->value : $collection;
79
+
80
+
try {
81
+
$dids = [];
82
+
$count = 0;
83
+
84
+
foreach ($this->discoverDids($collection, $limit) as $did) {
85
+
$dids[] = $did;
86
+
$count++;
87
+
88
+
// Start import for this DID
89
+
$this->importService->import($did, [$collection]);
90
+
91
+
if ($onProgress) {
92
+
$onProgress($did, $count);
93
+
}
94
+
}
95
+
96
+
$incomplete = $limit !== null && count($dids) >= $limit;
97
+
98
+
return DiscoveryResult::success($dids, $incomplete);
99
+
} catch (Throwable $e) {
100
+
return DiscoveryResult::failed($e->getMessage());
101
+
}
102
+
}
103
+
104
+
/**
105
+
* Count total DIDs with records in a collection.
106
+
*
107
+
* Note: This iterates through all results, which can be slow.
108
+
*/
109
+
public function count(string|BackedEnum $collection): int
110
+
{
111
+
$count = 0;
112
+
113
+
foreach ($this->discoverDids($collection) as $_) {
114
+
$count++;
115
+
}
116
+
117
+
return $count;
118
+
}
119
+
}
+55
src/Export/ExportResult.php
+55
src/Export/ExportResult.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpParity\Export;
4
+
5
+
/**
6
+
* Value object representing the result of an export operation.
7
+
*/
8
+
readonly class ExportResult
9
+
{
10
+
public function __construct(
11
+
public bool $success,
12
+
public ?string $path = null,
13
+
public ?int $size = null,
14
+
public ?string $error = null,
15
+
) {}
16
+
17
+
/**
18
+
* Check if the export operation succeeded.
19
+
*/
20
+
public function isSuccess(): bool
21
+
{
22
+
return $this->success;
23
+
}
24
+
25
+
/**
26
+
* Check if the export operation failed.
27
+
*/
28
+
public function isFailed(): bool
29
+
{
30
+
return ! $this->success;
31
+
}
32
+
33
+
/**
34
+
* Create a successful result.
35
+
*/
36
+
public static function success(string $path, int $size): self
37
+
{
38
+
return new self(
39
+
success: true,
40
+
path: $path,
41
+
size: $size,
42
+
);
43
+
}
44
+
45
+
/**
46
+
* Create a failed result.
47
+
*/
48
+
public static function failed(string $error): self
49
+
{
50
+
return new self(
51
+
success: false,
52
+
error: $error,
53
+
);
54
+
}
55
+
}
+142
src/Export/ExportService.php
+142
src/Export/ExportService.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpParity\Export;
4
+
5
+
use BackedEnum;
6
+
use Generator;
7
+
use SocialDept\AtpClient\Facades\Atp;
8
+
use SocialDept\AtpParity\Import\ImportService;
9
+
use SocialDept\AtpParity\MapperRegistry;
10
+
use Throwable;
11
+
12
+
/**
13
+
* Service for exporting AT Protocol repositories.
14
+
*/
15
+
class ExportService
16
+
{
17
+
public function __construct(
18
+
protected MapperRegistry $registry,
19
+
protected ImportService $importService
20
+
) {}
21
+
22
+
/**
23
+
* Download a user's repository as CAR data.
24
+
*/
25
+
public function downloadRepo(string $did, ?string $since = null): RepoExport
26
+
{
27
+
$response = Atp::atproto->sync->getRepo($did, $since);
28
+
$carData = $response->body();
29
+
30
+
return new RepoExport(
31
+
did: $did,
32
+
carData: $carData,
33
+
size: strlen($carData),
34
+
);
35
+
}
36
+
37
+
/**
38
+
* Export a repository to a local file.
39
+
*/
40
+
public function exportToFile(string $did, string $path, ?string $since = null): ExportResult
41
+
{
42
+
try {
43
+
$export = $this->downloadRepo($did, $since);
44
+
45
+
if (! $export->saveTo($path)) {
46
+
return ExportResult::failed("Failed to write to file: {$path}");
47
+
}
48
+
49
+
return ExportResult::success($path, $export->size);
50
+
} catch (Throwable $e) {
51
+
return ExportResult::failed($e->getMessage());
52
+
}
53
+
}
54
+
55
+
/**
56
+
* Export and import records from a repository.
57
+
*
58
+
* This downloads the repository and imports records using the normal import pipeline.
59
+
* It's useful for bulk importing all records from a user.
60
+
*
61
+
* @param array<string>|null $collections Specific collections to import (null = all registered)
62
+
*/
63
+
public function exportAndImport(
64
+
string $did,
65
+
?array $collections = null,
66
+
?callable $onProgress = null
67
+
): ExportResult {
68
+
try {
69
+
// Use the import service to import the user's records
70
+
$result = $this->importService->importUser($did, $collections, $onProgress);
71
+
72
+
if ($result->isFailed()) {
73
+
return ExportResult::failed($result->error ?? 'Import failed');
74
+
}
75
+
76
+
return ExportResult::success(
77
+
path: "imported:{$did}",
78
+
size: $result->recordsSynced
79
+
);
80
+
} catch (Throwable $e) {
81
+
return ExportResult::failed($e->getMessage());
82
+
}
83
+
}
84
+
85
+
/**
86
+
* List available blobs for a repository.
87
+
*
88
+
* @return Generator<string> Yields blob CIDs
89
+
*/
90
+
public function listBlobs(string $did, ?string $since = null): Generator
91
+
{
92
+
$cursor = null;
93
+
94
+
do {
95
+
$response = Atp::atproto->sync->listBlobs(
96
+
did: $did,
97
+
since: $since,
98
+
limit: 500,
99
+
cursor: $cursor,
100
+
);
101
+
102
+
foreach ($response->cids as $cid) {
103
+
yield $cid;
104
+
}
105
+
106
+
$cursor = $response->cursor;
107
+
} while ($cursor !== null);
108
+
}
109
+
110
+
/**
111
+
* Download a specific blob.
112
+
*/
113
+
public function downloadBlob(string $did, string $cid): string
114
+
{
115
+
$response = Atp::atproto->sync->getBlob($did, $cid);
116
+
117
+
return $response->body();
118
+
}
119
+
120
+
/**
121
+
* Get the latest commit for a repository.
122
+
*/
123
+
public function getLatestCommit(string $did): array
124
+
{
125
+
$commit = Atp::atproto->sync->getLatestCommit($did);
126
+
127
+
return [
128
+
'cid' => $commit->cid,
129
+
'rev' => $commit->rev,
130
+
];
131
+
}
132
+
133
+
/**
134
+
* Get the hosting status for a repository.
135
+
*/
136
+
public function getRepoStatus(string $did): array
137
+
{
138
+
$status = Atp::atproto->sync->getRepoStatus($did);
139
+
140
+
return $status->toArray();
141
+
}
142
+
}
+40
src/Export/RepoExport.php
+40
src/Export/RepoExport.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpParity\Export;
4
+
5
+
/**
6
+
* Value object representing an exported repository as CAR data.
7
+
*/
8
+
readonly class RepoExport
9
+
{
10
+
public function __construct(
11
+
public string $did,
12
+
public string $carData,
13
+
public int $size,
14
+
) {}
15
+
16
+
/**
17
+
* Save the CAR data to a file.
18
+
*/
19
+
public function saveTo(string $path): bool
20
+
{
21
+
return file_put_contents($path, $this->carData) !== false;
22
+
}
23
+
24
+
/**
25
+
* Get the size in human-readable format.
26
+
*/
27
+
public function humanSize(): string
28
+
{
29
+
$units = ['B', 'KB', 'MB', 'GB'];
30
+
$size = $this->size;
31
+
$unit = 0;
32
+
33
+
while ($size >= 1024 && $unit < count($units) - 1) {
34
+
$size /= 1024;
35
+
$unit++;
36
+
}
37
+
38
+
return round($size, 2).' '.$units[$unit];
39
+
}
40
+
}