isEnabled(); } // ============================================ // 파일 업로드 헬퍼 함수 // ============================================ /** * R2로 파일 업로드 * * 사용 예: * $result = r2_upload_file($_FILES['bf_file'][0], $bo_table, $member['mb_no']); * * @param array $file $_FILES 배열 요소 * @param string $boTable 게시판 테이블명 * @param int $memberId 회원 ID * @param string $type 업로드 타입 (board|editor|profile) * @return array */ function r2_upload_file(array $file, string $boTable, int $memberId = 0, string $type = 'board'): array { return get_r2_handler()->handleUpload($file, $boTable, $memberId, $type); } /** * R2 파일 삭제 * * @param string $r2Key R2 객체 키 * @return bool */ function r2_delete_file(string $r2Key): bool { return get_r2_handler()->handleDelete($r2Key); } /** * R2 다운로드 URL 생성 * * @param string $r2Key R2 객체 키 * @return string */ function r2_get_download_url(string $r2Key): string { return get_r2_handler()->getDownloadUrl($r2Key); } /** * R2 파일 존재 여부 * * @param string $r2Key R2 객체 키 * @return bool */ function r2_file_exists(string $r2Key): bool { return get_r2_handler()->exists($r2Key); } // ============================================ // 그누보드 함수 확장 // ============================================ /** * get_file() 함수 확장 - R2 URL 적용 * * 기존 get_file() 함수 호출 후 이 함수로 R2 URL 적용 * * 사용 예: * $file = get_file($bo_table, $wr_id); * $file = r2_extend_files($file); * * @param array $files get_file() 결과 * @return array */ function r2_extend_files(array $files): array { if (!is_r2_enabled()) { return $files; } $handler = get_r2_handler(); foreach ($files as $key => $file) { // r2_key가 있는 경우에만 처리 if (!empty($file['r2_key'])) { $files[$key] = $handler->extendFileInfo($file, $file['r2_key']); } } return $files; } // ============================================ // 그누보드 액션 훅 (테마에서 사용) // ============================================ /** * 게시판 파일 업로드 전 훅 * * write_update.php 에서 호출 * * @param array $file * @param string $boTable * @param int $memberId * @return array|null R2 업로드 결과 또는 null (로컬 업로드 진행) */ function r2_before_upload(array $file, string $boTable, int $memberId): ?array { if (!is_r2_enabled()) { return null; } $result = r2_upload_file($file, $boTable, $memberId, 'board'); if ($result['success'] && $result['storage_type'] === 'r2') { return $result; } // 로컬 폴백 return null; } /** * 게시판 파일 삭제 전 훅 * * @param string $r2Key * @param string $localPath * @return bool */ function r2_before_delete(?string $r2Key, string $localPath): bool { // R2 파일 삭제 if (!empty($r2Key) && is_r2_enabled()) { r2_delete_file($r2Key); } // 로컬 파일 삭제는 그누보드 기본 로직에서 처리 return true; } // ============================================ // 마이그레이션 도구 // ============================================ /** * 로컬 파일을 R2로 마이그레이션 * * CLI에서 실행: * php -r "define('_GNUBOARD_', true); include 'extend/r2_hooks.php'; r2_migrate_board_files('free', 100);" * * @param string $boTable 게시판 테이블명 * @param int $limit 처리할 파일 수 * @return array 마이그레이션 결과 */ function r2_migrate_board_files(string $boTable, int $limit = 100): array { global $g5; if (!is_r2_enabled()) { return ['success' => false, 'error' => 'R2 not enabled']; } $handler = get_r2_handler(); $results = ['migrated' => 0, 'failed' => 0, 'skipped' => 0, 'errors' => []]; // 테이블명 검증 (영문, 숫자, 언더스코어만 허용) if (!preg_match('/^[a-zA-Z0-9_]+$/', $boTable)) { return ['success' => false, 'error' => 'Invalid table name']; } $limit = (int) $limit; // 아직 마이그레이션되지 않은 파일 조회 $sql = "SELECT bf_no, wr_id, bf_file, bf_source FROM {$g5['board_file_table']} WHERE bo_table = '" . sql_real_escape_string($boTable) . "' AND (bf_storage_type IS NULL OR bf_storage_type = 'local') AND bf_r2_key IS NULL LIMIT {$limit}"; $result = sql_query($sql); while ($row = sql_fetch_array($result)) { $localPath = G5_DATA_PATH . '/file/' . $boTable . '/' . $row['bf_file']; if (!file_exists($localPath)) { $results['skipped']++; continue; } // 회원 ID 조회 (게시글에서) $wrId = (int) $row['wr_id']; $write = sql_fetch("SELECT mb_id FROM {$g5['write_prefix']}{$boTable} WHERE wr_id = '{$wrId}'"); $memberId = 0; if (!empty($write['mb_id'])) { $mbId = sql_real_escape_string($write['mb_id']); $member = sql_fetch("SELECT mb_no FROM {$g5['member_table']} WHERE mb_id = '{$mbId}'"); $memberId = (int) ($member['mb_no'] ?? 0); } // R2 키 생성 $r2Key = $handler->generateR2Key($memberId, $boTable, $row['bf_file'], 'board'); // 마이그레이션 실행 $uploadResult = $handler->migrateLocalFile($localPath, $r2Key, false); if ($uploadResult['success']) { // DB 업데이트 $sql = "UPDATE {$g5['board_file_table']} SET bf_r2_key = '" . sql_real_escape_string($r2Key) . "', bf_storage_type = 'r2' WHERE bf_no = '{$row['bf_no']}'"; sql_query($sql); $results['migrated']++; } else { $results['failed']++; $results['errors'][] = "File {$row['bf_file']}: " . ($uploadResult['error'] ?? 'Unknown error'); } } return $results; } // ============================================ // 디버깅 도구 // ============================================ /** * R2 연결 테스트 * * @return array */ function r2_test_connection(): array { if (!is_r2_enabled()) { return ['success' => false, 'error' => 'R2 not enabled']; } try { $adapter = get_r2_handler()->getAdapter(); if (!$adapter) { return ['success' => false, 'error' => 'Adapter not initialized']; } // 버킷 목록 조회 테스트 $result = $adapter->listObjects('', 1); return [ 'success' => true, 'bucket' => $adapter->getBucket(), 'message' => 'Connection successful', ]; } catch (\Exception $e) { return ['success' => false, 'error' => $e->getMessage()]; } }