ailixir-api / app /Http /Controllers /Api /DockingController.php
AILIXIR Bot
Auto-sync: 201d269f287526269b8ffded5f450ff36b46c740
907b200
Raw
History Blame Contribute Delete
7.49 kB
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\Docking\SubmitDockingRequest;
use App\Jobs\ConvertSmilesJob;
use App\Jobs\RunDockingJob;
use App\Models\DockingJob;
use App\Traits\ApiResponseTrait;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Str;
use Laravel\Sanctum\PersonalAccessToken;
use Symfony\Component\HttpFoundation\StreamedResponse;
class DockingController
{
use ApiResponseTrait;
public function submit(SubmitDockingRequest $request)
{
$isSmiles = $request->filled('ligand_smiles');
if ($isSmiles) {
$ligandPath = null;
} else {
$ligandPath = $this->storeAsPdbqt($request->file('ligand_file'), true);
}
$proteinPath = $this->storeAsPdbqt($request->file('protein_file'), false);
$job = DockingJob::create([
'user_id' => $request->user()->id,
'input_type' => $isSmiles ? 'smiles' : 'file',
'smiles' => $isSmiles ? $request->ligand_smiles : null,
'protein_name' => $request->protein_name,
'ligand_name' => $request->ligand_name,
'protein_path' => $proteinPath,
'ligand_path' => $ligandPath,
'status' => 'pending',
]);
$params = [
'center_x' => $request->center_x,
'center_y' => $request->center_y,
'center_z' => $request->center_z,
'box_size_x' => $request->box_size_x,
'box_size_y' => $request->box_size_y,
'box_size_z' => $request->box_size_z,
'exhaustiveness' => $request->exhaustiveness ?? 8,
'n_poses' => $request->n_poses ?? 5,
];
if ($isSmiles) {
ConvertSmilesJob::dispatch($job, $request->ligand_smiles, $params);
} else {
RunDockingJob::dispatch($job, $params);
}
return $this->successResponse('Docking Job Successfully Queued', [
'job_id' => $job->id,
'status' => $job->status,
]);
}
public function history(Request $request)
{
$perPage = min((int) $request->query('per_page', 15), 100);
$paginator = DockingJob::dockingOnly()
->where('user_id', $request->user()->id)
->orderBy('created_at', 'desc')
->paginate($perPage)
->through(function ($job) {
return [
'id' => $job->id,
'status' => $job->status,
'protein' => $job->protein_name,
'ligand' => $job->ligand_name,
'created_at' => $job->created_at->toIso8601String(),
'download_url' => url('/api/docking/download/'.$job->id),
'scores' => $job->vina_scores ?? [],
'error' => $job->status === 'failed' ? ($job->result_data['error'] ?? null) : null,
];
});
return $this->paginatedResponse('Docking history retrieved successfully', $paginator);
}
public function status(Request $request, $id)
{
$job = DockingJob::dockingOnly()
->where('id', $id)
->where('user_id', $request->user()->id)
->first();
if (! $job) {
return $this->errorResponse('Docking job not found or unauthorized', 404);
}
return $this->successResponse('Job details retrieved successfully', [
'id' => $job->id,
'status' => $job->status,
'protein' => $job->protein_name,
'ligand' => $job->ligand_name,
'created_at' => $job->created_at->toIso8601String(),
'download_url' => url('/api/docking/download/'.$job->id),
'scores' => $job->vina_scores ?? [],
'error' => $job->status === 'failed' ? ($job->result_data['error'] ?? null) : null,
]);
}
public function download(Request $request, $id)
{
$token = $request->bearerToken() ?? $request->query('token');
if (! $token) {
return $this->errorResponse('Unauthenticated', 401);
}
$accessToken = PersonalAccessToken::findToken($token);
if (! $accessToken || ! $accessToken->tokenable) {
return $this->errorResponse('Invalid token', 401);
}
$job = DockingJob::dockingOnly()
->where('id', $id)
->where('user_id', $accessToken->tokenable->id)
->first();
if (! $job || $job->status !== 'completed') {
return $this->errorResponse('Docking file not available or unauthorized', 404);
}
$filePath = $job->result_data['output_file'] ?? null;
if (! $filePath || ! file_exists($filePath)) {
return $this->errorResponse('File not found on server', 404);
}
return new StreamedResponse(function () use ($filePath) {
$stream = fopen($filePath, 'rb');
if ($stream) {
fpassthru($stream);
fclose($stream);
}
}, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="docking_result_'.$job->id.'.pdbqt"',
'Content-Length' => filesize($filePath),
]);
}
private function storeAsPdbqt(UploadedFile $file, bool $isLigand): string
{
$ext = strtolower($file->getClientOriginalExtension());
$filename = Str::random(40).'.pdbqt';
$destPath = storage_path('app/private/docking/'.$filename);
if ($ext === 'pdbqt') {
$file->storeAs('docking', $filename);
} elseif ($ext === 'pdb') {
$this->convertPdbToPdbqt($file->getRealPath(), $destPath, $isLigand);
} else {
throw new HttpResponseException(
$this->errorResponse('Unsupported file format: .'.$ext.'. Only .pdb and .pdbqt files are accepted.', 422)
);
}
return $destPath;
}
private function convertPdbToPdbqt(string $source, string $dest, bool $isLigand): void
{
$pythonPath = env('DOCKING_PYTHON_PATH');
$obabel = $pythonPath ? dirname($pythonPath).'/obabel' : 'obabel';
if (! file_exists($obabel)) {
$obabel = 'obabel';
}
$result = Process::timeout(60)->run([
$obabel,
'-ipdb', $source,
'-opdbqt',
'-O', $dest,
]);
if (! $result->successful()) {
Log::error('PDB-to-PDBQT conversion failed', [
'source' => $source,
'dest' => $dest,
'error' => $result->errorOutput(),
]);
throw new HttpResponseException(
$this->errorResponse('Failed to convert PDB file to PDBQT format.', 500)
);
}
$content = file_get_contents($dest);
$lines = explode("\n", $content);
if ($isLigand) {
$lines = array_map(fn ($line) => str_starts_with($line, 'ATOM') ? 'HETATM'.substr($line, 6) : $line, $lines);
} else {
$lines = array_filter($lines, fn ($line) => preg_match('/^(ATOM|HETATM)/', $line));
$lines = array_values($lines);
}
file_put_contents($dest, implode("\n", $lines));
}
}