Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 131 additions & 93 deletions apps/files/lib/Listener/SyncLivePhotosListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use OCP\Files\Cache\CacheEntryRemovedEvent;
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
Expand Down Expand Up @@ -74,127 +75,159 @@ public function handle(Event $event): void {
$peerFile = $this->getLivePhotoPeer($event->getNode()->getId());
} elseif ($event instanceof CacheEntryRemovedEvent) {
$peerFile = $this->getLivePhotoPeer($event->getFileId());
} else {
return;
}

if ($peerFile === null) {
return;
}

if ($event instanceof BeforeNodeRenamedEvent) {
$sourceFile = $event->getSource();
$targetFile = $event->getTarget();
$targetParent = $targetFile->getParent();
$sourceExtension = $sourceFile->getExtension();
$peerFileExtension = $peerFile->getExtension();
$targetName = $targetFile->getName();
$targetPath = $targetFile->getPath();

// Prevent rename of the .mov file if the peer file do not have the same path.
if ($sourceFile->getMimetype() === 'video/quicktime') {
$peerFilePath = $this->pendingRenames[$peerFile->getId()] ?? $peerFile->getPath();
$targetPathWithoutExtension = preg_replace("/\.$sourceExtension$/", '', $targetPath);
$peerFilePathWithoutExtension = preg_replace("/\.$peerFileExtension$/", '', $peerFilePath);

if ($targetPathWithoutExtension !== $peerFilePathWithoutExtension) {
$event->abortOperation(new NotPermittedException("The video part of a live photo need to have the same name as the image"));
}

unset($this->pendingRenames[$peerFile->getId()]);
return;
}

if (!str_ends_with($targetName, ".".$sourceExtension)) {
$event->abortOperation(new NotPermittedException("Cannot change the extension of a live photo"));
}
$this->handleMove($event, $peerFile);

Check notice

Code scanning / Psalm

ArgumentTypeCoercion

Argument 2 of OCA\Files\Listener\SyncLivePhotosListener::handleMove expects OCP\Files\File, but parent type OCP\Files\Node provided
} elseif ($event instanceof BeforeNodeDeletedEvent) {
$this->handleDeletion($event, $peerFile);

Check notice

Code scanning / Psalm

ArgumentTypeCoercion

Argument 2 of OCA\Files\Listener\SyncLivePhotosListener::handleDeletion expects OCP\Files\File, but parent type OCP\Files\Node provided
} elseif ($event instanceof CacheEntryRemovedEvent) {
$peerFile->delete();
} elseif ($event instanceof BeforeNodeRestoredEvent) {

Check failure

Code scanning / Psalm

RedundantCondition

Type OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent for $event is always OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent
$this->handleRestore($event, $peerFile);

Check notice

Code scanning / Psalm

ArgumentTypeCoercion

Argument 2 of OCA\Files\Listener\SyncLivePhotosListener::handleRestore expects OCP\Files\File, but parent type OCP\Files\Node provided
}
}

try {
$targetParent->get($targetName);
$event->abortOperation(new NotPermittedException("A file already exist at destination path"));
} catch (NotFoundException $ex) {
}
try {
$peerTargetName = preg_replace("/\.$sourceExtension$/", '.mov', $targetName);
$targetParent->get($peerTargetName);
$event->abortOperation(new NotPermittedException("A file already exist at destination path"));
} catch (NotFoundException $ex) {
/**
* During rename events, which also include move operations,
* we rename the peer file using the same name.
* This means that a move operation on the .jpg will trigger
* another recursive one for the .mov.
* Move operations on the .mov file directly are currently blocked.
* The event listener being singleton, we can store the current state
* of pending renames inside the 'pendingRenames' property,
* to prevent infinite recursivity.
*/
private function handleMove(BeforeNodeRenamedEvent $event, Node $peerFile): void {
$sourceFile = $event->getSource();
$targetFile = $event->getTarget();
$targetParent = $targetFile->getParent();
$sourceExtension = $sourceFile->getExtension();
$peerFileExtension = $peerFile->getExtension();
$targetName = $targetFile->getName();
$targetPath = $targetFile->getPath();

// Prevent rename of the .mov file if the peer file do not have the same path.
if ($sourceFile->getMimetype() === 'video/quicktime') {
$peerFilePath = $this->pendingRenames[$peerFile->getId()] ?? $peerFile->getPath();
$targetPathWithoutExtension = preg_replace("/\.$sourceExtension$/", '', $targetPath);
$peerFilePathWithoutExtension = preg_replace("/\.$peerFileExtension$/", '', $peerFilePath);

if ($targetPathWithoutExtension !== $peerFilePathWithoutExtension) {
$event->abortOperation(new NotPermittedException("The video part of a live photo need to have the same name as the image"));
}

$peerTargetPath = preg_replace("/\.$sourceExtension$/", '.mov', $targetPath);
$this->pendingRenames[$sourceFile->getId()] = $targetPath;
try {
$peerFile->move($peerTargetPath);
} catch (\Throwable $ex) {
$event->abortOperation($ex);
}
unset($this->pendingRenames[$peerFile->getId()]);
return;
}

if ($event instanceof BeforeNodeDeletedEvent) {
$deletedFile = $event->getNode();
if ($deletedFile->getMimetype() === 'video/quicktime') {
if (isset($this->pendingDeletion[$peerFile->getId()])) {
unset($this->pendingDeletion[$peerFile->getId()]);
return;
} else {
$event->abortOperation(new NotPermittedException("Cannot delete the video part of a live photo"));
}
} else {
$this->pendingDeletion[$deletedFile->getId()] = true;
try {
$peerFile->delete();
} catch (\Throwable $ex) {
$event->abortOperation($ex);
}
}
return;
if (!str_ends_with($targetName, ".".$sourceExtension)) {
$event->abortOperation(new NotPermittedException("Cannot change the extension of a live photo"));
}

if ($event instanceof CacheEntryRemovedEvent) {
$peerFile->delete();
try {
$targetParent->get($targetName);
$event->abortOperation(new NotPermittedException("A file already exist at destination path"));
} catch (NotFoundException $ex) {
}
try {
$peerTargetName = preg_replace("/\.$sourceExtension$/", '.mov', $targetName);
$targetParent->get($peerTargetName);
$event->abortOperation(new NotPermittedException("A file already exist at destination path"));
} catch (NotFoundException $ex) {
}

if ($event instanceof BeforeNodeRestoredEvent) {
$sourceFile = $event->getSource();
$peerTargetPath = preg_replace("/\.$sourceExtension$/", '.mov', $targetPath);
$this->pendingRenames[$sourceFile->getId()] = $targetPath;
try {
$peerFile->move($peerTargetPath);
} catch (\Throwable $ex) {
$event->abortOperation($ex);
}
return;
}

if ($sourceFile->getMimetype() === 'video/quicktime') {
if (isset($this->pendingRestores[$peerFile->getId()])) {
unset($this->pendingRestores[$peerFile->getId()]);
return;
} else {
$event->abortOperation(new NotPermittedException("Cannot restore the video part of a live photo"));
}
/**
* During deletion event, we trigger another recursive delete on the peer file.
* Delete operations on the .mov file directly are currently blocked.
* The event listener being singleton, we can store the current state
* of pending deletions inside the 'pendingDeletions' property,
* to prevent infinite recursivity.
*/
private function handleDeletion(BeforeNodeDeletedEvent $event, Node $peerFile): void {
$deletedFile = $event->getNode();
if ($deletedFile->getMimetype() === 'video/quicktime') {
if (isset($this->pendingDeletion[$peerFile->getId()])) {
unset($this->pendingDeletion[$peerFile->getId()]);
return;
} else {
$user = $this->userSession->getUser();
if ($user === null) {
return;
}
$event->abortOperation(new NotPermittedException("Cannot delete the video part of a live photo"));
}
} else {
$this->pendingDeletion[$deletedFile->getId()] = true;
try {
$peerFile->delete();
} catch (\Throwable $ex) {
$event->abortOperation($ex);
}
}
return;
}

$peerTrashItem = $this->trashManager->getTrashNodeById($user, $peerFile->getId());
/**
* During restore event, we trigger another recursive restore on the peer file.
* Restore operations on the .mov file directly are currently blocked.
* The event listener being singleton, we can store the current state
* of pending restores inside the 'pendingRestores' property,
* to prevent infinite recursivity.
*/
private function handleRestore(BeforeNodeRestoredEvent $event, Node $peerFile): void {
$sourceFile = $event->getSource();

if ($sourceFile->getMimetype() === 'video/quicktime') {
if (isset($this->pendingRestores[$peerFile->getId()])) {
unset($this->pendingRestores[$peerFile->getId()]);
return;
} else {
$event->abortOperation(new NotPermittedException("Cannot restore the video part of a live photo"));
}
} else {
$user = $this->userSession->getUser();

Check notice

Code scanning / Psalm

PossiblyNullReference

Cannot call method getUser on possibly null value
if ($user === null) {
return;
}

// Peer file in not in the bin, no need to restore it.
if ($peerTrashItem === null) {
return;
}
$peerTrashItem = $this->trashManager->getTrashNodeById($user, $peerFile->getId());
// Peer file is not in the bin, no need to restore it.
if ($peerTrashItem === null) {
return;
}

$trashRoot = $this->trashManager->listTrashRoot($user);
$trashItem = $this->getTrashItem($trashRoot, $peerFile->getInternalPath());
$trashRoot = $this->trashManager->listTrashRoot($user);
$trashItem = $this->getTrashItem($trashRoot, $peerFile->getInternalPath());

if ($trashItem === null) {
$event->abortOperation(new NotFoundException("Couldn't find peer file in trashbin"));
}
if ($trashItem === null) {
$event->abortOperation(new NotFoundException("Couldn't find peer file in trashbin"));
}

$this->pendingRestores[$sourceFile->getId()] = true;
try {
$this->trashManager->restoreItem($trashItem);
} catch (\Throwable $ex) {
$event->abortOperation($ex);
}
$this->pendingRestores[$sourceFile->getId()] = true;
try {
$this->trashManager->restoreItem($trashItem);
} catch (\Throwable $ex) {
$event->abortOperation($ex);
}
}
}

/**
* Helper method to get the associated live photo file.
* We first look for it in the user folder, and if we
* cannot find it here, we look for it in the user's trashbin.
*/
private function getLivePhotoPeer(int $nodeId): ?Node {
if ($this->userFolder === null || $this->userSession === null) {
return null;
Expand Down Expand Up @@ -231,6 +264,11 @@ private function getLivePhotoPeer(int $nodeId): ?Node {
return null;
}

/**
* There is currently no method to restore a file based on its fileId or path.
* So we have to manually find a ITrashItem from the trash item list.
* TODO: This should be replaced by a proper method in the TrashManager.
*/
private function getTrashItem(array $trashFolder, string $path): ?ITrashItem {
foreach($trashFolder as $trashItem) {
if (str_starts_with($path, "files_trashbin/files".$trashItem->getTrashPath())) {
Expand Down