25 lines
3.3 KiB
JavaScript
25 lines
3.3 KiB
JavaScript
#!/usr/bin/env node
|
|||
|
|
'use strict';
|
||
|
|
// ============================================================================
|
||
|
|
// 데이터 백업 (v1.6.31, 요구 29) — DATA_ROOT(설정·사용자·사이트 기록·캐시·도메인·모듈·보관소)를 tar.gz 로 묶어 DATA_ROOT/backups/ 에 둔다.
|
||
|
|
// node scripts/backup-data.js [--keep 10] [--tag update] → backups/data-<시각>[-tag].tar.gz, 오래된 것은 keep 개만 남기고 지운다
|
||
|
|
// node scripts/backup-data.js --list
|
||
|
|
// 갱신 스크립트(deploy/remote-update.sh, install.sh)가 재시작 전에 한 번 부른다. 사이트 파일(SITES_ROOT)은 크기가 커서 포함하지 않는다(Git 보관·배포본이 따로 있음).
|
||
|
|
// 복구: tar -xzf backups/<파일> -C <DATA_ROOT 의 상위> (서비스 중지 상태에서). 설정 파일(data/config.json)·보관소(credentials.json)·session.secret 도 들어 있으므로 백업 파일은 0600 으로 둔다.
|
||
|
|
// ============================================================================
|
||
|
|
const fs = require('fs'); const path = require('path'); const { execFileSync } = require('child_process');
|
||
|
|
const DATA_ROOT = path.resolve(process.env.DATA_ROOT || path.join(__dirname, '..', 'data'));
|
||
|
|
const argv = process.argv.slice(2); const opt = (n, d) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : d; };
|
||
|
|
const dir = path.join(DATA_ROOT, 'backups'); fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||
|
|
if (argv.includes('--list')) { for (const f of fs.readdirSync(dir).filter((x) => x.endsWith('.tar.gz')).sort()) { const st = fs.statSync(path.join(dir, f)); console.log(`${f}\t${(st.size / 1048576).toFixed(1)} MB\t${st.mtime.toISOString()}`); } process.exit(0); }
|
||
|
|
const keep = Math.max(1, parseInt(opt('--keep', '10'), 10) || 10); const tag = opt('--tag', '');
|
||
|
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const out = path.join(dir, `data-${stamp}${tag ? '-' + tag : ''}.tar.gz`);
|
||
|
|
const excludes = ['--exclude=./backups', '--exclude=./run', '--exclude=./site-i18n/_runtime/*.tmp', '--exclude=*.tmp'];
|
||
|
|
// 읽을 수 없는 파일(다른 계정 소유 등)은 건너뛰고 나머지를 묶는다(--ignore-failed-read: 종료 코드 0, 경고만) — 건너뛴 파일은 아래에 보여 운영자가 알 수 있게 한다.
|
||
|
|
let warn = '';
|
||
|
|
try { const r = require('child_process').spawnSync('tar', ['-C', DATA_ROOT, ...excludes, '--ignore-failed-read', '--warning=no-file-changed', '-czf', out, '.'], { stdio: ['ignore', 'pipe', 'pipe'] }); warn = String(r.stderr || '').trim(); if (r.status !== 0 && r.status !== 1) throw new Error(warn || ('tar 종료 코드 ' + r.status)); fs.chmodSync(out, 0o600); }
|
||
|
|
catch (e) { console.error('백업 실패: ' + String(e.message).slice(0, 300)); try { fs.unlinkSync(out); } catch (_) {} process.exit(1); }
|
||
|
|
if (warn) console.error('백업에서 건너뛴 파일(읽기 권한 없음):\n' + warn.split('\n').filter((l) => /Cannot open|Permission denied/.test(l)).slice(0, 10).map((l) => ' ' + l.replace(/^tar: /, '')).join('\n'));
|
||
|
|
const files = fs.readdirSync(dir).filter((x) => x.endsWith('.tar.gz')).sort(); for (const f of files.slice(0, Math.max(0, files.length - keep))) { try { fs.unlinkSync(path.join(dir, f)); } catch (_) {} }
|
||
|
|
console.log(`백업 완료: ${out} (${(fs.statSync(out).size / 1048576).toFixed(1)} MB, 보관 ${Math.min(keep, files.length)}개)`);
|