611 lines
20 KiB
PHP
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;
|
|
}
|
|
}
|