feat: [additional] PPTX 관리 페이지 추가
- 파일시스템 스캔 기반 PPTX 목록 조회/다운로드 - 카테고리별 필터, 파일명 검색 기능 - 경로 트래버설 방지 보안 검증
This commit is contained in:
181
app/Http/Controllers/Additional/PptxController.php
Normal file
181
app/Http/Controllers/Additional/PptxController.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Additional;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
/**
|
||||
* 추가기능 > PPTX 관리 컨트롤러
|
||||
* 파일시스템 스캔 기반으로 PPTX 파일 목록 조회 및 다운로드
|
||||
*/
|
||||
class PptxController extends Controller
|
||||
{
|
||||
private array $scanPaths = [
|
||||
'/var/www/docs/pptx-output' => ['label' => '산출물', 'source' => 'docs'],
|
||||
'/var/www/docs/rules' => ['label' => '정책/규칙', 'source' => 'docs'],
|
||||
'/var/www/docs/guides' => ['label' => '가이드', 'source' => 'docs'],
|
||||
'/var/www/docs/projects' => ['label' => '프로젝트', 'source' => 'docs'],
|
||||
'/var/www/docs/plans' => ['label' => '계획', 'source' => 'docs'],
|
||||
'/var/www/mng/docs/pptx-output' => ['label' => '산출물', 'source' => 'mng'],
|
||||
'/var/www/mng/docs' => ['label' => '교육/문서', 'source' => 'mng'],
|
||||
'/var/www/mng/public/docs' => ['label' => '공개 문서', 'source' => 'mng'],
|
||||
];
|
||||
|
||||
/**
|
||||
* PPTX 목록 페이지
|
||||
*/
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('additional.pptx.index'));
|
||||
}
|
||||
|
||||
$files = $this->scanPptxFiles();
|
||||
$categories = collect($files)->pluck('category')->unique()->sort()->values()->all();
|
||||
$totalSize = collect($files)->sum('size_bytes');
|
||||
|
||||
return view('additional.pptx.index', [
|
||||
'files' => $files,
|
||||
'categories' => $categories,
|
||||
'totalCount' => count($files),
|
||||
'totalSize' => $this->formatFileSize($totalSize),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PPTX 파일 다운로드
|
||||
*/
|
||||
public function download(Request $request): BinaryFileResponse|Response
|
||||
{
|
||||
$filePath = $request->query('file');
|
||||
|
||||
if (! $filePath) {
|
||||
abort(400, '파일 경로가 지정되지 않았습니다.');
|
||||
}
|
||||
|
||||
// 경로 트래버설 방지
|
||||
if (str_contains($filePath, '..')) {
|
||||
abort(403, '잘못된 파일 경로입니다.');
|
||||
}
|
||||
|
||||
// .pptx 확장자만 허용
|
||||
if (strtolower(pathinfo($filePath, PATHINFO_EXTENSION)) !== 'pptx') {
|
||||
abort(403, '허용되지 않는 파일 형식입니다.');
|
||||
}
|
||||
|
||||
// 허용된 base path에 속하는지 검증
|
||||
$allowed = false;
|
||||
foreach (array_keys($this->scanPaths) as $basePath) {
|
||||
if (str_starts_with($filePath, $basePath.'/')) {
|
||||
$allowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $allowed) {
|
||||
abort(403, '허용되지 않는 경로입니다.');
|
||||
}
|
||||
|
||||
if (! file_exists($filePath)) {
|
||||
abort(404, '파일을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
return response()->download($filePath, basename($filePath), [
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 스캔 경로에서 PPTX 파일 수집
|
||||
*/
|
||||
private function scanPptxFiles(): array
|
||||
{
|
||||
$files = [];
|
||||
$seenPaths = [];
|
||||
|
||||
foreach ($this->scanPaths as $basePath => $meta) {
|
||||
if (! is_dir($basePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->findPptxInDirectory($basePath, $basePath, $meta, $files, $seenPaths);
|
||||
}
|
||||
|
||||
// 수정일 내림차순 정렬
|
||||
usort($files, fn ($a, $b) => $b['modified_at'] <=> $a['modified_at']);
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 디렉토리에서 PPTX 파일 재귀 검색
|
||||
*/
|
||||
private function findPptxInDirectory(string $dir, string $basePath, array $meta, array &$files, array &$seenPaths): void
|
||||
{
|
||||
$items = scandir($dir);
|
||||
if ($items === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item === '.' || $item === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fullPath = $dir.'/'.$item;
|
||||
|
||||
if (is_dir($fullPath)) {
|
||||
// mng/docs 경로에서 하위 디렉토리 중 다른 scanPath에 해당하는 것은 건너뜀
|
||||
if (isset($this->scanPaths[$fullPath])) {
|
||||
continue;
|
||||
}
|
||||
$this->findPptxInDirectory($fullPath, $basePath, $meta, $files, $seenPaths);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strtolower(pathinfo($item, PATHINFO_EXTENSION)) !== 'pptx') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 중복 방지 (realpath 기반)
|
||||
$realPath = realpath($fullPath);
|
||||
if ($realPath && isset($seenPaths[$realPath])) {
|
||||
continue;
|
||||
}
|
||||
if ($realPath) {
|
||||
$seenPaths[$realPath] = true;
|
||||
}
|
||||
|
||||
$relativePath = str_replace($basePath.'/', '', $fullPath);
|
||||
$sizeBytes = filesize($fullPath);
|
||||
$modifiedAt = filemtime($fullPath);
|
||||
|
||||
$files[] = [
|
||||
'name' => $item,
|
||||
'path' => $fullPath,
|
||||
'relative_path' => $relativePath,
|
||||
'category' => $meta['label'],
|
||||
'source' => $meta['source'],
|
||||
'source_dir' => str_replace('/var/www/', '', $basePath),
|
||||
'size_bytes' => $sizeBytes,
|
||||
'size' => $this->formatFileSize($sizeBytes),
|
||||
'modified_at' => $modifiedAt,
|
||||
'modified_date' => date('Y-m-d H:i', $modifiedAt),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function formatFileSize(int $bytes): string
|
||||
{
|
||||
if ($bytes >= 1048576) {
|
||||
return number_format($bytes / 1048576, 1).' MB';
|
||||
}
|
||||
|
||||
return number_format($bytes / 1024, 0).' KB';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user