Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
20 lines
3.1 KiB
JavaScript
20 lines
3.1 KiB
JavaScript
/** portal/source/api.mjs — Source 라우트. 화면 /sites/:id/source(사이트 상세 Source 탭) · API /api/source/:siteId (GET), /activate, /unlink, /export · 운영자 /internal/ops/source */
|
|
import { requireGroup, requireSitePerm } from '../auth/guard.mjs';
|
|
import { HttpError } from '../../lib/http.mjs';
|
|
import { verifyCtx } from '../../lib/serviceauth.mjs';
|
|
import { createSourceService } from './service.mjs';
|
|
import * as siteUi from '../sites/ui.mjs';
|
|
import { createReadStream, statSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
export async function register(app, deps) {
|
|
const source = createSourceService(deps); deps.source = source;
|
|
const siteOf = (ctx, perm = 'view') => { requireGroup(ctx); const s = deps.sites.getForGroup(ctx.group.id, ctx.params.siteId || ctx.params.id); if (!s) throw new HttpError(404, 'not_found', '사이트를 찾을 수 없습니다'); if (perm !== 'view') requireSitePerm(ctx, s, perm); return s; };
|
|
app.get('/sites/:id/source', (ctx) => ctx.html(siteUi.detailPage(ctx, deps, siteOf(ctx), { tab: 'source' })));
|
|
app.get('/api/source/:siteId', async (ctx) => ({ ok: true, ...(await source.info(siteOf(ctx))) }));
|
|
app.post('/api/source/:siteId/activate', (ctx) => ({ ok: true, option: source.activate(siteOf(ctx, 'admin'), ctx.user) }));
|
|
app.post('/api/source/:siteId/unlink', async (ctx) => ({ ok: true, option: await source.unlink(siteOf(ctx, 'admin'), ctx.user) }));
|
|
app.get('/api/source/:siteId/export', async (ctx) => { const s = siteOf(ctx); const o = source.get(s.id); if (!o || o.status === 'deleted') throw new HttpError(409, 'not_available', o ? '삭제 완료된 저장소는 내보낼 수 없습니다(복구 불가)' : 'Source 옵션이 없습니다'); if (!deps.webmaker) throw new HttpError(503, 'service_unreachable', 'Webmaker 연결 없음'); const dir = join(deps.config.dataDir, 'exports'); mkdirSync(dir, { recursive: true, mode: 0o700 }); const file = join(dir, `${s.id}-${Date.now()}.tar.gz`); await deps.webmaker.exportVersion(s.webmaker_project_id, ctx.query.version || null, file); ctx.ended = true; ctx.res.writeHead(200, { 'Content-Type': 'application/gzip', 'Content-Disposition': `attachment; filename="${s.id}-source.tar.gz"`, 'Content-Length': statSync(file).size, 'Cache-Control': 'no-store' }); createReadStream(file).on('close', () => { try { unlinkSync(file); } catch (_) {} }).pipe(ctx.res); });
|
|
app.get('/internal/ops/source', async (ctx) => { const v = await verifyCtx(ctx, (id) => (['hal', 'portal'].includes(id) ? deps.vault.get('service:' + id) : null)); if (!v.ok) throw new HttpError(401, 'service_auth_failed', v.reason); return { ok: true, options: deps.store.all('SELECT o.*, s.name AS site_name, s.webmaker_project_id FROM source_options o LEFT JOIN sites s ON s.id=o.site_id ORDER BY o.updated_at DESC LIMIT 1000') }; });
|
|
app.post('/internal/ops/source/sweep', async (ctx) => { const v = await verifyCtx(ctx, (id) => (['hal', 'portal'].includes(id) ? deps.vault.get('service:' + id) : null)); if (!v.ok) throw new HttpError(401, 'service_auth_failed', v.reason); return { ok: true, ...source.sweep() }; });
|
|
}
|