-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPackagedDirectorySynchronizer.php
More file actions
218 lines (189 loc) · 7.69 KB
/
PackagedDirectorySynchronizer.php
File metadata and controls
218 lines (189 loc) · 7.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
<?php
declare(strict_types=1);
/**
* Fast Forward Development Tools for PHP projects.
*
* This file is part of fast-forward/dev-tools project.
*
* @author Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
* @license https://opensource.org/licenses/MIT MIT License
*
* @see https://github.com/php-fast-forward/
* @see https://github.com/php-fast-forward/dev-tools
* @see https://github.com/php-fast-forward/dev-tools/issues
* @see https://php-fast-forward.github.io/dev-tools/
* @see https://datatracker.ietf.org/doc/html/rfc2119
*/
namespace FastForward\DevTools\Sync;
use FastForward\DevTools\Filesystem\FinderFactoryInterface;
use FastForward\DevTools\Filesystem\FilesystemInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Filesystem\Path;
/**
* Synchronizes one packaged directory of symlinked entries into a consumer repository.
*/
final class PackagedDirectorySynchronizer implements LoggerAwareInterface
{
/**
* Initializes the synchronizer with a filesystem and finder factory.
*
* @param FilesystemInterface $filesystem Filesystem instance for file operations
* @param FinderFactoryInterface $finderFactory Factory for locating packaged directories
* @param LoggerInterface $logger Logger for recording synchronization actions and decisions
*/
public function __construct(
private readonly FilesystemInterface $filesystem,
private readonly FinderFactoryInterface $finderFactory,
private LoggerInterface $logger,
) {}
/**
* {@inheritDoc}
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* Synchronizes packaged directory entries into the consumer repository.
*
* @param string $targetDir Absolute path to the consumer directory to populate
* @param string $packagePath Absolute path to the packaged directory to mirror
* @param string $directoryLabel Human-readable directory label used in log messages
*
* @return SynchronizeResult Result containing counts of created, preserved, and removed links
*/
public function synchronize(string $targetDir, string $packagePath, string $directoryLabel): SynchronizeResult
{
$result = new SynchronizeResult();
if (! $this->filesystem->exists($packagePath)) {
$this->logger->error('No packaged ' . $directoryLabel . ' found at: ' . $packagePath);
$result->markFailed();
return $result;
}
if (! $this->filesystem->exists($targetDir)) {
$this->filesystem->mkdir($targetDir);
$this->logger->info('Created ' . $directoryLabel . ' directory.');
}
$finder = $this->finderFactory
->create()
->directories()
->in($packagePath)
->depth('== 0');
foreach ($finder as $packagedDirectory) {
$entryName = $packagedDirectory->getFilename();
$targetLink = Path::makeAbsolute($entryName, $targetDir);
$sourcePath = $packagedDirectory->getRealPath();
$this->processLink($entryName, $targetLink, $sourcePath, $result);
}
return $result;
}
/**
* Routes an entry link to the appropriate handling method based on target state.
*
* @param string $entryName Name of the packaged entry being processed
* @param string $targetLink Absolute path where the symlink should exist
* @param string $sourcePath Absolute path to the packaged source directory
* @param SynchronizeResult $result Result tracker for reporting outcomes
*/
private function processLink(
string $entryName,
string $targetLink,
string $sourcePath,
SynchronizeResult $result,
): void {
if (! $this->filesystem->exists($targetLink)) {
$this->createNewLink($entryName, $targetLink, $sourcePath, $result);
return;
}
if (! $this->isSymlink($targetLink)) {
$this->preserveExistingNonSymlink($entryName, $result);
return;
}
$this->processExistingSymlink($entryName, $targetLink, $sourcePath, $result);
}
/**
* Creates a new symlink pointing to the packaged entry.
*
* @param string $entryName Name identifying the entry
* @param string $targetLink Absolute path where the symlink will be created
* @param string $sourcePath Absolute path to the packaged directory
* @param SynchronizeResult $result Result object for tracking creation
*/
private function createNewLink(
string $entryName,
string $targetLink,
string $sourcePath,
SynchronizeResult $result,
): void {
$relativeSourcePath = $this->filesystem->makePathRelative($sourcePath, $this->filesystem->dirname($targetLink));
$this->filesystem->symlink($relativeSourcePath, $targetLink);
$this->logger->info('Created link: ' . $entryName . ' -> ' . $relativeSourcePath);
$result->addCreatedLink($entryName);
}
/**
* Handles an existing non-symlink item at the target path.
*
* @param string $entryName Name of the entry with the conflicting item
* @param SynchronizeResult $result Result tracker for preserved items
*/
private function preserveExistingNonSymlink(string $entryName, SynchronizeResult $result): void
{
$this->logger->notice(
'Existing non-symlink found: ' . $entryName . ' (keeping as is, skipping link creation)'
);
$result->addPreservedLink($entryName);
}
/**
* Evaluates an existing symlink and determines whether to preserve or repair it.
*
* @param string $entryName Name of the entry with the existing symlink
* @param string $targetLink Absolute path to the existing symlink
* @param string $sourcePath Absolute path to the expected source directory
* @param SynchronizeResult $result Result tracker for preserved or removed links
*/
private function processExistingSymlink(
string $entryName,
string $targetLink,
string $sourcePath,
SynchronizeResult $result,
): void {
$linkPath = $this->filesystem->readlink($targetLink, true);
if (! $linkPath || ! $this->filesystem->exists($linkPath)) {
$this->repairBrokenLink($entryName, $targetLink, $sourcePath, $result);
return;
}
$this->logger->notice('Preserved existing link: ' . $entryName);
$result->addPreservedLink($entryName);
}
/**
* Removes a broken symlink and creates a fresh one pointing to the current source.
*
* @param string $entryName Name of the entry with the broken symlink
* @param string $targetLink Absolute path to the broken symlink
* @param string $sourcePath Absolute path to the current packaged directory
* @param SynchronizeResult $result Result tracker for removed and created items
*/
private function repairBrokenLink(
string $entryName,
string $targetLink,
string $sourcePath,
SynchronizeResult $result,
): void {
$this->filesystem->remove($targetLink);
$this->logger->notice('Existing link is broken: ' . $entryName . ' (removing and recreating)');
$result->addRemovedBrokenLink($entryName);
$this->createNewLink($entryName, $targetLink, $sourcePath, $result);
}
/**
* Checks if a path is a symbolic link.
*
* @param string $path the target path
*
* @return bool whether the path is a symbolic link
*/
private function isSymlink(string $path): bool
{
return null !== $this->filesystem->readlink($path);
}
}