mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-07 03:56:46 +03:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
315d892cf4 | ||
|
|
b7eaa94ca2 | ||
|
|
52dbe6a2d9 | ||
|
|
e423d78cf9 | ||
|
|
f6b51414a0 | ||
|
|
8a35c0368f | ||
|
|
6f64838b55 | ||
|
|
f37867ea0c | ||
|
|
4aa7e665ac | ||
|
|
4b570f555a | ||
|
|
97676f3b04 | ||
|
|
04e8b817d3 | ||
|
|
6d13ff29ca | ||
|
|
c179953649 | ||
|
|
eb2d76a4b7 | ||
|
|
145bee8d96 | ||
|
|
cad72da016 | ||
|
|
4a01a351f0 | ||
|
|
98973adfe5 | ||
|
|
f6b5df472a | ||
|
|
b09cabd154 | ||
|
|
f9c77bbee0 | ||
|
|
e95f1d99b5 | ||
|
|
d5b0102080 | ||
|
|
94aff30dda | ||
|
|
38206e7d3d |
@@ -5,10 +5,10 @@
|
||||
|
||||
# Node.js
|
||||
node_modules
|
||||
# We don't exclude bun.lock* as it's needed for the build
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
pnpm-debug.log
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
@@ -62,4 +62,3 @@ logs
|
||||
# Cache
|
||||
.cache
|
||||
.npm
|
||||
.pnpm-store
|
||||
|
||||
BIN
.github/assets/logo.png
vendored
Normal file
BIN
.github/assets/logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
3
.github/workflows/README.md
vendored
3
.github/workflows/README.md
vendored
@@ -24,8 +24,7 @@ This workflow runs on all branches and pull requests. It:
|
||||
- On push to any branch (except changes to README.md and docs)
|
||||
- On pull requests to any branch (except changes to README.md and docs)
|
||||
|
||||
**Key features:**
|
||||
- Uses pnpm for faster dependency installation
|
||||
- Uses Bun for dependency installation
|
||||
- Caches dependencies to speed up builds
|
||||
- Uploads build artifacts for 7 days
|
||||
|
||||
|
||||
31
.github/workflows/astro-build-test.yml
vendored
31
.github/workflows/astro-build-test.yml
vendored
@@ -16,31 +16,32 @@ jobs:
|
||||
build-and-test:
|
||||
name: Build and Test Astro Project
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
bun-version: '1.2.9'
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Check lockfile and install dependencies
|
||||
run: |
|
||||
# Check if bun.lock exists, if not check for bun.lockb
|
||||
if [ -f "bun.lock" ]; then
|
||||
echo "Using existing bun.lock file"
|
||||
elif [ -f "bun.lockb" ]; then
|
||||
echo "Found bun.lockb, creating symlink to bun.lock"
|
||||
ln -s bun.lockb bun.lock
|
||||
fi
|
||||
bun install
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
run: bunx vitest run
|
||||
|
||||
- name: Build Astro project
|
||||
run: pnpm build
|
||||
run: bunx astro build
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
5
.github/workflows/docker-build.yml
vendored
5
.github/workflows/docker-build.yml
vendored
@@ -18,11 +18,6 @@ jobs:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports: ['6379:6379']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
4
.github/workflows/docker-scan.yml
vendored
4
.github/workflows/docker-scan.yml
vendored
@@ -7,14 +7,14 @@ on:
|
||||
- 'Dockerfile'
|
||||
- '.dockerignore'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'bun.lock*'
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- '.dockerignore'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'bun.lock*'
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # Run weekly on Sunday at midnight
|
||||
|
||||
|
||||
79
Dockerfile
79
Dockerfile
@@ -1,82 +1,47 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
FROM node:lts-alpine AS base
|
||||
ENV PNPM_HOME=/usr/local/bin
|
||||
ENV PATH=$PNPM_HOME:$PATH
|
||||
RUN apk add --no-cache libc6-compat
|
||||
FROM oven/bun:1.2.9-alpine AS base
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache libc6-compat python3 make g++ gcc wget sqlite
|
||||
|
||||
# -----------------------------------
|
||||
# ----------------------------
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache python3 make g++ gcc
|
||||
COPY package.json ./
|
||||
COPY bun.lock* ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
|
||||
# Full dev install
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# -----------------------------------
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache python3 make g++ gcc
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
# ----------------------------
|
||||
FROM deps AS builder
|
||||
COPY . .
|
||||
|
||||
RUN pnpm build
|
||||
# Compile TypeScript scripts to JavaScript
|
||||
RUN bun run build
|
||||
RUN mkdir -p dist/scripts && \
|
||||
for script in scripts/*.ts; do \
|
||||
node_modules/.bin/tsc --outDir dist/scripts --module commonjs --target es2020 --esModuleInterop $script || true; \
|
||||
bun build "$script" --target=bun --outfile=dist/scripts/$(basename "${script%.ts}.js"); \
|
||||
done
|
||||
|
||||
# -----------------------------------
|
||||
# ----------------------------
|
||||
FROM deps AS pruner
|
||||
WORKDIR /app
|
||||
RUN bun install --production --frozen-lockfile
|
||||
|
||||
# Prune dev dependencies and just keep the production bits
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
pnpm prune --prod
|
||||
|
||||
# -----------------------------------
|
||||
# ----------------------------
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Only copy production node_modules and built output
|
||||
COPY --from=pruner /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
COPY --from=builder /app/scripts ./scripts
|
||||
COPY --from=builder /app/data ./data
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=4321
|
||||
ENV DATABASE_URL=file:data/gitea-mirror.db
|
||||
|
||||
# Make entrypoint executable
|
||||
RUN chmod +x /app/docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
|
||||
RUN apk add --no-cache wget sqlite && \
|
||||
mkdir -p /app/data && \
|
||||
addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 gitea-mirror && \
|
||||
chown -R gitea-mirror:nodejs /app/data
|
||||
|
||||
COPY --from=builder --chown=gitea-mirror:nodejs /app/dist ./dist
|
||||
COPY --from=pruner --chown=gitea-mirror:nodejs /app/node_modules ./node_modules
|
||||
COPY --from=builder --chown=gitea-mirror:nodejs /app/package.json ./package.json
|
||||
COPY --from=builder --chown=gitea-mirror:nodejs /app/scripts ./scripts
|
||||
RUN chmod +x ./docker-entrypoint.sh && \
|
||||
mkdir -p /app/data && \
|
||||
addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 gitea-mirror && \
|
||||
chown -R gitea-mirror:nodejs /app/data
|
||||
|
||||
USER gitea-mirror
|
||||
|
||||
@@ -86,8 +51,4 @@ EXPOSE 4321
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:4321/ || exit 1
|
||||
|
||||
# Create a startup script that initializes the database before starting the application
|
||||
COPY --from=builder --chown=gitea-mirror:nodejs /app/docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
RUN chmod +x ./docker-entrypoint.sh
|
||||
|
||||
CMD ["./docker-entrypoint.sh"]
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
|
||||
239
README.md
239
README.md
@@ -1,11 +1,30 @@
|
||||
# Gitea Mirror
|
||||
|
||||
|
||||
<p align="center">
|
||||
<i>A modern web application for automatically mirroring repositories from GitHub to your self-hosted Gitea instance.</i><br>
|
||||
<sub>Designed for developers, teams, and organizations who want to retain full control of their code while still collaborating on GitHub.</sub>
|
||||
<img src=".github/assets/logo.png" alt="Gitea Mirror Logo" width="120" />
|
||||
<h1>Gitea Mirror</h1>
|
||||
<p><i>A modern web app for automatically mirroring repositories from GitHub to your self-hosted Gitea.</i></p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/arunavo4/gitea-mirror/releases/latest"><img src="https://img.shields.io/github/v/tag/arunavo4/gitea-mirror?label=release" alt="release"/></a>
|
||||
<a href="https://github.com/arunavo4/gitea-mirror/actions/workflows/astro-build-test.yml"><img src="https://img.shields.io/github/actions/workflow/status/arunavo4/gitea-mirror/astro-build-test.yml?branch=main" alt="build"/></a>
|
||||
<a href="https://github.com/arunavo4/gitea-mirror/pkgs/container/gitea-mirror"><img src="https://img.shields.io/badge/ghcr.io-container-blue?logo=github" alt="container"/></a>
|
||||
<a href="https://github.com/arunavo4/gitea-mirror/blob/main/LICENSE"><img src="https://img.shields.io/github/license/arunavo4/gitea-mirror" alt="license"/></a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# Using Docker (recommended)
|
||||
docker compose --profile production up -d
|
||||
|
||||
# Using Bun
|
||||
bun run setup && bun run dev
|
||||
|
||||
# Using LXC on Proxmox
|
||||
curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-installer.sh | bash
|
||||
````
|
||||
|
||||
See the [LXC Container Deployment Guide](scripts/README-lxc.md).
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/assets/dashboard.png" alt="Dashboard" width="80%"/>
|
||||
</p>
|
||||
@@ -50,7 +69,7 @@ See the [Quick Start Guide](docs/quickstart.md) for detailed instructions on get
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 22 or later
|
||||
- Bun 1.2.9 or later
|
||||
- A GitHub account with a personal access token
|
||||
- A Gitea instance with an access token
|
||||
|
||||
@@ -77,7 +96,7 @@ Before running the application in production mode for the first time, you need t
|
||||
|
||||
```bash
|
||||
# Initialize the database for production mode
|
||||
pnpm setup
|
||||
bun run setup
|
||||
```
|
||||
|
||||
This will create the necessary tables. On first launch, you'll be guided through creating your admin account with a secure password.
|
||||
@@ -95,13 +114,13 @@ Gitea Mirror provides multi-architecture Docker images that work on both ARM64 (
|
||||
docker compose --profile production up -d
|
||||
|
||||
# For development mode (requires configuration)
|
||||
# Ensure you have run pnpm setup first
|
||||
# Ensure you have run bun run setup first
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Docker Compose is the recommended method for running Gitea Mirror** as it automatically sets up the required Redis sidecar service that the application depends on.
|
||||
> **Docker Compose is the recommended method for running Gitea Mirror** as it provides a consistent environment with proper volume management for the SQLite database.
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
@@ -109,19 +128,15 @@ docker compose -f docker-compose.dev.yml up -d
|
||||
|
||||
##### Using Pre-built Images from GitHub Container Registry
|
||||
|
||||
If you want to run the container directly without Docker Compose, you'll need to set up a Redis instance separately:
|
||||
If you want to run the container directly without Docker Compose:
|
||||
|
||||
```bash
|
||||
# First, start a Redis container
|
||||
docker run -d --name gitea-mirror-redis redis:alpine
|
||||
|
||||
# Pull the latest multi-architecture image
|
||||
docker pull ghcr.io/arunavo4/gitea-mirror:latest
|
||||
|
||||
# Run the application with a link to the Redis container
|
||||
# Note: The REDIS_URL environment variable is required and must point to the Redis container
|
||||
docker run -d -p 4321:4321 --link gitea-mirror-redis:redis \
|
||||
-e REDIS_URL=redis://redis:6379 \
|
||||
# Run the application with a volume for persistent data
|
||||
docker run -d -p 4321:4321 \
|
||||
-v gitea-mirror-data:/app/data \
|
||||
ghcr.io/arunavo4/gitea-mirror:latest
|
||||
```
|
||||
|
||||
@@ -148,6 +163,24 @@ docker compose --profile production up -d
|
||||
|
||||
See [Docker build documentation](./scripts/README-docker.md) for more details.
|
||||
|
||||
##### Using LXC Containers (for Proxmox Homelab Setups)
|
||||
|
||||
Gitea Mirror can be deployed on Proxmox LXC containers, which is ideal for homelab setups:
|
||||
|
||||
```bash
|
||||
# One-command installation on an Ubuntu 22.04 LXC container
|
||||
curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-installer.sh | bash
|
||||
```
|
||||
|
||||
The installer script:
|
||||
- Downloads the Gitea Mirror repository
|
||||
- Installs all dependencies including Bun
|
||||
- Builds the application
|
||||
- Sets up a systemd service
|
||||
- Starts the application
|
||||
|
||||
See the [LXC Container Deployment Guide](scripts/README-lxc.md) for detailed instructions.
|
||||
|
||||
##### Building Your Own Image
|
||||
|
||||
For manual Docker builds (without the helper script):
|
||||
@@ -180,7 +213,6 @@ The Docker container can be configured with the following environment variables:
|
||||
- `HOST`: Host to bind to (default: `0.0.0.0`)
|
||||
- `PORT`: Port to listen on (default: `4321`)
|
||||
- `JWT_SECRET`: Secret key for JWT token generation (important for security)
|
||||
- `REDIS_URL`: URL for Redis connection (required, default: none). When using Docker Compose, this should be set to `redis://redis:6379` to connect to the Redis container.
|
||||
|
||||
|
||||
#### Manual Installation
|
||||
@@ -191,40 +223,40 @@ git clone https://github.com/arunavo4/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
|
||||
# Quick setup (installs dependencies and initializes the database)
|
||||
pnpm setup
|
||||
bun run setup
|
||||
|
||||
# Development Mode Options
|
||||
|
||||
# Run in development mode
|
||||
pnpm dev
|
||||
bun run dev
|
||||
|
||||
# Run in development mode with clean database (removes existing DB first)
|
||||
pnpm dev:clean
|
||||
bun run dev:clean
|
||||
|
||||
# Production Mode Options
|
||||
|
||||
# Build the application
|
||||
pnpm build
|
||||
bun run build
|
||||
|
||||
# Preview the production build
|
||||
pnpm preview
|
||||
bun run preview
|
||||
|
||||
# Start the production server (default)
|
||||
pnpm start
|
||||
bun run start
|
||||
|
||||
# Start the production server with a clean setup
|
||||
pnpm start:fresh
|
||||
bun run start:fresh
|
||||
|
||||
# Database Management
|
||||
|
||||
# Initialize the database
|
||||
pnpm init-db
|
||||
bun run init-db
|
||||
|
||||
# Reset users for testing first-time signup
|
||||
pnpm reset-users
|
||||
bun run reset-users
|
||||
|
||||
# Check database status
|
||||
pnpm check-db
|
||||
bun run check-db
|
||||
```
|
||||
|
||||
### Configuration
|
||||
@@ -239,7 +271,7 @@ Key configuration options include:
|
||||
- Scheduling options for automatic mirroring
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Redis is a required component for Gitea Mirror** as it's used for job queuing and caching.
|
||||
> **SQLite is the only database required for Gitea Mirror**, handling both data storage and real-time event notifications.
|
||||
|
||||
## 🚀 Development
|
||||
|
||||
@@ -247,10 +279,10 @@ Key configuration options include:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm setup
|
||||
bun run setup
|
||||
|
||||
# Start the development server
|
||||
pnpm dev
|
||||
bun run dev
|
||||
```
|
||||
|
||||
|
||||
@@ -330,12 +362,12 @@ docker compose -f docker-compose.dev.yml up -d
|
||||
|
||||
> [!TIP]
|
||||
> You can also create a `.env` file with your GitHub and Gitea credentials:
|
||||
>
|
||||
>
|
||||
> ```env
|
||||
> # GitHub credentials
|
||||
> GITHUB_TOKEN=your-github-token
|
||||
> GITHUB_USERNAME=your-github-username
|
||||
>
|
||||
>
|
||||
> # Gitea credentials (will be set up after you create a user in the local Gitea instance)
|
||||
> GITEA_TOKEN=your-local-gitea-token
|
||||
> GITEA_USERNAME=your-local-gitea-username
|
||||
@@ -344,10 +376,10 @@ docker compose -f docker-compose.dev.yml up -d
|
||||
## Technologies Used
|
||||
|
||||
- **Frontend**: Astro, React, Shadcn UI, Tailwind CSS v4
|
||||
- **Backend**: Node.js
|
||||
- **Database**: SQLite (default) or PostgreSQL
|
||||
- **Caching/Queue**: Redis
|
||||
- **Backend**: Bun
|
||||
- **Database**: SQLite (handles both data storage and event notifications)
|
||||
- **API Integration**: GitHub API (Octokit), Gitea API
|
||||
- **Deployment Options**: Docker containers, Proxmox LXC containers
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -357,27 +389,6 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Project Status
|
||||
|
||||
This project is now complete and ready for production use with version 1.0.0. All planned features have been implemented, thoroughly tested, and optimized for performance:
|
||||
|
||||
- ✅ User-friendly dashboard with status overview
|
||||
- ✅ Repository management interface
|
||||
- ✅ Organization management interface
|
||||
- ✅ Configuration management for GitHub and Gitea
|
||||
- ✅ Scheduling and automation
|
||||
- ✅ Activity logging and monitoring
|
||||
- ✅ Responsive design for all screen sizes
|
||||
- ✅ Modern toast notifications for better user feedback
|
||||
- ✅ First-time user signup experience
|
||||
- ✅ Better error handling and user guidance
|
||||
- ✅ Comprehensive error handling
|
||||
- ✅ Unit tests for components and API
|
||||
- ✅ Direct GitHub to Gitea mirroring (no external dependencies)
|
||||
- ✅ Docker and docker-compose support for easy deployment
|
||||
- ✅ Multi-architecture support (ARM64 and x86_64)
|
||||
- ✅ Light/dark mode toggle
|
||||
- ✅ Persistent configuration storage
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -385,14 +396,14 @@ This project is now complete and ready for production use with version 1.0.0. Al
|
||||
|
||||
> [!WARNING]
|
||||
> If you encounter network-related warnings or errors when running Docker Compose, such as:
|
||||
>
|
||||
>
|
||||
> ```
|
||||
> WARN[0095] a network with name gitea-network exists but was not created by compose.
|
||||
> Set `external: true` to use an existing network
|
||||
> ```
|
||||
>
|
||||
>
|
||||
> or
|
||||
>
|
||||
>
|
||||
> ```
|
||||
> network gitea-network was found but has incorrect label com.docker.compose.network set to "" (expected: "gitea-network")
|
||||
> ```
|
||||
@@ -416,7 +427,7 @@ Try the following steps:
|
||||
|
||||
> [!TIP]
|
||||
> If you need to share the network with other Docker Compose projects, you can modify the `docker-compose.dev.yml` file to mark the network as external:
|
||||
>
|
||||
>
|
||||
> ```yaml
|
||||
> networks:
|
||||
> gitea-network:
|
||||
@@ -424,62 +435,60 @@ Try the following steps:
|
||||
> external: true
|
||||
> ```
|
||||
|
||||
### Redis Connection Issues
|
||||
|
||||
> [!CAUTION]
|
||||
> If the application fails to connect to Redis with errors like `ECONNREFUSED 127.0.0.1:6379`, ensure:
|
||||
>
|
||||
> 1. The Redis container is running:
|
||||
> ```bash
|
||||
> docker ps | grep redis
|
||||
> ```
|
||||
> 2. The `REDIS_URL` environment variable is correctly set to `redis://redis:6379` in your Docker Compose file.
|
||||
> 3. Both the application and Redis containers are on the same Docker network.
|
||||
> 4. If running without Docker Compose, ensure you've started a Redis container and linked it properly:
|
||||
> ```bash
|
||||
> # Start Redis container
|
||||
> docker run -d --name gitea-mirror-redis redis:alpine
|
||||
> # Run application with link to Redis
|
||||
> docker run -d -p 4321:4321 --link gitea-mirror-redis:redis \
|
||||
> -e REDIS_URL=redis://redis:6379 \
|
||||
> ghcr.io/arunavo4/gitea-mirror:latest
|
||||
> ```
|
||||
|
||||
|
||||
#### Improving Redis Connection Resilience
|
||||
### Database Persistence
|
||||
|
||||
> [!TIP]
|
||||
> For better Redis connection handling, you can modify the `src/lib/redis.ts` file to include retry logic and better error handling:
|
||||
> The application uses SQLite for all data storage and event notifications. Make sure the database file is properly mounted when using Docker:
|
||||
>
|
||||
> ```bash
|
||||
> # Run with a volume for persistent data storage
|
||||
> docker run -d -p 4321:4321 \
|
||||
> -v gitea-mirror-data:/app/data \
|
||||
> ghcr.io/arunavo4/gitea-mirror:latest
|
||||
> ```
|
||||
>
|
||||
> For homelab/self-hosted setups, you can use the provided Docker Compose file with automatic event cleanup:
|
||||
>
|
||||
> ```bash
|
||||
> # Clone the repository
|
||||
> git clone https://github.com/arunavo4/gitea-mirror.git
|
||||
> cd gitea-mirror
|
||||
>
|
||||
> # Start the application with Docker Compose
|
||||
> docker-compose -f docker-compose.homelab.yml up -d
|
||||
> ```
|
||||
>
|
||||
> This setup includes a cron job that runs daily to clean up old events and prevent the database from growing too large.
|
||||
|
||||
```typescript
|
||||
import Redis from "ioredis";
|
||||
|
||||
// Connect to Redis using REDIS_URL environment variable or default to redis://redis:6379
|
||||
const redisUrl = process.env.REDIS_URL ?? 'redis://redis:6379';
|
||||
#### Database Maintenance
|
||||
|
||||
console.log(`Connecting to Redis at: ${redisUrl}`);
|
||||
|
||||
// Configure Redis client with connection options
|
||||
const redisOptions = {
|
||||
retryStrategy: (times) => {
|
||||
// Retry with exponential backoff up to 30 seconds
|
||||
const delay = Math.min(times * 100, 3000);
|
||||
console.log(`Redis connection attempt ${times} failed. Retrying in ${delay}ms...`);
|
||||
return delay;
|
||||
},
|
||||
maxRetriesPerRequest: 5,
|
||||
enableReadyCheck: true,
|
||||
connectTimeout: 10000,
|
||||
};
|
||||
|
||||
export const redis = new Redis(redisUrl, redisOptions);
|
||||
export const redisPublisher = new Redis(redisUrl, redisOptions);
|
||||
export const redisSubscriber = new Redis(redisUrl, redisOptions);
|
||||
|
||||
// Log connection events
|
||||
redis.on('connect', () => console.log('Redis client connected'));
|
||||
redis.on('error', (err) => console.error('Redis client error:', err));
|
||||
```
|
||||
> [!TIP]
|
||||
> For database maintenance, you can use the provided scripts:
|
||||
>
|
||||
> ```bash
|
||||
> # Check database integrity
|
||||
> bun run check-db
|
||||
>
|
||||
> # Fix database issues
|
||||
> bun run fix-db
|
||||
>
|
||||
> # Reset user accounts (for development)
|
||||
> bun run reset-users
|
||||
>
|
||||
> # Clean up old events (keeps last 7 days by default)
|
||||
> bun run cleanup-events
|
||||
>
|
||||
> # Clean up old events with custom retention period (e.g., 30 days)
|
||||
> bun run cleanup-events 30
|
||||
> ```
|
||||
>
|
||||
> For automated maintenance, consider setting up a cron job to run the cleanup script periodically:
|
||||
>
|
||||
> ```bash
|
||||
> # Add this to your crontab (runs daily at 2 AM)
|
||||
> 0 2 * * * cd /path/to/gitea-mirror && bun run cleanup-events
|
||||
> ```
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
@@ -494,13 +503,13 @@ redis.on('error', (err) => console.error('Redis client error:', err));
|
||||
|
||||
> [!TIP]
|
||||
> If containers are not starting properly, check their health status:
|
||||
>
|
||||
>
|
||||
> ```bash
|
||||
> docker ps --format "{{.Names}}: {{.Status}}"
|
||||
> ```
|
||||
>
|
||||
>
|
||||
> For more detailed logs:
|
||||
>
|
||||
>
|
||||
> ```bash
|
||||
> docker logs gitea-mirror-dev
|
||||
> ```
|
||||
|
||||
@@ -11,7 +11,12 @@ export default defineConfig({
|
||||
mode: 'standalone',
|
||||
}),
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
plugins: [tailwindcss()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['bun']
|
||||
}
|
||||
}
|
||||
},
|
||||
integrations: [react()]
|
||||
});
|
||||
4
crontab
Normal file
4
crontab
Normal file
@@ -0,0 +1,4 @@
|
||||
# Run event cleanup daily at 2 AM
|
||||
0 2 * * * cd /app && bun run cleanup-events 30 >> /app/data/cleanup-events.log 2>&1
|
||||
|
||||
# Empty line at the end is required for cron to work properly
|
||||
@@ -51,7 +51,6 @@ services:
|
||||
- gitea-mirror-data:/app/data
|
||||
depends_on:
|
||||
- gitea
|
||||
- redis
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
@@ -75,7 +74,6 @@ services:
|
||||
- GITEA_ORGANIZATION=${GITEA_ORGANIZATION:-github-mirrors}
|
||||
- GITEA_ORG_VISIBILITY=${GITEA_ORG_VISIBILITY:-public}
|
||||
- DELAY=${DELAY:-3600}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4321/"]
|
||||
interval: 30s
|
||||
@@ -85,16 +83,7 @@ services:
|
||||
networks:
|
||||
- gitea-network
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
networks:
|
||||
- gitea-network
|
||||
|
||||
|
||||
# Define named volumes for data persistence
|
||||
volumes:
|
||||
@@ -102,8 +91,6 @@ volumes:
|
||||
gitea-config: # Gitea config volume
|
||||
gitea-mirror-data: # Gitea Mirror database volume
|
||||
|
||||
redis-data:
|
||||
|
||||
# Define networks
|
||||
networks:
|
||||
gitea-network:
|
||||
|
||||
38
docker-compose.homelab.yml
Normal file
38
docker-compose.homelab.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
gitea-mirror:
|
||||
image: ghcr.io/arunavo4/gitea-mirror:latest
|
||||
container_name: gitea-mirror
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4321:4321"
|
||||
volumes:
|
||||
- gitea-mirror-data:/app/data
|
||||
# Mount the crontab file
|
||||
- ./crontab:/etc/cron.d/gitea-mirror-cron
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- HOST=0.0.0.0
|
||||
- PORT=4321
|
||||
- DATABASE_URL=sqlite://data/gitea-mirror.db
|
||||
- DELAY=${DELAY:-3600}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:4321/health"]
|
||||
interval: 1m
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
# Install cron in the container and set up the cron job
|
||||
command: >
|
||||
sh -c "
|
||||
apt-get update && apt-get install -y cron curl &&
|
||||
chmod 0644 /etc/cron.d/gitea-mirror-cron &&
|
||||
crontab /etc/cron.d/gitea-mirror-cron &&
|
||||
service cron start &&
|
||||
bun dist/server/entry.mjs
|
||||
"
|
||||
|
||||
# Define named volumes for database persistence
|
||||
volumes:
|
||||
gitea-mirror-data: # Database volume
|
||||
@@ -19,8 +19,6 @@ services:
|
||||
- "4321:4321"
|
||||
volumes:
|
||||
- gitea-mirror-data:/app/data
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
@@ -44,7 +42,6 @@ services:
|
||||
- GITEA_ORGANIZATION=${GITEA_ORGANIZATION:-github-mirrors}
|
||||
- GITEA_ORG_VISIBILITY=${GITEA_ORG_VISIBILITY:-public}
|
||||
- DELAY=${DELAY:-3600}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/"]
|
||||
interval: 30s
|
||||
@@ -53,16 +50,6 @@ services:
|
||||
start_period: 15s
|
||||
profiles: ["production"]
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
# Define named volumes for database persistence
|
||||
volumes:
|
||||
gitea-mirror-data: # Database volume
|
||||
redis-data:
|
||||
|
||||
@@ -5,19 +5,19 @@ set -e
|
||||
# Ensure data directory exists
|
||||
mkdir -p /app/data
|
||||
|
||||
# If pnpm is available, run setup (for dev images), else run node init directly
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
echo "Running pnpm setup (if needed)..."
|
||||
pnpm setup || true
|
||||
# If bun is available, run setup (for dev images)
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
echo "Running bun setup (if needed)..."
|
||||
bun run setup || true
|
||||
fi
|
||||
|
||||
# Initialize the database if it doesn't exist
|
||||
if [ ! -f "/app/data/gitea-mirror.db" ]; then
|
||||
echo "Initializing database..."
|
||||
if [ -f "dist/scripts/init-db.js" ]; then
|
||||
node dist/scripts/init-db.js
|
||||
bun dist/scripts/init-db.js
|
||||
elif [ -f "dist/scripts/manage-db.js" ]; then
|
||||
node dist/scripts/manage-db.js init
|
||||
bun dist/scripts/manage-db.js init
|
||||
else
|
||||
echo "Warning: Could not find database initialization scripts in dist/scripts."
|
||||
echo "Creating and initializing database manually..."
|
||||
@@ -113,15 +113,29 @@ if [ ! -f "/app/data/gitea-mirror.db" ]; then
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
read INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_events_user_channel ON events(user_id, channel);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_read ON events(read);
|
||||
EOF
|
||||
echo "Database initialized with required tables."
|
||||
fi
|
||||
else
|
||||
echo "Database already exists, checking for issues..."
|
||||
if [ -f "dist/scripts/fix-db-issues.js" ]; then
|
||||
node dist/scripts/fix-db-issues.js
|
||||
bun dist/scripts/fix-db-issues.js
|
||||
elif [ -f "dist/scripts/manage-db.js" ]; then
|
||||
node dist/scripts/manage-db.js fix
|
||||
bun dist/scripts/manage-db.js fix
|
||||
fi
|
||||
|
||||
# Since the application is not used by anyone yet, we've removed the schema updates and migrations
|
||||
@@ -130,4 +144,4 @@ fi
|
||||
|
||||
# Start the application
|
||||
echo "Starting Gitea Mirror..."
|
||||
exec node ./dist/server/entry.mjs
|
||||
exec bun ./dist/server/entry.mjs
|
||||
|
||||
86
package.json
86
package.json
@@ -1,87 +1,85 @@
|
||||
{
|
||||
"name": "gitea-mirror",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
"bun": ">=1.2.9"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "pnpm install && pnpm manage-db init",
|
||||
"dev": "astro dev",
|
||||
"dev:clean": "pnpm cleanup-db && pnpm manage-db init && astro dev",
|
||||
"build": "astro build",
|
||||
"setup": "bun install && bun run manage-db init",
|
||||
"dev": "bunx --bun astro dev",
|
||||
"dev:clean": "bun run cleanup-db && bun run manage-db init && bunx --bun astro dev",
|
||||
"build": "bunx --bun astro build",
|
||||
"cleanup-db": "rm -f gitea-mirror.db data/gitea-mirror.db",
|
||||
"manage-db": "tsx scripts/manage-db.ts",
|
||||
"init-db": "tsx scripts/manage-db.ts init",
|
||||
"check-db": "tsx scripts/manage-db.ts check",
|
||||
"fix-db": "tsx scripts/manage-db.ts fix",
|
||||
"reset-users": "tsx scripts/manage-db.ts reset-users",
|
||||
"preview": "astro preview",
|
||||
"start": "node dist/server/entry.mjs",
|
||||
"start:fresh": "pnpm cleanup-db && pnpm manage-db init && node dist/server/entry.mjs",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"astro": "astro"
|
||||
"manage-db": "bun scripts/manage-db.ts",
|
||||
"init-db": "bun scripts/manage-db.ts init",
|
||||
"check-db": "bun scripts/manage-db.ts check",
|
||||
"fix-db": "bun scripts/manage-db.ts fix",
|
||||
"reset-users": "bun scripts/manage-db.ts reset-users",
|
||||
"cleanup-events": "bun scripts/cleanup-events.ts",
|
||||
"preview": "bunx --bun astro preview",
|
||||
"start": "bun dist/server/entry.mjs",
|
||||
"start:fresh": "bun run cleanup-db && bun run manage-db init && bun dist/server/entry.mjs",
|
||||
"test": "bunx --bun vitest run",
|
||||
"test:watch": "bunx --bun vitest",
|
||||
"astro": "bunx --bun astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.2.6",
|
||||
"@astrojs/node": "^9.2.1",
|
||||
"@astrojs/react": "^4.2.7",
|
||||
"@libsql/client": "^0.15.4",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@radix-ui/react-avatar": "^1.1.4",
|
||||
"@radix-ui/react-checkbox": "^1.1.5",
|
||||
"@radix-ui/react-dialog": "^1.1.7",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.9",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.13",
|
||||
"@radix-ui/react-radio-group": "^1.3.6",
|
||||
"@radix-ui/react-select": "^2.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-tabs": "^1.1.4",
|
||||
"@radix-ui/react-select": "^2.2.4",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.6",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"@tanstack/react-virtual": "^3.13.8",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"astro": "^5.7.10",
|
||||
"axios": "^1.8.4",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"astro": "^5.7.13",
|
||||
"axios": "^1.9.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"fuse.js": "^7.1.0",
|
||||
"ioredis": "^5.6.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.488.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"sonner": "^2.0.3",
|
||||
"superagent": "^10.2.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"sqlite3": "^5.1.7",
|
||||
"superagent": "^10.2.1",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"tw-animate-css": "^1.3.0",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.24.2"
|
||||
"zod": "^3.25.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"@types/superagent": "^8.1.9",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"better-sqlite3": "^9.6.0",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"tsx": "^4.19.3",
|
||||
"vitest": "^3.1.1"
|
||||
"tsx": "^4.19.4",
|
||||
"vitest": "^3.1.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0"
|
||||
"packageManager": "bun@1.2.9"
|
||||
}
|
||||
|
||||
7713
pnpm-lock.yaml
generated
7713
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@ The script uses environment variables from the `.env` file in the project root:
|
||||
3. Using with docker-compose:
|
||||
```bash
|
||||
# Ensure dependencies are installed and database is initialized
|
||||
pnpm setup
|
||||
bun run setup
|
||||
|
||||
# First build the image
|
||||
./scripts/build-docker.sh --load
|
||||
|
||||
152
scripts/README-lxc.md
Normal file
152
scripts/README-lxc.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# LXC Container Deployment Guide
|
||||
|
||||
This guide explains how to deploy the Gitea Mirror application on Proxmox LXC containers while keeping your existing Docker containers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Proxmox VE installed and configured
|
||||
- Basic knowledge of LXC containers and Proxmox
|
||||
- Access to Proxmox web interface or CLI
|
||||
|
||||
## Creating an LXC Container
|
||||
|
||||
1. In Proxmox web interface, create a new LXC container:
|
||||
- Choose Ubuntu 22.04 as the template
|
||||
- Allocate appropriate resources (2GB RAM, 2 CPU cores recommended)
|
||||
- At least 10GB of disk space
|
||||
- Configure networking as needed
|
||||
|
||||
2. Start the container and get a shell (either via Proxmox web console or SSH)
|
||||
|
||||
## Deploying Gitea Mirror
|
||||
|
||||
### Option 1: One-Command Installation (Recommended)
|
||||
|
||||
This method allows you to install Gitea Mirror with a single command, without having to copy files manually:
|
||||
|
||||
1. SSH into your LXC container:
|
||||
```bash
|
||||
ssh root@lxc-container-ip
|
||||
```
|
||||
|
||||
2. Run the installer script directly:
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-installer.sh | bash
|
||||
```
|
||||
|
||||
3. The installer will:
|
||||
- Download the Gitea Mirror repository
|
||||
- Install all dependencies including Bun
|
||||
- Build the application
|
||||
- Set up a systemd service
|
||||
- Start the application
|
||||
- Display access information
|
||||
|
||||
### Option 2: Manual Setup
|
||||
|
||||
If you prefer to set up manually or the automatic script doesn't work for your environment:
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
apt update
|
||||
apt install -y curl git sqlite3 build-essential
|
||||
```
|
||||
|
||||
2. Install Bun:
|
||||
```bash
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
export BUN_INSTALL="/root/.bun"
|
||||
export PATH="$BUN_INSTALL/bin:$PATH"
|
||||
```
|
||||
|
||||
3. Clone or copy your project:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/gitea-mirror.git /opt/gitea-mirror
|
||||
cd /opt/gitea-mirror
|
||||
```
|
||||
|
||||
4. Build and initialize:
|
||||
```bash
|
||||
bun install
|
||||
bun run build
|
||||
bun run manage-db init
|
||||
```
|
||||
|
||||
5. Create a systemd service manually:
|
||||
```bash
|
||||
nano /etc/systemd/system/gitea-mirror.service
|
||||
# Add the service configuration as shown below:
|
||||
|
||||
[Unit]
|
||||
Description=Gitea Mirror
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/gitea-mirror
|
||||
ExecStart=/root/.bun/bin/bun dist/server/entry.mjs
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
User=gitea-mirror
|
||||
Group=gitea-mirror
|
||||
Environment=NODE_ENV=production
|
||||
Environment=HOST=0.0.0.0
|
||||
Environment=PORT=4321
|
||||
Environment=DATABASE_URL=file:data/gitea-mirror.db
|
||||
Environment=JWT_SECRET=your-secure-secret-key
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
6. Enable and start the service:
|
||||
```bash
|
||||
systemctl enable gitea-mirror.service
|
||||
systemctl start gitea-mirror.service
|
||||
```
|
||||
|
||||
## Connecting LXC and Docker Containers
|
||||
|
||||
If you need your LXC container to communicate with Docker containers:
|
||||
|
||||
1. On your host machine, create a bridge network:
|
||||
```bash
|
||||
docker network create gitea-network
|
||||
```
|
||||
|
||||
2. Find the bridge interface created by Docker:
|
||||
```bash
|
||||
ip a | grep docker
|
||||
# Look for something like docker0 or br-xxxxxxxx
|
||||
```
|
||||
|
||||
3. In Proxmox, edit the LXC container's network configuration to use this bridge.
|
||||
|
||||
## Accessing the Application
|
||||
|
||||
Once deployed, you can access the Gitea Mirror application at:
|
||||
```
|
||||
http://lxc-container-ip:4321
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Check service status:
|
||||
```bash
|
||||
systemctl status gitea-mirror
|
||||
```
|
||||
|
||||
- View logs:
|
||||
```bash
|
||||
journalctl -u gitea-mirror -f
|
||||
```
|
||||
|
||||
- If the service fails to start, check permissions on the data directory:
|
||||
```bash
|
||||
chown -R gitea-mirror:gitea-mirror /opt/gitea-mirror/data
|
||||
```
|
||||
|
||||
- Verify Bun is installed correctly:
|
||||
```bash
|
||||
bun --version
|
||||
```
|
||||
@@ -19,38 +19,38 @@ This is a consolidated database management tool that handles all database-relate
|
||||
You can execute the database management tool using your package manager with various commands:
|
||||
|
||||
```bash
|
||||
# Checks database status (default action if no command is specified, equivalent to 'pnpm check-db')
|
||||
pnpm manage-db
|
||||
# Checks database status (default action if no command is specified, equivalent to 'bun run check-db')
|
||||
bun run manage-db
|
||||
|
||||
# Check database status
|
||||
pnpm check-db
|
||||
bun run check-db
|
||||
|
||||
# Initialize the database (only if it doesn't exist)
|
||||
pnpm init-db
|
||||
bun run init-db
|
||||
|
||||
# Fix database location issues
|
||||
pnpm fix-db
|
||||
bun run fix-db
|
||||
|
||||
# Automatic check, fix, and initialize if needed
|
||||
pnpm db-auto
|
||||
bun run db-auto
|
||||
|
||||
# Reset all users (for testing signup flow)
|
||||
pnpm reset-users
|
||||
bun run reset-users
|
||||
|
||||
# Update the database schema to the latest version
|
||||
pnpm update-schema
|
||||
bun run update-schema
|
||||
|
||||
# Remove database files completely
|
||||
pnpm cleanup-db
|
||||
bun run cleanup-db
|
||||
|
||||
# Complete setup (install dependencies and initialize database)
|
||||
pnpm setup
|
||||
bun run setup
|
||||
|
||||
# Start development server with a fresh database
|
||||
pnpm dev:clean
|
||||
bun run dev:clean
|
||||
|
||||
# Start production server with a fresh database
|
||||
pnpm start:fresh
|
||||
bun run start:fresh
|
||||
```
|
||||
|
||||
## Database File Location
|
||||
|
||||
38
scripts/check-events.ts
Normal file
38
scripts/check-events.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Script to check events in the database
|
||||
*/
|
||||
|
||||
import { Database } from "bun:sqlite";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
// Define the database path
|
||||
const dataDir = path.join(process.cwd(), "data");
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
console.error("Data directory not found:", dataDir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dbPath = path.join(dataDir, "gitea-mirror.db");
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
console.error("Database file not found:", dbPath);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Open the database
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Check if the events table exists
|
||||
const tableExists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='events'").get();
|
||||
|
||||
if (!tableExists) {
|
||||
console.error("Events table does not exist");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get all events
|
||||
const events = db.query("SELECT * FROM events").all();
|
||||
|
||||
console.log("Events in the database:");
|
||||
console.log(JSON.stringify(events, null, 2));
|
||||
43
scripts/cleanup-events.ts
Normal file
43
scripts/cleanup-events.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Script to clean up old events from the database
|
||||
* This script should be run periodically (e.g., daily) to prevent the events table from growing too large
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/cleanup-events.ts [days]
|
||||
*
|
||||
* Where [days] is the number of days to keep events (default: 7)
|
||||
*/
|
||||
|
||||
import { cleanupOldEvents } from "../src/lib/events";
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const daysToKeep = args.length > 0 ? parseInt(args[0], 10) : 7;
|
||||
|
||||
if (isNaN(daysToKeep) || daysToKeep < 1) {
|
||||
console.error("Error: Days to keep must be a positive number");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function runCleanup() {
|
||||
try {
|
||||
console.log(`Starting event cleanup (retention: ${daysToKeep} days)...`);
|
||||
|
||||
// Call the cleanupOldEvents function from the events module
|
||||
const result = await cleanupOldEvents(daysToKeep);
|
||||
|
||||
console.log(`Cleanup summary:`);
|
||||
console.log(`- Read events deleted: ${result.readEventsDeleted}`);
|
||||
console.log(`- Unread events deleted: ${result.unreadEventsDeleted}`);
|
||||
console.log(`- Total events deleted: ${result.readEventsDeleted + result.unreadEventsDeleted}`);
|
||||
|
||||
console.log("Event cleanup completed successfully");
|
||||
} catch (error) {
|
||||
console.error("Error running event cleanup:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the cleanup
|
||||
runCleanup();
|
||||
4
scripts/docker-diagnostics.sh
Executable file → Normal file
4
scripts/docker-diagnostics.sh
Executable file → Normal file
@@ -105,12 +105,12 @@ echo -e "${BLUE} Recommendations ${NC}"
|
||||
echo -e "${BLUE}=====================================================${NC}"
|
||||
|
||||
echo -e "\n${YELLOW}For local development:${NC}"
|
||||
echo -e "1. ${GREEN}pnpm setup${NC} (initialize database and install dependencies)"
|
||||
echo -e "1. ${GREEN}bun run setup${NC} (initialize database and install dependencies)"
|
||||
echo -e "2. ${GREEN}./scripts/build-docker.sh --load${NC} (build and load into Docker)"
|
||||
echo -e "3. ${GREEN}docker-compose -f docker-compose.dev.yml up -d${NC} (start the development container)"
|
||||
|
||||
echo -e "\n${YELLOW}For production deployment (using Docker Compose):${NC}"
|
||||
echo -e "1. ${GREEN}pnpm setup${NC} (if not already done, to ensure database schema is ready)"
|
||||
echo -e "1. ${GREEN}bun run setup${NC} (if not already done, to ensure database schema is ready)"
|
||||
echo -e "2. ${GREEN}docker-compose --profile production up -d${NC} (start the production container)"
|
||||
|
||||
echo -e "\n${YELLOW}For CI/CD builds:${NC}"
|
||||
|
||||
188
scripts/gitea-mirror-lxc-installer.sh
Executable file
188
scripts/gitea-mirror-lxc-installer.sh
Executable file
@@ -0,0 +1,188 @@
|
||||
#!/bin/bash
|
||||
# Gitea Mirror LXC Container Installer
|
||||
# This is a self-contained script to install Gitea Mirror in an LXC container
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-installer.sh | bash
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration variables - change these as needed
|
||||
INSTALL_DIR="/opt/gitea-mirror"
|
||||
REPO_URL="https://github.com/arunavo4/gitea-mirror.git"
|
||||
SERVICE_USER="gitea-mirror"
|
||||
PORT=4321
|
||||
|
||||
# Color codes for better readability
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Print banner
|
||||
echo -e "${BLUE}"
|
||||
echo "╔════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ Gitea Mirror LXC Container Installer ║"
|
||||
echo "║ ║"
|
||||
echo "╚════════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
|
||||
# Ensure script is run as root
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e "${RED}This script must be run as root${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Starting Gitea Mirror installation...${NC}"
|
||||
|
||||
# Check if we're in an LXC container
|
||||
if [ -d /proc/vz ] && [ ! -d /proc/bc ]; then
|
||||
echo -e "${YELLOW}Running in an OpenVZ container. Some features may not work.${NC}"
|
||||
elif [ -f /proc/1/environ ] && grep -q container=lxc /proc/1/environ; then
|
||||
echo -e "${GREEN}Running in an LXC container. Good!${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Not running in a container. This script is designed for LXC containers.${NC}"
|
||||
read -p "Continue anyway? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${RED}Installation aborted.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install dependencies
|
||||
echo -e "${BLUE}Step 1/7: Installing dependencies...${NC}"
|
||||
apt update
|
||||
apt install -y curl git sqlite3 build-essential openssl
|
||||
|
||||
# Create service user
|
||||
echo -e "${BLUE}Step 2/7: Creating service user...${NC}"
|
||||
if id "$SERVICE_USER" &>/dev/null; then
|
||||
echo -e "${YELLOW}User $SERVICE_USER already exists${NC}"
|
||||
else
|
||||
useradd -m -s /bin/bash "$SERVICE_USER"
|
||||
echo -e "${GREEN}Created user $SERVICE_USER${NC}"
|
||||
fi
|
||||
|
||||
# Install Bun
|
||||
echo -e "${BLUE}Step 3/7: Installing Bun runtime...${NC}"
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}Bun is already installed${NC}"
|
||||
bun --version
|
||||
else
|
||||
echo -e "${GREEN}Installing Bun...${NC}"
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
export BUN_INSTALL=${BUN_INSTALL:-"/root/.bun"}
|
||||
export PATH="$BUN_INSTALL/bin:$PATH"
|
||||
echo -e "${GREEN}Bun installed successfully${NC}"
|
||||
bun --version
|
||||
fi
|
||||
|
||||
# Clone repository
|
||||
echo -e "${BLUE}Step 4/7: Downloading Gitea Mirror...${NC}"
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
echo -e "${YELLOW}Directory $INSTALL_DIR already exists${NC}"
|
||||
read -p "Update existing installation? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
cd "$INSTALL_DIR"
|
||||
git pull
|
||||
echo -e "${GREEN}Repository updated${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Using existing installation${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}Cloning repository...${NC}"
|
||||
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||
echo -e "${GREEN}Repository cloned to $INSTALL_DIR${NC}"
|
||||
fi
|
||||
|
||||
# Set up application
|
||||
echo -e "${BLUE}Step 5/7: Setting up application...${NC}"
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
# Create data directory with proper permissions
|
||||
mkdir -p data
|
||||
chown -R "$SERVICE_USER:$SERVICE_USER" data
|
||||
|
||||
# Install dependencies and build
|
||||
echo -e "${GREEN}Installing dependencies and building application...${NC}"
|
||||
bun install
|
||||
bun run build
|
||||
|
||||
# Initialize database if it doesn't exist
|
||||
echo -e "${GREEN}Initializing database...${NC}"
|
||||
if [ ! -f "data/gitea-mirror.db" ]; then
|
||||
bun run manage-db init
|
||||
chown "$SERVICE_USER:$SERVICE_USER" data/gitea-mirror.db
|
||||
fi
|
||||
|
||||
# Generate a random JWT secret if not provided
|
||||
JWT_SECRET=${JWT_SECRET:-$(openssl rand -hex 32)}
|
||||
|
||||
# Create systemd service
|
||||
echo -e "${BLUE}Step 6/7: Creating systemd service...${NC}"
|
||||
|
||||
# Store Bun path in a variable for better maintainability
|
||||
BUN_PATH=$(command -v bun)
|
||||
echo -e "${GREEN}Using Bun from: $BUN_PATH${NC}"
|
||||
|
||||
cat >/etc/systemd/system/gitea-mirror.service <<SERVICE
|
||||
[Unit]
|
||||
Description=Gitea Mirror
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart=$BUN_PATH dist/server/entry.mjs
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
User=$SERVICE_USER
|
||||
Group=$SERVICE_USER
|
||||
Environment=NODE_ENV=production
|
||||
Environment=HOST=0.0.0.0
|
||||
Environment=PORT=$PORT
|
||||
Environment=DATABASE_URL=file:data/gitea-mirror.db
|
||||
Environment=JWT_SECRET=${JWT_SECRET}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SERVICE
|
||||
|
||||
# Start service
|
||||
echo -e "${BLUE}Step 7/7: Starting service...${NC}"
|
||||
systemctl daemon-reload
|
||||
systemctl enable gitea-mirror.service
|
||||
systemctl start gitea-mirror.service
|
||||
|
||||
# Check if service started successfully
|
||||
if systemctl is-active --quiet gitea-mirror.service; then
|
||||
echo -e "${GREEN}Gitea Mirror service started successfully!${NC}"
|
||||
else
|
||||
echo -e "${RED}Failed to start Gitea Mirror service. Check logs with: journalctl -u gitea-mirror${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get IP address
|
||||
IP_ADDRESS=$(hostname -I | awk '{print $1}')
|
||||
|
||||
# Print success message
|
||||
echo -e "${GREEN}"
|
||||
echo "╔════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ Gitea Mirror Installation Complete ║"
|
||||
echo "║ ║"
|
||||
echo "╚════════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
echo -e "${GREEN}Gitea Mirror is now running at: http://$IP_ADDRESS:$PORT${NC}"
|
||||
echo
|
||||
echo -e "${YELLOW}Important security information:${NC}"
|
||||
echo -e "JWT_SECRET: ${JWT_SECRET}"
|
||||
echo -e "${YELLOW}Please save this JWT_SECRET in a secure location.${NC}"
|
||||
echo
|
||||
echo -e "${BLUE}To check service status:${NC} systemctl status gitea-mirror"
|
||||
echo -e "${BLUE}To view logs:${NC} journalctl -u gitea-mirror -f"
|
||||
echo -e "${BLUE}Data directory:${NC} $INSTALL_DIR/data"
|
||||
echo
|
||||
echo -e "${GREEN}Thank you for installing Gitea Mirror!${NC}"
|
||||
29
scripts/make-events-old.ts
Normal file
29
scripts/make-events-old.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Script to make events appear older for testing cleanup
|
||||
*/
|
||||
|
||||
import { db, events } from "../src/lib/db";
|
||||
|
||||
async function makeEventsOld() {
|
||||
try {
|
||||
console.log("Making events appear older...");
|
||||
|
||||
// Calculate a timestamp from 2 days ago
|
||||
const oldDate = new Date();
|
||||
oldDate.setDate(oldDate.getDate() - 2);
|
||||
|
||||
// Update all events to have an older timestamp
|
||||
const result = await db
|
||||
.update(events)
|
||||
.set({ createdAt: oldDate });
|
||||
|
||||
console.log(`Updated ${result.changes || 0} events to appear older`);
|
||||
} catch (error) {
|
||||
console.error("Error updating event timestamps:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the function
|
||||
makeEventsOld();
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { client, db } from "../src/lib/db";
|
||||
import { configs } from "../src/lib/db";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// Command line arguments
|
||||
@@ -21,61 +20,66 @@ const dataDbFile = path.join(dataDir, "gitea-mirror.db");
|
||||
const dataDevDbFile = path.join(dataDir, "gitea-mirror-dev.db");
|
||||
|
||||
// Database path - ensure we use absolute path
|
||||
const dbPath =
|
||||
process.env.DATABASE_URL || `file:${path.join(dataDir, "gitea-mirror.db")}`;
|
||||
const dbPath = path.join(dataDir, "gitea-mirror.db");
|
||||
|
||||
/**
|
||||
* Ensure all required tables exist
|
||||
*/
|
||||
async function ensureTablesExist() {
|
||||
// Create or open the database
|
||||
const db = new Database(dbPath);
|
||||
|
||||
const requiredTables = [
|
||||
"users",
|
||||
"configs",
|
||||
"repositories",
|
||||
"organizations",
|
||||
"mirror_jobs",
|
||||
"events",
|
||||
];
|
||||
|
||||
for (const table of requiredTables) {
|
||||
try {
|
||||
await client.execute(`SELECT 1 FROM ${table} LIMIT 1`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("SQLITE_ERROR")) {
|
||||
// Check if table exists
|
||||
const result = db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`).get();
|
||||
|
||||
if (!result) {
|
||||
console.warn(`⚠️ Table '${table}' is missing. Creating it now...`);
|
||||
|
||||
switch (table) {
|
||||
case "users":
|
||||
await client.execute(
|
||||
`CREATE TABLE users (
|
||||
db.exec(`
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)`
|
||||
);
|
||||
)
|
||||
`);
|
||||
break;
|
||||
case "configs":
|
||||
await client.execute(
|
||||
`CREATE TABLE configs (
|
||||
db.exec(`
|
||||
CREATE TABLE configs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
github_config TEXT NOT NULL,
|
||||
gitea_config TEXT NOT NULL,
|
||||
include TEXT NOT NULL DEFAULT '[]',
|
||||
include TEXT NOT NULL DEFAULT '["*"]',
|
||||
exclude TEXT NOT NULL DEFAULT '[]',
|
||||
schedule_config TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)`
|
||||
);
|
||||
)
|
||||
`);
|
||||
break;
|
||||
case "repositories":
|
||||
await client.execute(
|
||||
`CREATE TABLE repositories (
|
||||
db.exec(`
|
||||
CREATE TABLE repositories (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
@@ -104,12 +108,12 @@ async function ensureTablesExist() {
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
)`
|
||||
);
|
||||
)
|
||||
`);
|
||||
break;
|
||||
case "organizations":
|
||||
await client.execute(
|
||||
`CREATE TABLE organizations (
|
||||
db.exec(`
|
||||
CREATE TABLE organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
@@ -125,12 +129,12 @@ async function ensureTablesExist() {
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
)`
|
||||
);
|
||||
)
|
||||
`);
|
||||
break;
|
||||
case "mirror_jobs":
|
||||
await client.execute(
|
||||
`CREATE TABLE mirror_jobs (
|
||||
db.exec(`
|
||||
CREATE TABLE mirror_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
repository_id TEXT,
|
||||
@@ -142,15 +146,33 @@ async function ensureTablesExist() {
|
||||
message TEXT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)`
|
||||
);
|
||||
)
|
||||
`);
|
||||
break;
|
||||
case "events":
|
||||
db.exec(`
|
||||
CREATE TABLE events (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
read INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
db.exec(`
|
||||
CREATE INDEX idx_events_user_channel ON events(user_id, channel);
|
||||
CREATE INDEX idx_events_created_at ON events(created_at);
|
||||
CREATE INDEX idx_events_read ON events(read);
|
||||
`);
|
||||
break;
|
||||
}
|
||||
console.log(`✅ Table '${table}' created successfully.`);
|
||||
} else {
|
||||
console.error(`❌ Error checking table '${table}':`, error);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error checking table '${table}':`, error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +190,7 @@ async function checkDatabase() {
|
||||
);
|
||||
console.warn("This file should be in the data directory.");
|
||||
console.warn(
|
||||
'Run "pnpm manage-db fix" to fix this issue or "pnpm cleanup-db" to remove it.'
|
||||
'Run "bun run manage-db fix" to fix this issue or "bun run cleanup-db" to remove it.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,10 +202,11 @@ async function checkDatabase() {
|
||||
|
||||
// Check for users
|
||||
try {
|
||||
const userCountResult = await client.execute(
|
||||
`SELECT COUNT(*) as count FROM users`
|
||||
);
|
||||
const userCount = userCountResult.rows[0].count;
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Check for users
|
||||
const userCountResult = db.query(`SELECT COUNT(*) as count FROM users`).get();
|
||||
const userCount = userCountResult?.count || 0;
|
||||
|
||||
if (userCount === 0) {
|
||||
console.log("ℹ️ No users found in the database.");
|
||||
@@ -197,10 +220,8 @@ async function checkDatabase() {
|
||||
}
|
||||
|
||||
// Check for configurations
|
||||
const configCountResult = await client.execute(
|
||||
`SELECT COUNT(*) as count FROM configs`
|
||||
);
|
||||
const configCount = configCountResult.rows[0].count;
|
||||
const configCountResult = db.query(`SELECT COUNT(*) as count FROM configs`).get();
|
||||
const configCount = configCountResult?.count || 0;
|
||||
|
||||
if (configCount === 0) {
|
||||
console.log("ℹ️ No configurations found in the database.");
|
||||
@@ -215,12 +236,12 @@ async function checkDatabase() {
|
||||
} catch (error) {
|
||||
console.error("❌ Error connecting to the database:", error);
|
||||
console.warn(
|
||||
'The database file might be corrupted. Consider running "pnpm manage-db init" to recreate it.'
|
||||
'The database file might be corrupted. Consider running "bun run manage-db init" to recreate it.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn("⚠️ WARNING: Database file not found in data directory.");
|
||||
console.warn('Run "pnpm manage-db init" to create it.');
|
||||
console.warn('Run "bun run manage-db init" to create it.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,15 +256,16 @@ async function initializeDatabase() {
|
||||
if (fs.existsSync(dataDbFile)) {
|
||||
console.log("⚠️ Database already exists at data/gitea-mirror.db");
|
||||
console.log(
|
||||
'If you want to recreate the database, run "pnpm cleanup-db" first.'
|
||||
'If you want to recreate the database, run "bun run cleanup-db" first.'
|
||||
);
|
||||
console.log(
|
||||
'Or use "pnpm manage-db reset-users" to just remove users without recreating tables.'
|
||||
'Or use "bun run manage-db reset-users" to just remove users without recreating tables.'
|
||||
);
|
||||
|
||||
// Check if we can connect to it
|
||||
try {
|
||||
await client.execute(`SELECT COUNT(*) as count FROM users`);
|
||||
const db = new Database(dbPath);
|
||||
db.query(`SELECT COUNT(*) as count FROM users`).get();
|
||||
console.log("✅ Database is valid and accessible.");
|
||||
return;
|
||||
} catch (error) {
|
||||
@@ -257,135 +279,136 @@ async function initializeDatabase() {
|
||||
console.log(`Initializing database at ${dbPath}...`);
|
||||
|
||||
try {
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Create tables if they don't exist
|
||||
await client.execute(
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)`
|
||||
);
|
||||
)
|
||||
`);
|
||||
|
||||
// NOTE: We no longer create a default admin user - user will create one via signup page
|
||||
|
||||
await client.execute(
|
||||
`CREATE TABLE IF NOT EXISTS configs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
github_config TEXT NOT NULL,
|
||||
gitea_config TEXT NOT NULL,
|
||||
include TEXT NOT NULL DEFAULT '["*"]',
|
||||
exclude TEXT NOT NULL DEFAULT '[]',
|
||||
schedule_config TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
`
|
||||
);
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS configs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
github_config TEXT NOT NULL,
|
||||
gitea_config TEXT NOT NULL,
|
||||
include TEXT NOT NULL DEFAULT '["*"]',
|
||||
exclude TEXT NOT NULL DEFAULT '[]',
|
||||
schedule_config TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
await client.execute(
|
||||
`CREATE TABLE IF NOT EXISTS repositories (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
clone_url TEXT NOT NULL,
|
||||
owner TEXT NOT NULL,
|
||||
organization TEXT,
|
||||
mirrored_location TEXT DEFAULT '',
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS repositories (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
clone_url TEXT NOT NULL,
|
||||
owner TEXT NOT NULL,
|
||||
organization TEXT,
|
||||
mirrored_location TEXT DEFAULT '',
|
||||
is_private INTEGER NOT NULL DEFAULT 0,
|
||||
is_fork INTEGER NOT NULL DEFAULT 0,
|
||||
forked_from TEXT,
|
||||
has_issues INTEGER NOT NULL DEFAULT 0,
|
||||
is_starred INTEGER NOT NULL DEFAULT 0,
|
||||
is_archived INTEGER NOT NULL DEFAULT 0,
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
has_lfs INTEGER NOT NULL DEFAULT 0,
|
||||
has_submodules INTEGER NOT NULL DEFAULT 0,
|
||||
default_branch TEXT NOT NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'public',
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
last_mirrored INTEGER,
|
||||
error_message TEXT,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
)
|
||||
`);
|
||||
|
||||
is_private INTEGER NOT NULL DEFAULT 0,
|
||||
is_fork INTEGER NOT NULL DEFAULT 0,
|
||||
forked_from TEXT,
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
avatar_url TEXT NOT NULL,
|
||||
membership_role TEXT NOT NULL DEFAULT 'member',
|
||||
is_included INTEGER NOT NULL DEFAULT 1,
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
last_mirrored INTEGER,
|
||||
error_message TEXT,
|
||||
repository_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
)
|
||||
`);
|
||||
|
||||
has_issues INTEGER NOT NULL DEFAULT 0,
|
||||
is_starred INTEGER NOT NULL DEFAULT 0,
|
||||
is_archived INTEGER NOT NULL DEFAULT 0,
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS mirror_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
repository_id TEXT,
|
||||
repository_name TEXT,
|
||||
organization_id TEXT,
|
||||
organization_name TEXT,
|
||||
details TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
message TEXT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
has_lfs INTEGER NOT NULL DEFAULT 0,
|
||||
has_submodules INTEGER NOT NULL DEFAULT 0,
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
read INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
default_branch TEXT NOT NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'public',
|
||||
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
last_mirrored INTEGER,
|
||||
error_message TEXT,
|
||||
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
);
|
||||
`
|
||||
);
|
||||
|
||||
await client.execute(
|
||||
`CREATE TABLE IF NOT EXISTS organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
|
||||
avatar_url TEXT NOT NULL,
|
||||
membership_role TEXT NOT NULL DEFAULT 'member',
|
||||
|
||||
is_included INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
last_mirrored INTEGER,
|
||||
error_message TEXT,
|
||||
|
||||
repository_count INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
);
|
||||
`
|
||||
);
|
||||
|
||||
await client.execute(
|
||||
`CREATE TABLE IF NOT EXISTS mirror_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
repository_id TEXT,
|
||||
repository_name TEXT,
|
||||
organization_id TEXT,
|
||||
organization_name TEXT,
|
||||
details TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
message TEXT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
`
|
||||
);
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_events_user_channel ON events(user_id, channel);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_read ON events(read);
|
||||
`);
|
||||
|
||||
// Insert default config if none exists
|
||||
const configCountResult = await client.execute(
|
||||
`SELECT COUNT(*) as count FROM configs`
|
||||
);
|
||||
const configCount = configCountResult.rows[0].count;
|
||||
const configCountResult = db.query(`SELECT COUNT(*) as count FROM configs`).get();
|
||||
const configCount = configCountResult?.count || 0;
|
||||
|
||||
if (configCount === 0) {
|
||||
// Get the first user
|
||||
const firstUserResult = await client.execute(
|
||||
`SELECT id FROM users LIMIT 1`
|
||||
);
|
||||
if (firstUserResult.rows.length > 0) {
|
||||
const userId = firstUserResult.rows[0].id;
|
||||
const firstUserResult = db.query(`SELECT id FROM users LIMIT 1`).get();
|
||||
|
||||
if (firstUserResult) {
|
||||
const userId = firstUserResult.id;
|
||||
const configId = uuidv4();
|
||||
const githubConfig = JSON.stringify({
|
||||
username: process.env.GITHUB_USERNAME || "",
|
||||
@@ -415,24 +438,23 @@ async function initializeDatabase() {
|
||||
nextRun: null,
|
||||
});
|
||||
|
||||
await client.execute(
|
||||
`
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO configs (id, user_id, name, is_active, github_config, gitea_config, include, exclude, schedule_config, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
configId,
|
||||
userId,
|
||||
"Default Configuration",
|
||||
1,
|
||||
githubConfig,
|
||||
giteaConfig,
|
||||
include,
|
||||
exclude,
|
||||
scheduleConfig,
|
||||
Date.now(),
|
||||
Date.now(),
|
||||
]
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
configId,
|
||||
userId,
|
||||
"Default Configuration",
|
||||
1,
|
||||
githubConfig,
|
||||
giteaConfig,
|
||||
include,
|
||||
exclude,
|
||||
scheduleConfig,
|
||||
Date.now(),
|
||||
Date.now()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -452,21 +474,20 @@ async function resetUsers() {
|
||||
|
||||
try {
|
||||
// Check if the database exists
|
||||
const dbFilePath = dbPath.replace("file:", "");
|
||||
const doesDbExist = fs.existsSync(dbFilePath);
|
||||
const doesDbExist = fs.existsSync(dbPath);
|
||||
|
||||
if (!doesDbExist) {
|
||||
console.log(
|
||||
"❌ Database file doesn't exist. Run 'pnpm manage-db init' first to create it."
|
||||
"❌ Database file doesn't exist. Run 'bun run manage-db init' first to create it."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Count existing users
|
||||
const userCountResult = await client.execute(
|
||||
`SELECT COUNT(*) as count FROM users`
|
||||
);
|
||||
const userCount = userCountResult.rows[0].count;
|
||||
const userCountResult = db.query(`SELECT COUNT(*) as count FROM users`).get();
|
||||
const userCount = userCountResult?.count || 0;
|
||||
|
||||
if (userCount === 0) {
|
||||
console.log("ℹ️ No users found in the database. Nothing to reset.");
|
||||
@@ -474,63 +495,43 @@ async function resetUsers() {
|
||||
}
|
||||
|
||||
// Delete all users
|
||||
await client.execute(`DELETE FROM users`);
|
||||
db.exec(`DELETE FROM users`);
|
||||
console.log(`✅ Deleted ${userCount} users from the database.`);
|
||||
|
||||
// Check dependent configurations that need to be removed
|
||||
const configCount = await client.execute(
|
||||
`SELECT COUNT(*) as count FROM configs`
|
||||
);
|
||||
const configCountResult = db.query(`SELECT COUNT(*) as count FROM configs`).get();
|
||||
const configCount = configCountResult?.count || 0;
|
||||
|
||||
if (
|
||||
configCount.rows &&
|
||||
configCount.rows[0] &&
|
||||
Number(configCount.rows[0].count) > 0
|
||||
) {
|
||||
await client.execute(`DELETE FROM configs`);
|
||||
console.log(`✅ Deleted ${configCount.rows[0].count} configurations.`);
|
||||
if (configCount > 0) {
|
||||
db.exec(`DELETE FROM configs`);
|
||||
console.log(`✅ Deleted ${configCount} configurations.`);
|
||||
}
|
||||
|
||||
// Check for dependent repositories
|
||||
const repoCount = await client.execute(
|
||||
`SELECT COUNT(*) as count FROM repositories`
|
||||
);
|
||||
const repoCountResult = db.query(`SELECT COUNT(*) as count FROM repositories`).get();
|
||||
const repoCount = repoCountResult?.count || 0;
|
||||
|
||||
if (
|
||||
repoCount.rows &&
|
||||
repoCount.rows[0] &&
|
||||
Number(repoCount.rows[0].count) > 0
|
||||
) {
|
||||
await client.execute(`DELETE FROM repositories`);
|
||||
console.log(`✅ Deleted ${repoCount.rows[0].count} repositories.`);
|
||||
if (repoCount > 0) {
|
||||
db.exec(`DELETE FROM repositories`);
|
||||
console.log(`✅ Deleted ${repoCount} repositories.`);
|
||||
}
|
||||
|
||||
// Check for dependent organizations
|
||||
const orgCount = await client.execute(
|
||||
`SELECT COUNT(*) as count FROM organizations`
|
||||
);
|
||||
const orgCountResult = db.query(`SELECT COUNT(*) as count FROM organizations`).get();
|
||||
const orgCount = orgCountResult?.count || 0;
|
||||
|
||||
if (
|
||||
orgCount.rows &&
|
||||
orgCount.rows[0] &&
|
||||
Number(orgCount.rows[0].count) > 0
|
||||
) {
|
||||
await client.execute(`DELETE FROM organizations`);
|
||||
console.log(`✅ Deleted ${orgCount.rows[0].count} organizations.`);
|
||||
if (orgCount > 0) {
|
||||
db.exec(`DELETE FROM organizations`);
|
||||
console.log(`✅ Deleted ${orgCount} organizations.`);
|
||||
}
|
||||
|
||||
// Check for dependent mirror jobs
|
||||
const jobCount = await client.execute(
|
||||
`SELECT COUNT(*) as count FROM mirror_jobs`
|
||||
);
|
||||
const jobCountResult = db.query(`SELECT COUNT(*) as count FROM mirror_jobs`).get();
|
||||
const jobCount = jobCountResult?.count || 0;
|
||||
|
||||
if (
|
||||
jobCount.rows &&
|
||||
jobCount.rows[0] &&
|
||||
Number(jobCount.rows[0].count) > 0
|
||||
) {
|
||||
await client.execute(`DELETE FROM mirror_jobs`);
|
||||
console.log(`✅ Deleted ${jobCount.rows[0].count} mirror jobs.`);
|
||||
if (jobCount > 0) {
|
||||
db.exec(`DELETE FROM mirror_jobs`);
|
||||
console.log(`✅ Deleted ${jobCount} mirror jobs.`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
@@ -629,19 +630,20 @@ async function fixDatabaseIssues() {
|
||||
console.warn(
|
||||
"⚠️ WARNING: Production database file not found in data directory."
|
||||
);
|
||||
console.warn('Run "pnpm manage-db init" to create it.');
|
||||
console.warn('Run "bun run manage-db init" to create it.');
|
||||
} else {
|
||||
console.log("✅ Production database file found in data directory.");
|
||||
|
||||
// Check if we can connect to the database
|
||||
try {
|
||||
// Try to query the database
|
||||
await db.select().from(configs).limit(1);
|
||||
const db = new Database(dbPath);
|
||||
db.query(`SELECT 1 FROM sqlite_master LIMIT 1`).get();
|
||||
console.log(`✅ Successfully connected to the database.`);
|
||||
} catch (error) {
|
||||
console.error("❌ Error connecting to the database:", error);
|
||||
console.warn(
|
||||
'The database file might be corrupted. Consider running "pnpm manage-db init" to recreate it.'
|
||||
'The database file might be corrupted. Consider running "bun run manage-db init" to recreate it.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -692,7 +694,7 @@ Available commands:
|
||||
reset-users - Remove all users and their data
|
||||
auto - Automatic mode: check, fix, and initialize if needed
|
||||
|
||||
Usage: pnpm manage-db [command]
|
||||
Usage: bun run manage-db [command]
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
27
scripts/mark-events-read.ts
Normal file
27
scripts/mark-events-read.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Script to mark all events as read
|
||||
*/
|
||||
|
||||
import { db, events } from "../src/lib/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
async function markEventsAsRead() {
|
||||
try {
|
||||
console.log("Marking all events as read...");
|
||||
|
||||
// Update all events to mark them as read
|
||||
const result = await db
|
||||
.update(events)
|
||||
.set({ read: true })
|
||||
.where(eq(events.read, false));
|
||||
|
||||
console.log(`Marked ${result.changes || 0} events as read`);
|
||||
} catch (error) {
|
||||
console.error("Error marking events as read:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the function
|
||||
markEventsAsRead();
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { GitHubConfigForm } from "./GitHubConfigForm";
|
||||
import { GiteaConfigForm } from "./GiteaConfigForm";
|
||||
import { ScheduleConfigForm } from "./ScheduleConfigForm";
|
||||
} from '@/components/ui/card';
|
||||
import { GitHubConfigForm } from './GitHubConfigForm';
|
||||
import { GiteaConfigForm } from './GiteaConfigForm';
|
||||
import { ScheduleConfigForm } from './ScheduleConfigForm';
|
||||
import type {
|
||||
ConfigApiResponse,
|
||||
GiteaConfig,
|
||||
@@ -16,12 +16,13 @@ import type {
|
||||
SaveConfigApiRequest,
|
||||
SaveConfigApiResponse,
|
||||
ScheduleConfig,
|
||||
} from "@/types/config";
|
||||
import { Button } from "../ui/button";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { apiRequest } from "@/lib/utils";
|
||||
import { Copy, CopyCheck, RefreshCw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
} from '@/types/config';
|
||||
import { Button } from '../ui/button';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { apiRequest } from '@/lib/utils';
|
||||
import { Copy, CopyCheck, RefreshCw } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
type ConfigState = {
|
||||
githubConfig: GitHubConfig;
|
||||
@@ -32,8 +33,8 @@ type ConfigState = {
|
||||
export function ConfigTabs() {
|
||||
const [config, setConfig] = useState<ConfigState>({
|
||||
githubConfig: {
|
||||
username: "",
|
||||
token: "",
|
||||
username: '',
|
||||
token: '',
|
||||
skipForks: false,
|
||||
privateRepositories: false,
|
||||
mirrorIssues: false,
|
||||
@@ -41,16 +42,14 @@ export function ConfigTabs() {
|
||||
preserveOrgStructure: false,
|
||||
skipStarredIssues: false,
|
||||
},
|
||||
|
||||
giteaConfig: {
|
||||
url: "",
|
||||
username: "",
|
||||
token: "",
|
||||
organization: "github-mirrors",
|
||||
visibility: "public",
|
||||
starredReposOrg: "github",
|
||||
url: '',
|
||||
username: '',
|
||||
token: '',
|
||||
organization: 'github-mirrors',
|
||||
visibility: 'public',
|
||||
starredReposOrg: 'github',
|
||||
},
|
||||
|
||||
scheduleConfig: {
|
||||
enabled: false,
|
||||
interval: 3600,
|
||||
@@ -58,27 +57,21 @@ export function ConfigTabs() {
|
||||
});
|
||||
const { user, refreshUser } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [dockerCode, setDockerCode] = useState<string>("");
|
||||
const [dockerCode, setDockerCode] = useState<string>('');
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||
const [isSyncing, setIsSyncing] = useState<boolean>(false);
|
||||
const [isConfigSaved, setIsConfigSaved] = useState<boolean>(false);
|
||||
|
||||
// Check if all required fields are filled to enable the Save Configuration button
|
||||
const isConfigFormValid = (): boolean => {
|
||||
const { githubConfig, giteaConfig } = config;
|
||||
|
||||
// Check GitHub required fields
|
||||
const isGitHubValid = !!(
|
||||
githubConfig.username?.trim() && githubConfig.token?.trim()
|
||||
githubConfig.username.trim() && githubConfig.token.trim()
|
||||
);
|
||||
|
||||
// Check Gitea required fields
|
||||
const isGiteaValid = !!(
|
||||
giteaConfig.url?.trim() &&
|
||||
giteaConfig.username?.trim() &&
|
||||
giteaConfig.token?.trim()
|
||||
giteaConfig.url.trim() &&
|
||||
giteaConfig.username.trim() &&
|
||||
giteaConfig.token.trim()
|
||||
);
|
||||
|
||||
return isGitHubValid && isGiteaValid;
|
||||
};
|
||||
|
||||
@@ -86,11 +79,12 @@ export function ConfigTabs() {
|
||||
const updateLastAndNextRun = () => {
|
||||
const lastRun = config.scheduleConfig.lastRun
|
||||
? new Date(config.scheduleConfig.lastRun)
|
||||
: new Date(); // fallback to now if lastRun is null
|
||||
: new Date();
|
||||
const intervalInSeconds = config.scheduleConfig.interval;
|
||||
const nextRun = new Date(lastRun.getTime() + intervalInSeconds * 1000);
|
||||
|
||||
setConfig((prev) => ({
|
||||
const nextRun = new Date(
|
||||
lastRun.getTime() + intervalInSeconds * 1000,
|
||||
);
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
scheduleConfig: {
|
||||
...prev.scheduleConfig,
|
||||
@@ -99,37 +93,31 @@ export function ConfigTabs() {
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
updateLastAndNextRun();
|
||||
}, [config.scheduleConfig.interval]);
|
||||
|
||||
const handleImportGitHubData = async () => {
|
||||
if (!user?.id) return;
|
||||
setIsSyncing(true);
|
||||
try {
|
||||
if (!user?.id) return;
|
||||
|
||||
setIsSyncing(true);
|
||||
|
||||
const result = await apiRequest<{ success: boolean; message?: string }>(
|
||||
`/sync?userId=${user.id}`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
{ method: 'POST' },
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
toast.success(
|
||||
"GitHub data imported successfully! Head to the Dashboard to start mirroring repositories."
|
||||
);
|
||||
} else {
|
||||
toast.error(
|
||||
`Failed to import GitHub data: ${result.message || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
result.success
|
||||
? toast.success(
|
||||
'GitHub data imported successfully! Head to the Dashboard to start mirroring repositories.',
|
||||
)
|
||||
: toast.error(
|
||||
`Failed to import GitHub data: ${
|
||||
result.message || 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Error importing GitHub data: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
}`,
|
||||
);
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
@@ -137,94 +125,76 @@ export function ConfigTabs() {
|
||||
};
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
if (!user?.id) return;
|
||||
const reqPayload: SaveConfigApiRequest = {
|
||||
userId: user.id,
|
||||
githubConfig: config.githubConfig,
|
||||
giteaConfig: config.giteaConfig,
|
||||
scheduleConfig: config.scheduleConfig,
|
||||
};
|
||||
try {
|
||||
if (!user || !user.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reqPyload: SaveConfigApiRequest = {
|
||||
userId: user.id,
|
||||
githubConfig: config.githubConfig,
|
||||
giteaConfig: config.giteaConfig,
|
||||
scheduleConfig: config.scheduleConfig,
|
||||
};
|
||||
const response = await fetch("/api/config", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(reqPyload),
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
});
|
||||
|
||||
const result: SaveConfigApiResponse = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
await refreshUser();
|
||||
setIsConfigSaved(true);
|
||||
|
||||
toast.success(
|
||||
"Configuration saved successfully! Now import your GitHub data to begin."
|
||||
'Configuration saved successfully! Now import your GitHub data to begin.',
|
||||
);
|
||||
} else {
|
||||
toast.error(
|
||||
`Failed to save configuration: ${result.message || "Unknown error"}`
|
||||
`Failed to save configuration: ${result.message || 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`An error occurred while saving the configuration: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
|
||||
const fetchConfig = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await apiRequest<ConfigApiResponse>(
|
||||
`/config?userId=${user.id}`,
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
{ method: 'GET' },
|
||||
);
|
||||
|
||||
// Check if we have a valid config response
|
||||
if (response && !response.error) {
|
||||
setConfig({
|
||||
githubConfig: response.githubConfig || config.githubConfig,
|
||||
giteaConfig: response.giteaConfig || config.giteaConfig,
|
||||
scheduleConfig: response.scheduleConfig || config.scheduleConfig,
|
||||
githubConfig:
|
||||
response.githubConfig || config.githubConfig,
|
||||
giteaConfig:
|
||||
response.giteaConfig || config.giteaConfig,
|
||||
scheduleConfig:
|
||||
response.scheduleConfig || config.scheduleConfig,
|
||||
});
|
||||
|
||||
// If we got a valid config from the server, it means it was previously saved
|
||||
if (response.id) {
|
||||
setIsConfigSaved(true);
|
||||
}
|
||||
if (response.id) setIsConfigSaved(true);
|
||||
}
|
||||
// If there's an error, we'll just use the default config defined in state
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
// Don't show error for first-time users, just use the default config
|
||||
console.warn("Could not fetch configuration, using defaults:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
console.warn(
|
||||
'Could not fetch configuration, using defaults:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchConfig();
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
const generateDockerCode = () => {
|
||||
return `services:
|
||||
const generateDockerCode = () => `
|
||||
services:
|
||||
gitea-mirror:
|
||||
image: arunavo4/gitea-mirror:latest
|
||||
restart: unless-stopped
|
||||
@@ -243,27 +213,93 @@ export function ConfigTabs() {
|
||||
- GITEA_ORGANIZATION=${config.giteaConfig.organization}
|
||||
- GITEA_ORG_VISIBILITY=${config.giteaConfig.visibility}
|
||||
- DELAY=${config.scheduleConfig.interval}`;
|
||||
};
|
||||
|
||||
const code = generateDockerCode();
|
||||
setDockerCode(code);
|
||||
setDockerCode(generateDockerCode());
|
||||
}, [config]);
|
||||
|
||||
const handleCopyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => {
|
||||
setIsCopied(true);
|
||||
toast.success("Docker configuration copied to clipboard!");
|
||||
toast.success('Docker configuration copied to clipboard!');
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
},
|
||||
(err) => {
|
||||
toast.error("Could not copy text to clipboard.");
|
||||
}
|
||||
() => toast.error('Could not copy text to clipboard.'),
|
||||
);
|
||||
};
|
||||
|
||||
function ConfigCardSkeleton() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex-row justify-between">
|
||||
<div className="flex flex-col gap-y-1.5 m-0">
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<Skeleton className="h-4 w-72" />
|
||||
</div>
|
||||
<div className="flex gap-x-4">
|
||||
<Skeleton className="h-10 w-36" />
|
||||
<Skeleton className="h-10 w-36" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex gap-x-4">
|
||||
<div className="w-1/2 border rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Skeleton className="h-6 w-40" />
|
||||
<Skeleton className="h-9 w-32" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-20 w-full" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/2 border rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Skeleton className="h-6 w-40" />
|
||||
<Skeleton className="h-9 w-32" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-20 w-full" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border rounded-lg p-4">
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-16 w-full" />
|
||||
<Skeleton className="h-8 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function DockerConfigSkeleton() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-40" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</CardHeader>
|
||||
<CardContent className="relative">
|
||||
<Skeleton className="h-8 w-8 absolute top-4 right-10 rounded-md" />
|
||||
<Skeleton className="h-48 w-full rounded-md" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<div>loading...</div>
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<ConfigCardSkeleton />
|
||||
<DockerConfigSkeleton />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<Card>
|
||||
@@ -275,17 +311,16 @@ export function ConfigTabs() {
|
||||
mirroring.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-x-4">
|
||||
<Button
|
||||
onClick={handleImportGitHubData}
|
||||
disabled={isSyncing || !isConfigSaved}
|
||||
title={
|
||||
!isConfigSaved
|
||||
? "Save configuration first"
|
||||
? 'Save configuration first'
|
||||
: isSyncing
|
||||
? "Import in progress"
|
||||
: "Import GitHub Data"
|
||||
? 'Import in progress'
|
||||
: 'Import GitHub Data'
|
||||
}
|
||||
>
|
||||
{isSyncing ? (
|
||||
@@ -305,66 +340,57 @@ export function ConfigTabs() {
|
||||
disabled={!isConfigFormValid()}
|
||||
title={
|
||||
!isConfigFormValid()
|
||||
? "Please fill all required fields"
|
||||
: "Save Configuration"
|
||||
? 'Please fill all required fields'
|
||||
: 'Save Configuration'
|
||||
}
|
||||
>
|
||||
Save Configuration
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex gap-x-4">
|
||||
<GitHubConfigForm
|
||||
config={config.githubConfig}
|
||||
setConfig={(update) =>
|
||||
setConfig((prev) => ({
|
||||
setConfig={update =>
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
githubConfig:
|
||||
typeof update === "function"
|
||||
typeof update === 'function'
|
||||
? update(prev.githubConfig)
|
||||
: update,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<GiteaConfigForm
|
||||
config={config?.giteaConfig ?? ({} as GiteaConfig)}
|
||||
setConfig={(update) =>
|
||||
setConfig((prev) => ({
|
||||
config={config.giteaConfig}
|
||||
setConfig={update =>
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
giteaConfig:
|
||||
typeof update === "function"
|
||||
typeof update === 'function'
|
||||
? update(prev.giteaConfig)
|
||||
: update,
|
||||
githubConfig: prev?.githubConfig ?? ({} as GitHubConfig),
|
||||
scheduleConfig:
|
||||
prev?.scheduleConfig ?? ({} as ScheduleConfig),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ScheduleConfigForm
|
||||
config={config?.scheduleConfig ?? ({} as ScheduleConfig)}
|
||||
setConfig={(update) =>
|
||||
setConfig((prev) => ({
|
||||
config={config.scheduleConfig}
|
||||
setConfig={update =>
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
scheduleConfig:
|
||||
typeof update === "function"
|
||||
typeof update === 'function'
|
||||
? update(prev.scheduleConfig)
|
||||
: update,
|
||||
githubConfig: prev?.githubConfig ?? ({} as GitHubConfig),
|
||||
giteaConfig: prev?.giteaConfig ?? ({} as GiteaConfig),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Docker Configuration</CardTitle>
|
||||
@@ -372,7 +398,6 @@ export function ConfigTabs() {
|
||||
Equivalent Docker configuration for your current settings.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="relative">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -386,7 +411,6 @@ export function ConfigTabs() {
|
||||
<Copy className="text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<pre className="bg-muted p-4 rounded-md overflow-auto text-sm">
|
||||
{dockerCode}
|
||||
</pre>
|
||||
|
||||
@@ -9,6 +9,8 @@ import { apiRequest } from "@/lib/utils";
|
||||
import type { DashboardApiResponse } from "@/types/dashboard";
|
||||
import { useSSE } from "@/hooks/useSEE";
|
||||
import { toast } from "sonner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export function Dashboard() {
|
||||
const { user } = useAuth();
|
||||
@@ -59,8 +61,6 @@ export function Dashboard() {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
const response = await apiRequest<DashboardApiResponse>(
|
||||
`/dashboard?userId=${user.id}`,
|
||||
{
|
||||
@@ -93,8 +93,61 @@ export function Dashboard() {
|
||||
fetchDashboardData();
|
||||
}, [user]);
|
||||
|
||||
// Status Card Skeleton component
|
||||
function StatusCardSkeleton() {
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardTitle>
|
||||
<Skeleton className="h-4 w-4 rounded-full" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-8 w-16 mb-1" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return isLoading || !connected ? (
|
||||
<div>loading...</div>
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatusCardSkeleton />
|
||||
<StatusCardSkeleton />
|
||||
<StatusCardSkeleton />
|
||||
<StatusCardSkeleton />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-x-6 items-start">
|
||||
{/* Repository List Skeleton */}
|
||||
<div className="w-1/2 border rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<Skeleton className="h-9 w-24" />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-16 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity Skeleton */}
|
||||
<div className="w-1/2 border rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<Skeleton className="h-9 w-24" />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-16 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
|
||||
@@ -4,9 +4,10 @@ import { SiGitea } from "react-icons/si";
|
||||
import { ModeToggle } from "@/components/theme/ModeToggle";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
import { toast } from "sonner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export function Header() {
|
||||
const { user, logout } = useAuth();
|
||||
const { user, logout, isLoading } = useAuth();
|
||||
|
||||
const handleLogout = async () => {
|
||||
toast.success("Logged out successfully");
|
||||
@@ -15,6 +16,16 @@ export function Header() {
|
||||
logout();
|
||||
};
|
||||
|
||||
// Auth buttons skeleton loader
|
||||
function AuthButtonsSkeleton() {
|
||||
return (
|
||||
<>
|
||||
<Skeleton className="h-10 w-10 rounded-full" /> {/* Avatar placeholder */}
|
||||
<Skeleton className="h-10 w-24" /> {/* Button placeholder */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="border-b bg-background">
|
||||
<div className="flex h-[4.5rem] items-center justify-between px-6">
|
||||
@@ -25,7 +36,10 @@ export function Header() {
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<ModeToggle />
|
||||
{user ? (
|
||||
|
||||
{isLoading ? (
|
||||
<AuthButtonsSkeleton />
|
||||
) : user ? (
|
||||
<>
|
||||
<Avatar>
|
||||
<AvatarImage src="" alt="@shadcn" />
|
||||
|
||||
@@ -22,7 +22,7 @@ The application is built using:
|
||||
- <span class="font-semibold text-foreground">React</span>: Component library for interactive UI elements
|
||||
- <span class="font-semibold text-foreground">Shadcn UI</span>: UI component library built on Tailwind CSS
|
||||
- <span class="font-semibold text-foreground">SQLite</span>: Database for storing configuration and state
|
||||
- <span class="font-semibold text-foreground">Node.js</span>: Runtime environment for the backend
|
||||
- <span class="font-semibold text-foreground">Bun</span>: Runtime environment for the backend
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
@@ -30,7 +30,7 @@ The application is built using:
|
||||
graph TD
|
||||
subgraph "Gitea Mirror"
|
||||
Frontend["Frontend<br/>(Astro)"]
|
||||
Backend["Backend<br/>(Node.js)"]
|
||||
Backend["Backend<br/>(Bun)"]
|
||||
Database["Database<br/>(SQLite)"]
|
||||
|
||||
Frontend <--> Backend
|
||||
@@ -60,9 +60,9 @@ Key frontend components:
|
||||
- **Configuration**: Settings for GitHub and Gitea connections
|
||||
- **Activity Log**: Detailed log of mirroring operations
|
||||
|
||||
### Backend (Node.js)
|
||||
### Backend (Bun)
|
||||
|
||||
The backend is built with Node.js and provides API endpoints for the frontend to interact with. It handles:
|
||||
The backend is built with Bun and provides API endpoints for the frontend to interact with. It handles:
|
||||
|
||||
- Authentication and user management
|
||||
- GitHub API integration
|
||||
|
||||
@@ -23,7 +23,7 @@ The following environment variables can be used to configure Gitea Mirror:
|
||||
|
||||
| Variable | Description | Default Value | Example |
|
||||
|----------|-------------|---------------|---------|
|
||||
| `NODE_ENV` | Node environment (development, production, test) | `development` | `production` |
|
||||
| `NODE_ENV` | Runtime environment (development, production, test) | `development` | `production` |
|
||||
| `DATABASE_URL` | SQLite database URL | `sqlite://data/gitea-mirror.db` | `sqlite://path/to/your/database.db` |
|
||||
| `JWT_SECRET` | Secret key for JWT authentication | `your-secret-key-change-this-in-production` | `your-secure-random-string` |
|
||||
| `HOST` | Server host | `localhost` | `0.0.0.0` |
|
||||
|
||||
@@ -16,7 +16,7 @@ Before you begin, make sure you have:
|
||||
|
||||
1. <span class="font-semibold text-foreground">A GitHub account with a personal access token</span>
|
||||
2. <span class="font-semibold text-foreground">A Gitea instance with an access token</span>
|
||||
3. <span class="font-semibold text-foreground">Docker and docker-compose (recommended) or Node.js 18+ installed</span>
|
||||
3. <span class="font-semibold text-foreground">Docker and docker-compose (recommended) or Bun 1.2.9+ installed</span>
|
||||
|
||||
## Installation Options
|
||||
|
||||
@@ -51,7 +51,7 @@ If you prefer to run the application directly on your system:
|
||||
|
||||
2. Run the quick setup script:
|
||||
```bash
|
||||
pnpm setup
|
||||
bun run setup
|
||||
```
|
||||
This installs dependencies and initializes the database.
|
||||
|
||||
@@ -59,13 +59,13 @@ If you prefer to run the application directly on your system:
|
||||
|
||||
**Development Mode:**
|
||||
```bash
|
||||
pnpm dev
|
||||
bun run dev
|
||||
```
|
||||
|
||||
**Production Mode:**
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm start
|
||||
bun run build
|
||||
bun run start
|
||||
```
|
||||
|
||||
4. Access the application at [http://localhost:4321](http://localhost:4321)
|
||||
|
||||
@@ -1,34 +1,61 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import type { MirrorJob } from "@/lib/db/schema";
|
||||
|
||||
interface UseSSEOptions {
|
||||
userId?: string;
|
||||
onMessage: (data: MirrorJob) => void;
|
||||
maxReconnectAttempts?: number;
|
||||
reconnectDelay?: number;
|
||||
}
|
||||
|
||||
export const useSSE = ({ userId, onMessage }: UseSSEOptions) => {
|
||||
export const useSSE = ({
|
||||
userId,
|
||||
onMessage,
|
||||
maxReconnectAttempts = 5,
|
||||
reconnectDelay = 3000
|
||||
}: UseSSEOptions) => {
|
||||
const [connected, setConnected] = useState<boolean>(false);
|
||||
const [reconnectCount, setReconnectCount] = useState<number>(0);
|
||||
const onMessageRef = useRef(onMessage);
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const reconnectTimeoutRef = useRef<number | null>(null);
|
||||
|
||||
// Update the ref when onMessage changes
|
||||
useEffect(() => {
|
||||
onMessageRef.current = onMessage;
|
||||
}, [onMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
// Create a stable connect function that can be called for reconnection
|
||||
const connect = useCallback(() => {
|
||||
if (!userId) return;
|
||||
|
||||
// Clean up any existing connection
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
}
|
||||
|
||||
// Clear any pending reconnect timeout
|
||||
if (reconnectTimeoutRef.current) {
|
||||
window.clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
// Create new EventSource connection
|
||||
const eventSource = new EventSource(`/api/sse?userId=${userId}`);
|
||||
eventSourceRef.current = eventSource;
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
try {
|
||||
// Check if this is an error message from our server
|
||||
if (event.data.startsWith('{"error":')) {
|
||||
console.warn("SSE server error:", event.data);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedMessage: MirrorJob = JSON.parse(event.data);
|
||||
|
||||
// console.log("Received new log:", parsedMessage);
|
||||
|
||||
onMessageRef.current(parsedMessage); // Use ref instead of prop directly
|
||||
onMessageRef.current(parsedMessage);
|
||||
} catch (error) {
|
||||
console.error("Error parsing message:", error);
|
||||
console.error("Error parsing SSE message:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,19 +63,50 @@ export const useSSE = ({ userId, onMessage }: UseSSEOptions) => {
|
||||
|
||||
eventSource.onopen = () => {
|
||||
setConnected(true);
|
||||
setReconnectCount(0); // Reset reconnect counter on successful connection
|
||||
console.log(`Connected to SSE for user: ${userId}`);
|
||||
};
|
||||
|
||||
eventSource.onerror = () => {
|
||||
console.error("SSE connection error");
|
||||
eventSource.onerror = (error) => {
|
||||
console.error("SSE connection error:", error);
|
||||
setConnected(false);
|
||||
eventSource.close();
|
||||
};
|
||||
eventSourceRef.current = null;
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
// Attempt to reconnect if we haven't exceeded max attempts
|
||||
if (reconnectCount < maxReconnectAttempts) {
|
||||
const nextReconnectDelay = Math.min(reconnectDelay * Math.pow(1.5, reconnectCount), 30000);
|
||||
console.log(`Attempting to reconnect in ${nextReconnectDelay}ms (attempt ${reconnectCount + 1}/${maxReconnectAttempts})`);
|
||||
|
||||
reconnectTimeoutRef.current = window.setTimeout(() => {
|
||||
setReconnectCount(prev => prev + 1);
|
||||
connect();
|
||||
}, nextReconnectDelay);
|
||||
} else {
|
||||
console.error(`Failed to reconnect after ${maxReconnectAttempts} attempts`);
|
||||
}
|
||||
};
|
||||
}, [userId]); // Only depends on userId now
|
||||
}, [userId, maxReconnectAttempts, reconnectDelay, reconnectCount]);
|
||||
|
||||
// Set up the connection
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
|
||||
connect();
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
eventSourceRef.current = null;
|
||||
}
|
||||
|
||||
if (reconnectTimeoutRef.current) {
|
||||
window.clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [userId, connect]);
|
||||
|
||||
return { connected };
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
// Environment variables
|
||||
export const ENV = {
|
||||
// Node environment (development, production, test)
|
||||
// Runtime environment (development, production, test)
|
||||
NODE_ENV: process.env.NODE_ENV || "development",
|
||||
|
||||
// Database URL - use SQLite by default
|
||||
|
||||
@@ -1,21 +1,56 @@
|
||||
import { z } from "zod";
|
||||
import { createClient } from "@libsql/client";
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||
|
||||
import { Database } from "bun:sqlite";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { configSchema } from "./schema";
|
||||
|
||||
// Define the database URL - for development we'll use a local SQLite file
|
||||
const dataDir = path.join(process.cwd(), "data");
|
||||
const dbUrl =
|
||||
process.env.DATABASE_URL || `file:${path.join(dataDir, "gitea-mirror.db")}`;
|
||||
// Ensure data directory exists
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create a client connection to the database
|
||||
export const client = createClient({ url: dbUrl });
|
||||
const dbPath = path.join(dataDir, "gitea-mirror.db");
|
||||
|
||||
// Create a drizzle instance
|
||||
export const db = drizzle(client);
|
||||
// Create an empty database file if it doesn't exist
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
fs.writeFileSync(dbPath, "");
|
||||
}
|
||||
|
||||
// Create SQLite database instance using Bun's native driver
|
||||
let sqlite: Database;
|
||||
try {
|
||||
sqlite = new Database(dbPath);
|
||||
console.log("Successfully connected to SQLite database using Bun's native driver");
|
||||
} catch (error) {
|
||||
console.error("Error opening database:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Create drizzle instance with the SQLite client
|
||||
export const db = drizzle({ client: sqlite });
|
||||
|
||||
// Simple async wrapper around SQLite API for compatibility
|
||||
// This maintains backward compatibility with existing code
|
||||
export const client = {
|
||||
async execute(sql: string, params?: any[]) {
|
||||
try {
|
||||
const stmt = sqlite.query(sql);
|
||||
if (/^\s*select/i.test(sql)) {
|
||||
const rows = stmt.all(params ?? []);
|
||||
return { rows } as { rows: any[] };
|
||||
}
|
||||
stmt.run(params ?? []);
|
||||
return { rows: [] } as { rows: any[] };
|
||||
} catch (error) {
|
||||
console.error(`Error executing SQL: ${sql}`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Define the tables
|
||||
export const users = sqliteTable("users", {
|
||||
@@ -31,6 +66,18 @@ export const users = sqliteTable("users", {
|
||||
.default(new Date()),
|
||||
});
|
||||
|
||||
// New table for event notifications (replacing Redis pub/sub)
|
||||
export const events = sqliteTable("events", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id),
|
||||
channel: text("channel").notNull(),
|
||||
payload: text("payload", { mode: "json" }).notNull(),
|
||||
read: integer("read", { mode: "boolean" }).notNull().default(false),
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(new Date()),
|
||||
});
|
||||
|
||||
const githubSchema = configSchema.shape.githubConfig;
|
||||
const giteaSchema = configSchema.shape.giteaConfig;
|
||||
const scheduleSchema = configSchema.shape.scheduleConfig;
|
||||
|
||||
@@ -140,3 +140,15 @@ export const organizationSchema = z.object({
|
||||
});
|
||||
|
||||
export type Organization = z.infer<typeof organizationSchema>;
|
||||
|
||||
// Event schema (for SQLite-based pub/sub)
|
||||
export const eventSchema = z.object({
|
||||
id: z.string().uuid().optional(),
|
||||
userId: z.string().uuid(),
|
||||
channel: z.string().min(1),
|
||||
payload: z.any(),
|
||||
read: z.boolean().default(false),
|
||||
createdAt: z.date().default(() => new Date()),
|
||||
});
|
||||
|
||||
export type Event = z.infer<typeof eventSchema>;
|
||||
|
||||
161
src/lib/events.ts
Normal file
161
src/lib/events.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { db, events } from "./db";
|
||||
import { eq, and, gt, lt } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Publishes an event to a specific channel for a user
|
||||
* This replaces Redis pub/sub with SQLite storage
|
||||
*/
|
||||
export async function publishEvent({
|
||||
userId,
|
||||
channel,
|
||||
payload,
|
||||
}: {
|
||||
userId: string;
|
||||
channel: string;
|
||||
payload: any;
|
||||
}): Promise<string> {
|
||||
try {
|
||||
const eventId = uuidv4();
|
||||
console.log(`Publishing event to channel ${channel} for user ${userId}`);
|
||||
|
||||
// Insert the event into the SQLite database
|
||||
await db.insert(events).values({
|
||||
id: eventId,
|
||||
userId,
|
||||
channel,
|
||||
payload: JSON.stringify(payload),
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
console.log(`Event published successfully with ID ${eventId}`);
|
||||
return eventId;
|
||||
} catch (error) {
|
||||
console.error("Error publishing event:", error);
|
||||
throw new Error("Failed to publish event");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets new events for a specific user and channel
|
||||
* This replaces Redis subscribe with SQLite polling
|
||||
*/
|
||||
export async function getNewEvents({
|
||||
userId,
|
||||
channel,
|
||||
lastEventTime,
|
||||
}: {
|
||||
userId: string;
|
||||
channel: string;
|
||||
lastEventTime?: Date;
|
||||
}): Promise<any[]> {
|
||||
try {
|
||||
console.log(`Getting new events for user ${userId} in channel ${channel}`);
|
||||
if (lastEventTime) {
|
||||
console.log(`Looking for events after ${lastEventTime.toISOString()}`);
|
||||
}
|
||||
|
||||
// Build the query
|
||||
let query = db
|
||||
.select()
|
||||
.from(events)
|
||||
.where(
|
||||
and(
|
||||
eq(events.userId, userId),
|
||||
eq(events.channel, channel),
|
||||
eq(events.read, false)
|
||||
)
|
||||
)
|
||||
.orderBy(events.createdAt);
|
||||
|
||||
// Add time filter if provided
|
||||
if (lastEventTime) {
|
||||
query = query.where(gt(events.createdAt, lastEventTime));
|
||||
}
|
||||
|
||||
// Execute the query
|
||||
const newEvents = await query;
|
||||
console.log(`Found ${newEvents.length} new events`);
|
||||
|
||||
// Mark events as read
|
||||
if (newEvents.length > 0) {
|
||||
console.log(`Marking ${newEvents.length} events as read`);
|
||||
await db
|
||||
.update(events)
|
||||
.set({ read: true })
|
||||
.where(
|
||||
and(
|
||||
eq(events.userId, userId),
|
||||
eq(events.channel, channel),
|
||||
eq(events.read, false)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Parse the payloads
|
||||
return newEvents.map(event => ({
|
||||
...event,
|
||||
payload: JSON.parse(event.payload as string),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error getting new events:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up old events to prevent the database from growing too large
|
||||
* Should be called periodically (e.g., daily via a cron job)
|
||||
*
|
||||
* @param maxAgeInDays Number of days to keep events (default: 7)
|
||||
* @param cleanupUnreadAfterDays Number of days after which to clean up unread events (default: 2x maxAgeInDays)
|
||||
* @returns Object containing the number of read and unread events deleted
|
||||
*/
|
||||
export async function cleanupOldEvents(
|
||||
maxAgeInDays: number = 7,
|
||||
cleanupUnreadAfterDays?: number
|
||||
): Promise<{ readEventsDeleted: number; unreadEventsDeleted: number }> {
|
||||
try {
|
||||
console.log(`Cleaning up events older than ${maxAgeInDays} days...`);
|
||||
|
||||
// Calculate the cutoff date for read events
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - maxAgeInDays);
|
||||
|
||||
// Delete read events older than the cutoff date
|
||||
const readResult = await db
|
||||
.delete(events)
|
||||
.where(
|
||||
and(
|
||||
eq(events.read, true),
|
||||
lt(events.createdAt, cutoffDate)
|
||||
)
|
||||
);
|
||||
|
||||
const readEventsDeleted = readResult.changes || 0;
|
||||
console.log(`Deleted ${readEventsDeleted} read events`);
|
||||
|
||||
// Calculate the cutoff date for unread events (default to 2x the retention period)
|
||||
const unreadCutoffDate = new Date();
|
||||
const unreadMaxAge = cleanupUnreadAfterDays || (maxAgeInDays * 2);
|
||||
unreadCutoffDate.setDate(unreadCutoffDate.getDate() - unreadMaxAge);
|
||||
|
||||
// Delete unread events that are significantly older
|
||||
const unreadResult = await db
|
||||
.delete(events)
|
||||
.where(
|
||||
and(
|
||||
eq(events.read, false),
|
||||
lt(events.createdAt, unreadCutoffDate)
|
||||
)
|
||||
);
|
||||
|
||||
const unreadEventsDeleted = unreadResult.changes || 0;
|
||||
console.log(`Deleted ${unreadEventsDeleted} unread events`);
|
||||
|
||||
return { readEventsDeleted, unreadEventsDeleted };
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up old events:", error);
|
||||
return { readEventsDeleted: 0, unreadEventsDeleted: 0 };
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RepoStatus } from "@/types/Repository";
|
||||
import { db, mirrorJobs } from "./db";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { redisPublisher } from "./redis";
|
||||
import { publishEvent } from "./events";
|
||||
|
||||
export async function createMirrorJob({
|
||||
userId,
|
||||
@@ -40,10 +40,16 @@ export async function createMirrorJob({
|
||||
};
|
||||
|
||||
try {
|
||||
// Insert the job into the database
|
||||
await db.insert(mirrorJobs).values(job);
|
||||
|
||||
// Publish the event using SQLite instead of Redis
|
||||
const channel = `mirror-status:${userId}`;
|
||||
await redisPublisher.publish(channel, JSON.stringify(job));
|
||||
await publishEvent({
|
||||
userId,
|
||||
channel,
|
||||
payload: job
|
||||
});
|
||||
|
||||
return jobId;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import Redis from "ioredis";
|
||||
|
||||
// Connect to Redis using REDIS_URL environment variable or default to redis://redis:6379
|
||||
// This ensures we have a fallback URL when running with Docker Compose
|
||||
const redisUrl = process.env.REDIS_URL ?? 'redis://redis:6379';
|
||||
|
||||
console.log(`Connecting to Redis at: ${redisUrl}`);
|
||||
|
||||
// Configure Redis client with connection options
|
||||
const redisOptions = {
|
||||
retryStrategy: (times: number) => {
|
||||
// Retry with exponential backoff up to 30 seconds
|
||||
const delay = Math.min(times * 100, 3000);
|
||||
console.log(`Redis connection attempt ${times} failed. Retrying in ${delay}ms...`);
|
||||
return delay;
|
||||
},
|
||||
maxRetriesPerRequest: 5,
|
||||
enableReadyCheck: true,
|
||||
connectTimeout: 10000,
|
||||
};
|
||||
|
||||
export const redis = new Redis(redisUrl, redisOptions);
|
||||
export const redisPublisher = new Redis(redisUrl, redisOptions); // For publishing
|
||||
export const redisSubscriber = new Redis(redisUrl, redisOptions); // For subscribing
|
||||
|
||||
// Log connection events
|
||||
redis.on('connect', () => console.log('Redis client connected'));
|
||||
redis.on('error', (err) => console.error('Redis client error:', err));
|
||||
redis.on('ready', () => console.log('Redis client ready'));
|
||||
redis.on('reconnecting', () => console.log('Redis client reconnecting...'));
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { redisSubscriber } from "@/lib/redis";
|
||||
import { getNewEvents } from "@/lib/events";
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
@@ -11,50 +11,89 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
|
||||
const channel = `mirror-status:${userId}`;
|
||||
let isClosed = false;
|
||||
const POLL_INTERVAL = 5000; // Poll every 5 seconds (reduced from 2 seconds for low-traffic usage)
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
let lastEventTime: Date | undefined = undefined;
|
||||
let pollIntervalId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const handleMessage = (ch: string, message: string) => {
|
||||
if (isClosed || ch !== channel) return;
|
||||
// Function to send a message to the client
|
||||
const sendMessage = (message: string) => {
|
||||
if (isClosed) return;
|
||||
try {
|
||||
controller.enqueue(encoder.encode(`data: ${message}\n\n`));
|
||||
controller.enqueue(encoder.encode(message));
|
||||
} catch (err) {
|
||||
console.error("Stream enqueue error:", err);
|
||||
}
|
||||
};
|
||||
|
||||
redisSubscriber.subscribe(channel, (err) => {
|
||||
if (err) {
|
||||
isClosed = true;
|
||||
controller.error(err);
|
||||
// Function to poll for new events
|
||||
const pollForEvents = async () => {
|
||||
if (isClosed) return;
|
||||
|
||||
try {
|
||||
console.log(`Polling for events for user ${userId} in channel ${channel}`);
|
||||
|
||||
// Get new events from SQLite
|
||||
const events = await getNewEvents({
|
||||
userId,
|
||||
channel,
|
||||
lastEventTime,
|
||||
});
|
||||
|
||||
console.log(`Found ${events.length} new events`);
|
||||
|
||||
// Send events to client
|
||||
if (events.length > 0) {
|
||||
// Update last event time
|
||||
lastEventTime = events[events.length - 1].createdAt;
|
||||
|
||||
// Send each event to the client
|
||||
for (const event of events) {
|
||||
console.log(`Sending event: ${JSON.stringify(event.payload)}`);
|
||||
sendMessage(`data: ${JSON.stringify(event.payload)}\n\n`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error polling for events:", err);
|
||||
sendMessage(`data: {"error": "Error polling for events"}\n\n`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
redisSubscriber.on("message", handleMessage);
|
||||
// Send initial connection message
|
||||
sendMessage(": connected\n\n");
|
||||
|
||||
try {
|
||||
controller.enqueue(encoder.encode(": connected\n\n"));
|
||||
} catch (err) {
|
||||
console.error("Initial enqueue error:", err);
|
||||
}
|
||||
// Start polling for events
|
||||
pollForEvents();
|
||||
|
||||
// Set up polling interval
|
||||
pollIntervalId = setInterval(pollForEvents, POLL_INTERVAL);
|
||||
|
||||
// Send a heartbeat every 30 seconds to keep the connection alive
|
||||
const heartbeatInterval = setInterval(() => {
|
||||
if (!isClosed) {
|
||||
sendMessage(": heartbeat\n\n");
|
||||
} else {
|
||||
clearInterval(heartbeatInterval);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// Handle client disconnection
|
||||
request.signal?.addEventListener("abort", () => {
|
||||
if (!isClosed) {
|
||||
isClosed = true;
|
||||
redisSubscriber.off("message", handleMessage);
|
||||
redisSubscriber.unsubscribe(channel);
|
||||
if (pollIntervalId) {
|
||||
clearInterval(pollIntervalId);
|
||||
}
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
// extra safety in case cancel is triggered
|
||||
if (!isClosed) {
|
||||
isClosed = true;
|
||||
redisSubscriber.unsubscribe(channel);
|
||||
}
|
||||
// Extra safety in case cancel is triggered
|
||||
isClosed = true;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
56
src/pages/api/test-event.ts
Normal file
56
src/pages/api/test-event.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { publishEvent } from "@/lib/events";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { userId, message, status } = body;
|
||||
|
||||
if (!userId || !message || !status) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Missing required fields: userId, message, status",
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create a test event
|
||||
const eventData = {
|
||||
id: uuidv4(),
|
||||
userId,
|
||||
repositoryId: uuidv4(),
|
||||
repositoryName: "test-repo",
|
||||
message,
|
||||
status,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
// Publish the event
|
||||
const channel = `mirror-status:${userId}`;
|
||||
await publishEvent({
|
||||
userId,
|
||||
channel,
|
||||
payload: eventData,
|
||||
});
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: "Event published successfully",
|
||||
event: eventData,
|
||||
}),
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error publishing test event:", error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Failed to publish event",
|
||||
details: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user