Skip to content

Commit

Permalink
[stacked 9] Support video partners. (LycheeOrg#2373)
Browse files Browse the repository at this point in the history
  • Loading branch information
ildyria committed Apr 11, 2024
1 parent 23a312c commit f057211
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 0 deletions.
36 changes: 36 additions & 0 deletions app/Actions/Photo/Create.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Actions\Photo\Pipes\Init;
use App\Actions\Photo\Pipes\Shared;
use App\Actions\Photo\Pipes\Standalone;
use App\Actions\Photo\Pipes\VideoPartner;
use App\Assets\Features;
use App\Contracts\Exceptions\LycheeException;
use App\Contracts\Models\AbstractAlbum;
Expand All @@ -14,6 +15,7 @@
use App\DTO\PhotoCreate\DuplicateDTO;
use App\DTO\PhotoCreate\InitDTO;
use App\DTO\PhotoCreate\StandaloneDTO;
use App\DTO\PhotoCreate\VideoPartnerDTO;
use App\Exceptions\PhotoResyncedException;
use App\Exceptions\PhotoSkippedException;
use App\Image\Files\NativeLocalFile;
Expand Down Expand Up @@ -86,6 +88,11 @@ public function add(NativeLocalFile $sourceFile, ?AbstractAlbum $album, ?int $fi
return $this->handleStandalone($initDTO);
}

// livePartner !== null
if ($sourceFile->isSupportedVideo()) {
return $this->handleVideoLivePartner($initDTO);
}

$oldCodePath = new LegacyPhotoCreate($this->strategyParameters->importMode, $this->strategyParameters->intendedOwnerId);

return $oldCodePath->add($sourceFile, $album, $fileLastModifiedTime);
Expand Down Expand Up @@ -167,4 +174,33 @@ private function handleStandalone(InitDTO $initDTO): Photo
throw $e;
}
}

private function handleVideoLivePartner(InitDTO $initDTO): Photo
{
$dto = new VideoPartnerDTO($initDTO);

$pipes = [
VideoPartner\GetVideoPath::class,
VideoPartner\PlaceVideo::class,
VideoPartner\UpdateLivePartner::class,
Shared\Save::class,
];

try {
return app(Pipeline::class)
->send($dto)
->through($pipes)
->thenReturn()
->getPhoto();
} catch (LycheeException $e) {
// If source file could not be put into final destination, remove
// freshly created photo from DB to avoid having "zombie" entries.
try {
$dto->getPhoto()->delete();
} catch (\Throwable) {
// Sic! If anything goes wrong here, we still throw the original exception
}
throw $e;
}
}
}
20 changes: 20 additions & 0 deletions app/Actions/Photo/Pipes/VideoPartner/GetVideoPath.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace App\Actions\Photo\Pipes\VideoPartner;

use App\Contracts\PhotoCreate\VideoPartnerPipe;
use App\DTO\PhotoCreate\VideoPartnerDTO;

class GetVideoPath implements VideoPartnerPipe
{
public function handle(VideoPartnerDTO $state, \Closure $next): VideoPartnerDTO
{
$photoFile = $state->photo->size_variants->getOriginal()->getFile();
$photoPath = $photoFile->getRelativePath();
$photoExt = $photoFile->getOriginalExtension();
$videoExt = $state->videoFile->getOriginalExtension();
$state->videoPath = substr($photoPath, 0, -strlen($photoExt)) . $videoExt;

return $next($state);
}
}
112 changes: 112 additions & 0 deletions app/Actions/Photo/Pipes/VideoPartner/PlaceVideo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace App\Actions\Photo\Pipes\VideoPartner;

use App\Actions\Diagnostics\Pipes\Checks\BasicPermissionCheck;
use App\Contracts\Models\AbstractSizeVariantNamingStrategy;
use App\Contracts\PhotoCreate\VideoPartnerPipe;
use App\DTO\PhotoCreate\VideoPartnerDTO;
use App\Exceptions\ConfigurationException;
use App\Exceptions\Handler;
use App\Exceptions\Internal\LycheeAssertionError;
use App\Exceptions\MediaFileOperationException;
use App\Image\Files\FlysystemFile;
use App\Image\Files\NativeLocalFile;
use App\Image\StreamStat;

/**
* Puts the video source file into the final position at video target file.
*
* We need to distinguish two cases:
*
* A) The video source file is a native, local file.
*
* In this case, the video file has just been uploaded (and the photo
* partner is already on the target disk).
* The video file must be put onto the target disk, the same way as it
* would for a stand-alone upload.
*
* B) The video source file is a FlysystemFile, too.
*
* In this case, the video file is already on the final disk, but in the
* wrong position.
* In that case we can and must rename it.
* Note, that we must take a little extra care, if the final disk is also
* local and the video file has been imported via a symbolic link.
* We want to rename the symbolic link, not the target of the symbolic
* link.
*
* @param FlysystemFile $videoTargetFile
*
* @return StreamStat|null statistics about the uploaded video file; `null` if no file has been uploaded, but renamed in-place
*
* @throws MediaFileOperationException
* @throws ConfigurationException
*/
class PlaceVideo implements VideoPartnerPipe
{
public function handle(VideoPartnerDTO $state, \Closure $next): VideoPartnerDTO
{
$videoTargetFile = new FlysystemFile(AbstractSizeVariantNamingStrategy::getImageDisk(), $state->videoPath);

try {
if ($state->videoFile instanceof NativeLocalFile) {
// This is case A (see above)
// The code is very similar to
// AddStandaloneStrategy::putSourceIntoFinalDestination()
// except that we can skip the part about normalization of
// orientation, because we don't support that for videos.
if ($state->shallImportViaSymlink) {
if (!$videoTargetFile->isLocalFile()) {
throw new ConfigurationException('Symlinking is only supported on local filesystems');
}
$targetPath = $videoTargetFile->toLocalFile()->getPath();
$sourcePath = $state->videoFile->getRealPath();
// For symlinks we must manually create a non-existing
// parent directory.
// This mimics the behaviour of Flysystem for regular files.
$targetDirectory = pathinfo($targetPath, PATHINFO_DIRNAME);
if (!is_dir($targetDirectory)) {
$umask = \umask(0);
\Safe\mkdir($targetDirectory, BasicPermissionCheck::getConfiguredDirectoryPerm(), true);
\umask($umask);
}
\Safe\symlink($sourcePath, $targetPath);
$streamStat = StreamStat::createFromLocalFile($state->videoFile);
} else {
$streamStat = $videoTargetFile->write($state->videoFile->read(), true);
$state->videoFile->close();
$videoTargetFile->close();
if ($state->shallDeleteImported) {
// This may throw an exception, if the original has been
// readable, but is not writable
// In this case, the media file will have been copied, but
// cannot be "moved".
try {
$state->videoFile->delete();
} catch (MediaFileOperationException $e) {
// If deletion failed, we do not cancel the whole
// import, but fall back to copy-semantics and
// log the exception
Handler::reportSafely($e);
}
}
}
} elseif ($state->videoFile instanceof FlysystemFile) {
// It seems as if Flysystem calls a primitive \rename under the
// hood, if the storage adapter is the `Local` adapter.
// This also works for symbolic links, so we are good here.
$state->videoFile->move($videoTargetFile->getRelativePath());
$streamStat = null;
} else {
throw new LycheeAssertionError('Unexpected type of $videoFile: ' . get_class($state->videoFile));
}

$state->streamStat = $streamStat;
} catch (\ErrorException $e) {
throw new MediaFileOperationException('Could move/copy/symlink source file to final destination', $e);
}

return $next($state);
}
}
17 changes: 17 additions & 0 deletions app/Actions/Photo/Pipes/VideoPartner/UpdateLivePartner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace App\Actions\Photo\Pipes\VideoPartner;

use App\Contracts\PhotoCreate\VideoPartnerPipe;
use App\DTO\PhotoCreate\VideoPartnerDTO;

class UpdateLivePartner implements VideoPartnerPipe
{
public function handle(VideoPartnerDTO $state, \Closure $next): VideoPartnerDTO
{
$state->photo->live_photo_short_path = $state->videoPath;
$state->photo->live_photo_checksum = $state->streamStat?->checksum;

return $next($state);
}
}
19 changes: 19 additions & 0 deletions app/Contracts/PhotoCreate/VideoPartnerPipe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Contracts\PhotoCreate;

use App\DTO\PhotoCreate\VideoPartnerDTO;

/**
* Basic definition of a Video Partner pipe.
*/
interface VideoPartnerPipe
{
/**
* @param VideoPartnerDTO $state
* @param \Closure(VideoPartnerDTO $state): VideoPartnerDTO $next
*
* @return VideoPartnerDTO
*/
public function handle(VideoPartnerDTO $state, \Closure $next): VideoPartnerDTO;
}
35 changes: 35 additions & 0 deletions app/DTO/PhotoCreate/VideoPartnerDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\DTO\PhotoCreate;

use App\Contracts\Image\StreamStats;
use App\Contracts\PhotoCreate\PhotoDTO;
use App\Image\Files\BaseMediaFile;
use App\Models\Photo;

class VideoPartnerDTO implements PhotoDTO
{
public readonly bool $shallImportViaSymlink;
public readonly bool $shallDeleteImported;

// The resulting photo
public readonly Photo $photo;

public StreamStats|null $streamStat;

public string $videoPath;
public readonly BaseMediaFile $videoFile;

public function __construct(InitDTO $initDTO)
{
$this->videoFile = $initDTO->sourceFile;
$this->photo = $initDTO->livePartner;
$this->shallImportViaSymlink = $initDTO->importMode->shallImportViaSymlink;
$this->shallDeleteImported = $initDTO->importMode->shallDeleteImported;
}

public function getPhoto(): Photo
{
return $this->photo;
}
}

0 comments on commit f057211

Please sign in to comment.