File transfer
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* File Revision Storage Service
|
||||
* Handles compressed storage, path traversal prevention, and binary detection.
|
||||
*
|
||||
* @author Ball Studios <https://git.balls.studio>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Pterodactyl\Addons\AdvancedAdmin\Services\Revisions;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use RuntimeException;
|
||||
|
||||
class RevisionStorageService
|
||||
{
|
||||
private string $disk = 'local';
|
||||
|
||||
/**
|
||||
* Store file content, returning the storage key.
|
||||
* Content is gzip-compressed before storage.
|
||||
*/
|
||||
public function store(int $serverId, string $pathHash, int $revisionNumber, string $content): array
|
||||
{
|
||||
$key = $this->buildKey($serverId, $pathHash, $revisionNumber);
|
||||
$compressed = gzencode($content, 6);
|
||||
if ($compressed === false) {
|
||||
throw new RuntimeException('Failed to compress revision content.');
|
||||
}
|
||||
Storage::disk($this->disk)->put($key, $compressed);
|
||||
return [
|
||||
'storage_key' => $key,
|
||||
'size_bytes' => strlen($content),
|
||||
'compressed_size' => strlen($compressed),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve and decompress stored content.
|
||||
*/
|
||||
public function retrieve(string $storageKey): string
|
||||
{
|
||||
$this->guardKey($storageKey);
|
||||
|
||||
$compressed = Storage::disk($this->disk)->get($storageKey);
|
||||
if ($compressed === null) {
|
||||
throw new RuntimeException("Revision file not found: {$storageKey}");
|
||||
}
|
||||
$content = gzdecode($compressed);
|
||||
if ($content === false) {
|
||||
throw new RuntimeException("Failed to decompress revision: {$storageKey}");
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a stored revision file.
|
||||
*/
|
||||
public function delete(string $storageKey): void
|
||||
{
|
||||
$this->guardKey($storageKey);
|
||||
Storage::disk($this->disk)->delete($storageKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw compressed file for streaming download.
|
||||
*/
|
||||
public function readStream(string $storageKey)
|
||||
{
|
||||
$this->guardKey($storageKey);
|
||||
return Storage::disk($this->disk)->readStream($storageKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if content is binary (non-diffable).
|
||||
*/
|
||||
public function isBinary(string $content): bool
|
||||
{
|
||||
// Check for null bytes — reliable binary indicator
|
||||
return str_contains($content, "\0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute SHA-256 of content.
|
||||
*/
|
||||
public function contentHash(string $content): string
|
||||
{
|
||||
return hash('sha256', $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute path hash (server-scoped, prevents cross-server lookup).
|
||||
*/
|
||||
public function pathHash(int $serverId, string $filePath): string
|
||||
{
|
||||
return hash('sha256', $serverId . ':' . $filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a file path is within the server's allowed root.
|
||||
* Prevents path traversal attacks.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
public function guardFilePath(string $filePath): string
|
||||
{
|
||||
// Normalize — reject absolute paths or traversal sequences
|
||||
$normalized = str_replace('\\', '/', $filePath);
|
||||
if (str_starts_with($normalized, '/') || str_contains($normalized, '../') || str_contains($normalized, './')) {
|
||||
abort(422, 'Invalid file path.');
|
||||
}
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
// ─── Internal ─────────────────────────────────────────────────────────────
|
||||
|
||||
private function buildKey(int $serverId, string $pathHash, int $revisionNumber): string
|
||||
{
|
||||
$base = config('advanced-admin.revisions.storage_path', 'addons/revisions');
|
||||
return "{$base}/{$serverId}/{$pathHash}/{$revisionNumber}.gz";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent storage key manipulation — only allow keys within our storage prefix.
|
||||
*/
|
||||
private function guardKey(string $storageKey): void
|
||||
{
|
||||
$base = config('advanced-admin.revisions.storage_path', 'addons/revisions');
|
||||
if (!str_starts_with($storageKey, $base . '/')) {
|
||||
abort(403, 'Invalid storage key.');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user