restic 봇 생성

This commit is contained in:
caadiq 2025-12-16 21:13:39 +09:00
parent ee761d3568
commit 97a3158c10
6 changed files with 389 additions and 0 deletions

13
restic/.env Normal file
View file

@ -0,0 +1,13 @@
# restic 백업 알림 봇 설정
# 디스코드 봇 토큰 (Bot 페이지에서 발급)
DISCORD_BOT_TOKEN=MTQ1MDQyMDEwMDc3NDYyNTQyMg.GLoqvA.QO9E6E2s5uLCQfl4a_OZR6A_PVKjwcQ7KwsMAM
# 성공 알림 채널 ID
DISCORD_SUCCESS_CHANNEL_ID=1450289742175408279
# 실패 알림 채널 ID
DISCORD_FAILURE_CHANNEL_ID=1450289802090909716
# 웹훅 서버 포트
WEBHOOK_PORT=5001

12
restic/Dockerfile Normal file
View file

@ -0,0 +1,12 @@
FROM python:3.14.2-slim
WORKDIR /app
# 의존성 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 소스 복사
COPY main.py .
CMD ["python", "main.py"]

86
restic/README.md Normal file
View file

@ -0,0 +1,86 @@
# 📦 Restic Backup Discord Bot
restic 백업 결과를 디스코드로 알려주는 봇입니다.
![Python](https://img.shields.io/badge/Python-3.14-3776AB?logo=python)
![Discord](https://img.shields.io/badge/Discord.py-2.6-5865F2?logo=discord)
---
## ✨ 주요 기능
- ✅ **성공 알림** - 백업 성공 시 지정 채널로 알림
- ❌ **실패 알림** - 백업 실패 시 별도 채널로 알림
- 📊 **상세 정보** - 파일 수, 크기, 소요 시간, 스냅샷 ID 표시
---
## 🛠️ 기술 스택
| 기술 | 설명 |
| -------------- | ---------------- |
| **Python** | 런타임 |
| **discord.py** | 디스코드 API |
| **aiohttp** | 비동기 HTTP 서버 |
---
## 📡 웹훅 엔드포인트
| 엔드포인트 | 설명 |
| --------------- | -------------- |
| `POST /success` | 백업 성공 알림 |
| `POST /failure` | 백업 실패 알림 |
### 요청 형식
**성공:**
```json
{
"files": "1382",
"size": "316.110 MiB",
"duration": "0:01",
"snapshot_id": "8042b460"
}
```
**실패:**
```json
{
"error": "에러 메시지"
}
```
---
## 🚀 실행 방법
```bash
docker compose up -d --build
```
---
## ⚙️ 환경 변수
| 변수 | 설명 |
| ---------------------------- | ----------------- |
| `DISCORD_BOT_TOKEN` | 디스코드 봇 토큰 |
| `DISCORD_SUCCESS_CHANNEL_ID` | 성공 알림 채널 ID |
| `DISCORD_FAILURE_CHANNEL_ID` | 실패 알림 채널 ID |
| `WEBHOOK_PORT` | 웹훅 포트 (5001) |
---
## 📁 구조
```
restic/
├── main.py # 봇 메인 로직
├── docker-compose.yml # Docker Compose 설정
├── Dockerfile # 컨테이너 빌드
├── requirements.txt # Python 의존성
└── .env # 환경 변수
```

20
restic/docker-compose.yml Normal file
View file

@ -0,0 +1,20 @@
services:
discord-bot-restic:
build: .
container_name: discord-bot-restic
env_file:
- .env
environment:
- PYTHONUNBUFFERED=1
ports:
# 호스트 restic 스크립트 접근용 (localhost만)
- "127.0.0.1:5001:5001"
networks:
- bot
labels:
- "com.centurylinklabs.watchtower.enable=false"
restart: unless-stopped
networks:
bot:
external: true

256
restic/main.py Normal file
View file

@ -0,0 +1,256 @@
"""
restic 백업 디스코드 알림
- 백업 성공/실패 각각 다른 채널로 알림
- HTTP 웹훅으로 알림 수신
"""
import os
import discord
from discord.ext import commands
from aiohttp import web
import asyncio
from datetime import datetime
# 환경변수
BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
SUCCESS_CHANNEL_ID = int(os.getenv("DISCORD_SUCCESS_CHANNEL_ID", "0"))
FAILURE_CHANNEL_ID = int(os.getenv("DISCORD_FAILURE_CHANNEL_ID", "0"))
WEBHOOK_PORT = int(os.getenv("WEBHOOK_PORT", "5001"))
# 봇 설정
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
def format_size(size_bytes: int) -> str:
"""바이트를 읽기 쉬운 형식으로 변환"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size_bytes < 1024:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024
return f"{size_bytes:.2f} PB"
@bot.event
async def on_ready():
"""봇 시작 시 실행"""
print(f"✅ 봇 로그인: {bot.user}")
try:
synced = await bot.tree.sync()
print(f"{len(synced)}개 명령어 동기화 완료")
except Exception as e:
print(f"❌ 명령어 동기화 실패: {e}")
async def send_success_notification(data: dict):
"""백업 성공 알림 전송"""
if not SUCCESS_CHANNEL_ID:
print("❌ SUCCESS_CHANNEL_ID가 설정되지 않음")
return
channel = bot.get_channel(SUCCESS_CHANNEL_ID)
if not channel:
print(f"❌ 채널을 찾을 수 없음: {SUCCESS_CHANNEL_ID}")
return
try:
# 스냅샷 ID 축약 (앞 8자리만)
snapshot_id = data.get('snapshot_id', 'N/A')
if len(snapshot_id) > 8:
snapshot_id = snapshot_id[:8]
embed = discord.Embed(
title="✅ 백업 완료",
color=0x2ecc71, # 초록색
timestamp=datetime.now()
)
# 통계 정보
stats = f"📁 **{data.get('files', '0')}** 파일 • 💾 **{data.get('size', 'N/A')}** • ⏱️ **{data.get('duration', 'N/A')}**"
embed.description = stats
# 백업 폴더 목록 (1024자 제한)
folders = data.get('folders', [])
if folders:
folder_list = " ".join([f"`{f}`" for f in folders])
# 디스코드 필드 길이 제한 적용
if len(folder_list) > 1000:
folder_list = folder_list[:1000] + "..."
embed.add_field(name="📂 백업 대상", value=folder_list, inline=False)
# 하단 정보
embed.set_footer(text=f"🆔 {snapshot_id} • 🖥️ {os.uname().nodename}")
await channel.send(embed=embed)
print(f"✅ 성공 알림 전송")
except Exception as e:
print(f"❌ 알림 전송 실패: {e}")
async def send_failure_notification(data: dict):
"""백업 실패 알림 전송"""
if not FAILURE_CHANNEL_ID:
print("❌ FAILURE_CHANNEL_ID가 설정되지 않음")
return
channel = bot.get_channel(FAILURE_CHANNEL_ID)
if not channel:
print(f"❌ 채널을 찾을 수 없음: {FAILURE_CHANNEL_ID}")
return
try:
error_msg = data.get('error', 'Unknown error')
# 에러 메시지 길이 제한
if len(error_msg) > 500:
error_msg = error_msg[:500] + "..."
embed = discord.Embed(
title="🛑 백업 실패",
color=0xe74c3c, # 빨간색
timestamp=datetime.now()
)
# 에러 내용
embed.description = f"```\n{error_msg}\n```"
# 하단 정보
embed.set_footer(text=f"🖥️ {os.uname().nodename}")
await channel.send(embed=embed)
print(f"✅ 실패 알림 전송")
except Exception as e:
print(f"❌ 실패 알림 전송 오류: {e}")
# HTTP 웹훅 핸들러
async def success_handler(request):
"""백업 성공 웹훅"""
try:
data = await request.json()
await send_success_notification(data)
return web.Response(text="OK")
except Exception as e:
print(f"❌ 성공 웹훅 오류: {e}")
return web.Response(text="Error", status=500)
async def failure_handler(request):
"""백업 실패 웹훅"""
try:
data = await request.json()
await send_failure_notification(data)
return web.Response(text="OK")
except Exception as e:
print(f"❌ 실패 웹훅 오류: {e}")
return web.Response(text="Error", status=500)
async def send_sync_success_notification(data: dict):
"""동기화 성공 알림 전송"""
if not SUCCESS_CHANNEL_ID:
return
channel = bot.get_channel(SUCCESS_CHANNEL_ID)
if not channel:
return
try:
embed = discord.Embed(
title="✅ 동기화 완료",
color=0x3498db, # 파란색
timestamp=datetime.now()
)
stats = f"💾 **{data.get('size', 'N/A')}** • ⏱️ **{data.get('duration', 'N/A')}**"
embed.description = stats
embed.add_field(name="📍 대상", value=f"`{data.get('target', 'N/A')}`", inline=False)
embed.set_footer(text=f"🖥️ {os.uname().nodename}")
await channel.send(embed=embed)
print(f"✅ 동기화 성공 알림 전송")
except Exception as e:
print(f"❌ 동기화 성공 알림 전송 오류: {e}")
async def send_sync_failure_notification(data: dict):
"""동기화 실패 알림 전송"""
if not FAILURE_CHANNEL_ID:
return
channel = bot.get_channel(FAILURE_CHANNEL_ID)
if not channel:
return
try:
error_msg = data.get('error', 'Unknown error')
if len(error_msg) > 500:
error_msg = error_msg[:500] + "..."
embed = discord.Embed(
title="🛑 동기화 실패",
color=0xe74c3c,
timestamp=datetime.now()
)
embed.description = f"```\n{error_msg}\n```"
embed.add_field(name="📍 대상", value=f"`{data.get('target', 'N/A')}`", inline=False)
embed.set_footer(text=f"🖥️ {os.uname().nodename}")
await channel.send(embed=embed)
print(f"✅ 동기화 실패 알림 전송")
except Exception as e:
print(f"❌ 동기화 실패 알림 전송 오류: {e}")
async def sync_success_handler(request):
"""동기화 성공 웹훅"""
try:
data = await request.json()
await send_sync_success_notification(data)
return web.Response(text="OK")
except Exception as e:
print(f"❌ 동기화 성공 웹훅 오류: {e}")
return web.Response(text="Error", status=500)
async def sync_failure_handler(request):
"""동기화 실패 웹훅"""
try:
data = await request.json()
await send_sync_failure_notification(data)
return web.Response(text="OK")
except Exception as e:
print(f"❌ 동기화 실패 웹훅 오류: {e}")
return web.Response(text="Error", status=500)
async def start_webhook_server():
"""웹훅 서버 시작"""
app = web.Application()
app.router.add_post("/success", success_handler)
app.router.add_post("/failure", failure_handler)
app.router.add_post("/sync-success", sync_success_handler)
app.router.add_post("/sync-failure", sync_failure_handler)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "0.0.0.0", WEBHOOK_PORT)
await site.start()
print(f"✅ 웹훅 서버 시작: http://0.0.0.0:{WEBHOOK_PORT}")
print(f" - 백업 성공: POST /success")
print(f" - 백업 실패: POST /failure")
print(f" - 동기화 성공: POST /sync-success")
print(f" - 동기화 실패: POST /sync-failure")
async def main():
"""메인 함수"""
# 웹훅 서버 시작
await start_webhook_server()
# 봇 시작
await bot.start(BOT_TOKEN)
if __name__ == "__main__":
asyncio.run(main())

2
restic/requirements.txt Normal file
View file

@ -0,0 +1,2 @@
discord.py==2.6.4
aiohttp==3.13.2