* @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); } }