- R2StorageAdapter: S3 호환 클라이언트 래퍼
- R2FileHandler: 그누보드 통합 핸들러
- Presigned URL 지원
- 유저별 경로 분리 (users/{member_id}/...)
- 대용량 파일 멀티파트 업로드 지원
- 로컬 스토리지 폴백
- DB 마이그레이션 스크립트 포함
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
307 lines
7.8 KiB
PHP
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()];
|
|
}
|
|
}
|