Files
pterodactyl_addon/app/Addons/AdvancedAdmin/Http/Controllers/Client/FileRevisionController.php
2026-06-11 20:33:44 -05:00

151 lines
5.8 KiB
PHP

<?php
/**
* File Revision Controller — client API
*
* @author Ball Studios <https://git.balls.studio>
* @license MIT
*/
namespace Pterodactyl\Addons\AdvancedAdmin\Http\Controllers\Client;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Pterodactyl\Addons\AdvancedAdmin\Models\FileRevision;
use Pterodactyl\Addons\AdvancedAdmin\Services\Revisions\DiffService;
use Pterodactyl\Addons\AdvancedAdmin\Services\Revisions\RevisionService;
use Pterodactyl\Addons\AdvancedAdmin\Services\Revisions\RevisionStorageService;
use Pterodactyl\Models\Server;
class FileRevisionController extends Controller
{
public function __construct(
private readonly RevisionService $revisionService,
private readonly DiffService $diffService,
private readonly RevisionStorageService $storage,
) {}
/** GET /api/client/servers/{server}/addons/files/revisions?path= */
public function index(Request $request, Server $server): JsonResponse
{
$this->requirePermission($request, $server, 'file.read');
$path = $request->string('path', '')->toString();
abort_if(empty($path), 422, 'path parameter is required.');
$path = $this->storage->guardFilePath($path);
$pathHash = $this->storage->pathHash($server->id, $path);
$revisions = FileRevision::query()
->where('server_id', $server->id)
->where('path_hash', $pathHash)
->with('author:id,username,email')
->orderByDesc('revision_number')
->paginate(25);
return response()->json($revisions);
}
/** GET /api/client/servers/{server}/addons/files/revisions/{revision} */
public function show(Request $request, Server $server, FileRevision $revision): JsonResponse
{
$this->requirePermission($request, $server, 'file.read');
$this->assertBelongsToServer($revision, $server);
return response()->json(['data' => $revision->load('author:id,username,email')]);
}
/** GET /api/client/servers/{server}/addons/files/revisions/{revision}/diff */
public function diff(Request $request, Server $server, FileRevision $revision): JsonResponse
{
$this->requirePermission($request, $server, 'file.read');
$this->assertBelongsToServer($revision, $server);
$content = $this->storage->retrieve($revision->storage_key);
// If binary, return a placeholder diff
if ($this->storage->isBinary($content)) {
return response()->json(['binary' => true, 'lines' => []]);
}
// Fetch previous revision for comparison
$previous = FileRevision::query()
->where('server_id', $server->id)
->where('path_hash', $revision->path_hash)
->where('revision_number', $revision->revision_number - 1)
->first();
$oldContent = $previous ? $this->storage->retrieve($previous->storage_key) : '';
$lines = $this->diffService->diff($oldContent, $content, basename($revision->file_path));
return response()->json(['binary' => false, 'lines' => $lines]);
}
/** GET /api/client/servers/{server}/addons/files/revisions/{revision}/download */
public function download(Request $request, Server $server, FileRevision $revision): StreamedResponse
{
$this->requirePermission($request, $server, 'file.read');
$this->assertBelongsToServer($revision, $server);
$content = $this->storage->retrieve($revision->storage_key);
$filename = basename($revision->file_path) . ".rev{$revision->revision_number}";
return response()->streamDownload(function () use ($content) {
echo $content;
}, $filename, ['Content-Type' => 'application/octet-stream']);
}
/** POST /api/client/servers/{server}/addons/files/revisions/{revision}/restore */
public function restore(Request $request, Server $server, FileRevision $revision): JsonResponse
{
$this->requirePermission($request, $server, 'file.update');
$this->assertBelongsToServer($revision, $server);
$content = $this->revisionService->restore($revision, $request->user()->id);
return response()->json([
'restored' => true,
'content' => $content,
'revision' => $revision->revision_number,
]);
}
/** DELETE /api/client/servers/{server}/addons/files/revisions/{revision} */
public function destroy(Request $request, Server $server, FileRevision $revision): JsonResponse
{
// Only admins can manually delete revisions
abort_unless($request->user()?->root_admin, 403, 'Admin access required.');
$this->assertBelongsToServer($revision, $server);
$this->revisionService->delete($revision);
return response()->json(['deleted' => true]);
}
// ─── Helpers ──────────────────────────────────────────────────────────────
private function requirePermission(Request $request, Server $server, string $permission): void
{
$user = $request->user();
abort_unless($user, 401);
$isOwner = $server->owner_id === $user->id;
$isAdmin = $user->root_admin;
if (!$isOwner && !$isAdmin) {
$subuser = $server->subusers()->where('user_id', $user->id)->first();
abort_unless(
$subuser && in_array($permission, $subuser->permissions ?? [], true),
403,
"Permission '{$permission}' required."
);
}
}
private function assertBelongsToServer(FileRevision $revision, Server $server): void
{
abort_unless($revision->server_id === $server->id, 404);
}
}