|
32 | 32 | use OCP\Files\Cache\CacheEntryRemovedEvent; |
33 | 33 | use OCP\Files\Events\Node\BeforeNodeDeletedEvent; |
34 | 34 | use OCP\Files\Events\Node\BeforeNodeRenamedEvent; |
| 35 | +use OCP\Files\File; |
35 | 36 | use OCP\Files\Folder; |
36 | 37 | use OCP\Files\Node; |
37 | 38 | use OCP\Files\NotFoundException; |
@@ -74,127 +75,159 @@ public function handle(Event $event): void { |
74 | 75 | $peerFile = $this->getLivePhotoPeer($event->getNode()->getId()); |
75 | 76 | } elseif ($event instanceof CacheEntryRemovedEvent) { |
76 | 77 | $peerFile = $this->getLivePhotoPeer($event->getFileId()); |
77 | | - } else { |
78 | | - return; |
79 | 78 | } |
80 | 79 |
|
81 | 80 | if ($peerFile === null) { |
82 | 81 | return; |
83 | 82 | } |
84 | 83 |
|
85 | 84 | if ($event instanceof BeforeNodeRenamedEvent) { |
86 | | - $sourceFile = $event->getSource(); |
87 | | - $targetFile = $event->getTarget(); |
88 | | - $targetParent = $targetFile->getParent(); |
89 | | - $sourceExtension = $sourceFile->getExtension(); |
90 | | - $peerFileExtension = $peerFile->getExtension(); |
91 | | - $targetName = $targetFile->getName(); |
92 | | - $targetPath = $targetFile->getPath(); |
93 | | - |
94 | | - // Prevent rename of the .mov file if the peer file do not have the same path. |
95 | | - if ($sourceFile->getMimetype() === 'video/quicktime') { |
96 | | - $peerFilePath = $this->pendingRenames[$peerFile->getId()] ?? $peerFile->getPath(); |
97 | | - $targetPathWithoutExtension = preg_replace("/\.$sourceExtension$/", '', $targetPath); |
98 | | - $peerFilePathWithoutExtension = preg_replace("/\.$peerFileExtension$/", '', $peerFilePath); |
99 | | - |
100 | | - if ($targetPathWithoutExtension !== $peerFilePathWithoutExtension) { |
101 | | - $event->abortOperation(new NotPermittedException("The video part of a live photo need to have the same name as the image")); |
102 | | - } |
103 | | - |
104 | | - unset($this->pendingRenames[$peerFile->getId()]); |
105 | | - return; |
106 | | - } |
107 | | - |
108 | | - if (!str_ends_with($targetName, ".".$sourceExtension)) { |
109 | | - $event->abortOperation(new NotPermittedException("Cannot change the extension of a live photo")); |
110 | | - } |
| 85 | + $this->handleMove($event, $peerFile); |
| 86 | + } elseif ($event instanceof BeforeNodeDeletedEvent) { |
| 87 | + $this->handleDeletion($event, $peerFile); |
| 88 | + } elseif ($event instanceof CacheEntryRemovedEvent) { |
| 89 | + $peerFile->delete(); |
| 90 | + } elseif ($event instanceof BeforeNodeRestoredEvent) { |
| 91 | + $this->handleRestore($event, $peerFile); |
| 92 | + } |
| 93 | + } |
111 | 94 |
|
112 | | - try { |
113 | | - $targetParent->get($targetName); |
114 | | - $event->abortOperation(new NotPermittedException("A file already exist at destination path")); |
115 | | - } catch (NotFoundException $ex) { |
116 | | - } |
117 | | - try { |
118 | | - $peerTargetName = preg_replace("/\.$sourceExtension$/", '.mov', $targetName); |
119 | | - $targetParent->get($peerTargetName); |
120 | | - $event->abortOperation(new NotPermittedException("A file already exist at destination path")); |
121 | | - } catch (NotFoundException $ex) { |
| 95 | + /** |
| 96 | + * During rename events, which also include move operations, |
| 97 | + * we rename the peer file using the same name. |
| 98 | + * This means that a move operation on the .jpg will trigger |
| 99 | + * another recursive one for the .mov. |
| 100 | + * Move operations on the .mov file directly are currently blocked. |
| 101 | + * The event listener being singleton, we can store the current state |
| 102 | + * of pending renames inside the 'pendingRenames' property, |
| 103 | + * to prevent infinite recursivity. |
| 104 | + */ |
| 105 | + private function handleMove(BeforeNodeRenamedEvent $event, Node $peerFile): void { |
| 106 | + $sourceFile = $event->getSource(); |
| 107 | + $targetFile = $event->getTarget(); |
| 108 | + $targetParent = $targetFile->getParent(); |
| 109 | + $sourceExtension = $sourceFile->getExtension(); |
| 110 | + $peerFileExtension = $peerFile->getExtension(); |
| 111 | + $targetName = $targetFile->getName(); |
| 112 | + $targetPath = $targetFile->getPath(); |
| 113 | + |
| 114 | + // Prevent rename of the .mov file if the peer file do not have the same path. |
| 115 | + if ($sourceFile->getMimetype() === 'video/quicktime') { |
| 116 | + $peerFilePath = $this->pendingRenames[$peerFile->getId()] ?? $peerFile->getPath(); |
| 117 | + $targetPathWithoutExtension = preg_replace("/\.$sourceExtension$/", '', $targetPath); |
| 118 | + $peerFilePathWithoutExtension = preg_replace("/\.$peerFileExtension$/", '', $peerFilePath); |
| 119 | + |
| 120 | + if ($targetPathWithoutExtension !== $peerFilePathWithoutExtension) { |
| 121 | + $event->abortOperation(new NotPermittedException("The video part of a live photo need to have the same name as the image")); |
122 | 122 | } |
123 | 123 |
|
124 | | - $peerTargetPath = preg_replace("/\.$sourceExtension$/", '.mov', $targetPath); |
125 | | - $this->pendingRenames[$sourceFile->getId()] = $targetPath; |
126 | | - try { |
127 | | - $peerFile->move($peerTargetPath); |
128 | | - } catch (\Throwable $ex) { |
129 | | - $event->abortOperation($ex); |
130 | | - } |
| 124 | + unset($this->pendingRenames[$peerFile->getId()]); |
131 | 125 | return; |
132 | 126 | } |
133 | 127 |
|
134 | | - if ($event instanceof BeforeNodeDeletedEvent) { |
135 | | - $deletedFile = $event->getNode(); |
136 | | - if ($deletedFile->getMimetype() === 'video/quicktime') { |
137 | | - if (isset($this->pendingDeletion[$peerFile->getId()])) { |
138 | | - unset($this->pendingDeletion[$peerFile->getId()]); |
139 | | - return; |
140 | | - } else { |
141 | | - $event->abortOperation(new NotPermittedException("Cannot delete the video part of a live photo")); |
142 | | - } |
143 | | - } else { |
144 | | - $this->pendingDeletion[$deletedFile->getId()] = true; |
145 | | - try { |
146 | | - $peerFile->delete(); |
147 | | - } catch (\Throwable $ex) { |
148 | | - $event->abortOperation($ex); |
149 | | - } |
150 | | - } |
151 | | - return; |
| 128 | + if (!str_ends_with($targetName, ".".$sourceExtension)) { |
| 129 | + $event->abortOperation(new NotPermittedException("Cannot change the extension of a live photo")); |
152 | 130 | } |
153 | 131 |
|
154 | | - if ($event instanceof CacheEntryRemovedEvent) { |
155 | | - $peerFile->delete(); |
| 132 | + try { |
| 133 | + $targetParent->get($targetName); |
| 134 | + $event->abortOperation(new NotPermittedException("A file already exist at destination path")); |
| 135 | + } catch (NotFoundException $ex) { |
| 136 | + } |
| 137 | + try { |
| 138 | + $peerTargetName = preg_replace("/\.$sourceExtension$/", '.mov', $targetName); |
| 139 | + $targetParent->get($peerTargetName); |
| 140 | + $event->abortOperation(new NotPermittedException("A file already exist at destination path")); |
| 141 | + } catch (NotFoundException $ex) { |
156 | 142 | } |
157 | 143 |
|
158 | | - if ($event instanceof BeforeNodeRestoredEvent) { |
159 | | - $sourceFile = $event->getSource(); |
| 144 | + $peerTargetPath = preg_replace("/\.$sourceExtension$/", '.mov', $targetPath); |
| 145 | + $this->pendingRenames[$sourceFile->getId()] = $targetPath; |
| 146 | + try { |
| 147 | + $peerFile->move($peerTargetPath); |
| 148 | + } catch (\Throwable $ex) { |
| 149 | + $event->abortOperation($ex); |
| 150 | + } |
| 151 | + return; |
| 152 | + } |
160 | 153 |
|
161 | | - if ($sourceFile->getMimetype() === 'video/quicktime') { |
162 | | - if (isset($this->pendingRestores[$peerFile->getId()])) { |
163 | | - unset($this->pendingRestores[$peerFile->getId()]); |
164 | | - return; |
165 | | - } else { |
166 | | - $event->abortOperation(new NotPermittedException("Cannot restore the video part of a live photo")); |
167 | | - } |
| 154 | + /** |
| 155 | + * During deletion event, we trigger another recursive delete on the peer file. |
| 156 | + * Delete operations on the .mov file directly are currently blocked. |
| 157 | + * The event listener being singleton, we can store the current state |
| 158 | + * of pending deletions inside the 'pendingDeletions' property, |
| 159 | + * to prevent infinite recursivity. |
| 160 | + */ |
| 161 | + private function handleDeletion(BeforeNodeDeletedEvent $event, Node $peerFile): void { |
| 162 | + $deletedFile = $event->getNode(); |
| 163 | + if ($deletedFile->getMimetype() === 'video/quicktime') { |
| 164 | + if (isset($this->pendingDeletion[$peerFile->getId()])) { |
| 165 | + unset($this->pendingDeletion[$peerFile->getId()]); |
| 166 | + return; |
168 | 167 | } else { |
169 | | - $user = $this->userSession->getUser(); |
170 | | - if ($user === null) { |
171 | | - return; |
172 | | - } |
| 168 | + $event->abortOperation(new NotPermittedException("Cannot delete the video part of a live photo")); |
| 169 | + } |
| 170 | + } else { |
| 171 | + $this->pendingDeletion[$deletedFile->getId()] = true; |
| 172 | + try { |
| 173 | + $peerFile->delete(); |
| 174 | + } catch (\Throwable $ex) { |
| 175 | + $event->abortOperation($ex); |
| 176 | + } |
| 177 | + } |
| 178 | + return; |
| 179 | + } |
173 | 180 |
|
174 | | - $peerTrashItem = $this->trashManager->getTrashNodeById($user, $peerFile->getId()); |
| 181 | + /** |
| 182 | + * During restore event, we trigger another recursive restore on the peer file. |
| 183 | + * Restore operations on the .mov file directly are currently blocked. |
| 184 | + * The event listener being singleton, we can store the current state |
| 185 | + * of pending restores inside the 'pendingRestores' property, |
| 186 | + * to prevent infinite recursivity. |
| 187 | + */ |
| 188 | + private function handleRestore(BeforeNodeRestoredEvent $event, Node $peerFile): void { |
| 189 | + $sourceFile = $event->getSource(); |
| 190 | + |
| 191 | + if ($sourceFile->getMimetype() === 'video/quicktime') { |
| 192 | + if (isset($this->pendingRestores[$peerFile->getId()])) { |
| 193 | + unset($this->pendingRestores[$peerFile->getId()]); |
| 194 | + return; |
| 195 | + } else { |
| 196 | + $event->abortOperation(new NotPermittedException("Cannot restore the video part of a live photo")); |
| 197 | + } |
| 198 | + } else { |
| 199 | + $user = $this->userSession->getUser(); |
| 200 | + if ($user === null) { |
| 201 | + return; |
| 202 | + } |
175 | 203 |
|
176 | | - // Peer file in not in the bin, no need to restore it. |
177 | | - if ($peerTrashItem === null) { |
178 | | - return; |
179 | | - } |
| 204 | + $peerTrashItem = $this->trashManager->getTrashNodeById($user, $peerFile->getId()); |
| 205 | + // Peer file is not in the bin, no need to restore it. |
| 206 | + if ($peerTrashItem === null) { |
| 207 | + return; |
| 208 | + } |
180 | 209 |
|
181 | | - $trashRoot = $this->trashManager->listTrashRoot($user); |
182 | | - $trashItem = $this->getTrashItem($trashRoot, $peerFile->getInternalPath()); |
| 210 | + $trashRoot = $this->trashManager->listTrashRoot($user); |
| 211 | + $trashItem = $this->getTrashItem($trashRoot, $peerFile->getInternalPath()); |
183 | 212 |
|
184 | | - if ($trashItem === null) { |
185 | | - $event->abortOperation(new NotFoundException("Couldn't find peer file in trashbin")); |
186 | | - } |
| 213 | + if ($trashItem === null) { |
| 214 | + $event->abortOperation(new NotFoundException("Couldn't find peer file in trashbin")); |
| 215 | + } |
187 | 216 |
|
188 | | - $this->pendingRestores[$sourceFile->getId()] = true; |
189 | | - try { |
190 | | - $this->trashManager->restoreItem($trashItem); |
191 | | - } catch (\Throwable $ex) { |
192 | | - $event->abortOperation($ex); |
193 | | - } |
| 217 | + $this->pendingRestores[$sourceFile->getId()] = true; |
| 218 | + try { |
| 219 | + $this->trashManager->restoreItem($trashItem); |
| 220 | + } catch (\Throwable $ex) { |
| 221 | + $event->abortOperation($ex); |
194 | 222 | } |
195 | 223 | } |
196 | 224 | } |
197 | 225 |
|
| 226 | + /** |
| 227 | + * Helper method to get the associated live photo file. |
| 228 | + * We first look for it in the user folder, and if we |
| 229 | + * cannot find it here, we look for it in the user's trashbin. |
| 230 | + */ |
198 | 231 | private function getLivePhotoPeer(int $nodeId): ?Node { |
199 | 232 | if ($this->userFolder === null || $this->userSession === null) { |
200 | 233 | return null; |
@@ -231,6 +264,11 @@ private function getLivePhotoPeer(int $nodeId): ?Node { |
231 | 264 | return null; |
232 | 265 | } |
233 | 266 |
|
| 267 | + /** |
| 268 | + * There is currently no method to restore a file based on its fileId or path. |
| 269 | + * So we have to manually find a ITrashItem from the trash item list. |
| 270 | + * TODO: This should be replaced by a proper method in the TrashManager. |
| 271 | + */ |
234 | 272 | private function getTrashItem(array $trashFolder, string $path): ?ITrashItem { |
235 | 273 | foreach($trashFolder as $trashItem) { |
236 | 274 | if (str_starts_with($path, "files_trashbin/files".$trashItem->getTrashPath())) { |
|
0 commit comments