initialize($config); } public function initialize(array $config): self { foreach ($config as $k => $v) { if (property_exists($this, $k)) { $this->{$k} = $v; } } return $this; } /** * 파일 업로드 요청 * 추가일 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 임시 업로드 파일 경로 (writable로 move() 필요 없음) $tmpFile = $file->getTempName(); if (!is_file($tmpFile)) { $this->set_error('upload_temp_file_missing'); log_message('error', 'do_upload2 temp file missing: ' . $tmpFile); return false; } // ✅ 클라우드에 올라갈 "Key"를 직접 만든다 (로컬 경로 절대 넣지 말기) // 예시: upload/tmp/랜덤파일명 또는 upload/apt_file/{rcpt_no}/... $objectKey = $filePath . $newName; $up = $this->upload_object_storage($objectKey, $tmpFile, 'file'); if ($up === false) { $this->set_error('upload_destination_cloud_error'); 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), // xxxx.jpg 'base_name' => pathinfo($objectKey, PATHINFO_FILENAME), // xxxx 'ext' => pathinfo($objectKey, PATHINFO_EXTENSION), // jpg ]; log_message('debug', 's3_data=' . json_encode($this->s3_data ?? null, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); 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', ]); } 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, "
Directory access is forbidden.
"); @chmod($index, 0606); } } return true; } }