From 6c4af860f55e597e2b5ae600061d362fcb61f625 Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Wed, 15 Jul 2026 18:10:27 +0530 Subject: [PATCH] FIX_POLICY_DOCUMNET_S3_DOWNLOAD_ISSUE --- app/Config/Acl.php | 1 + app/Config/Routes.php | 5 + .../PolicyTransactionController.php | 4 + app/Controllers/StorageFilesController.php | 196 ++++++ app/Libraries/S3Service.php | 112 ++++ app/Models/PTFileModel.php | 2 +- .../policy_transaction_endorsement_list.php | 2 +- .../policy_transaction_inception_list.php | 4 +- .../policy_transaction_inception_list_2.php | 4 +- app/Views/storage_files.php | 573 ++++++++++++++++++ 10 files changed, 897 insertions(+), 6 deletions(-) create mode 100644 app/Controllers/StorageFilesController.php create mode 100644 app/Views/storage_files.php diff --git a/app/Config/Acl.php b/app/Config/Acl.php index f273ba13..c471f1bb 100644 --- a/app/Config/Acl.php +++ b/app/Config/Acl.php @@ -31,6 +31,7 @@ class Acl '#^/test/testingquerys#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]], '#^/test/viewrfq#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]], '#^/fedeploy#' => ['roles' => [ADMIN_ROLE_ID]], + '#^/storage/files#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]], '#^/visitOffBoardCheck#' => ['roles' => [ADMIN_ROLE_ID]], '#^/logs#' => ['roles' => [ADMIN_ROLE_ID]], '#^/util/log_list#' => ['roles' => [ADMIN_ROLE_ID]], diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 52915d2b..beeddf81 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -33,6 +33,11 @@ $routes->get('/swagger/tpa', 'SwaggerController::tpa', ['filter' => 'authMVC']); $routes->get('/swagger/tpa-spec', 'SwaggerController::tpaSpec', ['filter' => 'authMVC']); $routes->get('/fedeploy', 'DeployController::fedeploy_view', ['filter' => 'authMVC']); $routes->post('/fedeploy', 'DeployController::fedeploy', ['filter' => 'authMVC']); +$routes->group('/storage/files', ['filter' => 'authMVC'], function ($routes) { + $routes->get('/', 'StorageFilesController::index'); + $routes->get('list', 'StorageFilesController::listFiles'); + $routes->get('folders', 'StorageFilesController::folders'); +}); $routes->get('/visitOffBoardCheck', 'EmployeeController::visitOffBoardCheck'); $routes->get('/metaDashboardDemo', 'TestingController::metaDashboardDemo'); $routes->get('/apacheSuperSetDemo', 'TestingController::apacheSuperSetDemo'); diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index bfda0f28..851a63ee 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -1929,6 +1929,7 @@ class PolicyTransactionController extends BaseController $ptFileQuery = $this->PTFileModel + ->select('pt_files.*') ->join('policy_transaction', 'pt_files.pt_id = policy_transaction.id') ->where('pt_files.pt_id', $id) ->where('pt_files.is_active', 1); @@ -2201,6 +2202,7 @@ class PolicyTransactionController extends BaseController $ptFileQuery = $this->PTFileModel + ->select('pt_files.*') ->join('policy_transaction', 'pt_files.pt_id = policy_transaction.id') ->where('pt_files.pt_id', $id) ->where('pt_files.is_active', 1); @@ -3414,6 +3416,7 @@ class PolicyTransactionController extends BaseController $data['base_cd_amount'] = $pt_bp_amt['amount'] ?? null; $ptFileQuery = $this->PTFileModel + ->select('pt_files.*') ->join('policy_transaction', 'pt_files.pt_id = policy_transaction.id') ->where('pt_files.pt_id', $id) ->where('pt_files.is_active', 1); @@ -3731,6 +3734,7 @@ class PolicyTransactionController extends BaseController if (!empty($uploadData)) { $ptFileQuery = $this->PTFileModel + ->select('pt_files.*') ->join('policy_transaction', 'pt_files.pt_id = policy_transaction.id') ->where('pt_files.pt_id', $pt_id) ->where('pt_files.is_active', 1); diff --git a/app/Controllers/StorageFilesController.php b/app/Controllers/StorageFilesController.php new file mode 100644 index 00000000..9fc0c0b8 --- /dev/null +++ b/app/Controllers/StorageFilesController.php @@ -0,0 +1,196 @@ + 'S3 Files', + 'page_title' => 'S3 File Browser', + 'bucket' => $storage->getBucket(), + 'uses_s3' => $storage->usesS3(), + 'list_url' => base_url('storage/files/list'), + 'folders_url' => base_url('storage/files/folders'), + ]; + + $this->loadLayout('storage_files', $data); + } + + /** + * GET /storage/files/folders + */ + public function folders() + { + $storage = \Config\Services::getFileStorageService(false); + if (! $storage->usesS3()) { + return $this->response->setJSON([ + 'success' => false, + 'folders' => [], + 'message' => 'S3 storage is not enabled', + ])->setStatusCode(400); + } + + $s3 = \Config\Services::getS3Service(false); + $listed = $s3->listRootFolders($storage->getBucket()); + $fromS3 = $listed['folders'] ?? []; + + // Merge known local upload module folders so empty S3 prefixes still appear. + $local = $this->localUploadFolders(); + $folders = array_values(array_unique(array_merge($fromS3, $local))); + sort($folders); + + return $this->response->setJSON([ + 'success' => true, + 'bucket' => $storage->getBucket(), + 'folders' => $folders, + 'message' => $listed['message'] ?? 'OK', + ]); + } + + /** + * GET /storage/files/list?folder=&search=&page=1&per_page=10 + * + * Fetches from S3 in small batches; stops once the requested page + * (+1 row to detect has_more) is satisfied. Does not load the whole bucket. + */ + public function listFiles() + { + $storage = \Config\Services::getFileStorageService(false); + if (! $storage->usesS3()) { + return $this->response->setJSON([ + 'success' => false, + 'message' => 'S3 storage is not enabled', + ])->setStatusCode(400); + } + + $folder = basename(str_replace('\\', '/', trim((string) $this->request->getGet('folder')))); + if ($folder === '' || $folder === '.' || $folder === '..' || preg_match('/[^a-zA-Z0-9_\-]/', $folder)) { + return $this->response->setJSON([ + 'success' => false, + 'message' => 'Invalid folder name', + ])->setStatusCode(400); + } + + $search = trim((string) $this->request->getGet('search')); + $page = max(1, (int) ($this->request->getGet('page') ?? 1)); + $perPage = (int) ($this->request->getGet('per_page') ?? self::PER_PAGE); + $perPage = max(1, min(50, $perPage)); + $needRows = ($page * $perPage) + 1; // one extra → has_more + + $s3 = \Config\Services::getS3Service(false); + $bucket = $storage->getBucket(); + $prefix = $folder . '/'; + + $matched = []; + $token = null; + $s3More = true; + $listErr = null; + + while (count($matched) < $needRows && $s3More) { + $batch = $s3->listFilesPage($prefix, 100, $token, $bucket); + if (! ($batch['success'] ?? false)) { + $listErr = $batch['message'] ?? 'List failed'; + break; + } + + foreach ($batch['files'] ?? [] as $file) { + $key = (string) ($file['key'] ?? ''); + $fileName = basename($key); + if ($search !== '' && stripos($fileName, $search) === false) { + continue; + } + $matched[] = [ + 'key' => $key, + 'file_name' => $fileName, + 'size' => (int) ($file['size'] ?? 0), + 'size_label' => $this->formatBytes((int) ($file['size'] ?? 0)), + 'last_modified' => (string) ($file['last_modified'] ?? ''), + ]; + } + + $token = $batch['next_token'] ?? null; + $s3More = ! empty($batch['is_truncated']) && $token !== null; + } + + if ($listErr !== null) { + return $this->response->setJSON([ + 'success' => false, + 'message' => $listErr, + ])->setStatusCode(500); + } + + $offset = ($page - 1) * $perPage; + $pageRows = array_slice($matched, $offset, $perPage); + $hasMore = count($matched) > ($offset + count($pageRows)) + || ($s3More && count($pageRows) === $perPage); + + // Optional short-lived download links for the 10 visible rows only. + foreach ($pageRows as &$row) { + $presigned = $storage->getPresignedUrl( + WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . $folder, + $row['file_name'], + 300 + ); + $row['download_url'] = ($presigned['success'] ?? false) ? ($presigned['url'] ?? null) : null; + } + unset($row); + + return $this->response->setJSON([ + 'success' => true, + 'bucket' => $bucket, + 'folder' => $folder, + 'search' => $search, + 'page' => $page, + 'per_page' => $perPage, + 'has_more' => $hasMore, + 'has_previous' => $page > 1, + 'count' => count($pageRows), + 'files' => $pageRows, + 'message' => 'OK', + ]); + } + + /** @return list */ + private function localUploadFolders(): array + { + $root = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads'; + $out = []; + $entries = @scandir($root); + if (! is_array($entries)) { + return $out; + } + foreach ($entries as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + if (is_dir($root . DIRECTORY_SEPARATOR . $entry)) { + $out[] = $entry; + } + } + return $out; + } + + private function formatBytes(int $bytes): string + { + if ($bytes < 1024) { + return $bytes . ' B'; + } + $units = ['KB', 'MB', 'GB', 'TB']; + $value = (float) $bytes; + foreach ($units as $unit) { + $value /= 1024; + if ($value < 1024) { + return round($value, 2) . ' ' . $unit; + } + } + return round($value, 2) . ' PB'; + } +} diff --git a/app/Libraries/S3Service.php b/app/Libraries/S3Service.php index 391a5f59..6b8280fd 100644 --- a/app/Libraries/S3Service.php +++ b/app/Libraries/S3Service.php @@ -294,6 +294,118 @@ class S3Service * @param string|null $bucket Override source bucket (optional) * @return array ['success' => bool, 'files' => array, 'message' => string] */ + /** + * List files in S3 bucket with optional MaxKeys / ContinuationToken pagination. + * + * @return array{success:bool,files:array,next_token:?string,is_truncated:bool,message:string} + */ + public function listFilesPage(string $prefix = '', int $maxKeys = 100, ?string $continuationToken = null, ?string $bucket = null): array + { + try { + $targetBucket = $this->resolveBucket($bucket); + $params = [ + 'Bucket' => $targetBucket, + 'Prefix' => $prefix, + 'MaxKeys' => max(1, min(1000, $maxKeys)), + ]; + if ($continuationToken !== null && $continuationToken !== '') { + $params['ContinuationToken'] = $continuationToken; + } + + $result = $this->s3Client->listObjectsV2($params); + $files = []; + if (isset($result['Contents'])) { + foreach ($result['Contents'] as $object) { + $key = (string) ($object['Key'] ?? ''); + if ($key === '' || substr($key, -1) === '/') { + continue; + } + $files[] = [ + 'key' => $key, + 'size' => (int) ($object['Size'] ?? 0), + 'last_modified' => $object['LastModified']->format('Y-m-d H:i:s'), + 'url' => $this->buildBaseUrl($targetBucket) . $key, + ]; + } + } + + $isTruncated = ! empty($result['IsTruncated']); + $nextToken = $isTruncated ? ($result['NextContinuationToken'] ?? null) : null; + + return [ + 'success' => true, + 'files' => $files, + 'next_token' => $nextToken, + 'is_truncated' => $isTruncated, + 'message' => 'Files retrieved successfully', + ]; + } catch (AwsException $e) { + log_message('error', 'S3 List Page Error: ' . $e->getMessage()); + return [ + 'success' => false, + 'files' => [], + 'next_token' => null, + 'is_truncated' => false, + 'message' => 'Failed to list files: ' . $e->getMessage(), + ]; + } + } + + /** + * List top-level "folders" (common prefixes) in the bucket. + * + * @return array{success:bool,folders:list,message:string} + */ + public function listRootFolders(?string $bucket = null): array + { + try { + $targetBucket = $this->resolveBucket($bucket); + $folders = []; + $token = null; + + do { + $params = [ + 'Bucket' => $targetBucket, + 'Delimiter' => '/', + 'MaxKeys' => 1000, + ]; + if ($token !== null) { + $params['ContinuationToken'] = $token; + } + + $result = $this->s3Client->listObjectsV2($params); + if (! empty($result['CommonPrefixes'])) { + foreach ($result['CommonPrefixes'] as $prefixRow) { + $p = rtrim((string) ($prefixRow['Prefix'] ?? ''), '/'); + if ($p !== '') { + $folders[] = $p; + } + } + } + + $token = ! empty($result['IsTruncated']) + ? ($result['NextContinuationToken'] ?? null) + : null; + } while ($token !== null); + + $folders = array_values(array_unique($folders)); + sort($folders); + + return [ + 'success' => true, + 'folders' => $folders, + 'message' => 'Folders retrieved successfully', + ]; + } catch (AwsException $e) { + log_message('error', 'S3 List Folders Error: ' . $e->getMessage()); + return [ + 'success' => false, + 'folders' => [], + 'message' => 'Failed to list folders: ' . $e->getMessage(), + ]; + } + } + public function listFiles(string $prefix = '', ?string $bucket = null): array { try { diff --git a/app/Models/PTFileModel.php b/app/Models/PTFileModel.php index 702cb30d..7f1a8811 100644 --- a/app/Models/PTFileModel.php +++ b/app/Models/PTFileModel.php @@ -28,7 +28,7 @@ class PTFileModel extends Model public function getPolicyDriveFilesIndex($policy_id,$policy_doc_name) { - $clients = $this->select('pt_files.doc_name,pt_files.file_name,pt_files.url,pt.client_id,pt.client_policy_id,"policy" as file_type') + $clients = $this->select('pt_files.id,pt_files.doc_name,pt_files.file_name,pt_files.url,pt.client_id,pt.client_policy_id,"policy" as file_type') ->join('policy_transaction pt', 'pt_files.pt_id = pt.id') ->where('pt.client_policy_id', $policy_id) ->where('pt.is_active',1) diff --git a/app/Views/policy_transaction_endorsement_list.php b/app/Views/policy_transaction_endorsement_list.php index 7246570e..05103840 100644 --- a/app/Views/policy_transaction_endorsement_list.php +++ b/app/Views/policy_transaction_endorsement_list.php @@ -1116,7 +1116,7 @@ var link = $('') .attr('href', url) - .attr('download', true) + .attr('download', '') .attr('style', 'font-size:18px;') .attr('data-id', item.id) .addClass('mdi mdi-download') diff --git a/app/Views/policy_transaction_inception_list.php b/app/Views/policy_transaction_inception_list.php index b0007253..4a1b6881 100644 --- a/app/Views/policy_transaction_inception_list.php +++ b/app/Views/policy_transaction_inception_list.php @@ -1369,7 +1369,7 @@ function appendFileTableBody(data) var link = $('') .attr('href', url) - .attr('download', true) + .attr('download', '') .attr('style', 'font-size:18px;') .attr('data-id', item.id) .addClass('mdi mdi-download') @@ -1472,7 +1472,7 @@ function appendVehicleFileTableBody(data) // Create the download link var downloadLink = $('') .attr('href', '' + item.id) - .attr('download', true) + .attr('download', '') .attr('style', 'font-size:18px;') .attr('data-id', item.id) .addClass('mdi mdi-download') diff --git a/app/Views/policy_transaction_inception_list_2.php b/app/Views/policy_transaction_inception_list_2.php index f74aac7e..83cac660 100644 --- a/app/Views/policy_transaction_inception_list_2.php +++ b/app/Views/policy_transaction_inception_list_2.php @@ -1133,7 +1133,7 @@ function appendFileTableBody(data) var link = $('') .attr('href', url) - .attr('download', true) + .attr('download', '') .attr('style', 'font-size:18px;') .attr('data-id', item.id) .addClass('mdi mdi-download') @@ -1229,7 +1229,7 @@ function appendVehicleFileTableBody(data) // Create the download link var downloadLink = $('') .attr('href', '' + item.id) - .attr('download', true) + .attr('download', '') .attr('style', 'font-size:18px;') .attr('data-id', item.id) .addClass('mdi mdi-download') diff --git a/app/Views/storage_files.php b/app/Views/storage_files.php new file mode 100644 index 00000000..10471a30 --- /dev/null +++ b/app/Views/storage_files.php @@ -0,0 +1,573 @@ + + +
+
+
+
+

File browser

+

Browse upload objects by folder. Results load 10 at a time.

+
+
+ + +
+
+ + +
+ FILE_STORAGE_DRIVER is not set to s3, or the upload bucket is missing. Listing is disabled. +
+ + +
+
+
+ +
+ +
+
+
+ +
+ > +
+
+
+ +
+ +
+
+
+
+ +
+
+ + + + + + + + + + + + + + +
File nameSizeLast modifiedAction
+
+ + Select a folder to load files +
+
+
+
+
Page —
+
+ + +
+
+
+
+
+ +