feat(core): core:update 기본을 코어 변경분만 적용(3-way skip)으로 전환
core:update Step 7 이 targets 전체를 무조건 재복사하고 orphan 을 삭제하여 사용자 .htaccess 커스텀 블록·_bundled 커스텀 확장이 소실되던 사고(공개 )를 차단한다. 백업(base)↔_pending(theirs) 3-way md5 비교로 코어가 실제 변경/추가한 파일만 산출해 적용하고, 코어 미변경 파일은 복사·권한·mtime 을 전부 스킵해 현재 디스크 상태(사용자 수정 포함)를 보존한다. 전체 덮어쓰기 + orphan 정리가 필요하면 --prune 으로 opt-in. 백업 부재(--no-backup) 시 base 가 없어 전체 덮어쓰기로 안전 회귀 + 안내를 출력한다. 기존 stale 정리 테스트는 새 기본(증분)에서 orphan 을 삭제하지 않으므로 prune 컨텍스트로 전환했고, 증분 3-way 판정·목록 적용·fallback 단위 테스트와 --prune 커맨드 표면 테스트를 추가했다. 번들 ja 언어팩은 코어 apply_mode 안내 키(g7-core-ja)와 함께, 같은 빌드에서 동기화된 관리자↔유저 크로스링크 라벨(board/ecommerce/admin_basic/basic)의 일본어 번역을 반영하고 각 패키지 버전·CHANGELOG 를 갱신했다.
This commit is contained in:
1 parent
efc8a7ae6b
commit
d0943d6cb6
26 files changed
+921
-29
No files matched your search
@@ -885,7 +885,16 @@ class CoreUpdateService
|
||||
}
|
||||
|
||||
/**
|
||||
* 코어 업데이트 대상 파일만 선택적으로 덮어씁니다.
|
||||
* 코어 업데이트 대상 파일을 덮어씁니다.
|
||||
*
|
||||
* 기본(증분) 모드 — `$applyList` 가 주어지고 `$prune === false`:
|
||||
* - 코어(신 버전)가 실제로 추가·변경한 파일만 적용한다(3-way 판정 산출물).
|
||||
* - 코어가 건드리지 않은 파일은 복사·chmod·chown·mtime 갱신을 전부 스킵하여 현재
|
||||
* 디스크 상태(사용자 수정 포함 가능)를 그대로 보존한다.
|
||||
* - orphan(소스에 없는 대상 파일) 삭제를 하지 않는다 — 사용자가 추가한 신규 파일 보존.
|
||||
*
|
||||
* prune 모드 — `$prune === true` 또는 `$applyList === null`(백업 부재 fallback):
|
||||
* - targets 전체를 무조건 덮어쓰고, 소스에 없는 orphan 을 삭제한다(기존 동작).
|
||||
*
|
||||
* 주의: ExtensionPendingHelper::copyToActive()는 PHP copy()를 사용하여
|
||||
* 파일 퍼미션/소유자를 보존하지 않으므로, 코어 업데이트에서는 사용하지 않습니다.
|
||||
@@ -893,12 +902,29 @@ class CoreUpdateService
|
||||
*
|
||||
* @param string $sourcePath 소스 경로 (_pending 내)
|
||||
* @param \Closure|null $onProgress 진행 콜백
|
||||
* @param bool $prune true 면 전체 덮어쓰기 + orphan 삭제(기존 동작), false 면 증분 적용
|
||||
* @param array<int, string>|null $applyList 적용 대상 상대경로 목록(target 접두사 포함).
|
||||
* `CoreBackupHelper::computeApplyList()` 산출물. null 이면 증분 불가로 판단해 전체 덮어쓰기.
|
||||
*/
|
||||
public function applyUpdate(string $sourcePath, ?\Closure $onProgress = null): void
|
||||
public function applyUpdate(string $sourcePath, ?\Closure $onProgress = null, bool $prune = false, ?array $applyList = null): void
|
||||
{
|
||||
$targets = config('app.update.targets', []);
|
||||
$excludes = config('app.update.excludes', []);
|
||||
|
||||
// 증분 모드 여부: prune 이 아니고 적용 목록이 주어진 경우에만 증분 적용.
|
||||
// applyList 가 null 이면(백업 부재 등) 안전을 위해 전체 덮어쓰기로 회귀한다.
|
||||
$incremental = ! $prune && $applyList !== null;
|
||||
|
||||
// 증분 모드에서 각 target 하위 파일을 O(1) 로 조회하기 위한 lookup 맵.
|
||||
// computeApplyList 는 target 접두사를 포함한 상대경로(예: 'public/.htaccess')를
|
||||
// 반환하므로, 그대로 정규화 키로 담는다.
|
||||
$applySet = [];
|
||||
if ($incremental) {
|
||||
foreach ($applyList as $rel) {
|
||||
$applySet[$this->normalizeRelativePath((string) $rel)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$applied = [];
|
||||
|
||||
foreach ($targets as $target) {
|
||||
@@ -909,21 +935,45 @@ class CoreUpdateService
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalizedTarget = $this->normalizeRelativePath($target);
|
||||
|
||||
$onProgress?->__invoke('apply', $target);
|
||||
|
||||
if (File::isDirectory($src)) {
|
||||
// 증분 모드: 이 target 하위의 적용 대상만 추린 뒤, target 접두사를 벗겨
|
||||
// copyDirectory 의 내부 상대경로($itemRelativePath)와 직접 매칭되는
|
||||
// 화이트리스트 맵을 만든다.
|
||||
$targetApplyList = null;
|
||||
if ($incremental) {
|
||||
$targetApplyList = $this->buildTargetApplyList($applySet, $normalizedTarget);
|
||||
}
|
||||
|
||||
// `{domain}/_bundled` 타깃은 최상위 한 레벨의 orphan 을 보존한다 — 사용자가
|
||||
// `_bundled/` 바로 아래에 직접 만든 커스텀 확장 디렉토리/파일이 코어 업데이트
|
||||
// 소스(번들 확장만 포함)에 없다는 이유로 삭제되던 결함 차단.
|
||||
// 번들 확장 디렉토리 *내부* stale 정리는 그대로 유지된다.
|
||||
$preserveTopLevelOrphans = str_ends_with($this->normalizeRelativePath($target), '_bundled');
|
||||
FilePermissionHelper::copyDirectory($src, $dest, $onProgress, $excludes, removeOrphans: true, preserveTopLevelOrphans: $preserveTopLevelOrphans);
|
||||
// 번들 확장 디렉토리 *내부* stale 정리는 prune 모드에서만 수행된다.
|
||||
$preserveTopLevelOrphans = str_ends_with($normalizedTarget, '_bundled');
|
||||
FilePermissionHelper::copyDirectory(
|
||||
$src,
|
||||
$dest,
|
||||
$onProgress,
|
||||
$excludes,
|
||||
removeOrphans: $prune,
|
||||
preserveTopLevelOrphans: $preserveTopLevelOrphans,
|
||||
applyList: $targetApplyList,
|
||||
);
|
||||
} else {
|
||||
// 단일 파일 target — 증분 모드에서는 적용 목록에 있을 때만 복사.
|
||||
if ($incremental && ! isset($applySet[$normalizedTarget])) {
|
||||
$applied[$normalizedTarget] = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
File::ensureDirectoryExists(dirname($dest));
|
||||
FilePermissionHelper::copyFile($src, $dest);
|
||||
}
|
||||
|
||||
$applied[$this->normalizeRelativePath($target)] = true;
|
||||
$applied[$normalizedTarget] = true;
|
||||
}
|
||||
|
||||
// 자동 발견 폴백 — 부모 프로세스(구버전) 의 stale `app.update.targets` 가
|
||||
@@ -1005,6 +1055,31 @@ class CoreUpdateService
|
||||
return trim(str_replace(['\\', '/'], '/', $path), '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 적용 집합에서 특정 target 하위 항목만 추려, target 접두사를 제거한
|
||||
* `copyDirectory` 내부 상대경로 화이트리스트 맵을 만듭니다.
|
||||
*
|
||||
* `computeApplyList` 는 target 접두사를 포함한 경로(예: `public/.htaccess`)를 담지만,
|
||||
* `copyDirectory` 는 target 루트 기준 상대경로(`.htaccess`)로 항목을 순회하므로 접두사를
|
||||
* 벗겨야 매칭된다.
|
||||
*
|
||||
* @param array<string, bool> $applySet 전체 적용 대상 정규화 경로 lookup 맵
|
||||
* @param string $normalizedTarget 정규화된 target 경로 (예: `public`)
|
||||
* @return array<string, bool> target 내부 상대경로 화이트리스트 (빈 맵일 수 있음 → 전부 스킵)
|
||||
*/
|
||||
private function buildTargetApplyList(array $applySet, string $normalizedTarget): array
|
||||
{
|
||||
$prefix = $normalizedTarget.'/';
|
||||
$out = [];
|
||||
foreach ($applySet as $rel => $_) {
|
||||
if (str_starts_with((string) $rel, $prefix)) {
|
||||
$out[substr((string) $rel, strlen($prefix))] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 정규화된 path 가 이미 처리된 상위/하위 path 와 관계가 있는지 검사합니다.
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user