Initial commit: Cloud Control Panel
This commit is contained in:
356
src/Storage/Drivers/LocalStorageDriver.php
Normal file
356
src/Storage/Drivers/LocalStorageDriver.php
Normal file
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Din9xtrCloud\Storage\Drivers;
|
||||
|
||||
use JsonException;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use FilesystemIterator;
|
||||
use RuntimeException;
|
||||
|
||||
final readonly class LocalStorageDriver implements StorageDriverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private string $basePath,
|
||||
private int $defaultLimitBytes,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getTotalBytes(int|string $userId): int
|
||||
{
|
||||
return $this->defaultLimitBytes;
|
||||
}
|
||||
|
||||
public function getUsedBytes(int|string $userId): int
|
||||
{
|
||||
return array_sum($this->getUsedBytesByCategory($userId));
|
||||
}
|
||||
|
||||
public function getUsedBytesByCategory(int|string $userId): array
|
||||
{
|
||||
$userPath = $this->basePath . '/users/' . $userId;
|
||||
|
||||
if (!is_dir($userPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach (scandir($userPath) as $entry) {
|
||||
if ($entry === '.' || $entry === '..' || $entry === '.tus') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fullPath = $userPath . '/' . $entry;
|
||||
|
||||
if (!is_dir($fullPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[$entry] = $this->getDirectorySize($fullPath);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getDirectorySize(string $path): int
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$size = 0;
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
$size += $file->getSize();
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
|
||||
public function createFolder(int|string $userId, string $name): void
|
||||
{
|
||||
$path = $this->basePath . '/users/' . $userId . '/' . $name;
|
||||
|
||||
if (is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mkdir($path, 0755, true);
|
||||
}
|
||||
|
||||
public function getUserFolderPath(int|string $userId, string $folder): string
|
||||
{
|
||||
return $this->basePath . '/users/' . $userId . '/' . $folder;
|
||||
}
|
||||
|
||||
public function getUserPath(int|string $userId): string
|
||||
{
|
||||
return $this->basePath . '/users/' . $userId;
|
||||
}
|
||||
|
||||
public function storeUploadedFile(
|
||||
int|string $userId,
|
||||
string $folder,
|
||||
UploadedFileInterface $file,
|
||||
string $targetName
|
||||
): void
|
||||
{
|
||||
$dir = $this->getUserFolderPath($userId, $folder);
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
throw new RuntimeException('Folder not found');
|
||||
}
|
||||
|
||||
$file->moveTo($dir . '/' . $targetName);
|
||||
}
|
||||
|
||||
private function tusDir(int|string $userId): string
|
||||
{
|
||||
return $this->basePath . '/users/' . $userId . '/.tus';
|
||||
}
|
||||
|
||||
private function tusMeta(int|string $userId, string $id): string
|
||||
{
|
||||
return $this->tusDir($userId) . "/$id.meta";
|
||||
}
|
||||
|
||||
private function tusBin(int|string $userId, string $id): string
|
||||
{
|
||||
return $this->tusDir($userId) . "/$id.bin";
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function tusInit(
|
||||
int|string $userId,
|
||||
string $uploadId,
|
||||
int $size,
|
||||
array $metadata
|
||||
): void
|
||||
{
|
||||
$dir = $this->tusDir($userId);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
file_put_contents(
|
||||
$this->tusMeta($userId, $uploadId),
|
||||
json_encode([
|
||||
'size' => $size,
|
||||
'offset' => 0,
|
||||
'metadata' => $metadata,
|
||||
], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
touch($this->tusBin($userId, $uploadId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function tusWriteChunk(
|
||||
int|string $userId,
|
||||
string $uploadId,
|
||||
int $offset,
|
||||
StreamInterface $stream
|
||||
): int
|
||||
{
|
||||
$metaFile = $this->tusMeta($userId, $uploadId);
|
||||
$binFile = $this->tusBin($userId, $uploadId);
|
||||
|
||||
if (!is_file($metaFile) || !is_file($binFile)) {
|
||||
throw new RuntimeException('Upload not found');
|
||||
}
|
||||
|
||||
$metaContent = file_get_contents($metaFile);
|
||||
if ($metaContent === false) {
|
||||
throw new RuntimeException('Failed to read TUS metadata');
|
||||
}
|
||||
|
||||
$meta = json_decode(
|
||||
$metaContent,
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
|
||||
if ($offset !== $meta['offset']) {
|
||||
throw new RuntimeException('Invalid upload offset');
|
||||
}
|
||||
|
||||
$fp = fopen($binFile, 'c+');
|
||||
if ($fp === false) {
|
||||
throw new RuntimeException('Failed to open binary file for writing');
|
||||
}
|
||||
|
||||
if (fseek($fp, $offset) !== 0) {
|
||||
fclose($fp);
|
||||
throw new RuntimeException('Failed to seek to offset');
|
||||
}
|
||||
|
||||
$detachedStream = $stream->detach();
|
||||
if ($detachedStream === null) {
|
||||
fclose($fp);
|
||||
throw new RuntimeException('Stream is already detached');
|
||||
}
|
||||
|
||||
$written = stream_copy_to_stream($detachedStream, $fp);
|
||||
fclose($fp);
|
||||
|
||||
if ($written === false) {
|
||||
throw new RuntimeException('Failed to write chunk');
|
||||
}
|
||||
|
||||
$meta['offset'] += $written;
|
||||
|
||||
if ($meta['offset'] >= $meta['size']) {
|
||||
$this->finalizeTusUpload($userId, $uploadId, $meta);
|
||||
} else {
|
||||
$updatedMeta = json_encode($meta, JSON_THROW_ON_ERROR);
|
||||
if (file_put_contents($metaFile, $updatedMeta) === false) {
|
||||
throw new RuntimeException('Failed to update TUS metadata');
|
||||
}
|
||||
}
|
||||
|
||||
return $written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize TUS upload
|
||||
*
|
||||
* @param int|string $userId
|
||||
* @param string $uploadId
|
||||
* @param array{
|
||||
* size: int,
|
||||
* offset: int,
|
||||
* metadata: array<string, string>,
|
||||
* created_at: string,
|
||||
* expires_at: string
|
||||
* } $meta
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private function finalizeTusUpload(
|
||||
int|string $userId,
|
||||
string $uploadId,
|
||||
array $meta
|
||||
): void
|
||||
{
|
||||
$folder = $meta['metadata']['folder'] ?? 'default';
|
||||
$filename = $meta['metadata']['filename'] ?? $uploadId;
|
||||
|
||||
$targetDir = $this->getUserFolderPath($userId, $folder);
|
||||
if (!is_dir($targetDir)) {
|
||||
if (!mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
|
||||
throw new RuntimeException("Failed to create target directory: $targetDir");
|
||||
}
|
||||
}
|
||||
|
||||
$targetFile = $targetDir . '/' . $filename;
|
||||
|
||||
if (file_exists($targetFile)) {
|
||||
$filename = $this->addSuffixToFilename($filename, $targetDir);
|
||||
$targetFile = $targetDir . '/' . $filename;
|
||||
}
|
||||
|
||||
$sourceFile = $this->tusBin($userId, $uploadId);
|
||||
|
||||
if (!rename($sourceFile, $targetFile)) {
|
||||
throw new RuntimeException("Failed to move file from $sourceFile to $targetFile");
|
||||
}
|
||||
|
||||
$metaFile = $this->tusMeta($userId, $uploadId);
|
||||
if (!unlink($metaFile)) {
|
||||
throw new RuntimeException("Failed to delete metadata file: $metaFile");
|
||||
}
|
||||
}
|
||||
|
||||
private function addSuffixToFilename(string $filename, string $targetDir): string
|
||||
{
|
||||
$pathInfo = pathinfo($filename);
|
||||
$name = $pathInfo['filename'];
|
||||
$ext = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : '';
|
||||
|
||||
$counter = 1;
|
||||
|
||||
do {
|
||||
$newFilename = $name . '_' . $counter . $ext;
|
||||
$newPath = $targetDir . '/' . $newFilename;
|
||||
$counter++;
|
||||
} while (file_exists($newPath) && $counter < 100);
|
||||
|
||||
return $newFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function tusGetStatus(
|
||||
int|string $userId,
|
||||
string $uploadId
|
||||
): array
|
||||
{
|
||||
$metaFile = $this->tusMeta($userId, $uploadId);
|
||||
|
||||
if (!is_file($metaFile)) {
|
||||
throw new RuntimeException('Upload not found');
|
||||
}
|
||||
|
||||
$metaContent = file_get_contents($metaFile);
|
||||
if ($metaContent === false) {
|
||||
throw new RuntimeException('Failed to read TUS metadata');
|
||||
}
|
||||
|
||||
$meta = json_decode(
|
||||
$metaContent,
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
|
||||
return [
|
||||
'size' => (int)($meta['size'] ?? 0),
|
||||
'offset' => (int)($meta['offset'] ?? 0),
|
||||
'metadata' => $meta['metadata'] ?? [],
|
||||
'created_at' => $meta['created_at'] ?? date('c'),
|
||||
'expires_at' => $meta['expires_at'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
public function getFilePath(
|
||||
int|string $userId,
|
||||
string $folder,
|
||||
string $filename
|
||||
): string
|
||||
{
|
||||
$path = $this->getUserFolderPath($userId, $folder) . '/' . $filename;
|
||||
|
||||
if (!is_file($path)) {
|
||||
throw new RuntimeException('File not found');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function deleteFile(
|
||||
int|string $userId,
|
||||
string $folder,
|
||||
string $filename
|
||||
): void
|
||||
{
|
||||
$path = $this->getFilePath($userId, $folder, $filename);
|
||||
|
||||
if (!unlink($path)) {
|
||||
throw new RuntimeException('Failed to delete file');
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/Storage/Drivers/StorageDriverInterface.php
Normal file
82
src/Storage/Drivers/StorageDriverInterface.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Din9xtrCloud\Storage\Drivers;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
interface StorageDriverInterface
|
||||
{
|
||||
public function getTotalBytes(int|string $userId): int;
|
||||
|
||||
public function getUsedBytes(int|string $userId): int;
|
||||
|
||||
/** @return array<string, int> */
|
||||
public function getUsedBytesByCategory(int|string $userId): array;
|
||||
|
||||
public function createFolder(int|string $userId, string $name): void;
|
||||
|
||||
public function storeUploadedFile(
|
||||
int|string $userId,
|
||||
string $folder,
|
||||
UploadedFileInterface $file,
|
||||
string $targetName
|
||||
): void;
|
||||
|
||||
public function getUserFolderPath(int|string $userId, string $folder): string;
|
||||
|
||||
/**
|
||||
* Initialize TUS upload
|
||||
*
|
||||
* @param int|string $userId
|
||||
* @param string $uploadId
|
||||
* @param int $size
|
||||
* @param array<string, string> $metadata folder
|
||||
*/
|
||||
public function tusInit(
|
||||
int|string $userId,
|
||||
string $uploadId,
|
||||
int $size,
|
||||
array $metadata
|
||||
): void;
|
||||
|
||||
public function tusWriteChunk(
|
||||
int|string $userId,
|
||||
string $uploadId,
|
||||
int $offset,
|
||||
StreamInterface $stream
|
||||
): int;
|
||||
|
||||
/**
|
||||
* Get TUS upload status
|
||||
*
|
||||
* @param int|string $userId
|
||||
* @param string $uploadId
|
||||
* @return array{
|
||||
* size: int,
|
||||
* offset: int,
|
||||
* metadata: array<string, string>,
|
||||
* created_at: string,
|
||||
* expires_at: string|null
|
||||
* }
|
||||
*/
|
||||
public function tusGetStatus(
|
||||
int|string $userId,
|
||||
string $uploadId
|
||||
): array;
|
||||
|
||||
public function getUserPath(int|string $userId): string;
|
||||
|
||||
public function getFilePath(
|
||||
int|string $userId,
|
||||
string $folder,
|
||||
string $filename
|
||||
): string;
|
||||
|
||||
public function deleteFile(
|
||||
int|string $userId,
|
||||
string $folder,
|
||||
string $filename
|
||||
): void;
|
||||
}
|
||||
27
src/Storage/StorageGuard.php
Normal file
27
src/Storage/StorageGuard.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Din9xtrCloud\Storage;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
|
||||
final readonly class StorageGuard
|
||||
{
|
||||
public function __construct(private LoggerInterface $logger)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function assertEnoughSpace(string $path, int $bytes): void
|
||||
{
|
||||
$free = disk_free_space($path);
|
||||
|
||||
if ($free !== false && $free < $bytes) {
|
||||
|
||||
$this->logger->warning("Physical disk is full", ['path' => $path]);
|
||||
|
||||
throw new RuntimeException('Physical disk is full');
|
||||
}
|
||||
}
|
||||
}
|
||||
332
src/Storage/StorageService.php
Normal file
332
src/Storage/StorageService.php
Normal file
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Din9xtrCloud\Storage;
|
||||
|
||||
use Din9xtrCloud\Models\User;
|
||||
use Din9xtrCloud\Storage\Drivers\StorageDriverInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Random\RandomException;
|
||||
use RuntimeException;
|
||||
use ZipArchive;
|
||||
|
||||
final readonly class StorageService
|
||||
{
|
||||
public function __construct(
|
||||
private StorageDriverInterface $driver,
|
||||
private StorageGuard $guard,
|
||||
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getDriver(): StorageDriverInterface
|
||||
{
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
public function getStats(User $user): StorageStats
|
||||
{
|
||||
$total = $this->driver->getTotalBytes($user->id);
|
||||
$used = $this->driver->getUsedBytes($user->id);
|
||||
$free = max(0, $total - $used);
|
||||
$percent = $total > 0
|
||||
? (int)round(($used / $total) * 100)
|
||||
: 0;
|
||||
|
||||
return new StorageStats(
|
||||
totalBytes: $total,
|
||||
usedBytes: $used,
|
||||
freeBytes: $free,
|
||||
percent: min(100, $percent),
|
||||
byFolder: $this->driver->getUsedBytesByCategory($user->id)
|
||||
);
|
||||
}
|
||||
|
||||
public function createFolder(User $user, string $name): void
|
||||
{
|
||||
$this->driver->createFolder($user->id, $name);
|
||||
}
|
||||
|
||||
public function uploadFile(
|
||||
User $user,
|
||||
string $folder,
|
||||
UploadedFileInterface $file
|
||||
): void
|
||||
{
|
||||
$this->assertValidFolderName($folder);
|
||||
|
||||
$stats = $this->getStats($user);
|
||||
$fileSize = (int)$file->getSize();
|
||||
|
||||
if ($stats->freeBytes < $fileSize) {
|
||||
throw new RuntimeException('Storage limit exceeded');
|
||||
}
|
||||
|
||||
$folderPath = $this->driver->getUserPath($user->id);
|
||||
$this->guard->assertEnoughSpace($folderPath, $fileSize);
|
||||
|
||||
$safeName = $this->generateSafeFilename(
|
||||
$file->getClientFilename() ?? 'file',
|
||||
$folderPath . '/' . $folder
|
||||
);
|
||||
$this->driver->storeUploadedFile(
|
||||
$user->id,
|
||||
$folder,
|
||||
$file,
|
||||
$safeName
|
||||
);
|
||||
}
|
||||
|
||||
private function generateSafeFilename(string $original, string $folderPath): string
|
||||
{
|
||||
$original = $this->fixFileNameEncoding($original);
|
||||
$name = pathinfo($original, PATHINFO_FILENAME);
|
||||
$ext = pathinfo($original, PATHINFO_EXTENSION);
|
||||
|
||||
$safe = preg_replace('/[\/:*?"<>|\\\\]/', '_', $name);
|
||||
|
||||
if (empty($safe)) {
|
||||
$safe = 'file';
|
||||
}
|
||||
$baseName = $safe . ($ext ? '.' . $ext : '');
|
||||
$basePath = $folderPath . '/' . $baseName;
|
||||
|
||||
if (!file_exists($basePath)) {
|
||||
error_log("Returning: " . $baseName);
|
||||
return $baseName;
|
||||
}
|
||||
|
||||
$counter = 1;
|
||||
$maxAttempts = 100;
|
||||
|
||||
do {
|
||||
$newFilename = $safe . '_' . $counter . ($ext ? '.' . $ext : '');
|
||||
$newPath = $folderPath . '/' . $newFilename;
|
||||
$counter++;
|
||||
} while (file_exists($newPath) && $counter <= $maxAttempts);
|
||||
|
||||
return $newFilename;
|
||||
}
|
||||
|
||||
private function assertValidFolderName(string $folder): void
|
||||
{
|
||||
if ($folder === '' || !preg_match('/^[a-zA-Z\x{0400}-\x{04FF}0-9_\- ]+$/u', $folder)) {
|
||||
throw new RuntimeException('Invalid folder');
|
||||
}
|
||||
}
|
||||
|
||||
private function fixFileNameEncoding(string $filename): string
|
||||
{
|
||||
$detected = mb_detect_encoding($filename, ['UTF-8', 'Windows-1251', 'ISO-8859-5', 'KOI8-R'], true);
|
||||
|
||||
if ($detected && $detected !== 'UTF-8') {
|
||||
$converted = mb_convert_encoding($filename, 'UTF-8', $detected);
|
||||
$filename = $converted !== false ? $converted : "unknown_file";
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize TUS upload
|
||||
*
|
||||
* @param User $user
|
||||
* @param string $uploadId
|
||||
* @param int $size
|
||||
* @param array<string, string> $metadata folder
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function initTusUpload(
|
||||
User $user,
|
||||
string $uploadId,
|
||||
int $size,
|
||||
array $metadata
|
||||
): void
|
||||
{
|
||||
$stats = $this->getStats($user);
|
||||
|
||||
$userFolderPath = $this->driver->getUserPath($user->id);
|
||||
|
||||
$this->guard->assertEnoughSpace($userFolderPath, $size);
|
||||
|
||||
if ($stats->freeBytes < $size) {
|
||||
throw new RuntimeException('Storage limit exceeded');
|
||||
}
|
||||
|
||||
$this->driver->tusInit(
|
||||
userId: $user->id,
|
||||
uploadId: $uploadId,
|
||||
size: $size,
|
||||
metadata: $metadata
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write TUS chunk
|
||||
*
|
||||
* @param User $user
|
||||
* @param string $uploadId
|
||||
* @param int $offset
|
||||
* @param StreamInterface $stream
|
||||
* @return int Number of bytes written
|
||||
*/
|
||||
public function writeTusChunk(
|
||||
User $user,
|
||||
string $uploadId,
|
||||
int $offset,
|
||||
StreamInterface $stream
|
||||
): int
|
||||
{
|
||||
return $this->driver->tusWriteChunk(
|
||||
userId: $user->id,
|
||||
uploadId: $uploadId,
|
||||
offset: $offset,
|
||||
stream: $stream
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TUS upload status
|
||||
*
|
||||
* @param User $user
|
||||
* @param string $uploadId
|
||||
* @return array{
|
||||
* size: int,
|
||||
* offset: int,
|
||||
* metadata: array<string, string>,
|
||||
* created_at: string,
|
||||
* expires_at: string|null
|
||||
* }
|
||||
*/
|
||||
public function getTusStatus(User $user, string $uploadId): array
|
||||
{
|
||||
return $this->driver->tusGetStatus($user->id, $uploadId);
|
||||
}
|
||||
|
||||
public function deleteFile(
|
||||
User $user,
|
||||
string $folder,
|
||||
string $filename
|
||||
): void
|
||||
{
|
||||
$this->assertValidFolderName($folder);
|
||||
|
||||
if ($filename === '' || str_contains($filename, '..')) {
|
||||
throw new RuntimeException('Invalid filename');
|
||||
}
|
||||
|
||||
$this->driver->deleteFile($user->id, $folder, $filename);
|
||||
}
|
||||
|
||||
public function getFileForDownload(
|
||||
User $user,
|
||||
string $folder,
|
||||
string $filename
|
||||
): string
|
||||
{
|
||||
$this->assertValidFolderName($folder);
|
||||
|
||||
if ($filename === '' || str_contains($filename, '..')) {
|
||||
throw new RuntimeException('Invalid filename');
|
||||
}
|
||||
|
||||
return $this->driver->getFilePath($user->id, $folder, $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $files
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function buildZipForDownload(
|
||||
User $user,
|
||||
string $folder,
|
||||
array $files
|
||||
): string
|
||||
{
|
||||
$this->assertValidFolderName($folder);
|
||||
|
||||
$tmpDir = sys_get_temp_dir() . '/cloud-' . $user->id;
|
||||
if (!is_dir($tmpDir)) {
|
||||
mkdir($tmpDir, 0700, true);
|
||||
}
|
||||
|
||||
$zipPath = $tmpDir . '/download-' . bin2hex(random_bytes(8)) . '.zip';
|
||||
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipPath, ZipArchive::CREATE) !== true) {
|
||||
throw new RuntimeException('Cannot create zip');
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file === '' || str_contains($file, '..')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $this->driver->getFilePath($user->id, $folder, $file);
|
||||
$zip->addFile($path, $file);
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
return $zipPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $files
|
||||
*/
|
||||
public function deleteMultipleFiles(
|
||||
User $user,
|
||||
string $folder,
|
||||
array $files
|
||||
): void
|
||||
{
|
||||
$this->assertValidFolderName($folder);
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file === '' || str_contains($file, '..')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $this->driver->getFilePath($user->id, $folder, $file);
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteFolder(User $user, string $folder): void
|
||||
{
|
||||
$this->assertValidFolderName($folder);
|
||||
|
||||
$path = $this->driver->getUserFolderPath($user->id, $folder);
|
||||
|
||||
if (!is_dir($path)) {
|
||||
throw new RuntimeException("Folder not found: $folder");
|
||||
}
|
||||
$this->deleteDirectoryRecursive($path);
|
||||
}
|
||||
|
||||
private function deleteDirectoryRecursive(string $dir): void
|
||||
{
|
||||
$items = scandir($dir);
|
||||
if ($items === false) {
|
||||
throw new RuntimeException("Failed to read directory: $dir");
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
|
||||
$path = $dir . '/' . $item;
|
||||
if (is_dir($path)) {
|
||||
$this->deleteDirectoryRecursive($path);
|
||||
} else {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
@rmdir($dir);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
18
src/Storage/StorageStats.php
Normal file
18
src/Storage/StorageStats.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Din9xtrCloud\Storage;
|
||||
|
||||
final readonly class StorageStats
|
||||
{
|
||||
public function __construct(
|
||||
public int $totalBytes,
|
||||
public int $usedBytes,
|
||||
public int $freeBytes,
|
||||
public int $percent,
|
||||
/** @var array<string, int> */
|
||||
public array $byFolder = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
40
src/Storage/UserStorageInitializer.php
Normal file
40
src/Storage/UserStorageInitializer.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Din9xtrCloud\Storage;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class UserStorageInitializer
|
||||
{
|
||||
private const array CATEGORIES = [
|
||||
'documents',
|
||||
'media',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly string $basePath
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function init(int|string $userId): void
|
||||
{
|
||||
$userPath = $this->basePath . '/users/' . $userId;
|
||||
|
||||
foreach (self::CATEGORIES as $dir) {
|
||||
$path = $userPath . '/' . $dir;
|
||||
|
||||
if (!is_dir($path)) {
|
||||
if (!mkdir($path, 0775, true)) {
|
||||
throw new RuntimeException(
|
||||
sprintf('can`t crate dir: %s', $path)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user