Move components logic to actions

This commit is contained in:
brufdev
2025-02-02 18:09:19 +00:00
parent 059e1cd54a
commit 6993bb910d
6 changed files with 163 additions and 214 deletions

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Actions;
use App\Models\Vault;
use App\Models\VaultNode;
use Exception;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Collection;
use ZipArchive;
final readonly class ExportVault
{
public function handle(Vault $vault): string
{
$zip = new ZipArchive();
$relativePath = 'public/' . Str::random(16) . '.zip';
$path = Storage::disk('local')->path($relativePath);
$nodes = $vault->nodes()->whereNull('parent_id')->get();
if ($nodes->count() === 0) {
throw new Exception(__('Your vault is empty'));
}
Storage::disk('local')->put($relativePath, '');
if ($zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
throw new Exception(__('Something went wrong'));
}
$this->exportNodes($zip, $nodes);
$zip->close();
return $path;
}
/**
* @param Collection<int, VaultNode> $nodes
*/
private function exportNodes(ZipArchive &$zip, Collection $nodes, string $path = ''): void
{
foreach ($nodes as $node) {
$nodePath = mb_ltrim("$path/$node->name", '/');
$nodePath .= $node->is_file ? ".$node->extension" : '';
$relativePath = new GetPathFromVaultNode()->handle($node);
if (!Storage::disk('local')->exists($relativePath)) {
throw new Exception(
sprintf(
"%s missing on disk: {$nodePath}",
$node->is_file ? 'File' : 'Folder',
),
);
}
if ($node->is_file) {
if ($node->extension === 'md') {
$zip->addFromString($nodePath, (string) $node->content);
} else {
$relativePath = new GetPathFromVaultNode()->handle($node);
$zip->addFile(
Storage::disk('local')->path($relativePath),
$nodePath,
);
}
} else {
$zip->addEmptyDir($nodePath);
if ($node->children()->count()) {
$this->exportNodes($zip, $node->children()->get(), $nodePath);
}
}
}
}
}

View File

@@ -6,7 +6,6 @@ namespace App\Actions;
use App\Models\Vault;
use App\Models\VaultNode;
use App\Services\VaultFile;
use App\Services\VaultFiles\Note;
use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;
@@ -15,53 +14,19 @@ final readonly class ProcessImportedFile
{
public function handle(Vault $vault, VaultNode $parent, string $fileName, string $filePath): void
{
$pathInfo = pathinfo($fileName);
$name = $pathInfo['filename'];
$extension = $pathInfo['extension'] ?? '';
if (!in_array($extension, VaultFile::extensions())) {
abort(400);
}
$content = null;
if (in_array($extension, Note::extensions())) {
$extension = 'md';
$content = file_get_contents($filePath);
}
// Find new filename if it already exists
$nodeExists = $vault->nodes()
->where('parent_id', $parent->id)
->where('is_file', true)
->where('name', 'like', "$name")
->where('extension', 'md')
->exists();
if ($nodeExists) {
/** @var list<string> $nodes */
$nodes = array_column(
$vault->nodes()
->select('name')
->where('parent_id', $parent->id)
->where('is_file', true)
->where('name', 'like', "$name-%")
->where('extension', 'md')
->get()
->toArray(),
'name',
);
natcasesort($nodes);
$name .= count($nodes) && preg_match('/-(\d+)$/', end($nodes), $matches) === 1 ?
'-' . ((int) $matches[1] + 1) :
'-1';
}
$node = $vault->nodes()->createQuietly([
$attributes = [
'parent_id' => $parent->id,
'is_file' => true,
'name' => $name,
'extension' => $extension,
'content' => $content,
]);
];
$pathInfo = pathinfo($fileName);
$attributes['name'] = $pathInfo['filename'];
$attributes['extension'] = $pathInfo['extension'] ?? '';
$attributes['content'] = null;
if (in_array($attributes['extension'], Note::extensions())) {
$attributes['extension'] = 'md';
$attributes['content'] = (string) file_get_contents($filePath);
}
$node = new CreateVaultNode()->handle($vault, $attributes);
$relativePath = new GetPathFromVaultNode()->handle($node);
$pathInfo = pathinfo($relativePath);

View File

@@ -7,7 +7,6 @@ namespace App\Actions;
use App\Models\User;
use App\Services\VaultFile;
use App\Services\VaultFiles\Note;
use Illuminate\Support\Facades\Storage;
use ZipArchive;
final readonly class ProcessImportedVault
@@ -15,32 +14,10 @@ final readonly class ProcessImportedVault
public function handle(string $fileName, string $filePath): void
{
$nodeIds = ['.' => null];
$vaultName = pathinfo($fileName, PATHINFO_FILENAME);
/** @var User $currentUser */
$currentUser = auth()->user();
// Create vault with zip name
$vaultName = pathinfo($fileName, PATHINFO_FILENAME);
// Find new vault name if it already exists
$vaultExists = $currentUser->vaults()
->where('name', 'like', "$vaultName")
->exists();
if ($vaultExists) {
/** @var list<string> $vaults */
$vaults = array_column(
$currentUser->vaults()
->select('name')
->where('name', 'like', "$vaultName-%")
->get()
->toArray(),
'name',
);
natcasesort($vaults);
$vaultName .= count($vaults) && preg_match('/-(\d+)$/', end($vaults), $matches) === 1 ?
'-' . ((int) $matches[1] + 1) :
'-1';
}
$vault = $currentUser->vaults()->create([
$vault = new CreateVault()->handle($currentUser, [
'name' => $vaultName,
]);
@@ -56,48 +33,36 @@ final readonly class ProcessImportedVault
$isFile = !str_ends_with($entryName, '/');
$flags = $isFile ? PATHINFO_FILENAME : PATHINFO_BASENAME;
$name = pathinfo($entryName, $flags);
$extension = null;
$content = null;
$attributes = [
'is_file' => $isFile,
'name' => pathinfo($entryName, $flags),
'extension' => null,
'content' => null,
];
if (!$isFile) {
// ZipArchive folder paths end with a / that should
// be removed in order for pathinfo() return the correct dirname
$entryDirName = mb_rtrim($entryName, '/');
$entryParentDirName = pathinfo($entryDirName, PATHINFO_DIRNAME);
$parentId = $nodeIds[$entryParentDirName];
$attributes['parent_id'] = $nodeIds[$entryParentDirName];
} else {
$pathInfo = pathinfo($entryName);
$entryDirName = $pathInfo['dirname'];
$extension = $pathInfo['extension'] ?? '';
$parentId = $nodeIds[$entryDirName];
$attributes['extension'] = $pathInfo['extension'] ?? '';
$attributes['parent_id'] = $nodeIds[$entryDirName];
if (!in_array($extension, VaultFile::extensions())) {
if (!in_array($attributes['extension'], VaultFile::extensions())) {
continue;
}
if (in_array($extension, Note::extensions())) {
$extension = 'md';
$content = $zip->getFromIndex($i);
if (in_array($attributes['extension'], Note::extensions())) {
$attributes['extension'] = 'md';
}
}
$node = $vault->nodes()->createQuietly([
'parent_id' => $parentId,
'is_file' => $isFile,
'name' => $name,
'extension' => $extension,
'content' => $content,
]);
$relativePath = new GetPathFromVaultNode()->handle($node);
if ($isFile) {
/** @var string $contents */
$contents = $zip->getFromIndex($i);
Storage::disk('local')->put($relativePath, $contents);
} else {
Storage::disk('local')->makeDirectory($relativePath);
$attributes['content'] = (string) $zip->getFromIndex($i);
}
$node = new CreateVaultNode()->handle($vault, $attributes);
if (!array_key_exists($entryDirName, $nodeIds)) {
$nodeIds[$entryDirName] = $node->id;

View File

@@ -4,21 +4,16 @@ declare(strict_types=1);
namespace App\Livewire\Vault;
use App\Actions\GetPathFromVaultNode;
use App\Actions\DeleteVault;
use App\Actions\ExportVault;
use App\Livewire\Forms\VaultForm;
use App\Models\User;
use App\Models\Vault;
use App\Models\VaultNode;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Livewire\Component;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Collection;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Throwable;
use ZipArchive;
final class Index extends Component
{
@@ -33,41 +28,34 @@ final class Index extends Component
$this->dispatch('toast', message: __('Vault created'), type: 'success');
}
public function export(Vault $vault): ?BinaryFileResponse
public function export(Vault $vault, ExportVault $exportVault): ?BinaryFileResponse
{
$this->authorize('view', $vault);
$zip = new ZipArchive();
$zipFileName = $vault->id . '.zip';
$nodes = $vault->nodes()->whereNull('parent_id')->get();
if ($zip->open(public_path($zipFileName), ZipArchive::CREATE) !== true) {
$this->dispatch('toast', message: __('Something went wrong'), type: 'error');
try {
$path = $exportVault->handle($vault);
} catch (Throwable $e) {
$this->dispatch('toast', message: $e->getMessage(), type: 'error');
return null;
}
$this->exportNodes($zip, $nodes);
$zip->close();
return response()->download(public_path($zipFileName), $vault->name . '.zip')->deleteFileAfterSend(true);
return response()->download($path, $vault->name . '.zip')->deleteFileAfterSend(true);
}
public function delete(Vault $vault): void
{
$this->authorize('delete', $vault);
DB::beginTransaction();
try {
$rootNodes = $vault->nodes()->whereNull('parent_id')->get();
foreach ($rootNodes as $node) {
$this->deleteNode($node);
}
$vault->delete();
DB::commit();
$this->dispatch('toast', message: __('Vault deleted'), type: 'success');
} catch (Throwable) {
DB::rollBack();
$this->dispatch('toast', message: __('Something went wrong'), type: 'error');
new DeleteVault()->handle($vault);
} catch (Throwable $e) {
$this->dispatch('toast', message: $e->getMessage(), type: 'error');
return;
}
$this->dispatch('toast', message: __('Vault deleted'), type: 'success');
}
public function render(): Factory|View
@@ -79,38 +67,4 @@ final class Index extends Component
'vaults' => $currentUser->vaults()->orderBy('updated_at', 'DESC')->get(),
]);
}
/**
* @param Collection<int, VaultNode> $nodes
*/
private function exportNodes(ZipArchive &$zip, Collection $nodes, string $path = ''): void
{
foreach ($nodes as $node) {
$nodePath = Str::ltrim("$path/$node->name", '/');
if ($node->is_file) {
if ($node->extension === 'md') {
$zip->addFromString("$nodePath.$node->extension", (string) $node->content);
} else {
$relativePath = new GetPathFromVaultNode()->handle($node);
$filePath = Storage::disk('local')->path($relativePath);
$zip->addFile($filePath, "$nodePath.$node->extension");
}
} else {
$zip->addEmptyDir($nodePath);
if ($node->children->count()) {
$this->exportNodes($zip, $node->children, $nodePath);
}
}
}
}
private function deleteNode(VaultNode $node): void
{
foreach ($node->childs as $child) {
$this->deleteNode($child);
}
$node->delete();
}
}

View File

@@ -4,10 +4,11 @@ declare(strict_types=1);
namespace App\Livewire\Vault;
use App\Actions\GetPathFromVaultNode;
use App\Actions\DeleteVaultNode;
use App\Actions\GetUrlFromVaultNode;
use App\Actions\GetVaultNodeFromPath;
use App\Actions\ResolveTwoPaths;
use App\Actions\UpdateVault;
use App\Livewire\Forms\VaultForm;
use App\Livewire\Forms\VaultNodeForm;
use App\Models\Vault;
@@ -36,13 +37,10 @@ final class Show extends Component
#[Url(as: 'file')]
public ?int $selectedFile = null;
public ?string $selectedFilePath = null;
public ?string $selectedFileUrl = null;
public bool $isEditMode = true;
/** @var list<VaultNode> */
private array $deletedNodes = [];
public function mount(Vault $vault): void
{
$this->authorize('view', $vault);
@@ -52,7 +50,10 @@ final class Show extends Component
$this->getTemplates();
if ((int) $this->selectedFile > 0) {
$selectedFile = $vault->nodes()->where('id', $this->selectedFile)->first();
$selectedFile = $vault->nodes()
->where('id', $this->selectedFile)
->where('is_file', true)
->first();
if (!$selectedFile) {
$this->selectedFile = null;
@@ -69,11 +70,13 @@ final class Show extends Component
$this->authorize('view', $node->vault);
if (!$node->vault || !$node->vault->is($this->vault) || !$node->is_file) {
$this->selectedFile = null;
return;
}
$this->selectedFile = $node->id;
$this->selectedFilePath = new GetUrlFromVaultNode()->handle($node);
$this->selectedFileUrl = new GetUrlFromVaultNode()->handle($node);
$this->nodeForm->setNode($node);
if ($node->extension === 'md') {
@@ -85,12 +88,11 @@ final class Show extends Component
public function openFilePath(string $path): void
{
/**
* @var string $currentPath
*
* @phpstan-ignore-next-line larastan.noUnnecessaryCollectionCall
*/
$currentPath = $this->nodeForm->node->ancestorsAndSelf()->get()->last()->full_path;
/** @var string $currentPath */
$currentPath = is_null($this->nodeForm->node)
? ''
/** @phpstan-ignore-next-line larastan.noUnnecessaryCollectionCall */
: $this->nodeForm->node->ancestorsAndSelf()->get()->last()->full_path;
$resolvedPath = new ResolveTwoPaths()->handle($currentPath, $path);
$node = new GetVaultNodeFromPath()->handle($this->vault->id, $resolvedPath);
@@ -110,14 +112,13 @@ final class Show extends Component
return;
}
$this->selectedFile = $node->id;
$this->selectedFilePath = new GetPathFromVaultNode()->handle($node);
$this->selectedFileUrl = new GetUrlFromVaultNode()->handle($node);
$this->nodeForm->setNode($node);
}
public function closeFile(): void
{
$this->reset(['selectedFile', 'selectedFilePath']);
$this->reset(['selectedFile', 'selectedFileUrl']);
$this->nodeForm->reset('node');
}
@@ -157,7 +158,9 @@ final class Show extends Component
return;
}
$this->vault->update(['templates_node_id' => $node->id]);
new UpdateVault()->handle($this->vault, [
'templates_node_id' => $node->id,
]);
$this->getTemplates();
$this->dispatch('toast', message: __('Template folder updated'), type: 'success');
}
@@ -212,30 +215,38 @@ final class Show extends Component
{
$this->authorize('delete', $node->vault);
DB::beginTransaction();
try {
if ($node->is_file) {
$this->deleteFile($node);
} else {
$this->deleteFolder($node);
DB::beginTransaction();
$deletedNodes = new DeleteVaultNode()->handle($node);
$this->dispatch('node-updated');
$openFileDeleted = !is_null(
array_find(
$deletedNodes,
fn (VaultNode $node): bool => $node->id === $this->selectedFile,
)
);
if ($openFileDeleted) {
$this->closeFile();
}
DB::commit();
$this->dispatch('node-updated');
$templateDeleted = !is_null(
array_find(
$this->deletedNodes,
$deletedNodes,
fn (VaultNode $node): bool => $node->parent_id === $this->vault->templates_node_id,
)
);
if ($templateDeleted) {
$this->getTemplates();
}
$this->deletedNodes = [];
DB::commit();
$message = $node->is_file ? __('File deleted') : __('Folder deleted');
$this->dispatch('toast', message: $message, type: 'success');
} catch (Throwable) {
} catch (Throwable $e) {
DB::rollBack();
$this->dispatch('toast', message: $e->getMessage(), type: 'error');
}
}
@@ -243,28 +254,4 @@ final class Show extends Component
{
return view('livewire.vault.show');
}
private function deleteFile(VaultNode $node): void
{
if ($this->selectedFile === $node->id) {
$this->closeFile();
}
$this->deletedNodes[] = $node;
$node->delete();
}
private function deleteFolder(VaultNode $node): void
{
foreach ($node->childs as $child) {
if ($child->is_file) {
$this->deleteFile($child);
} else {
$this->deleteFolder($child);
}
}
$this->deletedNodes[] = $node;
$node->delete();
}
}

View File

@@ -163,20 +163,20 @@
<x-markdownEditor />
@elseif (in_array($nodeForm->extension, App\Services\VaultFiles\Image::extensions()))
<div>
<img src="{{ $selectedFilePath }}" />
<img src="{{ $selectedFileUrl }}" />
</div>
@elseif (in_array($nodeForm->extension, App\Services\VaultFiles\Pdf::extensions()))
<object type="application/pdf" data="{{ $selectedFilePath }}"
<object type="application/pdf" data="{{ $selectedFileUrl }}"
class="w-full h-full"></object>
@elseif (in_array($nodeForm->extension, App\Services\VaultFiles\Video::extensions()))
<video class="w-full" controls>
<source src="{{ $selectedFilePath }}" />
<source src="{{ $selectedFileUrl }}" />
{{ __('Your browser does not support the video tag') }}
</video>
@elseif (in_array($nodeForm->extension, App\Services\VaultFiles\Audio::extensions()))
<div class="flex items-start justify-center w-full">
<audio class="w-full" controls>
<source src="{{ $selectedFilePath }}">
<source src="{{ $selectedFileUrl }}">
{{ __('Your browser does not support the audio tag') }}
</audio>
</div>