Files
confirms/app/Libraries/MyUpload.php
2026-03-03 21:41:02 +09:00

611 lines
20 KiB
PHP

<?php
namespace App\Libraries;
use Aws\S3\S3Client;
use Aws\Exception\AwsException;
use Aws\Credentials\CredentialProvider;
use Aws\Credentials\Credentials;
use CodeIgniter\HTTP\Files\UploadedFile;
class MyUpload
{
// ===== CI3 Upload 유사 설정값 =====
public string $upload_path = '';
public string $allowed_types = '*'; // 'jpg|png|gif|pdf' 형태
public int $max_size = 0; // KB 단위(0이면 무제한)
public int $max_filename = 0;
public bool $overwrite = false;
public bool $remove_spaces = true;
public bool $xss_clean = false; // CI3의 file XSS 검사와 완전 동일하진 않음(아래 설명)
public string $_file_name_override = '';
// ===== 업로드 결과값(CI3 유사) =====
public string $file_temp = '';
public int $file_size = 0;
public string $file_type = '';
public string $file_name = '';
public string $file_ext = '';
public string $client_name = '';
public string $orig_name = '';
public int $image_width = 0;
public int $image_height = 0;
public string $image_type = '';
public int $image_size_str = 0;
protected array $errors = [];
protected array $s3_data = [];
public function __construct(array $config = [])
{
if ($config)
$this->initialize($config);
}
public function initialize(array $config): self
{
foreach ($config as $k => $v) {
if (property_exists($this, $k)) {
$this->{$k} = $v;
}
}
return $this;
}
/**
* 파일 업로드 요청 (Object Storage 전용)
* 추가일 2025.12.24
* 작성자 - yangsh
*/
public function do_upload2(UploadedFile $file, $filePath = null): array|false
{
if (!$file->isValid()) {
$this->set_error('upload_invalid_file');
return false;
}
// 업로드 전인데 hasMoved()가 true면 비정상 상태
if ($file->hasMoved()) {
$this->set_error('upload_file_already_moved');
return false;
}
$newName = $file->getRandomName();
// ✅ PHP 임시 업로드 파일 경로
$tmpFile = $file->getTempName();
if (!is_file($tmpFile)) {
$this->set_error('upload_temp_file_missing');
log_message('error', '[MyUpload] Temp file missing: ' . $tmpFile);
return false;
}
// ✅ 클라우드에 올라갈 "Key"
$objectKey = $filePath . $newName;
// 클라우드 업로드 (필수)
$up = $this->upload_object_storage($objectKey, $tmpFile, 'file');
if ($up === false) {
$this->set_error('cloud_upload_failed');
log_message('error', '[MyUpload] Cloud upload failed: ' . $objectKey);
@unlink($tmpFile);
return false;
}
// tmp 파일 삭제
@unlink($tmpFile);
$this->s3_data = [
'object_key' => $objectKey,
'object_storage_url' => $up['object_storage_url'] ?? null,
'origin_name' => $file->getClientName(),
'file_name' => basename($objectKey),
'base_name' => pathinfo($objectKey, PATHINFO_FILENAME),
'ext' => pathinfo($objectKey, PATHINFO_EXTENSION),
];
log_message('info', '[MyUpload] Cloud upload success: ' . $objectKey);
return $this->s3_data;
}
/**
*
*/
public function deleteFile($key)
{
$s3Client = $this->makeS3Client();
try {
$s3Client->deleteObject([
'Bucket' => NCLOUD_S3_BUCKET,
'Key' => ltrim($key, '/'),
]);
return true;
} catch (\Throwable $e) {
return false;
}
}
/**
* S3(NCLOUD) 파일 업로드
* 추가일 2025.12.24
* 작성자 - yangsh
*/
public function do_upload(string $field = 'userfile'): bool
{
$request = service('request');
$file = $request->getFile($field);
var_dump($file);
if (!$file) {
$this->set_error('upload_no_file_selected');
return false;
}
if (!$file->isValid()) {
// CI3의 $_FILES 에러코드 대응(가까운 메시지로 매핑)
$err = $file->getError();
switch ($err) {
case UPLOAD_ERR_INI_SIZE:
$this->set_error('upload_file_exceeds_limit');
break;
case UPLOAD_ERR_FORM_SIZE:
$this->set_error('upload_file_exceeds_form_limit');
break;
case UPLOAD_ERR_PARTIAL:
$this->set_error('upload_file_partial');
break;
case UPLOAD_ERR_NO_FILE:
$this->set_error('upload_no_file_selected');
break;
case UPLOAD_ERR_NO_TMP_DIR:
$this->set_error('upload_no_temp_directory');
break;
case UPLOAD_ERR_CANT_WRITE:
$this->set_error('upload_unable_to_write_file');
break;
case UPLOAD_ERR_EXTENSION:
$this->set_error('upload_stopped_by_extension');
break;
default:
$this->set_error('upload_no_file_selected');
break;
}
return false;
}
// upload_path가 비어있으면 CI3도 실패함
if (!$this->upload_path) {
$this->set_error('upload_no_file_selected');
return false;
}
// (너가 올린 코드처럼) 디렉토리 생성
$this->make_dirs($this->upload_path);
// temp file / client file name
$this->file_temp = $file->getTempName();
$this->client_name = $file->getClientName();
// file size (bytes)
$this->file_size = (int) $file->getSize(); // bytes
// mime / type
$this->file_type = strtolower((string) $file->getClientMimeType());
$this->file_name = $this->_prep_filename($this->client_name);
$this->file_ext = $this->get_extension($this->file_name);
// CI3처럼 override 처리
if ($this->_file_name_override !== '') {
$this->file_name = $this->_prep_filename($this->_file_name_override);
if (strpos($this->_file_name_override, '.') === false) {
$this->file_name .= $this->file_ext;
} else {
$this->file_ext = $this->get_extension($this->_file_name_override);
}
}
// allowed type 체크
if (!$this->is_allowed_filetype($file->getClientExtension(), $this->file_type)) {
$this->set_error('upload_invalid_filetype');
return false;
}
// max size 체크 (CI3는 KB 기반)
if (!$this->is_allowed_filesize($this->file_size)) {
$this->set_error('upload_invalid_filesize');
return false;
}
// 파일명 정리/길이 제한/공백 제거
$this->file_name = $this->clean_file_name($this->file_name);
if ($this->max_filename > 0) {
$this->file_name = $this->limit_filename_length($this->file_name, $this->max_filename);
}
if ($this->remove_spaces) {
$this->file_name = preg_replace("/\s+/", "_", $this->file_name);
}
$this->orig_name = $this->file_name;
// overwrite=false면 충돌 시 파일명 변경(CI3 유사)
if (!$this->overwrite) {
$this->file_name = $this->set_filename($this->upload_path, $this->file_name);
if ($this->file_name === false)
return false;
}
// (주의) CI3의 do_xss_clean 완전 동일 구현은 어려움
// 여기서는 위험한 확장자/패턴 정도만 1차 차단 수준으로 처리(원하면 더 강화 가능)
if ($this->xss_clean) {
if (!$this->basic_xss_guard($this->file_name, $this->file_type)) {
$this->set_error('upload_unable_to_write_file');
return false;
}
}
// ===== 너의 기존 로직 핵심: 로컬 move 대신 S3 업로드 =====
$destKeyLikeCi3 = $this->upload_path . $this->file_name;
if (!$this->upload_object_storage($destKeyLikeCi3, $this->file_temp, 'file')) {
$this->set_error('upload_destination_cloud_error');
return false;
}
// 이미지면 width/height
$this->set_image_properties($destKeyLikeCi3, $this->file_temp);
return true;
}
// CI3 스타일: data()
public function data(): array
{
$data = [
'file_name' => $this->file_name,
'file_type' => $this->file_type,
'file_path' => $this->upload_path,
'full_path' => rtrim($this->upload_path, '/') . '/' . $this->file_name,
'raw_name' => $this->raw_name($this->file_name),
'orig_name' => $this->orig_name,
'client_name' => $this->client_name,
'file_ext' => $this->file_ext,
'file_size' => $this->file_size,
'is_image' => ($this->image_width > 0 && $this->image_height > 0),
'image_width' => $this->image_width,
'image_height' => $this->image_height,
'image_type' => $this->image_type,
'image_size_str' => $this->image_size_str,
];
if (!empty($this->s3_data)) {
$data = array_merge($data, $this->s3_data);
}
return $data;
}
public function display_errors(string $open = '', string $close = ''): string
{
if (!$this->errors)
return '';
return $open . implode($close . $open, $this->errors) . $close;
}
public function get_errors(): array
{
return $this->errors;
}
protected function set_error(string $msg): void
{
$this->errors[] = $msg;
}
/**
* S3(NCLOUD) 파일 업로드
* 수정일 2025.12.24
* 작성자 - yangsh
*/
public function upload_object_storage(string $key, string $temp_file, string $type = 'file'): array|false
{
// CI3 코드의 경로 치환 로직 유지 (FCPATH는 CI4에도 존재)
$object_storage_upload_path = str_replace(FCPATH, '/', $key);
$object_storage_upload_path = str_replace('/image/confirms_upload/', '/upload/', $object_storage_upload_path);
$object_storage_upload_path = str_replace('//', '/', $object_storage_upload_path);
$object_storage_upload_path = str_replace('/home/www/upload/confirms_upload/', '/upload/', $object_storage_upload_path);
$s3Client = $this->makeS3Client();
try {
// $body = file_get_contents($temp_file);
// $response = $s3Client->putObject([
// 'Bucket' => NCLOUD_S3_BUCKET,
// 'Key' => ltrim($object_storage_upload_path, '/'),
// 'Body' => $body,
// 'ACL' => 'public-read',
// ]);
$response = $s3Client->putObject([
'Bucket' => NCLOUD_S3_BUCKET,
'Key' => ltrim($object_storage_upload_path, '/'),
'SourceFile' => $temp_file, // ✅ 동영상도 OK
'ACL' => 'public-read',
// (선택) 타입별 ContentType 지정 (브라우저 재생/다운로드에 중요)
// 'ContentType' => $this->guessMime($temp_file, $type),
]);
// $this->s3_data = [
// 'object_storage_upload_path' => $object_storage_upload_path,
// 'object_storage_url' => $response['ObjectURL'] ?? null,
// ];
return [
'object_storage_url' => $response['ObjectURL'] ?? null,
];
} catch (\Throwable $e) {
// 운영에서는 echo 지양. 로그로 남기는 걸 추천
// log_message('error', $e->getMessage());
log_message('error', '[S3 UPLOAD FAIL] ' . $e->getMessage());
log_message('error', '[S3 UPLOAD TRACE] ' . $e->getTraceAsString());
return false;
}
}
/**
* S3(NCLOUD) 파일 업로드 - 썸네일
* 추가일 2025.12.24
* 작성자 - yangsh
*/
public function upload_object_storage_imagick2($key, $blobData): bool
{
$object_storage_upload_path = str_replace(FCPATH, '/', $key);
$object_storage_upload_path = str_replace('/image/confirms_upload/', '/upload/', $object_storage_upload_path);
$object_storage_upload_path = str_replace('//', '/', $object_storage_upload_path);
$object_storage_upload_path = str_replace('/home/www/upload/confirms_upload/', '/upload/', $object_storage_upload_path);
$s3Client = $this->makeS3Client();
try {
$response = $s3Client->putObject([
'Bucket' => NCLOUD_S3_BUCKET,
'Key' => ltrim($object_storage_upload_path, '/'),
'Body' => $blobData,
'ACL' => 'public-read',
'ContentType' => 'image/jpeg',
]);
} catch (\Throwable $e) {
// 운영에서는 echo 지양. 로그로 남기는 걸 추천
log_message('error', '[S3 UPLOAD FAIL] ' . $e->getMessage());
log_message('error', '[S3 UPLOAD TRACE] ' . $e->getTraceAsString());
return false;
}
return true;
}
public function upload_object_storage_imagick(string $key, $blobData): bool
{
$object_storage_upload_path = str_replace(FCPATH, '/', $key);
$object_storage_upload_path = str_replace('/image/confirms_upload/', '/upload/', $object_storage_upload_path);
$object_storage_upload_path = str_replace('//', '/', $object_storage_upload_path);
$object_storage_upload_path = str_replace('/home/www/upload/confirms_upload/', '/upload/', $object_storage_upload_path);
$s3Client = $this->makeS3Client();
try {
$s3Client->putObject([
'Bucket' => NCLOUD_S3_BUCKET,
'Key' => ltrim($object_storage_upload_path, '/'),
'Body' => $blobData,
'ACL' => 'public-read',
]);
} catch (\Throwable $e) {
return false;
}
return true;
}
public function upload_object_storage_rename(string $key, string $new_name): bool
{
$s3Client = $this->makeS3Client();
try {
$s3Client->copyObject([
'Bucket' => NCLOUD_S3_BUCKET,
'CopySource' => NCLOUD_S3_BUCKET . '/' . ltrim($key, '/'),
'Key' => ltrim($new_name, '/'),
'ACL' => 'public-read',
]);
$s3Client->deleteObject([
'Bucket' => NCLOUD_S3_BUCKET,
'Key' => ltrim($key, '/'),
]);
return true;
} catch (\Throwable $e) {
return false;
}
}
protected function makeS3Client(): S3Client
{
$region = defined('NCLOUD_S3_REGION') ? NCLOUD_S3_REGION : 'ap-northeast-2';
// ✅ credentials를 provider로 강제하면 provider-chain(IMDS) 안 탐
$creds = new Credentials(NCLOUD_S3_KEY, NCLOUD_S3_SECRET);
$provider = CredentialProvider::fromCredentials($creds);
return new S3Client([
'version' => 'latest',
'region' => $region,
'endpoint' => NCLOUD_S3_ENDPOINT,
'credentials' => $provider,
'use_path_style_endpoint' => true,
]);
}
// --------------------------------------------------------------------
// === CI3 Upload 유사 유틸들 ===
protected function is_allowed_filetype(string $ext, string $mime): bool
{
if ($this->allowed_types === '*' || $this->allowed_types === '')
return true;
$ext = strtolower(ltrim($ext, '.'));
$allowed = array_map('strtolower', explode('|', $this->allowed_types));
return in_array($ext, $allowed, true);
}
protected function is_allowed_filesize(int $bytes): bool
{
if ($this->max_size <= 0)
return true;
$kb = (int) ceil($bytes / 1024);
return $kb <= $this->max_size;
}
protected function clean_file_name(string $filename): string
{
// CI3처럼 위험문자 제거
$bad = ['<!--', '-->', '<', '>', '"', "'", '&', '$', '=', ';', '?', '/', '\\', '%', '#', '{', '}', '[', ']', ':', ',', '`', '|'];
return str_replace($bad, '', $filename);
}
protected function limit_filename_length(string $filename, int $length): string
{
if (mb_strlen($filename) <= $length)
return $filename;
$ext = $this->get_extension($filename);
$name = $this->raw_name($filename);
$name = mb_substr($name, 0, max(1, $length - mb_strlen($ext)));
return $name . $ext;
}
protected function set_filename(string $path, string $filename)
{
$path = rtrim($path, '/') . '/';
$name = $this->raw_name($filename);
$ext = $this->get_extension($filename);
$i = 0;
$candidate = $filename;
while (is_file($path . $candidate)) {
$i++;
$candidate = $name . '_' . $i . $ext;
if ($i > 9999) {
$this->set_error('upload_filename_overflow');
return false;
}
}
return $candidate;
}
protected function set_image_properties(string $keyLike, string $tempFile): void
{
// 실제 파일은 S3로 올라갔으니 임시파일에서만 추출
$info = @getimagesize($tempFile);
if (!$info)
return;
$this->image_width = (int) $info[0];
$this->image_height = (int) $info[1];
$this->image_type = (string) ($info['mime'] ?? '');
$this->image_size_str = (int) ($info[3] ?? 0);
}
protected function get_extension(string $filename): string
{
$x = strrchr($filename, '.');
return $x === false ? '' : strtolower($x);
}
protected function raw_name(string $filename): string
{
$ext = $this->get_extension($filename);
return $ext ? substr($filename, 0, -strlen($ext)) : $filename;
}
protected function _prep_filename(string $filename): string
{
// 다중 점 처리 등 CI3 유사
return str_replace(' ', '_', $filename);
}
protected function basic_xss_guard(string $filename, string $mime): bool
{
// CI3 do_xss_clean 완전판은 아니고 최소 방어
$lower = strtolower($filename);
if (preg_match('/\.(php|phtml|phar|php\d|cgi|pl|asp|aspx|jsp)$/i', $lower)) {
return false;
}
// mime이 text/html 같은 경우도 거부하고 싶으면 여기서 추가
return true;
}
// --------------------------------------------------------------------
// 너가 올린 make_dirs 로직 이식( VIEW_PATH 없는 부분 보완 )
protected function make_dirs(string $path, bool $last_is_file = false)
{
$dir_arr = explode("/", $path);
if (empty($dir_arr[0])) {
$path = "";
} elseif ($dir_arr[0] === "." || $dir_arr[0] === "..") {
$path = realpath($dir_arr[0]) ?: '';
$dir_arr[0] = "";
} else {
$path = "/";
}
foreach ($dir_arr as $dir) {
$dir = trim($dir);
if (!$dir)
continue;
if ($last_is_file) {
if ($dir === $dir_arr[count($dir_arr) - 1]) {
return false;
}
}
$path = isset($path) ? ($path . "/" . $dir) : $dir;
if (@is_dir($path))
continue;
@mkdir($path, 0707, true);
@chmod($path, 0707);
// CI3는 VIEW_PATH/index.html 복사였는데 CI4에서는 없을 수 있으니 직접 생성
$index = rtrim($path, '/') . '/index.html';
if (!is_file($index)) {
@file_put_contents($index, "<html><head><title>403 Forbidden</title></head><body><p>Directory access is forbidden.</p></body></html>");
@chmod($index, 0606);
}
}
return true;
}
}