Files
gnuboard-r2-storage/extend/r2_hooks.php
kappa dd86ccb782 feat: Gnuboard5 Cloudflare R2 Storage Module
- R2StorageAdapter: S3 호환 클라이언트 래퍼
- R2FileHandler: 그누보드 통합 핸들러
- Presigned URL 지원
- 유저별 경로 분리 (users/{member_id}/...)
- 대용량 파일 멀티파트 업로드 지원
- 로컬 스토리지 폴백
- DB 마이그레이션 스크립트 포함

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 14:36:38 +09:00

307 lines
7.8 KiB
PHP

<?php
/**
* R2 Storage Hooks for Gnuboard5
*
* 이 파일을 그누보드 extend/ 디렉토리에 배치하면 자동으로 로드됩니다.
*
* 설치 방법:
* 1. extend/r2-storage/ 폴더를 그누보드 extend/ 디렉토리에 복사
* 2. extend/r2-storage/ 에서 composer install 실행
* 3. extend/r2-storage/r2_config.php 에 R2 인증 정보 입력
* 4. 이 파일(r2_hooks.php)을 extend/ 디렉토리에 복사
*
* @package Gnuboard\R2Storage
*/
if (!defined('_GNUBOARD_')) exit;
// Composer autoload
$r2AutoloadPath = __DIR__ . '/r2-storage/vendor/autoload.php';
if (!file_exists($r2AutoloadPath)) {
// Composer 설치 안됨 - 경고 로그
if (defined('G5_DATA_PATH')) {
error_log('[R2 Storage] Composer autoload not found. Please run: cd extend/r2-storage && composer install');
}
return;
}
require_once $r2AutoloadPath;
// R2 설정 로드
$r2ConfigPath = __DIR__ . '/r2-storage/r2_config.php';
if (file_exists($r2ConfigPath)) {
require_once $r2ConfigPath;
}
use Gnuboard\R2Storage\R2FileHandler;
// ============================================
// 전역 R2 핸들러 인스턴스
// ============================================
$GLOBALS['r2_handler'] = new R2FileHandler();
/**
* R2 핸들러 가져오기
*
* @return R2FileHandler
*/
function get_r2_handler(): R2FileHandler
{
return $GLOBALS['r2_handler'];
}
/**
* R2 활성화 여부
*
* @return bool
*/
function is_r2_enabled(): bool
{
return get_r2_handler()->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' => []];
// 아직 마이그레이션되지 않은 파일 조회
$sql = "SELECT bf_no, wr_id, bf_file, bf_source
FROM {$g5['board_file_table']}
WHERE bo_table = '{$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 조회 (게시글에서)
$write = sql_fetch("SELECT mb_id FROM {$g5['write_prefix']}{$boTable} WHERE wr_id = '{$row['wr_id']}'");
$memberId = 0;
if ($write['mb_id']) {
$member = sql_fetch("SELECT mb_no FROM {$g5['member_table']} WHERE mb_id = '{$write['mb_id']}'");
$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()];
}
}