https://github.com/gnuboard/g7/issues/125 — 상품 상세설명을 HTML 로 저장할 때 HTMLPurifier 가 모듈 vendor 폴더 안에 정의 캐시를 만들려다 실패해 저장이 매번 500 으로 끝나던 문제를 고친다. vendor 를 읽기 전용으로 두는 표준 배포에서 그 쓰기는 예외가 아니라 PHP 경고로 나오고 Laravel 이 이를 ErrorException 으로 승격시킨다. 캐시는 설정 해시당 1회만 기록되므로 캐시가 영영 생기지 않아 재시도해도 같은 결과였다. 캐시 경로를 storage 아래로 옮기고, 그 경로마저 확보하지 못하면 캐시만 끄고 정화는 그대로 수행한다 — 캐시는 성능 장치이고 정화는 보안 장치라, 전자의 실패가 후자를 건너뛰게 만들면 안 된다. 저장은 성공하므로 운영자에게 도달하는 흔적이 로그 하나뿐이라 error 수준으로 남긴다 (출하 기본 로그 수준이 error 라 warning 은 기본 설치 상태에서 파일에 남지 않는다). 그 과정에서 갈라져 있던 두 축을 코어 한 곳으로 모은다. - 쓰기 디렉토리 확보: 억제 생성·chmod·setgid·소유권 상속·쓰기 판정 절차가 정적 게시와 정의 캐시 두 곳에 서로 다른 하드닝으로 복제돼 있었다(억제 mkdir·setgid·clearstatcache 가 사본마다 한쪽씩 빠져 있었다). FilePermissionHelper 의 ensureWritableDirectory 와 hardenDirectory 로 통합하고, 실패 사유는 out 파라미터로 올려 정책(조용한 성능 저하 대 시끄러운 실패)은 호출부가 정하게 둔다. - 확장 저장 경로: storage_path('app/modules/…') 손조립이 30곳에 흩어져 있어 테스트 격리 분기를 넣으려면 사본마다 복제해야 했고, 한 곳만 빠뜨려도 그 확장의 테스트가 운영 설정 파일을 덮어쓴다. 디스크 root 를 단일 출처로 읽는 ExtensionStoragePath 로 전환하고 테스트 분기는 config/filesystems.php 한 줄에서 끝낸다. 함께 고친 것 - 테스트가 운영 라우트 캐시로 부팅해 확장 allowlist 가 라우트 축에서 통째로 무력화되던 문제. 삭제가 아니라 경로를 돌린다 — 라우트 캐시는 확장 작업 전까지 재생성되지 않아, 삭제하면 운영 사이트가 그때까지 라우트 파일 스캔 경로로 떨어진다. - PHPUnit 프로세스가 확장 vendor 의 제3자 composer 패키지를 오토로드하지 않아 그 패키지를 쓰는 코드 경로가 통째로 테스트 불가였던 문제. 확장 자신의 오토로더를 그대로 쓰면 활성 디렉토리가 _bundled 를 이기고 base path 유추까지 깨지므로, 생성된 맵에서 제3자 항목만 골라 별도 로더로 등록한다. - 게시 폴더가 setgid 를 갖지 않아, 명령줄과 웹이 번갈아 만든 하위 폴더를 다른 쪽이 쓰지 못하던 문제. - 관리자 템플릿이 HTML 정화 라이브러리를 직접 지정하지 않아 전이 의존으로 딸려온 구버전이 쓰이던 문제. 동반 산출물 - 규정 표(·AGENTS.md) 6행 + storage-driver/service-repository/testing-guide 문서 - audit 룰 2종 + coverage 6항목. 저장소가 이미 전량 전환돼 전수 실행이 공허 통과하므로 판정식은 픽스처 36건이 잠근다 - INSTALL.md 에 설치 후 파일 권한 절 추가 (vendor 쓰기 권한 불요를 명시)
77 lines
3.4 KiB
PHP
77 lines
3.4 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
/**
|
|
* 확장(모듈/플러그인) 저장 경로의 **로컬 파일시스템 절대 경로** 해석기.
|
|
*
|
|
* `StorageInterface` 로 읽고 쓸 수 있는 자리에는 이 클래스가 필요 없다 — 그쪽을 쓴다.
|
|
* 이 해석기는 파일시스템 경로 **문자열 자체가 필요한** 자리를 위한 것이다:
|
|
*
|
|
* - 제3자 라이브러리에 캐시·임시 디렉토리를 넘길 때 (HTMLPurifier `Cache.SerializerPath` 등)
|
|
* - 설정 JSON 을 `file_put_contents` 계열로 직접 다루는 자리
|
|
*
|
|
* `AbstractModule::getStorageBasePath()` 를 쓰지 않는 이유는 그 반환값이
|
|
* `Storage::disk()->path()` 위임이라, 확장이 카테고리 디스크를 비로컬(S3 등)로 오버라이드하면
|
|
* 파일시스템 경로가 아니게 되기 때문이다. 그러면 라이브러리가 그 값을 상대경로로 보고 현재
|
|
* 작업 디렉토리 기준으로 해석해 **조용히 엉뚱한 곳에 쓴다.**
|
|
*
|
|
* 경로 레이아웃은 `modules`/`plugins` 디스크의 root(`config/filesystems.php`)를 단일 출처로
|
|
* 삼는다. 그 root 가 테스트 환경을 인지하므로, 각 확장이 `app()->runningUnitTests()` 분기를
|
|
* 자기 안에 복사할 필요가 없다 — 복사본은 한 곳만 빠뜨려도 그 확장의 테스트가 조용히 운영
|
|
* 설정 파일을 덮어쓴다.
|
|
*/
|
|
class ExtensionStoragePath
|
|
{
|
|
/**
|
|
* 모듈 저장 경로의 절대 경로를 반환합니다.
|
|
*
|
|
* @param string $identifier 모듈 식별자 (예: sirsoft-ecommerce)
|
|
* @param string $category 카테고리 (예: settings, cache/htmlpurifier). 빈 문자열이면 모듈 루트
|
|
* @return string 절대 경로 (존재 여부와 무관한 순수 계산)
|
|
*/
|
|
public static function module(string $identifier, string $category = ''): string
|
|
{
|
|
return static::resolve('modules', $identifier, $category);
|
|
}
|
|
|
|
/**
|
|
* 플러그인 저장 경로의 절대 경로를 반환합니다.
|
|
*
|
|
* @param string $identifier 플러그인 식별자 (예: sirsoft-pay_kginicis)
|
|
* @param string $category 카테고리 (예: settings). 빈 문자열이면 플러그인 루트
|
|
* @return string 절대 경로 (존재 여부와 무관한 순수 계산)
|
|
*/
|
|
public static function plugin(string $identifier, string $category = ''): string
|
|
{
|
|
return static::resolve('plugins', $identifier, $category);
|
|
}
|
|
|
|
/**
|
|
* 디스크 root 를 기준으로 `{root}/{identifier}[/{category}]` 를 조립합니다.
|
|
*
|
|
* root 는 `config/filesystems.php` 가 단일 출처다. 설정이 비어 있는 비정상 상황에서만
|
|
* 운영 기본 레이아웃으로 되돌아간다 — 여기서 예외를 던지면 설정 파일 하나 때문에
|
|
* 확장 기능 전체가 멈춘다.
|
|
*
|
|
* @param string $disk 디스크 이름 (modules | plugins)
|
|
* @param string $identifier 확장 식별자
|
|
* @param string $category 카테고리
|
|
* @return string 절대 경로
|
|
*/
|
|
protected static function resolve(string $disk, string $identifier, string $category): string
|
|
{
|
|
$root = config("filesystems.disks.{$disk}.root");
|
|
|
|
if (! is_string($root) || $root === '') {
|
|
$root = storage_path('app/'.$disk);
|
|
}
|
|
|
|
$path = rtrim($root, '/\\').'/'.trim($identifier, '/\\');
|
|
|
|
$category = trim($category, '/\\');
|
|
|
|
return $category === '' ? $path : $path.'/'.$category;
|
|
}
|
|
}
|