Files
vstash/src/Controller/ConsoleApiController.php

470 lines
18 KiB
PHP
Raw Normal View History

2025-06-05 09:17:47 -04:00
<?php
namespace App\Controller;
use App\Entity\S3Credential;
use App\Entity\S3Bucket;
use App\Entity\S3Object;
use App\Service\S3Service;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ConsoleApiController extends AbstractController
{
public function __construct(
private S3Service $s3Service,
private EntityManagerInterface $entityManager
) {}
2025-06-05 09:56:25 -04:00
private function checkAuth(Request $request): ?JsonResponse
{
if (!$request->getSession()->get('console_logged_in')) {
return new JsonResponse(['error' => 'Unauthorized'], Response::HTTP_UNAUTHORIZED);
}
return null;
}
2025-06-05 09:17:47 -04:00
// Credentials Management
public function credentials(Request $request): JsonResponse
{
2025-06-05 09:56:25 -04:00
if ($resp = $this->checkAuth($request)) {
return $resp;
}
2025-06-05 09:17:47 -04:00
if ($request->getMethod() === 'GET') {
$credentials = $this->entityManager->getRepository(S3Credential::class)->findAll();
return new JsonResponse([
'credentials' => array_map(function($cred) {
return [
'id' => $cred->getId(),
'access_key' => $cred->getAccessKey(),
'user_name' => $cred->getUserName(),
'is_active' => $cred->isActive(),
'created_at' => $cred->getCreatedAt()->format('Y-m-d H:i:s'),
'bucket_count' => $cred->getBuckets()->count()
];
}, $credentials)
]);
}
if ($request->getMethod() === 'POST') {
$data = json_decode($request->getContent(), true);
$accessKey = $data['access_key'] ?? 'AKIA' . strtoupper(bin2hex(random_bytes(10)));
$secretKey = $data['secret_key'] ?? base64_encode(random_bytes(30));
$userName = $data['user_name'] ?? null;
$credential = $this->s3Service->createCredential($accessKey, $secretKey, $userName);
return new JsonResponse([
'id' => $credential->getId(),
'access_key' => $credential->getAccessKey(),
'secret_key' => $credential->getSecretKey(),
'user_name' => $credential->getUserName(),
'is_active' => $credential->isActive(),
'created_at' => $credential->getCreatedAt()->format('Y-m-d H:i:s')
], 201);
}
return new JsonResponse(['error' => 'Method not allowed'], 405);
}
public function credentialDetail(int $id, Request $request): JsonResponse
{
2025-06-05 09:56:25 -04:00
if ($resp = $this->checkAuth($request)) {
return $resp;
}
2025-06-05 09:17:47 -04:00
$credential = $this->entityManager->getRepository(S3Credential::class)->find($id);
if (!$credential) {
return new JsonResponse(['error' => 'Credential not found'], 404);
}
if ($request->getMethod() === 'GET') {
return new JsonResponse([
'id' => $credential->getId(),
'access_key' => $credential->getAccessKey(),
'secret_key' => $credential->getSecretKey(),
'user_name' => $credential->getUserName(),
'is_active' => $credential->isActive(),
'created_at' => $credential->getCreatedAt()->format('Y-m-d H:i:s'),
'buckets' => array_map(function($bucket) {
return [
'name' => $bucket->getName(),
'region' => $bucket->getRegion(),
'created_at' => $bucket->getCreatedAt()->format('Y-m-d H:i:s')
];
}, $credential->getBuckets()->toArray())
]);
}
if ($request->getMethod() === 'PUT') {
$data = json_decode($request->getContent(), true);
if (isset($data['user_name'])) {
$credential->setUserName($data['user_name']);
}
if (isset($data['is_active'])) {
$credential->setIsActive($data['is_active']);
}
$this->entityManager->flush();
return new JsonResponse(['message' => 'Credential updated']);
}
if ($request->getMethod() === 'DELETE') {
$this->entityManager->remove($credential);
$this->entityManager->flush();
return new JsonResponse(['message' => 'Credential deleted']);
}
return new JsonResponse(['error' => 'Method not allowed'], 405);
}
// Buckets Management
public function buckets(Request $request): JsonResponse
{
2025-06-05 09:56:25 -04:00
if ($resp = $this->checkAuth($request)) {
return $resp;
}
2025-06-05 09:17:47 -04:00
if ($request->getMethod() === 'GET') {
$buckets = $this->entityManager->getRepository(S3Bucket::class)->findAll();
return new JsonResponse([
'buckets' => array_map(function($bucket) {
$objectCount = $this->entityManager->getRepository(S3Object::class)->count(['bucket' => $bucket]);
$totalSize = $this->entityManager->createQueryBuilder()
->select('SUM(o.size)')
->from(S3Object::class, 'o')
->where('o.bucket = :bucket')
->setParameter('bucket', $bucket)
->getQuery()
->getSingleScalarResult() ?: 0;
return [
'name' => $bucket->getName(),
'region' => $bucket->getRegion(),
'owner' => $bucket->getOwner()->getUserName() ?: $bucket->getOwner()->getAccessKey(),
'created_at' => $bucket->getCreatedAt()->format('Y-m-d H:i:s'),
'object_count' => $objectCount,
'total_size' => $totalSize,
'total_size_human' => $this->formatBytes($totalSize)
];
}, $buckets)
]);
}
if ($request->getMethod() === 'POST') {
$data = json_decode($request->getContent(), true);
$bucketName = $data['name'] ?? null;
$ownerId = $data['owner_id'] ?? null;
$region = $data['region'] ?? 'us-east-1';
if (!$bucketName || !$ownerId) {
return new JsonResponse(['error' => 'Missing bucket name or owner'], 400);
}
$owner = $this->entityManager->getRepository(S3Credential::class)->find($ownerId);
if (!$owner) {
return new JsonResponse(['error' => 'Owner not found'], 404);
}
try {
$bucket = $this->s3Service->createBucket($bucketName, $owner, $region);
return new JsonResponse([
'name' => $bucket->getName(),
'region' => $bucket->getRegion(),
'owner' => $bucket->getOwner()->getUserName() ?: $bucket->getOwner()->getAccessKey(),
'created_at' => $bucket->getCreatedAt()->format('Y-m-d H:i:s')
], 201);
} catch (\Exception $e) {
return new JsonResponse(['error' => $e->getMessage()], 400);
}
}
return new JsonResponse(['error' => 'Method not allowed'], 405);
}
public function bucketDetail(string $name, Request $request): JsonResponse
{
2025-06-05 09:56:25 -04:00
if ($resp = $this->checkAuth($request)) {
return $resp;
}
2025-06-05 09:17:47 -04:00
$bucket = $this->s3Service->findBucketByName($name);
if (!$bucket) {
return new JsonResponse(['error' => 'Bucket not found'], 404);
}
if ($request->getMethod() === 'GET') {
$objects = $this->s3Service->listObjects($bucket);
$totalSize = array_sum(array_map(fn($obj) => $obj->getSize(), $objects));
return new JsonResponse([
'name' => $bucket->getName(),
'region' => $bucket->getRegion(),
'owner' => $bucket->getOwner()->getUserName() ?: $bucket->getOwner()->getAccessKey(),
'created_at' => $bucket->getCreatedAt()->format('Y-m-d H:i:s'),
'object_count' => count($objects),
'total_size' => $totalSize,
'total_size_human' => $this->formatBytes($totalSize),
'objects' => array_map(function($obj) {
return [
'key' => $obj->getObjectKey(),
'size' => $obj->getSize(),
'size_human' => $this->formatBytes($obj->getSize()),
'content_type' => $obj->getContentType(),
'etag' => $obj->getEtag(),
'is_multipart' => $obj->isMultipart(),
'created_at' => $obj->getCreatedAt()->format('Y-m-d H:i:s')
];
}, $objects)
]);
}
if ($request->getMethod() === 'DELETE') {
try {
$this->s3Service->deleteBucket($bucket);
return new JsonResponse(['message' => 'Bucket deleted']);
} catch (\Exception $e) {
return new JsonResponse(['error' => $e->getMessage()], 400);
}
}
return new JsonResponse(['error' => 'Method not allowed'], 405);
}
// Objects Management
public function objects(string $bucketName, Request $request): JsonResponse
{
2025-06-05 09:56:25 -04:00
if ($resp = $this->checkAuth($request)) {
return $resp;
}
2025-06-05 09:17:47 -04:00
$bucket = $this->s3Service->findBucketByName($bucketName);
if (!$bucket) {
return new JsonResponse(['error' => 'Bucket not found'], 404);
}
if ($request->getMethod() === 'GET') {
$prefix = $request->query->get('prefix', '');
$objects = $this->s3Service->listObjects($bucket, $prefix);
return new JsonResponse([
'objects' => array_map(function($obj) {
return [
'key' => $obj->getObjectKey(),
'size' => $obj->getSize(),
'size_human' => $this->formatBytes($obj->getSize()),
'content_type' => $obj->getContentType(),
'etag' => $obj->getEtag(),
'is_multipart' => $obj->isMultipart(),
'created_at' => $obj->getCreatedAt()->format('Y-m-d H:i:s')
];
}, $objects)
]);
}
if ($request->getMethod() === 'DELETE') {
$data = json_decode($request->getContent(), true);
$keys = $data['keys'] ?? [];
$deleted = [];
foreach ($keys as $key) {
$object = $this->s3Service->findObject($bucket, $key);
if ($object) {
$this->s3Service->deleteObject($object);
$deleted[] = $key;
}
}
return new JsonResponse(['deleted' => $deleted]);
}
return new JsonResponse(['error' => 'Method not allowed'], 405);
}
public function objectDetail(string $bucketName, string $objectKey, Request $request): JsonResponse
{
2025-06-05 09:56:25 -04:00
if ($resp = $this->checkAuth($request)) {
return $resp;
}
2025-06-05 09:17:47 -04:00
$bucket = $this->s3Service->findBucketByName($bucketName);
if (!$bucket) {
return new JsonResponse(['error' => 'Bucket not found'], 404);
}
$object = $this->s3Service->findObject($bucket, $objectKey);
if (!$object) {
return new JsonResponse(['error' => 'Object not found'], 404);
}
if ($request->getMethod() === 'GET') {
return new JsonResponse([
'key' => $object->getObjectKey(),
'size' => $object->getSize(),
'size_human' => $this->formatBytes($object->getSize()),
'content_type' => $object->getContentType(),
'etag' => $object->getEtag(),
'is_multipart' => $object->isMultipart(),
'part_count' => $object->getPartCount(),
'metadata' => $object->getMetadata(),
'storage_path' => $object->getStoragePath(),
'created_at' => $object->getCreatedAt()->format('Y-m-d H:i:s'),
'updated_at' => $object->getUpdatedAt()->format('Y-m-d H:i:s')
]);
}
if ($request->getMethod() === 'DELETE') {
$this->s3Service->deleteObject($object);
return new JsonResponse(['message' => 'Object deleted']);
}
return new JsonResponse(['error' => 'Method not allowed'], 405);
}
// Multipart Uploads
public function multipartUploads(string $bucketName, Request $request): JsonResponse
{
2025-06-05 09:56:25 -04:00
if ($resp = $this->checkAuth($request)) {
return $resp;
}
2025-06-05 09:17:47 -04:00
$bucket = $this->s3Service->findBucketByName($bucketName);
if (!$bucket) {
return new JsonResponse(['error' => 'Bucket not found'], 404);
}
$uploads = $this->s3Service->listMultipartUploads($bucket);
return new JsonResponse([
'uploads' => array_map(function($upload) {
$parts = $this->s3Service->listParts($upload);
$totalSize = array_sum(array_map(fn($part) => $part->getSize(), $parts));
return [
'upload_id' => $upload->getUploadId(),
'object_key' => $upload->getObjectKey(),
'initiated_by' => $upload->getInitiatedBy()->getUserName() ?: $upload->getInitiatedBy()->getAccessKey(),
'content_type' => $upload->getContentType(),
'initiated_at' => $upload->getInitiatedAt()->format('Y-m-d H:i:s'),
'part_count' => count($parts),
'total_size' => $totalSize,
'total_size_human' => $this->formatBytes($totalSize)
];
}, $uploads)
]);
}
// Presigned URLs
public function presignedUrls(Request $request): JsonResponse
{
2025-06-05 09:56:25 -04:00
if ($resp = $this->checkAuth($request)) {
return $resp;
}
2025-06-05 09:17:47 -04:00
if ($request->getMethod() === 'GET') {
$urls = $this->entityManager->getRepository(\App\Entity\S3PresignedUrl::class)
->createQueryBuilder('p')
->where('p.expiresAt > :now')
->setParameter('now', new \DateTime())
->orderBy('p.createdAt', 'DESC')
->setMaxResults(100)
->getQuery()
->getResult();
return new JsonResponse([
'urls' => array_map(function($url) {
return [
'bucket_name' => $url->getBucketName(),
'object_key' => $url->getObjectKey(),
'method' => $url->getMethod(),
'access_key' => $url->getAccessKey(),
'expires_at' => $url->getExpiresAt()->format('Y-m-d H:i:s'),
2025-06-05 10:07:14 -04:00
'created_at' => $url->getCreatedAt()->format('Y-m-d H:i:s'),
'url' => $url->getUrl()
2025-06-05 09:17:47 -04:00
];
}, $urls)
]);
}
if ($request->getMethod() === 'POST') {
$data = json_decode($request->getContent(), true);
$bucketName = $data['bucket_name'] ?? null;
$objectKey = $data['object_key'] ?? null;
$method = $data['method'] ?? 'GET';
$expiresIn = $data['expires_in'] ?? 3600;
$accessKey = $data['access_key'] ?? null;
if (!$bucketName || !$objectKey || !$accessKey) {
return new JsonResponse(['error' => 'Missing required fields'], 400);
}
$credential = $this->s3Service->findCredentialByAccessKey($accessKey);
if (!$credential) {
return new JsonResponse(['error' => 'Invalid access key'], 404);
}
2025-06-05 10:07:14 -04:00
$url = $this->s3Service->generatePresignedUrl($bucketName, $objectKey, $credential, $method, $expiresIn);
2025-06-05 09:17:47 -04:00
return new JsonResponse(['url' => $url], 201);
}
return new JsonResponse(['error' => 'Method not allowed'], 405);
}
// Statistics
public function stats(Request $request): JsonResponse
{
2025-06-05 09:56:25 -04:00
if ($resp = $this->checkAuth($request)) {
return $resp;
}
2025-06-05 09:17:47 -04:00
$credentialCount = $this->entityManager->getRepository(S3Credential::class)->count([]);
$bucketCount = $this->entityManager->getRepository(S3Bucket::class)->count([]);
$objectCount = $this->entityManager->getRepository(S3Object::class)->count([]);
$totalSize = $this->entityManager->createQueryBuilder()
->select('SUM(o.size)')
->from(S3Object::class, 'o')
->getQuery()
->getSingleScalarResult() ?: 0;
$multipartUploads = $this->entityManager->getRepository(\App\Entity\S3MultipartUpload::class)->count([]);
$activePresignedUrls = $this->entityManager->getRepository(\App\Entity\S3PresignedUrl::class)
->createQueryBuilder('p')
->select('COUNT(p.id)')
->where('p.expiresAt > :now')
->setParameter('now', new \DateTime())
->getQuery()
->getSingleScalarResult();
return new JsonResponse([
'credentials' => $credentialCount,
'buckets' => $bucketCount,
'objects' => $objectCount,
'total_storage' => $totalSize,
'total_storage_human' => $this->formatBytes($totalSize),
'active_multipart_uploads' => $multipartUploads,
'active_presigned_urls' => $activePresignedUrls
]);
}
private function formatBytes(int $bytes): string
{
if ($bytes === 0) return '0 B';
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = floor(log($bytes, 1024));
return round($bytes / pow(1024, $i), 2) . ' ' . $units[$i];
}
}