const { execSync } = require("child_process"); const fs = require("fs"); const os = require("os"); const path = require("path"); // Load .env.local from project root if present (persists NAS_SSH_TARGET etc.) const envLocalPath = path.join(__dirname, "..", ".env.local"); if (fs.existsSync(envLocalPath)) { for (const line of fs.readFileSync(envLocalPath, "utf8").split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const idx = trimmed.indexOf("="); if (idx < 0) continue; const k = trimmed.slice(0, idx).trim(); const v = trimmed.slice(idx + 1).trim(); if (!(k in process.env)) process.env[k] = v; } } const isWin = process.platform === "win32"; const isMac = process.platform === "darwin"; const src = "dist"; // Windows 배포 경로 — Z: 매핑이 NAS 루트(/volume1/)인 경우 docker\webpage\frontend, // /volume1/docker/만 매핑된 경우 webpage\frontend, /volume1/docker/webpage 매핑이면 frontend. // NAS_FRONTEND_DEST_WIN env로 override (예: "Z:\\webpage\\frontend\\") const dstWin = process.env.NAS_FRONTEND_DEST_WIN || "Z:\\docker\\webpage\\frontend\\"; const dstMac = process.env.NAS_FRONTEND_DEST_MAC || "/Volumes/gahusb.synology.me/docker/webpage/frontend/"; const dst = isWin ? dstWin : dstMac; if (!fs.existsSync(src)) { console.error("dist not found. Run build first."); process.exit(1); } if (isWin) { // PowerShell single-quote literal로 path 전달 — backslash over-escape 회피 const cmd = `powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; $src='dist'; $dst='${dstWin}'; if(!(Test-Path $src)){ throw 'dist not found. Run build first.' }; if(!(Test-Path $dst)){ throw ('NAS 경로를 찾을 수 없음: ' + $dst + ' — Z: 매핑 또는 NAS_FRONTEND_DEST_WIN env 확인') }; $log = Join-Path (Get-Location) 'robocopy.log'; robocopy $src $dst /MIR /R:1 /W:1 /E /NFL /NDL /NP /V /TEE /LOG:$log; $rc = $LASTEXITCODE; if($rc -ge 8){ Write-Host ('robocopy failed with code ' + $rc + '. See ' + $log); exit $rc } else { exit 0 }"`; execSync(cmd, { stdio: "inherit" }); } else if (isMac) { const sshTarget = process.env.NAS_SSH_TARGET; const sshPath = process.env.NAS_SSH_PATH || "/volume1/docker/webpage/frontend/"; const sshPort = process.env.NAS_SSH_PORT; // SSH 경로: NAS_SSH_TARGET이 설정된 경우 항상 우선 if (sshTarget) { // 제어문자·줄바꿈 제거 (잘못된 export/copy-paste 대비) const cleanTarget = sshTarget.replace(/[\r\n\t]/g, "").trim(); const cleanPath = sshPath.replace(/[\r\n\t]/g, "").trim(); const cleanPort = sshPort ? sshPort.replace(/\D/g, "").trim() : ""; if (!cleanTarget) { console.error("NAS_SSH_TARGET 값이 비어있습니다. .env.local 또는 환경변수를 확인하세요."); printSshHint(); process.exit(1); } if (cleanPort && !/^\d{1,5}$/.test(cleanPort)) { console.error(`NAS_SSH_PORT 값이 잘못됐습니다: "${sshPort}" → 숫자만 입력하세요.`); process.exit(1); } // macOS Keychain은 서브프로세스(rsync)에서 SSH 키를 자동 로드하지 못함 → -i 명시 const keyFile = (process.env.NAS_SSH_KEY || path.join(os.homedir(), ".ssh", "id_rsa")) .replace(/[\r\n]/g, "").trim(); if (!fs.existsSync(keyFile)) { console.error(`SSH 키 파일을 찾을 수 없습니다: ${keyFile}`); console.error("NAS_SSH_KEY 환경변수를 올바른 키 경로로 설정하거나, ~/.ssh/id_rsa 가 있는지 확인하세요."); process.exit(1); } const portOpt = cleanPort ? `-p ${cleanPort}` : ""; // Synology는 rsync --server 모드를 별도 인증으로 막음 → tar | ssh 방식 사용 const sshBase = `ssh ${portOpt} -i ${keyFile} -o StrictHostKeyChecking=accept-new -o PreferredAuthentications=publickey` .replace(/\s+/g, " ").trim(); console.log(`Deploying via tar|ssh → ${cleanTarget}:${cleanPath}`); // 1단계: 원격 디렉토리 초기화 execSync( `${sshBase} ${cleanTarget} "rm -rf '${cleanPath}'/* 2>/dev/null; mkdir -p '${cleanPath}'"`, { stdio: "inherit" } ); // 2단계: 빌드 산출물 tar로 전송 → 원격에서 압축 해제 execSync( `cd ${src} && tar czf - . | ${sshBase} ${cleanTarget} "cd '${cleanPath}' && tar xzf -"`, { stdio: "inherit" } ); console.log("Deploy complete."); process.exit(0); } // SMB 마운트 경로 fallback if (!fs.existsSync(dst)) { console.error("NAS path not found: " + dst); printSshHint(); process.exit(1); } if (!dst.includes("docker/webpage/frontend")) { console.error("Safety check failed: unexpected dst path: " + dst); process.exit(1); } try { const testPath = `${dst}.deploy-write-test`; fs.writeFileSync(testPath, "ok"); fs.unlinkSync(testPath); } catch (err) { console.error("NAS write test failed (EIO / permission error)."); console.error( "macOS SMB → Synology 쓰기 실패는 흔한 이슈입니다. SSH 배포를 사용하세요.\n" ); printSshHint(); process.exit(1); } const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); const retry = (fn, attempts = 6) => { for (let i = 0; i < attempts; i += 1) { try { fn(); return; } catch (err) { if (i === attempts - 1) throw err; sleep(150); } } }; const removeTree = (target) => { const stat = fs.lstatSync(target); if (stat.isDirectory()) { retry(() => { const entries = fs.readdirSync(target); for (const entry of entries) { removeTree(`${target}/${entry}`); } }); retry(() => fs.rmdirSync(target)); } else { retry(() => fs.unlinkSync(target)); } }; if (process.env.NAS_CLEAN === "1") { try { const entries = fs.readdirSync(dst); for (const entry of entries) { removeTree(`${dst}${entry}`); } } catch (err) { console.warn("Clean skipped due to NAS lock:", err?.message ?? err); } } execSync(`ditto ${src} ${dst}`, { stdio: "inherit" }); } else { const baseArgs = ["rsync", "-r", "--delete", "--delete-delay", "-t"]; const cmd = `${baseArgs.join(" ")} ${src}/ ${dst}`; execSync(cmd, { stdio: "inherit" }); } function printSshHint() { console.error("──────────────────────────────────────────────────"); console.error("SSH 배포 설정 방법:"); console.error(" 프로젝트 루트에 .env.local 파일을 만들고 아래 내용을 입력하세요:"); console.error(""); console.error(" NAS_SSH_TARGET=@gahusb.synology.me"); console.error(" NAS_SSH_PORT= # 기본 22, DSM에서 확인"); console.error(""); console.error(" 이후 npm run release:nas 를 다시 실행하면 rsync over SSH로 배포됩니다."); console.error("──────────────────────────────────────────────────"); }