first commit
This commit is contained in:
58
Makefile
Normal file
58
Makefile
Normal file
@@ -0,0 +1,58 @@
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -Wall -Wextra -O2 -fPIC
|
||||
LDFLAGS ?= -lhiredis -lcrypto -lrabbitmq -lcjson
|
||||
|
||||
SRC_DIR := src
|
||||
BUILD_DIR := build
|
||||
TARGET := fsync-monitor
|
||||
|
||||
SOURCES := $(wildcard $(SRC_DIR)/*.c)
|
||||
OBJECTS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SOURCES))
|
||||
HEADERS := $(wildcard $(SRC_DIR)/*.h)
|
||||
|
||||
.PHONY: all build clean run install help
|
||||
|
||||
all: build
|
||||
|
||||
help:
|
||||
@echo "Fsync - File System Monitor with RabbitMQ"
|
||||
@echo ""
|
||||
@echo "Available targets:"
|
||||
@echo " make build - Build the fsync monitor"
|
||||
@echo " make run - Run the monitor (requires root)"
|
||||
@echo " make clean - Clean build artifacts"
|
||||
@echo " make install - Install to /usr/local/bin"
|
||||
@echo " make help - Show this help message"
|
||||
|
||||
build: $(BUILD_DIR) $(BUILD_DIR)/$(TARGET)
|
||||
|
||||
$(BUILD_DIR):
|
||||
mkdir -p $(BUILD_DIR)
|
||||
|
||||
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(HEADERS)
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
$(BUILD_DIR)/$(TARGET): $(OBJECTS)
|
||||
$(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@
|
||||
@echo "Built: $@"
|
||||
|
||||
run: build
|
||||
@if [ "$$(id -u)" != "0" ]; then \
|
||||
echo "Error: fsync-monitor requires root privileges"; \
|
||||
exit 1; \
|
||||
fi
|
||||
./$(BUILD_DIR)/$(TARGET) $(ARGS)
|
||||
|
||||
install: build
|
||||
@if [ "$$(id -u)" != "0" ]; then \
|
||||
echo "Error: Installation requires root privileges"; \
|
||||
exit 1; \
|
||||
fi
|
||||
install -m 755 $(BUILD_DIR)/$(TARGET) /usr/local/bin/
|
||||
@echo "Installed to /usr/local/bin/$(TARGET)"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
@echo "Cleaned build artifacts"
|
||||
|
||||
.SILENT: help
|
||||
359
README.md
Normal file
359
README.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# Fsync - File System Monitor with Redis and RabbitMQ
|
||||
|
||||
Distributed file synchronization daemon that monitors file changes across multiple Linux hosts using fanotify, publishes notifications via RabbitMQ, and automatically fetches updated files between hosts.
|
||||
|
||||
## Key Features
|
||||
|
||||
✅ **Cross-Host File Sync** - Automatically copy files between hosts when changes detected
|
||||
✅ **Real-time Monitoring** - fanotify API captures changes instantly
|
||||
✅ **Distributed Architecture** - Monitor multiple hosts in a cluster
|
||||
✅ **Priority Queue** - Fetch oldest/smallest files first
|
||||
✅ **Checksum Verification** - SHA256 integrity checks after transfer
|
||||
✅ **RabbitMQ Integration** - Publish/subscribe file updates
|
||||
✅ **Redis Logging** - Persistent event and transfer records
|
||||
✅ **Automatic Retries** - Recovers from transient failures
|
||||
|
||||
## How It Works
|
||||
|
||||
### Host A (Source) → Host B (Destination)
|
||||
|
||||
```
|
||||
1. File modified on Host A
|
||||
├─> Detect via fanotify
|
||||
├─> Calculate checksum
|
||||
└─> Publish to RabbitMQ
|
||||
|
||||
2. Host B receives notification
|
||||
├─> Parse message
|
||||
├─> Add to priority queue
|
||||
└─> Schedule fetch
|
||||
|
||||
3. Fetch file from Host A
|
||||
├─> SCP: host-a:/path/file → /tmp/fsync-cache/
|
||||
├─> Verify checksum
|
||||
└─> Record in Redis
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Configuration Management** - Sync config files across servers
|
||||
- **Log Aggregation** - Copy logs from multiple hosts
|
||||
- **Backup Automation** - Continuous sync of important files
|
||||
- **Development** - Keep code in sync across team machines
|
||||
- **Database Sync** - Mirror database files between replicas
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Start Services
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
This starts:
|
||||
- Redis (port 6379)
|
||||
- RabbitMQ (port 5672, UI: 15672)
|
||||
- Redis Commander (port 8081)
|
||||
|
||||
### 2. Build and Run
|
||||
|
||||
```bash
|
||||
make build
|
||||
sudo ./build/fsync-monitor
|
||||
```
|
||||
|
||||
### 3. Test Cross-Host Sync
|
||||
|
||||
**On Host A:**
|
||||
```bash
|
||||
echo "updated content" >> /var/www/app.js
|
||||
```
|
||||
|
||||
**On Host B:**
|
||||
```bash
|
||||
# File automatically fetched and cached
|
||||
ls -la /tmp/fsync-cache/host-a_app.js
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Cluster of Hosts │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Host A RabbitMQ Host B Host C │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│
|
||||
│ │Monitor │──>│Exchange │───>│Monitor │ │Monitor ││
|
||||
│ │Publish │ │fsync.* │ │Subscribe │ │Subscribe ││
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘│
|
||||
│ │ │ │ │ │
|
||||
│ └──────────────┴────────────────┴──────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────┐ │
|
||||
│ │ Redis │ │
|
||||
│ │ Storage │ │
|
||||
│ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## File Sync Details
|
||||
|
||||
### Detection → Publication → Subscription → Fetch
|
||||
|
||||
| Step | Component | Action |
|
||||
|------|-----------|--------|
|
||||
| 1 | fanotify | Detect file change |
|
||||
| 2 | Checksum | Calculate SHA256 |
|
||||
| 3 | Redis | Store event |
|
||||
| 4 | RabbitMQ | Publish to exchange |
|
||||
| 5 | RabbitMQ | Route to all subscribers |
|
||||
| 6 | Queue Mgr | Add to priority queue |
|
||||
| 7 | File Fetcher | Fetch via SCP |
|
||||
| 8 | Verification | Verify checksum |
|
||||
| 9 | Cache | Store locally |
|
||||
| 10 | Redis | Log transfer |
|
||||
|
||||
### Priority Algorithm
|
||||
|
||||
Files fetched in order of:
|
||||
1. **Age** - older changes processed first
|
||||
2. **Size** - smaller files before larger ones
|
||||
|
||||
```
|
||||
Queue Order:
|
||||
1st: 2-hour-old 2KB file
|
||||
2nd: 90-minute-old 50MB file
|
||||
3rd: 30-minute-old 100MB file
|
||||
4th: 20-minute-old 500KB file
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Basic Setup
|
||||
|
||||
Edit `src/config.h`:
|
||||
|
||||
```c
|
||||
#define REDIS_HOST "127.0.0.1"
|
||||
#define REDIS_PORT 6379
|
||||
|
||||
#define AMQP_HOST "127.0.0.1"
|
||||
#define AMQP_PORT 5672
|
||||
#define AMQP_USER "guest"
|
||||
#define AMQP_PASSWORD "guest"
|
||||
|
||||
#define CACHE_DIR "/tmp/fsync-cache"
|
||||
#define MAX_QUEUE_SIZE 1000
|
||||
```
|
||||
|
||||
### For Production
|
||||
|
||||
```c
|
||||
#define REDIS_HOST "redis.example.com"
|
||||
#define AMQP_HOST "rabbitmq.example.com"
|
||||
#define CACHE_DIR "/var/lib/fsync-cache"
|
||||
#define MAX_QUEUE_SIZE 5000
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Linux** 3.8+ (fanotify)
|
||||
- **Redis** 5.0+
|
||||
- **RabbitMQ** 3.8+
|
||||
- **SSH** configured between hosts
|
||||
- **GCC/Clang** compiler
|
||||
- **Libraries:**
|
||||
- libhiredis
|
||||
- libssl/libcrypto
|
||||
- librabbitmq
|
||||
- libcjson
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
libhiredis-dev \
|
||||
libssl-dev \
|
||||
librabbitmq-dev \
|
||||
libcjson-dev
|
||||
```
|
||||
|
||||
**RHEL/CentOS:**
|
||||
```bash
|
||||
sudo yum install -y \
|
||||
gcc make \
|
||||
libhiredis-devel \
|
||||
openssl-devel \
|
||||
librabbitmq-devel
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
fsync/
|
||||
├── src/
|
||||
│ ├── main.c # Event loop + RabbitMQ integration
|
||||
│ ├── monitor.c/h # fanotify monitoring
|
||||
│ ├── redis_client.c/h # Redis operations
|
||||
│ ├── amqp_client.c/h # RabbitMQ client (pub/sub)
|
||||
│ ├── queue_manager.c/h # Priority queue for file updates
|
||||
│ ├── file_fetcher.c/h # Remote file fetching via SCP
|
||||
│ ├── checksum.c/h # SHA256 verification
|
||||
│ └── config.h # Configuration constants
|
||||
├── docs/
|
||||
│ ├── API.md # Redis schema reference
|
||||
│ └── RABBITMQ.md # RabbitMQ setup guide
|
||||
├── config/fsync.conf # Runtime config template
|
||||
├── Makefile # Build system
|
||||
├── docker-compose.yml # Redis + RabbitMQ stack
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Command Line
|
||||
|
||||
```bash
|
||||
# Build
|
||||
make build
|
||||
|
||||
# Run (requires root)
|
||||
sudo ./build/fsync-monitor
|
||||
|
||||
# Run with custom hosts
|
||||
sudo ./build/fsync-monitor redis.local rabbitmq.local
|
||||
|
||||
# View logs
|
||||
sudo ./build/fsync-monitor 2>&1 | grep -i error
|
||||
|
||||
# Clean build
|
||||
make clean
|
||||
|
||||
# Install system-wide
|
||||
sudo make install
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### RabbitMQ Management UI
|
||||
|
||||
Open http://localhost:15672
|
||||
- Default: guest/guest
|
||||
- View queues, exchanges, bindings
|
||||
- Monitor message flow
|
||||
|
||||
### Redis CLI
|
||||
|
||||
```bash
|
||||
# Check events
|
||||
redis-cli LRANGE fsync:events:list 0 -1
|
||||
|
||||
# View specific event
|
||||
redis-cli HGETALL fsync:events:*
|
||||
|
||||
# Monitor transfers
|
||||
redis-cli KEYS "fsync:fetches:*"
|
||||
|
||||
# Watch in real-time
|
||||
redis-cli MONITOR
|
||||
```
|
||||
|
||||
### Local Cache
|
||||
|
||||
```bash
|
||||
# View cached files
|
||||
ls -lh /tmp/fsync-cache/
|
||||
|
||||
# Check cache size
|
||||
du -sh /tmp/fsync-cache/
|
||||
|
||||
# Clear old files (older than 7 days)
|
||||
find /tmp/fsync-cache/ -mtime +7 -delete
|
||||
```
|
||||
|
||||
## Example Workflow
|
||||
|
||||
### Scenario: Config File Update Across Cluster
|
||||
|
||||
```bash
|
||||
# On web-server-1
|
||||
echo "updated_setting=true" >> /etc/app.conf
|
||||
|
||||
# Automatically happens:
|
||||
# 1. web-server-1 detects change
|
||||
# 2. Calculates checksum
|
||||
# 3. Publishes to RabbitMQ
|
||||
# 4. web-server-2 receives notification
|
||||
# 5. Adds /etc/app.conf to fetch queue
|
||||
# 6. Fetches from web-server-1 via SCP
|
||||
# 7. Verifies checksum
|
||||
# 8. Caches at /tmp/fsync-cache/web-server-1_app.conf
|
||||
|
||||
# Verify on web-server-2
|
||||
ls -la /tmp/fsync-cache/ | grep app.conf
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
|
||||
```bash
|
||||
# Test Redis
|
||||
redis-cli PING
|
||||
|
||||
# Test RabbitMQ
|
||||
sudo docker logs fsync-rabbitmq
|
||||
```
|
||||
|
||||
### File Not Syncing
|
||||
|
||||
1. Check SSH access:
|
||||
```bash
|
||||
ssh user@remote-host 'cat /path/to/file'
|
||||
```
|
||||
|
||||
2. Check file permissions:
|
||||
```bash
|
||||
ls -la /path/to/file
|
||||
```
|
||||
|
||||
3. Check queue:
|
||||
```bash
|
||||
redis-cli LLEN fsync:events:list
|
||||
```
|
||||
|
||||
### Build Errors
|
||||
|
||||
Install missing development packages:
|
||||
```bash
|
||||
# Ubuntu
|
||||
sudo apt-get install libhiredis-dev libssl-dev librabbitmq-dev
|
||||
|
||||
# RHEL
|
||||
sudo yum install libhiredis-devel openssl-devel librabbitmq-devel
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Redis API Schema](docs/API.md)** - Event structure and queries
|
||||
- **[RabbitMQ Integration](docs/RABBITMQ.md)** - Messaging and sync details
|
||||
|
||||
## Performance
|
||||
|
||||
- **Event Processing:** <1ms (fanotify)
|
||||
- **Checksum Calculation:** 100-500ms per file
|
||||
- **File Transfer:** Network-dependent
|
||||
- **Queue Size:** Supports 1000+ pending items
|
||||
- **Memory:** ~1-2KB per queued file
|
||||
|
||||
## Security
|
||||
|
||||
- **SSH:** Key-based authentication required
|
||||
- **RabbitMQ:** Configure credentials in production
|
||||
- **Redis:** Add `requirepass` authentication
|
||||
- **Network:** Run on isolated/private networks
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
BIN
build/amqp_client.o
Normal file
BIN
build/amqp_client.o
Normal file
Binary file not shown.
BIN
build/checksum.o
Normal file
BIN
build/checksum.o
Normal file
Binary file not shown.
BIN
build/file_fetcher.o
Normal file
BIN
build/file_fetcher.o
Normal file
Binary file not shown.
BIN
build/fsync-monitor
Executable file
BIN
build/fsync-monitor
Executable file
Binary file not shown.
BIN
build/main.o
Normal file
BIN
build/main.o
Normal file
Binary file not shown.
BIN
build/monitor.o
Normal file
BIN
build/monitor.o
Normal file
Binary file not shown.
BIN
build/queue_manager.o
Normal file
BIN
build/queue_manager.o
Normal file
Binary file not shown.
BIN
build/redis_client.o
Normal file
BIN
build/redis_client.o
Normal file
Binary file not shown.
57
docker-compose.yml
Normal file
57
docker-compose.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: fsync-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
networks:
|
||||
- fsync-network
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:3.12-management-alpine
|
||||
container_name: fsync-rabbitmq
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: guest
|
||||
RABBITMQ_DEFAULT_PASS: guest
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
test: ["CMD", "rabbitmq-diagnostics", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
networks:
|
||||
- fsync-network
|
||||
|
||||
redis-commander:
|
||||
image: rediscommander/redis-commander:latest
|
||||
container_name: fsync-redis-commander
|
||||
environment:
|
||||
- REDIS_HOSTS=local:redis:6379
|
||||
ports:
|
||||
- "8081:8081"
|
||||
depends_on:
|
||||
- redis
|
||||
networks:
|
||||
- fsync-network
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
rabbitmq_data:
|
||||
|
||||
networks:
|
||||
fsync-network:
|
||||
driver: bridge
|
||||
239
docs/API.md
Normal file
239
docs/API.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# Fsync Redis Schema Documentation
|
||||
|
||||
This document describes the Redis data structures used by the Fsync file system monitor.
|
||||
|
||||
## Overview
|
||||
|
||||
Fsync stores file system events in Redis with the following information:
|
||||
- **Hostname**: The server where the change occurred
|
||||
- **File Path**: The relative path of the changed file
|
||||
- **Event Type**: The type of file system event
|
||||
- **Checksum**: SHA256 checksum of file contents (for modify events)
|
||||
- **Metadata**: File size, permissions, timestamp, PID
|
||||
|
||||
## Data Structures
|
||||
|
||||
### Event Hash Keys
|
||||
|
||||
Each file system event is stored as a Redis Hash with the following key format:
|
||||
|
||||
```
|
||||
fsync:events:{timestamp}:{hostname}:{pid}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
fsync:events:2026-05-18T13:28:51Z:server1:12345
|
||||
```
|
||||
|
||||
### Event Hash Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `hostname` | string | The hostname where the event occurred |
|
||||
| `path` | string | Absolute file path |
|
||||
| `event_type` | string | Type of event (OPEN, CLOSE_WRITE, MODIFY, CREATE, DELETE, MOVED_FROM, MOVED_TO, ATTRIB) |
|
||||
| `checksum` | string | SHA256 checksum of file contents (64 hex chars) |
|
||||
| `timestamp` | string | ISO8601 timestamp (2026-05-18T13:28:51Z) |
|
||||
| `file_size` | number | Size of file in bytes |
|
||||
| `permissions` | string | Octal permissions (e.g., "0644") |
|
||||
| `process_id` | number | PID of process that triggered the event |
|
||||
|
||||
### Example Event
|
||||
|
||||
```
|
||||
Key: fsync:events:2026-05-18T13:28:51Z:server1:1234
|
||||
Value:
|
||||
hostname "server1"
|
||||
path "/var/www/app.js"
|
||||
event_type "MODIFY"
|
||||
checksum "a1b2c3d4e5f6..."
|
||||
timestamp "2026-05-18T13:28:51Z"
|
||||
file_size "1024"
|
||||
permissions "0644"
|
||||
process_id "1234"
|
||||
```
|
||||
|
||||
### Events List
|
||||
|
||||
All event keys are added to a list for quick retrieval:
|
||||
|
||||
```
|
||||
Key: fsync:events:list
|
||||
Type: List (LPUSH)
|
||||
```
|
||||
|
||||
This allows efficient range queries of all recorded events.
|
||||
|
||||
## Common Queries
|
||||
|
||||
### Get All Events
|
||||
|
||||
```bash
|
||||
redis-cli LRANGE fsync:events:list 0 -1
|
||||
```
|
||||
|
||||
### Get Event Details
|
||||
|
||||
```bash
|
||||
redis-cli HGETALL fsync:events:2026-05-18T13:28:51Z:server1:1234
|
||||
```
|
||||
|
||||
### Get Events by Hostname
|
||||
|
||||
```bash
|
||||
# Get list of all events
|
||||
redis-cli LRANGE fsync:events:list 0 -1
|
||||
|
||||
# Filter by hostname using pattern
|
||||
redis-cli --pattern 'fsync:events:*:server1:*'
|
||||
```
|
||||
|
||||
### Get Events by Path
|
||||
|
||||
```bash
|
||||
# Scan and filter by path field
|
||||
redis-cli --scan --pattern 'fsync:events:*' | xargs redis-cli HGETALL
|
||||
```
|
||||
|
||||
### Get Recent Events (Last 100)
|
||||
|
||||
```bash
|
||||
redis-cli LRANGE fsync:events:list 0 99
|
||||
```
|
||||
|
||||
### Get Events Modified in Last Hour
|
||||
|
||||
```bash
|
||||
# Using Lua script or application-level filtering
|
||||
redis-cli LRANGE fsync:events:list 0 -1 | \
|
||||
xargs -I {} redis-cli HGET {} timestamp | \
|
||||
grep -E '(2026-05-18T1[2-3]:[0-5][0-9]:[0-5][0-9]Z)'
|
||||
```
|
||||
|
||||
## Redis CLI Examples
|
||||
|
||||
### Monitor Events in Real-time
|
||||
|
||||
```bash
|
||||
redis-cli MONITOR | grep fsync:events
|
||||
```
|
||||
|
||||
### Get Event Count
|
||||
|
||||
```bash
|
||||
redis-cli LLEN fsync:events:list
|
||||
```
|
||||
|
||||
### Delete Old Events
|
||||
|
||||
```bash
|
||||
redis-cli EVAL "
|
||||
local keys = redis.call('LRANGE', 'fsync:events:list', 0, -1)
|
||||
for _, key in ipairs(keys) do
|
||||
redis.call('DEL', key)
|
||||
end
|
||||
" 0
|
||||
```
|
||||
|
||||
### Export Events to JSON (with redis-cli)
|
||||
|
||||
```bash
|
||||
redis-cli --json LRANGE fsync:events:list 0 -1
|
||||
```
|
||||
|
||||
## Event Types
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `OPEN` | File was opened |
|
||||
| `CLOSE_WRITE` | File was closed after write |
|
||||
| `CLOSE_NOWRITE` | File was closed without write |
|
||||
| `MODIFY` | File contents were modified |
|
||||
| `CREATE` | File was created |
|
||||
| `DELETE` | File was deleted |
|
||||
| `MOVED_FROM` | File was moved (source) |
|
||||
| `MOVED_TO` | File was moved (destination) |
|
||||
| `ATTRIB` | File attributes changed (permissions, owner, etc.) |
|
||||
|
||||
## Checksum Calculation
|
||||
|
||||
SHA256 checksums are calculated for:
|
||||
- `MODIFY` events: Capture content changes
|
||||
- `CLOSE_WRITE` events: Verify final content
|
||||
- Other events: Empty hash or error indicator
|
||||
|
||||
**Special Cases:**
|
||||
- Files > 100MB: Checksum = `0000000000000000000000000000000000000000000000000000000000000000`
|
||||
- Files that can't be read: Checksum = `error`
|
||||
- Directories/symlinks: No checksum (empty string)
|
||||
|
||||
## Data Expiration
|
||||
|
||||
Events are automatically expired after 30 days using Redis `EXPIRE` command.
|
||||
|
||||
```
|
||||
EXPIRE fsync:events:{key} 2592000 # 30 days in seconds
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **List Growth**: The `fsync:events:list` can grow large. Consider periodic cleanup or archival.
|
||||
2. **Hash Storage**: Each event stores ~1-2KB of data in Redis memory.
|
||||
3. **Network Events**: High-frequency events (millions/day) may benefit from batching.
|
||||
|
||||
## Development Usage
|
||||
|
||||
### Python Example
|
||||
|
||||
```python
|
||||
import redis
|
||||
import json
|
||||
|
||||
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
|
||||
|
||||
# Get all event keys
|
||||
event_keys = r.lrange('fsync:events:list', 0, -1)
|
||||
|
||||
# Get event details
|
||||
for key in event_keys:
|
||||
event = r.hgetall(key)
|
||||
print(json.dumps(event, indent=2))
|
||||
```
|
||||
|
||||
### Node.js Example
|
||||
|
||||
```javascript
|
||||
const redis = require('redis');
|
||||
const client = redis.createClient();
|
||||
|
||||
client.lrange('fsync:events:list', 0, -1, (err, keys) => {
|
||||
keys.forEach(key => {
|
||||
client.hgetall(key, (err, event) => {
|
||||
console.log(event);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No Events Showing Up
|
||||
|
||||
1. Verify Redis connection: `redis-cli PING` → `PONG`
|
||||
2. Check fsync-monitor is running: `ps aux | grep fsync-monitor`
|
||||
3. Verify file system activity: `touch /tmp/test.txt`
|
||||
4. Check Redis logs: `redis-cli INFO stats`
|
||||
|
||||
### Memory Growing Too Large
|
||||
|
||||
1. Check event count: `redis-cli LLEN fsync:events:list`
|
||||
2. Manually cleanup old events
|
||||
3. Consider archiving to external storage
|
||||
4. Reduce monitored paths to high-priority directories
|
||||
|
||||
### Checksum Errors
|
||||
|
||||
1. Verify file permissions: `ls -la /path/to/file`
|
||||
2. Check OpenSSL installation: `which openssl`
|
||||
3. Monitor disk space: `df -h`
|
||||
401
docs/RABBITMQ.md
Normal file
401
docs/RABBITMQ.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# RabbitMQ Integration Guide
|
||||
|
||||
Fsync integrates with RabbitMQ for distributed file synchronization across multiple hosts.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Host A RabbitMQ Host B
|
||||
┌──────────────┐ ┌──────────┐ ┌──────────────┐
|
||||
│ Monitor │ │Exchange │ │ Monitor │
|
||||
│ (Publish) │────────>│fsync.* │────────>│ (Subscribe) │
|
||||
│ │ │ │ │ │
|
||||
│ Detects: │ └──────────┘ │ Receives: │
|
||||
│ - File mods │ │ │ - Updates │
|
||||
│ - Creates │ ┌─────────────┐ │ - Adds queue │
|
||||
│ - Deletes │ │Queue │ │ - Fetches │
|
||||
└──────────────┘ │fsync.files │ │ - Verifies │
|
||||
└─────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
## How File Sync Works
|
||||
|
||||
### 1. Local File Change Detection (Host A)
|
||||
|
||||
- File modified on Host A
|
||||
- Fanotify detects change immediately
|
||||
- Calculate SHA256 checksum
|
||||
- Store event in Redis
|
||||
- **Publish to RabbitMQ** with hostname, path, checksum
|
||||
|
||||
### 2. Remote Host Receives Update (Host B)
|
||||
|
||||
- Consume message from RabbitMQ queue
|
||||
- Parse JSON with file details
|
||||
- **Add to priority queue** (oldest/smallest first)
|
||||
- Record in Redis
|
||||
|
||||
### 3. File Fetch and Sync (Host B)
|
||||
|
||||
- Process queue every 5 seconds
|
||||
- Get next highest priority file
|
||||
- **Fetch via SCP** from Host A (hostname:path)
|
||||
- Verify checksum matches
|
||||
- Store locally in cache
|
||||
- Update Redis with status
|
||||
|
||||
## Message Format
|
||||
|
||||
Events published to RabbitMQ as JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"hostname": "server1",
|
||||
"path": "/var/www/app.js",
|
||||
"event": "MODIFY",
|
||||
"checksum": "a1b2c3d4e5f6...",
|
||||
"timestamp": 1684423051,
|
||||
"size": 1024
|
||||
}
|
||||
```
|
||||
|
||||
## Queue Priority Algorithm
|
||||
|
||||
Files fetched in this order:
|
||||
|
||||
1. **By Age** (oldest first) - files detected earliest get priority
|
||||
2. **By Size** (smallest first) - smaller files processed faster
|
||||
|
||||
Example queue:
|
||||
|
||||
```
|
||||
Order | Host | File | Age | Size | Status
|
||||
------|-------|----------------|-------|---------|--------
|
||||
1 | host2 | /etc/config | 2h | 2KB | Fetch now
|
||||
2 | host1 | /app/db.sql | 90m | 50MB | Next
|
||||
3 | host2 | /cache.tmp | 30m | 100MB | Waiting
|
||||
4 | host1 | /data/doc.pdf | 20m | 500KB | Waiting
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `src/config.h`:
|
||||
|
||||
```c
|
||||
/* RabbitMQ Server */
|
||||
#define AMQP_HOST "127.0.0.1"
|
||||
#define AMQP_PORT 5672
|
||||
#define AMQP_USER "guest"
|
||||
#define AMQP_PASSWORD "guest"
|
||||
#define AMQP_VHOST "/"
|
||||
|
||||
/* Exchange and Queue */
|
||||
#define AMQP_EXCHANGE "fsync.events"
|
||||
#define AMQP_QUEUE_UPDATES "fsync.file.updates"
|
||||
#define AMQP_ROUTING_KEY "fsync.file.*"
|
||||
|
||||
/* File Cache */
|
||||
#define CACHE_DIR "/tmp/fsync-cache"
|
||||
#define MAX_QUEUE_SIZE 1000
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Start Services
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
make build
|
||||
```
|
||||
|
||||
### 2. Run Monitor on Multiple Hosts
|
||||
|
||||
**Host A:**
|
||||
```bash
|
||||
sudo ./build/fsync-monitor
|
||||
```
|
||||
|
||||
**Host B:**
|
||||
```bash
|
||||
sudo ./build/fsync-monitor
|
||||
```
|
||||
|
||||
### 3. Test Cross-Host Sync
|
||||
|
||||
On Host A, modify a file:
|
||||
```bash
|
||||
echo "test data" >> /var/www/index.html
|
||||
```
|
||||
|
||||
Host B will:
|
||||
- Receive notification via RabbitMQ
|
||||
- Add `/var/www/index.html` to fetch queue
|
||||
- Fetch file from Host A via SCP
|
||||
- Verify checksum
|
||||
- Cache locally
|
||||
|
||||
### 4. Monitor Progress
|
||||
|
||||
**Check queue status:**
|
||||
```bash
|
||||
redis-cli LLEN fsync:events:list
|
||||
```
|
||||
|
||||
**View cached files:**
|
||||
```bash
|
||||
ls -lh /tmp/fsync-cache/
|
||||
```
|
||||
|
||||
**Monitor RabbitMQ:**
|
||||
Open http://localhost:15672 (guest/guest)
|
||||
|
||||
## Operational Scenarios
|
||||
|
||||
### Scenario 1: Config File Update
|
||||
|
||||
```
|
||||
Host1: /etc/app.conf modified
|
||||
├─> Checksum: a1b2c3d4
|
||||
└─> Publish to RabbitMQ
|
||||
│
|
||||
Host2: Receives notification
|
||||
├─> Add to queue (priority 1)
|
||||
├─> Fetch /etc/app.conf from Host1
|
||||
├─> Verify checksum matches
|
||||
└─> Cache at /tmp/fsync-cache/host1_app.conf
|
||||
```
|
||||
|
||||
### Scenario 2: Bulk File Changes
|
||||
|
||||
```
|
||||
1000 files modified on Host1
|
||||
└─> 1000 messages to RabbitMQ
|
||||
|
||||
Host2 receives 1000 updates
|
||||
└─> Add all to priority queue
|
||||
│
|
||||
Processing:
|
||||
├─> Fetch oldest files first
|
||||
├─> Then by size (KB before MB)
|
||||
├─> One at a time sequentially
|
||||
└─> Verify each checksum
|
||||
```
|
||||
|
||||
### Scenario 3: Multiple Source Hosts
|
||||
|
||||
```
|
||||
Host1 changes: /var/www/app.js
|
||||
└─> Publish to RabbitMQ
|
||||
|
||||
Host2 changes: /etc/nginx.conf
|
||||
└─> Publish to RabbitMQ
|
||||
|
||||
Host3 receives both notifications
|
||||
└─> Queue both files
|
||||
└─> Fetch oldest first
|
||||
└─> Then by size
|
||||
```
|
||||
|
||||
## File Transfer Mechanism
|
||||
|
||||
### SCP-Based Transfer
|
||||
|
||||
Current implementation uses `scp`:
|
||||
|
||||
```bash
|
||||
scp -o ConnectTimeout=30 hostname:path local_path
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
- SSH key authentication configured
|
||||
- SSH access between hosts
|
||||
- Read permissions on source file
|
||||
|
||||
### Checksum Verification
|
||||
|
||||
After transfer:
|
||||
1. Calculate SHA256 of fetched file
|
||||
2. Compare with checksum from notification
|
||||
3. If mismatch: retry (max 3 attempts)
|
||||
4. If success: cache and record in Redis
|
||||
|
||||
## Redis Storage
|
||||
|
||||
### Published Events
|
||||
|
||||
```
|
||||
Key: fsync:events:{timestamp}:{hostname}:{pid}
|
||||
|
||||
Example:
|
||||
fsync:events:2026-05-18T13:30:00Z:server1:1234
|
||||
├─ hostname: "server1"
|
||||
├─ path: "/var/www/app.js"
|
||||
├─ event_type: "MODIFY"
|
||||
├─ checksum: "a1b2c3d4..."
|
||||
└─ timestamp: "2026-05-18T13:30:00Z"
|
||||
```
|
||||
|
||||
### Fetch Records
|
||||
|
||||
```
|
||||
Key: fsync:fetches:success:{hostname}:{path}:{timestamp}
|
||||
Key: fsync:fetches:failed:{hostname}:{path}:{timestamp}
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Throughput
|
||||
|
||||
- **Event detection:** <1ms
|
||||
- **Publishing:** ~5-10ms per event
|
||||
- **File fetch:** Depends on size/network
|
||||
- **Verification:** ~100-500ms per file
|
||||
|
||||
### Queue Processing
|
||||
|
||||
- Check interval: 1 second
|
||||
- Process 1 file per 5 seconds (configurable)
|
||||
- Network-bound for file transfer
|
||||
|
||||
### Memory Usage
|
||||
|
||||
- Queue: ~1-2KB per pending file
|
||||
- Cache: Limited by disk space
|
||||
- Connections: ~5MB per RabbitMQ connection
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Queue Not Draining
|
||||
|
||||
```
|
||||
[QUEUE] Added: host2:/etc/config (queue size: 100)
|
||||
[FETCHER] Failed to fetch: host2:/etc/config
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
1. SSH key not configured
|
||||
2. File permissions on source
|
||||
3. Network connectivity
|
||||
4. Host not reachable
|
||||
|
||||
**Debug:**
|
||||
```bash
|
||||
# Test SSH access
|
||||
ssh user@host2 'ls -la /etc/config'
|
||||
|
||||
# Check firewall
|
||||
nc -zv host2 22
|
||||
```
|
||||
|
||||
### Checksum Mismatch
|
||||
|
||||
```
|
||||
[FETCHER] Checksum mismatch: /tmp/fsync-cache/host2_config
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
1. File changed during transfer
|
||||
2. Network corruption
|
||||
3. Filesystem error
|
||||
|
||||
**Action:** Automatic retry (up to 3 times)
|
||||
|
||||
### RabbitMQ Connection Lost
|
||||
|
||||
```
|
||||
Failed to open AMQP socket
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
docker-compose logs rabbitmq
|
||||
docker-compose restart rabbitmq
|
||||
```
|
||||
|
||||
### File Transfer Timeout
|
||||
|
||||
```
|
||||
[FETCHER] Failed to fetch after 3 attempts
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
1. Increase `FETCH_TIMEOUT_SEC` in config
|
||||
2. Check network bandwidth
|
||||
3. Check host load
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Cache Directory
|
||||
|
||||
```c
|
||||
#define CACHE_DIR "/var/lib/fsync-cache"
|
||||
```
|
||||
|
||||
Persist fetched files across restarts.
|
||||
|
||||
### Parallel Fetching (Future)
|
||||
|
||||
Currently processes 1 file at a time. Can be extended to:
|
||||
- Multi-threaded fetcher
|
||||
- Batch SCP transfers
|
||||
- Parallel checksums
|
||||
|
||||
### Routing Filters
|
||||
|
||||
Modify routing key to sync specific paths:
|
||||
|
||||
```c
|
||||
#define AMQP_ROUTING_KEY "fsync.file.var.www.*" /* Only /var/www */
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### SSH Authentication
|
||||
|
||||
- Configure SSH keys for password-less access
|
||||
- Use key-based authentication
|
||||
- Restrict SSH to internal network
|
||||
|
||||
### RabbitMQ
|
||||
|
||||
- Change default guest/guest credentials
|
||||
- Use TLS for AMQP connections
|
||||
- Restrict network access
|
||||
|
||||
### File Permissions
|
||||
|
||||
- Monitor has read access to synced files
|
||||
- Cache directory writable by monitor
|
||||
- Source files readable by SSH user
|
||||
|
||||
## Monitoring and Metrics
|
||||
|
||||
### Queue Depth
|
||||
|
||||
```bash
|
||||
redis-cli LLEN fsync:events:list
|
||||
```
|
||||
|
||||
### Fetch Success Rate
|
||||
|
||||
```bash
|
||||
redis-cli KEYS "fsync:fetches:*" | wc -l
|
||||
```
|
||||
|
||||
### Cache Size
|
||||
|
||||
```bash
|
||||
du -sh /tmp/fsync-cache/
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] Basic file sync via RabbitMQ
|
||||
- [x] Priority queue (age + size)
|
||||
- [x] Checksum verification
|
||||
- [ ] Multi-threaded fetching
|
||||
- [ ] Rsync support (resume capability)
|
||||
- [ ] Compression for transfers
|
||||
- [ ] Bandwidth throttling
|
||||
- [ ] Web UI for monitoring
|
||||
- [ ] Metrics export (Prometheus)
|
||||
299
src/amqp_client.c
Normal file
299
src/amqp_client.c
Normal file
@@ -0,0 +1,299 @@
|
||||
#include "amqp_client.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <sys/time.h>
|
||||
#include <amqp.h>
|
||||
#include <amqp_tcp_socket.h>
|
||||
#include <time.h>
|
||||
|
||||
struct amqp_conn {
|
||||
amqp_connection_state_t conn;
|
||||
amqp_channel_t channel;
|
||||
};
|
||||
|
||||
/* Helper: Check AMQP reply */
|
||||
static int check_amqp_error(amqp_rpc_reply_t reply)
|
||||
{
|
||||
if (reply.reply_type == AMQP_RESPONSE_NORMAL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (reply.reply_type) {
|
||||
case AMQP_RESPONSE_LIBRARY_EXCEPTION:
|
||||
fprintf(stderr, "AMQP library error: %s\n",
|
||||
amqp_error_string2(reply.library_error));
|
||||
break;
|
||||
case AMQP_RESPONSE_SERVER_EXCEPTION:
|
||||
fprintf(stderr, "AMQP server error\n");
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "AMQP unknown error\n");
|
||||
break;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Connect to RabbitMQ */
|
||||
amqp_conn_t* amqp_connect(const char *host, int port, const char *user,
|
||||
const char *password, const char *vhost)
|
||||
{
|
||||
if (!host || port <= 0 || !user || !password || !vhost) {
|
||||
errno = EINVAL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
amqp_conn_t *conn = (amqp_conn_t *)malloc(sizeof(amqp_conn_t));
|
||||
if (!conn) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
conn->conn = amqp_new_connection();
|
||||
if (!conn->conn) {
|
||||
free(conn);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
amqp_socket_t *socket = amqp_tcp_socket_new(conn->conn);
|
||||
if (!socket) {
|
||||
amqp_destroy_connection(conn->conn);
|
||||
free(conn);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int status = amqp_socket_open(socket, host, port);
|
||||
if (status != AMQP_STATUS_OK) {
|
||||
fprintf(stderr, "Failed to open AMQP socket: %s\n",
|
||||
amqp_error_string2(status));
|
||||
amqp_destroy_connection(conn->conn);
|
||||
free(conn);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
amqp_rpc_reply_t reply = amqp_login(conn->conn, vhost, 0,
|
||||
AMQP_DEFAULT_FRAME_SIZE,
|
||||
0, AMQP_SASL_METHOD_PLAIN,
|
||||
user, password);
|
||||
|
||||
if (check_amqp_error(reply) != 0) {
|
||||
amqp_destroy_connection(conn->conn);
|
||||
free(conn);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
conn->channel = 1;
|
||||
amqp_channel_open(conn->conn, conn->channel);
|
||||
|
||||
reply = amqp_get_rpc_reply(conn->conn);
|
||||
if (check_amqp_error(reply) != 0) {
|
||||
amqp_destroy_connection(conn->conn);
|
||||
free(conn);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
printf("Connected to RabbitMQ at %s:%d\n", host, port);
|
||||
return conn;
|
||||
}
|
||||
|
||||
/* Disconnect from RabbitMQ */
|
||||
void amqp_disconnect(amqp_conn_t *conn)
|
||||
{
|
||||
if (!conn) return;
|
||||
|
||||
amqp_channel_close(conn->conn, conn->channel, AMQP_REPLY_SUCCESS);
|
||||
amqp_connection_close(conn->conn, AMQP_REPLY_SUCCESS);
|
||||
amqp_destroy_connection(conn->conn);
|
||||
free(conn);
|
||||
}
|
||||
|
||||
/* Declare queue */
|
||||
int amqp_declare_queue(amqp_conn_t *conn, const char *queue_name)
|
||||
{
|
||||
if (!conn || !queue_name) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
amqp_queue_declare(conn->conn, conn->channel,
|
||||
amqp_cstring_bytes(queue_name),
|
||||
0, 1, 0, 0, amqp_empty_table);
|
||||
|
||||
amqp_rpc_reply_t reply = amqp_get_rpc_reply(conn->conn);
|
||||
return check_amqp_error(reply);
|
||||
}
|
||||
|
||||
/* Declare exchange */
|
||||
int amqp_declare_exchange(amqp_conn_t *conn, const char *exchange_name)
|
||||
{
|
||||
if (!conn || !exchange_name) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
amqp_exchange_declare(conn->conn, conn->channel,
|
||||
amqp_cstring_bytes(exchange_name),
|
||||
amqp_cstring_bytes("topic"),
|
||||
0, 1, 0, 0, amqp_empty_table);
|
||||
|
||||
amqp_rpc_reply_t reply = amqp_get_rpc_reply(conn->conn);
|
||||
return check_amqp_error(reply);
|
||||
}
|
||||
|
||||
/* Bind queue to exchange */
|
||||
int amqp_bind_queue(amqp_conn_t *conn, const char *queue_name,
|
||||
const char *exchange_name, const char *routing_key)
|
||||
{
|
||||
if (!conn || !queue_name || !exchange_name || !routing_key) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
amqp_queue_bind(conn->conn, conn->channel,
|
||||
amqp_cstring_bytes(queue_name),
|
||||
amqp_cstring_bytes(exchange_name),
|
||||
amqp_cstring_bytes(routing_key),
|
||||
amqp_empty_table);
|
||||
|
||||
amqp_rpc_reply_t reply = amqp_get_rpc_reply(conn->conn);
|
||||
return check_amqp_error(reply);
|
||||
}
|
||||
|
||||
/* Publish event */
|
||||
int amqp_publish_event(amqp_conn_t *conn, const char *exchange_name,
|
||||
const char *routing_key, const fs_event_t *event)
|
||||
{
|
||||
if (!conn || !exchange_name || !routing_key || !event) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Format event as JSON-like message */
|
||||
char message[4096];
|
||||
snprintf(message, sizeof(message),
|
||||
"{\"hostname\":\"%s\",\"path\":\"%s\",\"event\":\"%s\","
|
||||
"\"checksum\":\"%s\",\"timestamp\":%ld,\"size\":%llu}",
|
||||
event->hostname, event->path, event->event_type,
|
||||
event->checksum, event->timestamp,
|
||||
(unsigned long long)event->file_size);
|
||||
|
||||
amqp_basic_properties_t props;
|
||||
props._flags = AMQP_BASIC_CONTENT_TYPE_FLAG |
|
||||
AMQP_BASIC_DELIVERY_MODE_FLAG;
|
||||
props.content_type = amqp_cstring_bytes("application/json");
|
||||
props.delivery_mode = 2; /* Persistent */
|
||||
|
||||
int result = amqp_basic_publish(conn->conn, conn->channel,
|
||||
amqp_cstring_bytes(exchange_name),
|
||||
amqp_cstring_bytes(routing_key),
|
||||
0, 0, &props,
|
||||
amqp_cstring_bytes(message));
|
||||
|
||||
return (result == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Listen for messages (blocking) */
|
||||
int amqp_listen(amqp_conn_t *conn, const char *queue_name,
|
||||
message_callback_t callback, void *context)
|
||||
{
|
||||
if (!conn || !queue_name || !callback) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
amqp_basic_consume(conn->conn, conn->channel,
|
||||
amqp_cstring_bytes(queue_name),
|
||||
amqp_empty_bytes, 0, 1, 0, amqp_empty_table);
|
||||
|
||||
amqp_rpc_reply_t reply = amqp_get_rpc_reply(conn->conn);
|
||||
if (check_amqp_error(reply) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (1) {
|
||||
amqp_envelope_t envelope;
|
||||
amqp_rpc_reply_t res = amqp_consume_message(conn->conn, &envelope, 0, 0);
|
||||
|
||||
if (AMQP_RESPONSE_NORMAL != res.reply_type) {
|
||||
break;
|
||||
}
|
||||
|
||||
char msg_str[4096];
|
||||
size_t len = envelope.message.body.len < sizeof(msg_str) - 1 ?
|
||||
envelope.message.body.len : sizeof(msg_str) - 1;
|
||||
memcpy(msg_str, envelope.message.body.bytes, len);
|
||||
msg_str[len] = '\0';
|
||||
|
||||
callback(msg_str, context);
|
||||
|
||||
amqp_destroy_envelope(&envelope);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Consume single message (non-blocking) */
|
||||
int amqp_consume_one(amqp_conn_t *conn, const char *queue_name,
|
||||
message_callback_t callback, void *context)
|
||||
{
|
||||
if (!conn || !queue_name || !callback) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
amqp_envelope_t envelope;
|
||||
amqp_rpc_reply_t res = amqp_consume_message(conn->conn, &envelope, 0, 0);
|
||||
|
||||
if (res.reply_type != AMQP_RESPONSE_NORMAL) {
|
||||
return 0; /* No message available */
|
||||
}
|
||||
|
||||
char msg_str[4096];
|
||||
size_t len = envelope.message.body.len < sizeof(msg_str) - 1 ?
|
||||
envelope.message.body.len : sizeof(msg_str) - 1;
|
||||
memcpy(msg_str, envelope.message.body.bytes, len);
|
||||
msg_str[len] = '\0';
|
||||
|
||||
callback(msg_str, context);
|
||||
|
||||
amqp_destroy_envelope(&envelope);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Get queue depth */
|
||||
int amqp_get_queue_depth(amqp_conn_t *conn, const char *queue_name)
|
||||
{
|
||||
if (!conn || !queue_name) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Declare passive queue to get message count */
|
||||
amqp_queue_declare(conn->conn, conn->channel,
|
||||
amqp_cstring_bytes(queue_name),
|
||||
1, 0, 0, 0, amqp_empty_table);
|
||||
|
||||
amqp_rpc_reply_t reply = amqp_get_rpc_reply(conn->conn);
|
||||
if (check_amqp_error(reply) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
amqp_queue_declare_ok_t *ok = (amqp_queue_declare_ok_t *)reply.reply.decoded;
|
||||
return ok->message_count;
|
||||
}
|
||||
|
||||
/* Health check */
|
||||
int amqp_ping(amqp_conn_t *conn)
|
||||
{
|
||||
if (!conn) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Send a basic method to verify connection */
|
||||
amqp_basic_qos(conn->conn, conn->channel, 0, 1, 0);
|
||||
amqp_rpc_reply_t reply = amqp_get_rpc_reply(conn->conn);
|
||||
|
||||
return check_amqp_error(reply);
|
||||
}
|
||||
47
src/amqp_client.h
Normal file
47
src/amqp_client.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#ifndef FSYNC_AMQP_CLIENT_H
|
||||
#define FSYNC_AMQP_CLIENT_H
|
||||
|
||||
#include "monitor.h"
|
||||
|
||||
/* AMQP connection structure */
|
||||
typedef struct amqp_conn amqp_conn_t;
|
||||
|
||||
/* Message callback function */
|
||||
typedef void (*message_callback_t)(const char *message, void *context);
|
||||
|
||||
/* Connect to RabbitMQ */
|
||||
amqp_conn_t* amqp_connect(const char *host, int port, const char *user,
|
||||
const char *password, const char *vhost);
|
||||
|
||||
/* Disconnect from RabbitMQ */
|
||||
void amqp_disconnect(amqp_conn_t *conn);
|
||||
|
||||
/* Declare queue for receiving file updates */
|
||||
int amqp_declare_queue(amqp_conn_t *conn, const char *queue_name);
|
||||
|
||||
/* Declare exchange for publishing events */
|
||||
int amqp_declare_exchange(amqp_conn_t *conn, const char *exchange_name);
|
||||
|
||||
/* Bind queue to exchange */
|
||||
int amqp_bind_queue(amqp_conn_t *conn, const char *queue_name,
|
||||
const char *exchange_name, const char *routing_key);
|
||||
|
||||
/* Publish file change event */
|
||||
int amqp_publish_event(amqp_conn_t *conn, const char *exchange_name,
|
||||
const char *routing_key, const fs_event_t *event);
|
||||
|
||||
/* Listen for file update messages */
|
||||
int amqp_listen(amqp_conn_t *conn, const char *queue_name,
|
||||
message_callback_t callback, void *context);
|
||||
|
||||
/* Process single message non-blocking */
|
||||
int amqp_consume_one(amqp_conn_t *conn, const char *queue_name,
|
||||
message_callback_t callback, void *context);
|
||||
|
||||
/* Get queue depth */
|
||||
int amqp_get_queue_depth(amqp_conn_t *conn, const char *queue_name);
|
||||
|
||||
/* Health check */
|
||||
int amqp_ping(amqp_conn_t *conn);
|
||||
|
||||
#endif /* FSYNC_AMQP_CLIENT_H */
|
||||
107
src/checksum.c
Normal file
107
src/checksum.c
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "checksum.h"
|
||||
#include "config.h"
|
||||
#include <openssl/sha.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define READ_BUF_SIZE 65536
|
||||
|
||||
/* Calculate SHA256 checksum of a file */
|
||||
int checksum_file_sha256(const char *filepath, char *checksum_hex,
|
||||
size_t checksum_len)
|
||||
{
|
||||
if (!filepath || !checksum_hex || checksum_len < 65) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Check file size */
|
||||
struct stat st;
|
||||
if (stat(filepath, &st) == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (st.st_size > MAX_FILE_SIZE_FOR_CHECKSUM) {
|
||||
/* File too large, use empty hash */
|
||||
strcpy(checksum_hex, "0000000000000000000000000000000000000000000000000000000000000000");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fd = open(filepath, O_RDONLY);
|
||||
if (fd == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
SHA256_CTX sha256;
|
||||
SHA256_Init(&sha256);
|
||||
|
||||
unsigned char buffer[READ_BUF_SIZE];
|
||||
ssize_t bytes_read;
|
||||
|
||||
while ((bytes_read = read(fd, buffer, READ_BUF_SIZE)) > 0) {
|
||||
SHA256_Update(&sha256, buffer, bytes_read);
|
||||
}
|
||||
|
||||
close(fd);
|
||||
|
||||
if (bytes_read == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned char digest[SHA256_DIGEST_LENGTH];
|
||||
SHA256_Final(digest, &sha256);
|
||||
|
||||
/* Convert to hex */
|
||||
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
|
||||
snprintf(checksum_hex + (i * 2), 3, "%02x", digest[i]);
|
||||
}
|
||||
checksum_hex[64] = '\0';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Calculate SHA256 checksum of a buffer */
|
||||
int checksum_buffer_sha256(const unsigned char *buffer, size_t len,
|
||||
char *checksum_hex, size_t checksum_len)
|
||||
{
|
||||
if (!buffer || !checksum_hex || checksum_len < 65) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
SHA256_CTX sha256;
|
||||
SHA256_Init(&sha256);
|
||||
SHA256_Update(&sha256, buffer, len);
|
||||
|
||||
unsigned char digest[SHA256_DIGEST_LENGTH];
|
||||
SHA256_Final(digest, &sha256);
|
||||
|
||||
/* Convert to hex */
|
||||
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
|
||||
snprintf(checksum_hex + (i * 2), 3, "%02x", digest[i]);
|
||||
}
|
||||
checksum_hex[64] = '\0';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Verify a checksum matches the file */
|
||||
int checksum_verify(const char *filepath, const char *expected_checksum)
|
||||
{
|
||||
if (!filepath || !expected_checksum) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
char actual_checksum[65];
|
||||
if (checksum_file_sha256(filepath, actual_checksum, sizeof(actual_checksum)) == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return strcmp(actual_checksum, expected_checksum) == 0 ? 0 : 1;
|
||||
}
|
||||
17
src/checksum.h
Normal file
17
src/checksum.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef FSYNC_CHECKSUM_H
|
||||
#define FSYNC_CHECKSUM_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* Calculate SHA256 checksum of a file */
|
||||
int checksum_file_sha256(const char *filepath, char *checksum_hex,
|
||||
size_t checksum_len);
|
||||
|
||||
/* Calculate SHA256 checksum of a buffer */
|
||||
int checksum_buffer_sha256(const unsigned char *buffer, size_t len,
|
||||
char *checksum_hex, size_t checksum_len);
|
||||
|
||||
/* Verify a checksum matches the file */
|
||||
int checksum_verify(const char *filepath, const char *expected_checksum);
|
||||
|
||||
#endif /* FSYNC_CHECKSUM_H */
|
||||
61
src/config.h
Normal file
61
src/config.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#ifndef FSYNC_CONFIG_H
|
||||
#define FSYNC_CONFIG_H
|
||||
|
||||
/* Redis Configuration */
|
||||
#define REDIS_HOST "127.0.0.1"
|
||||
#define REDIS_PORT 6379
|
||||
#define REDIS_DB 0
|
||||
#define REDIS_TIMEOUT_SEC 5
|
||||
|
||||
/* RabbitMQ Configuration */
|
||||
#define AMQP_HOST "127.0.0.1"
|
||||
#define AMQP_PORT 5672
|
||||
#define AMQP_USER "guest"
|
||||
#define AMQP_PASSWORD "guest"
|
||||
#define AMQP_VHOST "/"
|
||||
#define AMQP_EXCHANGE "fsync.events"
|
||||
#define AMQP_QUEUE_UPDATES "fsync.file.updates"
|
||||
#define AMQP_ROUTING_KEY "fsync.file.*"
|
||||
|
||||
/* Fanotify Configuration */
|
||||
#define FANOTIFY_CLASS FAN_CLASS_NOTIF
|
||||
#define FANOTIFY_FLAGS (O_CLOEXEC | O_NONBLOCK)
|
||||
|
||||
/* Event Mask - what events to monitor */
|
||||
#define FANOTIFY_MASK (FAN_OPEN | FAN_CLOSE | FAN_MODIFY | FAN_CREATE | \
|
||||
FAN_DELETE | FAN_MOVE | FAN_ATTRIB)
|
||||
|
||||
/* Monitoring Paths */
|
||||
#define DEFAULT_MONITOR_PATH "/"
|
||||
#define MAX_MONITOR_PATHS 10
|
||||
|
||||
/* Buffer Sizes */
|
||||
#define EVENT_BUF_LEN 4096
|
||||
#define PATH_BUF_LEN 4096
|
||||
#define CHECKSUM_BUF_LEN 65
|
||||
#define HOSTNAME_MAX_LEN 256
|
||||
|
||||
/* Checksum Configuration */
|
||||
#define CHECKSUM_ALGORITHM "sha256"
|
||||
#define CHECKSUM_HEX_LEN 64
|
||||
|
||||
/* Queue Configuration */
|
||||
#define MAX_QUEUE_SIZE 1000
|
||||
#define CACHE_DIR "/tmp/fsync-cache"
|
||||
|
||||
/* Logging */
|
||||
#define LOG_LEVEL_DEBUG 0
|
||||
#define LOG_LEVEL_INFO 1
|
||||
#define LOG_LEVEL_WARN 2
|
||||
#define LOG_LEVEL_ERROR 3
|
||||
#define CURRENT_LOG_LEVEL LOG_LEVEL_INFO
|
||||
|
||||
/* Limits */
|
||||
#define MAX_EVENT_QUEUE_SIZE 1000
|
||||
#define MAX_FILE_SIZE_FOR_CHECKSUM (100 * 1024 * 1024) /* 100MB */
|
||||
|
||||
/* Polling Configuration */
|
||||
#define QUEUE_POLL_INTERVAL_MS 1000 /* Check queue every second */
|
||||
#define AMQP_CONSUME_TIMEOUT_MS 100 /* Non-blocking consume timeout */
|
||||
|
||||
#endif /* FSYNC_CONFIG_H */
|
||||
158
src/file_fetcher.c
Normal file
158
src/file_fetcher.c
Normal file
@@ -0,0 +1,158 @@
|
||||
#include "file_fetcher.h"
|
||||
#include "checksum.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
/* Initialize fetcher */
|
||||
int fetcher_init(fetcher_config_t *config, const char *cache_dir)
|
||||
{
|
||||
if (!config || !cache_dir) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
strncpy(config->local_cache_dir, cache_dir, sizeof(config->local_cache_dir) - 1);
|
||||
config->fetch_timeout_sec = 30;
|
||||
config->max_retries = 3;
|
||||
|
||||
/* Create cache directory if it doesn't exist */
|
||||
mkdir(cache_dir, 0755);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Fetch file from remote host */
|
||||
int fetcher_fetch_file(const file_update_t *update, const char *local_path,
|
||||
fetcher_config_t *config)
|
||||
{
|
||||
if (!update || !local_path || !config) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a stub implementation.
|
||||
* In a real scenario, you would:
|
||||
* 1. SSH to hostname
|
||||
* 2. Download file via scp/rsync
|
||||
* 3. Store to local_path
|
||||
*
|
||||
* For demonstration, we create a placeholder:
|
||||
*/
|
||||
|
||||
char cmd[2048];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"scp -q -o ConnectTimeout=%d %s:%s %s 2>/dev/null",
|
||||
config->fetch_timeout_sec,
|
||||
update->hostname,
|
||||
update->path,
|
||||
local_path);
|
||||
|
||||
int result = system(cmd);
|
||||
return (result == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Verify fetched file against checksum */
|
||||
int fetcher_verify_checksum(const char *file_path, const char *expected_checksum)
|
||||
{
|
||||
if (!file_path || !expected_checksum) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return checksum_verify(file_path, expected_checksum);
|
||||
}
|
||||
|
||||
/* Process queue item - fetch and verify */
|
||||
int fetcher_process_update(const file_update_t *update, fetcher_config_t *config)
|
||||
{
|
||||
if (!update || !config) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
char local_path[4096];
|
||||
snprintf(local_path, sizeof(local_path), "%s/%s_%s_%ld",
|
||||
config->local_cache_dir,
|
||||
update->hostname,
|
||||
strrchr(update->path, '/') ? strrchr(update->path, '/') + 1 : update->path,
|
||||
time(NULL));
|
||||
|
||||
printf("[FETCHER] Processing: %s:%s (size=%llu, age=%ld sec)\n",
|
||||
update->hostname,
|
||||
update->path,
|
||||
(unsigned long long)update->file_size,
|
||||
time(NULL) - update->detected_at);
|
||||
|
||||
/* Attempt to fetch file */
|
||||
for (int attempt = 0; attempt < config->max_retries; attempt++) {
|
||||
if (fetcher_fetch_file(update, local_path, config) == 0) {
|
||||
printf("[FETCHER] Downloaded: %s\n", local_path);
|
||||
|
||||
/* Verify checksum if available */
|
||||
if (update->checksum[0] != '\0') {
|
||||
if (fetcher_verify_checksum(local_path, update->checksum) == 0) {
|
||||
printf("[FETCHER] Checksum verified: %s\n", local_path);
|
||||
return 0;
|
||||
} else {
|
||||
printf("[FETCHER] Checksum mismatch: %s\n", local_path);
|
||||
unlink(local_path);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (attempt < config->max_retries - 1) {
|
||||
sleep(2);
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "[FETCHER] Failed to fetch: %s:%s\n",
|
||||
update->hostname, update->path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Record successful fetch in Redis */
|
||||
int fetcher_record_success(redis_conn_t *redis, const file_update_t *update,
|
||||
const char *local_path)
|
||||
{
|
||||
if (!redis || !update || !local_path) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Store fetch record in Redis */
|
||||
char key[512];
|
||||
snprintf(key, sizeof(key), "fsync:fetches:success:%s:%s:%ld",
|
||||
update->hostname, update->path, time(NULL));
|
||||
|
||||
/* Implementation would use redis_command here */
|
||||
printf("[REDIS] Recorded successful fetch: %s\n", key);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Record failed fetch attempt */
|
||||
int fetcher_record_failure(redis_conn_t *redis, const file_update_t *update,
|
||||
const char *reason)
|
||||
{
|
||||
if (!redis || !update) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
char key[512];
|
||||
snprintf(key, sizeof(key), "fsync:fetches:failed:%s:%s:%ld",
|
||||
update->hostname, update->path, time(NULL));
|
||||
|
||||
printf("[REDIS] Recorded fetch failure: %s (reason: %s)\n", key, reason ? reason : "unknown");
|
||||
|
||||
return 0;
|
||||
}
|
||||
36
src/file_fetcher.h
Normal file
36
src/file_fetcher.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#ifndef FSYNC_FILE_FETCHER_H
|
||||
#define FSYNC_FILE_FETCHER_H
|
||||
|
||||
#include "queue_manager.h"
|
||||
#include "redis_client.h"
|
||||
|
||||
/* Fetcher context */
|
||||
typedef struct {
|
||||
redis_conn_t *redis;
|
||||
char local_cache_dir[4096];
|
||||
int fetch_timeout_sec;
|
||||
int max_retries;
|
||||
} fetcher_config_t;
|
||||
|
||||
/* Initialize fetcher */
|
||||
int fetcher_init(fetcher_config_t *config, const char *cache_dir);
|
||||
|
||||
/* Fetch file from remote host (stub for distributed systems) */
|
||||
int fetcher_fetch_file(const file_update_t *update, const char *local_path,
|
||||
fetcher_config_t *config);
|
||||
|
||||
/* Verify fetched file against checksum */
|
||||
int fetcher_verify_checksum(const char *file_path, const char *expected_checksum);
|
||||
|
||||
/* Process queue item - fetch and verify */
|
||||
int fetcher_process_update(const file_update_t *update, fetcher_config_t *config);
|
||||
|
||||
/* Record successful fetch in Redis */
|
||||
int fetcher_record_success(redis_conn_t *redis, const file_update_t *update,
|
||||
const char *local_path);
|
||||
|
||||
/* Record failed fetch attempt */
|
||||
int fetcher_record_failure(redis_conn_t *redis, const file_update_t *update,
|
||||
const char *reason);
|
||||
|
||||
#endif /* FSYNC_FILE_FETCHER_H */
|
||||
357
src/main.c
Normal file
357
src/main.c
Normal file
@@ -0,0 +1,357 @@
|
||||
#include "monitor.h"
|
||||
#include "redis_client.h"
|
||||
#include "amqp_client.h"
|
||||
#include "queue_manager.h"
|
||||
#include "file_fetcher.h"
|
||||
#include "checksum.h"
|
||||
#include "config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/fanotify.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <time.h>
|
||||
#include <cjson/cJSON.h>
|
||||
|
||||
/* Global state */
|
||||
static int fanotify_fd = -1;
|
||||
static redis_conn_t *redis_conn = NULL;
|
||||
static amqp_conn_t *amqp_conn = NULL;
|
||||
static queue_manager_t *file_queue = NULL;
|
||||
static volatile int running = 1;
|
||||
|
||||
/* Signal handler */
|
||||
void signal_handler(int sig)
|
||||
{
|
||||
running = 0;
|
||||
}
|
||||
|
||||
/* Get event name from mask */
|
||||
const char* get_event_name(uint32_t mask)
|
||||
{
|
||||
if (mask & FAN_OPEN)
|
||||
return "OPEN";
|
||||
if (mask & FAN_CLOSE_WRITE)
|
||||
return "CLOSE_WRITE";
|
||||
if (mask & FAN_CLOSE_NOWRITE)
|
||||
return "CLOSE_NOWRITE";
|
||||
if (mask & FAN_MODIFY)
|
||||
return "MODIFY";
|
||||
if (mask & FAN_CREATE)
|
||||
return "CREATE";
|
||||
if (mask & FAN_DELETE)
|
||||
return "DELETE";
|
||||
if (mask & FAN_MOVED_FROM)
|
||||
return "MOVED_FROM";
|
||||
if (mask & FAN_MOVED_TO)
|
||||
return "MOVED_TO";
|
||||
if (mask & FAN_ATTRIB)
|
||||
return "ATTRIB";
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
/* Get hostname */
|
||||
int get_hostname_str(char *hostname, size_t len)
|
||||
{
|
||||
if (gethostname(hostname, len) == -1) {
|
||||
strncpy(hostname, "unknown", len - 1);
|
||||
hostname[len - 1] = '\0';
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Parse received message and add to queue */
|
||||
void message_callback(const char *message, void *context)
|
||||
{
|
||||
if (!message) return;
|
||||
|
||||
printf("[AMQP] Received message: %.100s\n", message);
|
||||
|
||||
/* Parse JSON message */
|
||||
cJSON *json = cJSON_Parse(message);
|
||||
if (!json) {
|
||||
fprintf(stderr, "[AMQP] Failed to parse JSON\n");
|
||||
return;
|
||||
}
|
||||
|
||||
file_update_t update;
|
||||
memset(&update, 0, sizeof(update));
|
||||
|
||||
/* Extract fields */
|
||||
cJSON *hostname = cJSON_GetObjectItem(json, "hostname");
|
||||
cJSON *path = cJSON_GetObjectItem(json, "path");
|
||||
cJSON *checksum = cJSON_GetObjectItem(json, "checksum");
|
||||
cJSON *size = cJSON_GetObjectItem(json, "file_size");
|
||||
cJSON *timestamp = cJSON_GetObjectItem(json, "detected_at");
|
||||
|
||||
if (hostname && hostname->valuestring)
|
||||
strncpy(update.hostname, hostname->valuestring, sizeof(update.hostname) - 1);
|
||||
if (path && path->valuestring)
|
||||
strncpy(update.path, path->valuestring, sizeof(update.path) - 1);
|
||||
if (checksum && checksum->valuestring)
|
||||
strncpy(update.checksum, checksum->valuestring, sizeof(update.checksum) - 1);
|
||||
if (size && size->type == cJSON_Number)
|
||||
update.file_size = (uint64_t)size->valuedouble;
|
||||
if (timestamp && timestamp->type == cJSON_Number)
|
||||
update.detected_at = (time_t)timestamp->valuedouble;
|
||||
else
|
||||
update.detected_at = time(NULL);
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
/* Add to queue */
|
||||
if (queue_manager_add(file_queue, &update) >= 0) {
|
||||
printf("[QUEUE] Added: %s:%s (queue size: %d)\n",
|
||||
update.hostname, update.path, queue_manager_size(file_queue));
|
||||
} else {
|
||||
fprintf(stderr, "[QUEUE] Failed to add to queue\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* Process local file system event */
|
||||
void process_event(const struct fanotify_event_metadata *metadata)
|
||||
{
|
||||
char proc_path[64];
|
||||
char file_path[PATH_BUF_LEN];
|
||||
char checksum[CHECKSUM_BUF_LEN];
|
||||
char hostname[HOSTNAME_MAX_LEN];
|
||||
struct stat st;
|
||||
fs_event_t event;
|
||||
|
||||
if (metadata->fd < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Get file path from /proc */
|
||||
snprintf(proc_path, sizeof(proc_path), "/proc/self/fd/%d", metadata->fd);
|
||||
ssize_t len = readlink(proc_path, file_path, sizeof(file_path) - 1);
|
||||
if (len <= 0) {
|
||||
close(metadata->fd);
|
||||
return;
|
||||
}
|
||||
file_path[len] = '\0';
|
||||
|
||||
/* Get file stats */
|
||||
if (fstat(metadata->fd, &st) == -1) {
|
||||
close(metadata->fd);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Only process regular files */
|
||||
if (!S_ISREG(st.st_mode)) {
|
||||
close(metadata->fd);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Get checksum for certain events */
|
||||
memset(checksum, 0, sizeof(checksum));
|
||||
if (metadata->mask & (FAN_MODIFY | FAN_CLOSE_WRITE)) {
|
||||
if (checksum_file_sha256(file_path, checksum, sizeof(checksum)) == -1) {
|
||||
strcpy(checksum, "error");
|
||||
}
|
||||
}
|
||||
|
||||
/* Prepare event structure */
|
||||
memset(&event, 0, sizeof(event));
|
||||
get_hostname_str(hostname, sizeof(hostname));
|
||||
strncpy(event.hostname, hostname, sizeof(event.hostname) - 1);
|
||||
strncpy(event.path, file_path, sizeof(event.path) - 1);
|
||||
strncpy(event.event_type, get_event_name(metadata->mask),
|
||||
sizeof(event.event_type) - 1);
|
||||
strncpy(event.checksum, checksum, sizeof(event.checksum) - 1);
|
||||
event.timestamp = time(NULL);
|
||||
event.file_size = st.st_size;
|
||||
event.permissions = st.st_mode & 07777;
|
||||
event.process_id = metadata->pid;
|
||||
|
||||
/* Store in Redis */
|
||||
redis_store_event(redis_conn, &event);
|
||||
|
||||
/* Publish to RabbitMQ */
|
||||
if (amqp_publish_event(amqp_conn, AMQP_EXCHANGE, AMQP_ROUTING_KEY, &event) == 0) {
|
||||
printf("[AMQP] Published: %s:%s\n", hostname, file_path);
|
||||
}
|
||||
|
||||
printf("[EVENT] %s %s:%s (size=%lld, checksum=%s)\n",
|
||||
get_event_name(metadata->mask), hostname, file_path,
|
||||
(long long)st.st_size, checksum);
|
||||
|
||||
close(metadata->fd);
|
||||
}
|
||||
|
||||
/* Initialize fanotify */
|
||||
int init_fanotify(void)
|
||||
{
|
||||
fanotify_fd = fanotify_init(FAN_CLASS_NOTIF, O_CLOEXEC | O_NONBLOCK);
|
||||
if (fanotify_fd == -1) {
|
||||
perror("fanotify_init");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fanotify_mark(fanotify_fd, FAN_MARK_ADD | FAN_MARK_FILESYSTEM,
|
||||
FAN_OPEN | FAN_CLOSE_WRITE | FAN_MODIFY |
|
||||
FAN_CREATE | FAN_DELETE | FAN_MOVE | FAN_ATTRIB,
|
||||
AT_FDCWD, "/") == -1) {
|
||||
perror("fanotify_mark");
|
||||
close(fanotify_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("Fanotify initialized (fd=%d)\n", fanotify_fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Main monitoring loop */
|
||||
int run_monitor(void)
|
||||
{
|
||||
unsigned char buf[4096];
|
||||
const struct fanotify_event_metadata *metadata;
|
||||
int ret;
|
||||
time_t last_fetch = 0;
|
||||
|
||||
while (running) {
|
||||
/* Process local fanotify events */
|
||||
ret = read(fanotify_fd, buf, sizeof(buf));
|
||||
if (ret > 0) {
|
||||
metadata = (struct fanotify_event_metadata *)buf;
|
||||
while (FAN_EVENT_OK(metadata, ret)) {
|
||||
if (metadata->vers == FANOTIFY_METADATA_VERSION) {
|
||||
process_event(metadata);
|
||||
}
|
||||
metadata = FAN_EVENT_NEXT(metadata, ret);
|
||||
}
|
||||
}
|
||||
|
||||
/* Check for RabbitMQ messages */
|
||||
amqp_consume_one(amqp_conn, AMQP_QUEUE_UPDATES, message_callback, NULL);
|
||||
|
||||
/* Process queue - fetch files every 5 seconds */
|
||||
if (time(NULL) - last_fetch >= 5) {
|
||||
int queue_size = queue_manager_size(file_queue);
|
||||
if (queue_size > 0) {
|
||||
file_update_t update;
|
||||
if (queue_manager_get_next(file_queue, &update) == 0) {
|
||||
printf("[FETCHER] Processing queue (%d items)\n", queue_size);
|
||||
|
||||
fetcher_config_t config;
|
||||
fetcher_init(&config, CACHE_DIR);
|
||||
|
||||
if (fetcher_process_update(&update, &config) == 0) {
|
||||
fetcher_record_success(redis_conn, &update, "cached");
|
||||
queue_manager_remove(file_queue, 0);
|
||||
} else {
|
||||
fetcher_record_failure(redis_conn, &update, "fetch failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
last_fetch = time(NULL);
|
||||
}
|
||||
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Cleanup */
|
||||
void cleanup(void)
|
||||
{
|
||||
running = 0;
|
||||
|
||||
if (fanotify_fd != -1) {
|
||||
close(fanotify_fd);
|
||||
}
|
||||
|
||||
if (file_queue) {
|
||||
queue_manager_destroy(file_queue);
|
||||
}
|
||||
|
||||
if (amqp_conn) {
|
||||
amqp_disconnect(amqp_conn);
|
||||
}
|
||||
|
||||
if (redis_conn) {
|
||||
redis_disconnect(redis_conn);
|
||||
}
|
||||
|
||||
printf("Cleaned up resources\n");
|
||||
}
|
||||
|
||||
/* Main entry point */
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
const char *redis_host = REDIS_HOST;
|
||||
int redis_port = REDIS_PORT;
|
||||
const char *amqp_host = AMQP_HOST;
|
||||
int amqp_port = AMQP_PORT;
|
||||
|
||||
printf("Fsync - File System Monitor with RabbitMQ\n");
|
||||
printf("Redis: %s:%d\n", redis_host, redis_port);
|
||||
printf("RabbitMQ: %s:%d\n", amqp_host, amqp_port);
|
||||
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
/* Connect to Redis */
|
||||
redis_conn = redis_connect(redis_host, redis_port);
|
||||
if (!redis_conn) {
|
||||
fprintf(stderr, "Failed to connect to Redis\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (redis_ping(redis_conn) != 0) {
|
||||
fprintf(stderr, "Redis health check failed\n");
|
||||
cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Redis connected successfully\n");
|
||||
|
||||
/* Connect to RabbitMQ */
|
||||
amqp_conn = amqp_connect(amqp_host, amqp_port, AMQP_USER,
|
||||
AMQP_PASSWORD, AMQP_VHOST);
|
||||
if (!amqp_conn) {
|
||||
fprintf(stderr, "Failed to connect to RabbitMQ\n");
|
||||
cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Setup AMQP exchange and queue */
|
||||
amqp_declare_exchange(amqp_conn, AMQP_EXCHANGE);
|
||||
amqp_declare_queue(amqp_conn, AMQP_QUEUE_UPDATES);
|
||||
amqp_bind_queue(amqp_conn, AMQP_QUEUE_UPDATES, AMQP_EXCHANGE, AMQP_ROUTING_KEY);
|
||||
|
||||
printf("RabbitMQ connected and configured\n");
|
||||
|
||||
/* Create file update queue */
|
||||
file_queue = queue_manager_create(MAX_QUEUE_SIZE);
|
||||
if (!file_queue) {
|
||||
fprintf(stderr, "Failed to create queue manager\n");
|
||||
cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Initialize fanotify */
|
||||
if (init_fanotify() != 0) {
|
||||
fprintf(stderr, "Failed to initialize fanotify\n");
|
||||
cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Starting file system monitor...\n");
|
||||
|
||||
if (run_monitor() != 0) {
|
||||
fprintf(stderr, "Monitor error\n");
|
||||
cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
cleanup();
|
||||
printf("Monitor stopped\n");
|
||||
return 0;
|
||||
}
|
||||
186
src/monitor.c
Normal file
186
src/monitor.c
Normal file
@@ -0,0 +1,186 @@
|
||||
#include "monitor.h"
|
||||
#include "config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/fanotify.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <libgen.h>
|
||||
|
||||
static int fanotify_fd = -1;
|
||||
static int running = 1;
|
||||
|
||||
/* Get event name from mask */
|
||||
static const char* get_event_name(uint32_t mask)
|
||||
{
|
||||
if (mask & FAN_OPEN)
|
||||
return "OPEN";
|
||||
if (mask & FAN_CLOSE)
|
||||
return mask & FAN_CLOSE_WRITE ? "CLOSE_WRITE" : "CLOSE_NOWRITE";
|
||||
if (mask & FAN_MODIFY)
|
||||
return "MODIFY";
|
||||
if (mask & FAN_CREATE)
|
||||
return "CREATE";
|
||||
if (mask & FAN_DELETE)
|
||||
return "DELETE";
|
||||
if (mask & FAN_MOVE)
|
||||
return "MOVE";
|
||||
if (mask & FAN_ATTRIB)
|
||||
return "ATTRIB";
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
/* Signal handler */
|
||||
static void signal_handler(int sig)
|
||||
{
|
||||
running = 0;
|
||||
}
|
||||
|
||||
/* Monitor initialization */
|
||||
int monitor_init(void)
|
||||
{
|
||||
/* Create fanotify file descriptor */
|
||||
fanotify_fd = fanotify_init(FANOTIFY_CLASS, FANOTIFY_FLAGS);
|
||||
if (fanotify_fd == -1) {
|
||||
perror("fanotify_init");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Mark root filesystem for monitoring */
|
||||
if (fanotify_mark(fanotify_fd, FAN_MARK_ADD | FAN_MARK_FILESYSTEM,
|
||||
FANOTIFY_MASK, AT_FDCWD, "/") == -1) {
|
||||
perror("fanotify_mark");
|
||||
close(fanotify_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Setup signal handlers */
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Start monitoring a specific path */
|
||||
int monitor_add_path(const char *path)
|
||||
{
|
||||
if (!path || fanotify_fd == -1) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fanotify_mark(fanotify_fd, FAN_MARK_ADD,
|
||||
FANOTIFY_MASK, AT_FDCWD, path) == -1) {
|
||||
perror("fanotify_mark");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Get file information */
|
||||
static int get_file_info(int fd, fs_event_t *event)
|
||||
{
|
||||
struct stat st;
|
||||
char proc_path[64];
|
||||
char link_path[PATH_BUF_LEN];
|
||||
|
||||
/* Get file stats */
|
||||
if (fstat(fd, &st) == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
event->file_size = st.st_size;
|
||||
event->permissions = st.st_mode & 07777;
|
||||
|
||||
/* Try to get file path from /proc/self/fd */
|
||||
snprintf(proc_path, sizeof(proc_path), "/proc/self/fd/%d", fd);
|
||||
ssize_t len = readlink(proc_path, link_path, sizeof(link_path) - 1);
|
||||
if (len > 0) {
|
||||
link_path[len] = '\0';
|
||||
strncpy(event->path, link_path, sizeof(event->path) - 1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Get hostname */
|
||||
static int get_hostname(char *hostname, size_t len)
|
||||
{
|
||||
if (gethostname(hostname, len) == -1) {
|
||||
strcpy(hostname, "unknown");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Main monitoring loop */
|
||||
int monitor_run(void)
|
||||
{
|
||||
if (fanotify_fd == -1) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned char buf[EVENT_BUF_LEN];
|
||||
const struct fanotify_event_metadata *metadata;
|
||||
int ret;
|
||||
|
||||
get_hostname(NULL, 0);
|
||||
|
||||
while (running) {
|
||||
ret = read(fanotify_fd, buf, sizeof(buf));
|
||||
if (ret <= 0) {
|
||||
if (ret == -1 && errno != EAGAIN) {
|
||||
perror("read");
|
||||
return -1;
|
||||
}
|
||||
usleep(100000);
|
||||
continue;
|
||||
}
|
||||
|
||||
metadata = (struct fanotify_event_metadata *)buf;
|
||||
while (FAN_EVENT_OK(metadata, ret)) {
|
||||
if (metadata->vers != FANOTIFY_METADATA_VERSION) {
|
||||
fprintf(stderr, "fanotify metadata version mismatch\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("[%s] PID=%d FD=%d\n",
|
||||
get_event_name(metadata->mask),
|
||||
metadata->pid,
|
||||
metadata->fd);
|
||||
|
||||
if (metadata->fd >= 0) {
|
||||
close(metadata->fd);
|
||||
}
|
||||
|
||||
metadata = FAN_EVENT_NEXT(metadata, ret);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Cleanup */
|
||||
void monitor_cleanup(void)
|
||||
{
|
||||
if (fanotify_fd != -1) {
|
||||
close(fanotify_fd);
|
||||
fanotify_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Get next event (stub for interface) */
|
||||
int monitor_get_event(fs_event_t *event)
|
||||
{
|
||||
if (!event) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
34
src/monitor.h
Normal file
34
src/monitor.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef FSYNC_MONITOR_H
|
||||
#define FSYNC_MONITOR_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
/* File system event structure */
|
||||
typedef struct {
|
||||
char hostname[256];
|
||||
char path[4096];
|
||||
char event_type[32];
|
||||
char checksum[65];
|
||||
time_t timestamp;
|
||||
uint64_t file_size;
|
||||
uint32_t permissions;
|
||||
pid_t process_id;
|
||||
} fs_event_t;
|
||||
|
||||
/* Monitor initialization */
|
||||
int monitor_init(void);
|
||||
|
||||
/* Start monitoring a path */
|
||||
int monitor_add_path(const char *path);
|
||||
|
||||
/* Main monitoring loop */
|
||||
int monitor_run(void);
|
||||
|
||||
/* Cleanup */
|
||||
void monitor_cleanup(void);
|
||||
|
||||
/* Get next event */
|
||||
int monitor_get_event(fs_event_t *event);
|
||||
|
||||
#endif /* FSYNC_MONITOR_H */
|
||||
181
src/queue_manager.c
Normal file
181
src/queue_manager.c
Normal file
@@ -0,0 +1,181 @@
|
||||
#include "queue_manager.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
struct queue_manager {
|
||||
file_update_t *items;
|
||||
int count;
|
||||
int max_size;
|
||||
};
|
||||
|
||||
/* Compare function for sorting: age first (older=higher priority), then size (smaller=higher) */
|
||||
static int compare_updates(const void *a, const void *b)
|
||||
{
|
||||
const file_update_t *u1 = (const file_update_t *)a;
|
||||
const file_update_t *u2 = (const file_update_t *)b;
|
||||
|
||||
/* Age comparison: older first (lower timestamp = higher priority) */
|
||||
if (u1->detected_at != u2->detected_at) {
|
||||
return (u1->detected_at < u2->detected_at) ? -1 : 1;
|
||||
}
|
||||
|
||||
/* Size comparison: smaller first */
|
||||
if (u1->file_size != u2->file_size) {
|
||||
return (u1->file_size < u2->file_size) ? -1 : 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Create queue manager */
|
||||
queue_manager_t* queue_manager_create(int max_size)
|
||||
{
|
||||
if (max_size <= 0) {
|
||||
errno = EINVAL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
queue_manager_t *qm = (queue_manager_t *)malloc(sizeof(queue_manager_t));
|
||||
if (!qm) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
qm->items = (file_update_t *)malloc(sizeof(file_update_t) * max_size);
|
||||
if (!qm->items) {
|
||||
free(qm);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
qm->count = 0;
|
||||
qm->max_size = max_size;
|
||||
|
||||
return qm;
|
||||
}
|
||||
|
||||
/* Destroy queue manager */
|
||||
void queue_manager_destroy(queue_manager_t *qm)
|
||||
{
|
||||
if (!qm) return;
|
||||
|
||||
free(qm->items);
|
||||
free(qm);
|
||||
}
|
||||
|
||||
/* Add file update to queue */
|
||||
int queue_manager_add(queue_manager_t *qm, const file_update_t *update)
|
||||
{
|
||||
if (!qm || !update) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (qm->count >= qm->max_size) {
|
||||
errno = ENOSPC;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Check if file already in queue */
|
||||
for (int i = 0; i < qm->count; i++) {
|
||||
if (strcmp(qm->items[i].hostname, update->hostname) == 0 &&
|
||||
strcmp(qm->items[i].path, update->path) == 0) {
|
||||
/* Update existing entry */
|
||||
qm->items[i] = *update;
|
||||
queue_manager_sort(qm);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add new item */
|
||||
qm->items[qm->count] = *update;
|
||||
qm->count++;
|
||||
|
||||
/* Re-sort queue */
|
||||
queue_manager_sort(qm);
|
||||
|
||||
return qm->count - 1;
|
||||
}
|
||||
|
||||
/* Get highest priority update */
|
||||
int queue_manager_get_next(queue_manager_t *qm, file_update_t *update)
|
||||
{
|
||||
if (!qm || !update || qm->count == 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* First item has highest priority */
|
||||
*update = qm->items[0];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Remove item from queue */
|
||||
int queue_manager_remove(queue_manager_t *qm, int index)
|
||||
{
|
||||
if (!qm || index < 0 || index >= qm->count) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Shift items */
|
||||
for (int i = index; i < qm->count - 1; i++) {
|
||||
qm->items[i] = qm->items[i + 1];
|
||||
}
|
||||
|
||||
qm->count--;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Get queue size */
|
||||
int queue_manager_size(queue_manager_t *qm)
|
||||
{
|
||||
return qm ? qm->count : 0;
|
||||
}
|
||||
|
||||
/* Check if file already in queue */
|
||||
int queue_manager_has_file(queue_manager_t *qm, const char *hostname,
|
||||
const char *path)
|
||||
{
|
||||
if (!qm || !hostname || !path) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < qm->count; i++) {
|
||||
if (strcmp(qm->items[i].hostname, hostname) == 0 &&
|
||||
strcmp(qm->items[i].path, path) == 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Get update at index */
|
||||
int queue_manager_get_at(queue_manager_t *qm, int index, file_update_t *update)
|
||||
{
|
||||
if (!qm || !update || index < 0 || index >= qm->count) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
*update = qm->items[index];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Clear all items */
|
||||
void queue_manager_clear(queue_manager_t *qm)
|
||||
{
|
||||
if (qm) {
|
||||
qm->count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sort queue by age then size */
|
||||
void queue_manager_sort(queue_manager_t *qm)
|
||||
{
|
||||
if (!qm || qm->count <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
qsort(qm->items, qm->count, sizeof(file_update_t), compare_updates);
|
||||
}
|
||||
51
src/queue_manager.h
Normal file
51
src/queue_manager.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#ifndef FSYNC_QUEUE_MANAGER_H
|
||||
#define FSYNC_QUEUE_MANAGER_H
|
||||
|
||||
#include <time.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* File update request structure */
|
||||
typedef struct {
|
||||
char hostname[256];
|
||||
char path[4096];
|
||||
time_t detected_at;
|
||||
uint64_t file_size;
|
||||
char checksum[65];
|
||||
int priority;
|
||||
} file_update_t;
|
||||
|
||||
/* Queue manager structure */
|
||||
typedef struct queue_manager queue_manager_t;
|
||||
|
||||
/* Create queue manager */
|
||||
queue_manager_t* queue_manager_create(int max_size);
|
||||
|
||||
/* Destroy queue manager */
|
||||
void queue_manager_destroy(queue_manager_t *qm);
|
||||
|
||||
/* Add file update to queue (auto-sorted by age then size) */
|
||||
int queue_manager_add(queue_manager_t *qm, const file_update_t *update);
|
||||
|
||||
/* Get highest priority update (oldest + smallest) */
|
||||
int queue_manager_get_next(queue_manager_t *qm, file_update_t *update);
|
||||
|
||||
/* Remove item from queue */
|
||||
int queue_manager_remove(queue_manager_t *qm, int index);
|
||||
|
||||
/* Get queue size */
|
||||
int queue_manager_size(queue_manager_t *qm);
|
||||
|
||||
/* Check if file already in queue */
|
||||
int queue_manager_has_file(queue_manager_t *qm, const char *hostname,
|
||||
const char *path);
|
||||
|
||||
/* Get update at index */
|
||||
int queue_manager_get_at(queue_manager_t *qm, int index, file_update_t *update);
|
||||
|
||||
/* Clear all items */
|
||||
void queue_manager_clear(queue_manager_t *qm);
|
||||
|
||||
/* Sort queue by age (oldest first) then size (smallest first) */
|
||||
void queue_manager_sort(queue_manager_t *qm);
|
||||
|
||||
#endif /* FSYNC_QUEUE_MANAGER_H */
|
||||
344
src/redis_client.c
Normal file
344
src/redis_client.c
Normal file
@@ -0,0 +1,344 @@
|
||||
#define _XOPEN_SOURCE 700
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include "redis_client.h"
|
||||
#include "config.h"
|
||||
#include <hiredis/hiredis.h>
|
||||
#include <hiredis/async.h>
|
||||
|
||||
struct redis_conn {
|
||||
redisContext *ctx;
|
||||
};
|
||||
|
||||
/* Connect to Redis */
|
||||
redis_conn_t* redis_connect(const char *host, int port)
|
||||
{
|
||||
redis_conn_t *conn = (redis_conn_t *)malloc(sizeof(redis_conn_t));
|
||||
if (!conn) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct timeval timeout = { .tv_sec = REDIS_TIMEOUT_SEC, .tv_usec = 0 };
|
||||
conn->ctx = redisConnectWithTimeout(host ? host : REDIS_HOST,
|
||||
port ? port : REDIS_PORT, timeout);
|
||||
|
||||
if (!conn->ctx || conn->ctx->err) {
|
||||
fprintf(stderr, "Redis connection error: %s\n",
|
||||
conn->ctx ? conn->ctx->errstr : "allocation failed");
|
||||
if (conn->ctx) redisFree(conn->ctx);
|
||||
free(conn);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
/* Disconnect from Redis */
|
||||
void redis_disconnect(redis_conn_t *conn)
|
||||
{
|
||||
if (!conn) return;
|
||||
|
||||
if (conn->ctx) {
|
||||
redisFree(conn->ctx);
|
||||
}
|
||||
free(conn);
|
||||
}
|
||||
|
||||
/* Get ISO8601 timestamp */
|
||||
static char* get_timestamp(char *buf, size_t len)
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
struct tm *tm_info = gmtime(&now);
|
||||
strftime(buf, len, "%Y-%m-%dT%H:%M:%SZ", tm_info);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Store a file system event */
|
||||
int redis_store_event(redis_conn_t *conn, const fs_event_t *event)
|
||||
{
|
||||
if (!conn || !event) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
char timestamp_str[32];
|
||||
get_timestamp(timestamp_str, sizeof(timestamp_str));
|
||||
|
||||
char key[512];
|
||||
snprintf(key, sizeof(key), "fsync:events:%s:%s:%ld",
|
||||
timestamp_str, event->hostname, time(NULL));
|
||||
|
||||
/* Create JSON-like string with event data */
|
||||
char *reply_cmd = redisCommand(conn->ctx,
|
||||
"HSET %s "
|
||||
"hostname %s "
|
||||
"path %s "
|
||||
"event_type %s "
|
||||
"checksum %s "
|
||||
"timestamp %s "
|
||||
"file_size %llu "
|
||||
"permissions %o "
|
||||
"process_id %d",
|
||||
key,
|
||||
event->hostname,
|
||||
event->path,
|
||||
event->event_type,
|
||||
event->checksum,
|
||||
timestamp_str,
|
||||
(unsigned long long)event->file_size,
|
||||
event->permissions,
|
||||
event->process_id);
|
||||
|
||||
redisReply *reply = (redisReply *)reply_cmd;
|
||||
|
||||
if (!reply) {
|
||||
fprintf(stderr, "Redis error: %s\n", conn->ctx->errstr);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int result = 0;
|
||||
if (reply->type == REDIS_REPLY_ERROR) {
|
||||
fprintf(stderr, "Redis error: %s\n", reply->str);
|
||||
result = -1;
|
||||
}
|
||||
|
||||
freeReplyObject(reply);
|
||||
|
||||
/* Add to events list */
|
||||
redisCommand(conn->ctx, "LPUSH fsync:events:list %s", key);
|
||||
|
||||
/* Set expiration (30 days) */
|
||||
redisCommand(conn->ctx, "EXPIRE %s %d", key, 30 * 24 * 60 * 60);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Retrieve events */
|
||||
int redis_get_events(redis_conn_t *conn, char ***events, int *count)
|
||||
{
|
||||
if (!conn || !events || !count) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
redisReply *reply = (redisReply *)redisCommand(conn->ctx,
|
||||
"LRANGE fsync:events:list 0 -1");
|
||||
|
||||
if (!reply || reply->type == REDIS_REPLY_ERROR) {
|
||||
fprintf(stderr, "Redis error: %s\n", reply ? reply->str : "null reply");
|
||||
if (reply) freeReplyObject(reply);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (reply->type != REDIS_REPLY_ARRAY) {
|
||||
freeReplyObject(reply);
|
||||
*count = 0;
|
||||
*events = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
*count = reply->elements;
|
||||
*events = (char **)malloc(sizeof(char *) * reply->elements);
|
||||
|
||||
if (!*events && reply->elements > 0) {
|
||||
freeReplyObject(reply);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < reply->elements; i++) {
|
||||
(*events)[i] = (char *)malloc(strlen(reply->element[i]->str) + 1);
|
||||
if (!(*events)[i]) {
|
||||
freeReplyObject(reply);
|
||||
return -1;
|
||||
}
|
||||
strcpy((*events)[i], reply->element[i]->str);
|
||||
}
|
||||
|
||||
freeReplyObject(reply);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Get events for a specific host */
|
||||
int redis_get_events_by_host(redis_conn_t *conn, const char *hostname,
|
||||
char ***events, int *count)
|
||||
{
|
||||
if (!conn || !hostname || !events || !count) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Get all events and filter by hostname */
|
||||
char **all_events;
|
||||
int all_count;
|
||||
|
||||
if (redis_get_events(conn, &all_events, &all_count) == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*events = NULL;
|
||||
*count = 0;
|
||||
|
||||
for (int i = 0; i < all_count; i++) {
|
||||
redisReply *hgetall = (redisReply *)redisCommand(conn->ctx,
|
||||
"HGETALL %s",
|
||||
all_events[i]);
|
||||
|
||||
if (!hgetall || hgetall->type != REDIS_REPLY_ARRAY) {
|
||||
if (hgetall) freeReplyObject(hgetall);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check hostname field */
|
||||
for (int j = 0; j < hgetall->elements; j += 2) {
|
||||
if (strcmp(hgetall->element[j]->str, "hostname") == 0) {
|
||||
if (strcmp(hgetall->element[j + 1]->str, hostname) == 0) {
|
||||
char **tmp = (char **)realloc(*events,
|
||||
sizeof(char *) * (*count + 1));
|
||||
if (!tmp) {
|
||||
freeReplyObject(hgetall);
|
||||
return -1;
|
||||
}
|
||||
*events = tmp;
|
||||
(*events)[*count] = (char *)malloc(strlen(all_events[i]) + 1);
|
||||
strcpy((*events)[*count], all_events[i]);
|
||||
(*count)++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
freeReplyObject(hgetall);
|
||||
}
|
||||
|
||||
redis_free_events(all_events, all_count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Get events for a specific path */
|
||||
int redis_get_events_by_path(redis_conn_t *conn, const char *path,
|
||||
char ***events, int *count)
|
||||
{
|
||||
if (!conn || !path || !events || !count) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
char **all_events;
|
||||
int all_count;
|
||||
|
||||
if (redis_get_events(conn, &all_events, &all_count) == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*events = NULL;
|
||||
*count = 0;
|
||||
|
||||
for (int i = 0; i < all_count; i++) {
|
||||
redisReply *hgetall = (redisReply *)redisCommand(conn->ctx,
|
||||
"HGETALL %s",
|
||||
all_events[i]);
|
||||
|
||||
if (!hgetall || hgetall->type != REDIS_REPLY_ARRAY) {
|
||||
if (hgetall) freeReplyObject(hgetall);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = 0; j < hgetall->elements; j += 2) {
|
||||
if (strcmp(hgetall->element[j]->str, "path") == 0) {
|
||||
if (strcmp(hgetall->element[j + 1]->str, path) == 0) {
|
||||
char **tmp = (char **)realloc(*events,
|
||||
sizeof(char *) * (*count + 1));
|
||||
if (!tmp) {
|
||||
freeReplyObject(hgetall);
|
||||
return -1;
|
||||
}
|
||||
*events = tmp;
|
||||
(*events)[*count] = (char *)malloc(strlen(all_events[i]) + 1);
|
||||
strcpy((*events)[*count], all_events[i]);
|
||||
(*count)++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
freeReplyObject(hgetall);
|
||||
}
|
||||
|
||||
redis_free_events(all_events, all_count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Clear old events */
|
||||
int redis_cleanup_old_events(redis_conn_t *conn, int days)
|
||||
{
|
||||
if (!conn || days <= 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
time_t cutoff = time(NULL) - (days * 24 * 60 * 60);
|
||||
|
||||
redisReply *reply = (redisReply *)redisCommand(conn->ctx,
|
||||
"LRANGE fsync:events:list 0 -1");
|
||||
|
||||
if (!reply || reply->type != REDIS_REPLY_ARRAY) {
|
||||
if (reply) freeReplyObject(reply);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int deleted = 0;
|
||||
for (int i = 0; i < reply->elements; i++) {
|
||||
const char *key = reply->element[i]->str;
|
||||
redisReply *hget = (redisReply *)redisCommand(conn->ctx,
|
||||
"HGET %s timestamp", key);
|
||||
|
||||
if (hget && hget->type == REDIS_REPLY_STRING) {
|
||||
struct tm tm = {};
|
||||
strptime(hget->str, "%Y-%m-%dT%H:%M:%SZ", &tm);
|
||||
time_t event_time = mktime(&tm);
|
||||
|
||||
if (event_time < cutoff) {
|
||||
redisCommand(conn->ctx, "DEL %s", key);
|
||||
deleted++;
|
||||
}
|
||||
}
|
||||
if (hget) freeReplyObject(hget);
|
||||
}
|
||||
|
||||
freeReplyObject(reply);
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/* Health check */
|
||||
int redis_ping(redis_conn_t *conn)
|
||||
{
|
||||
if (!conn) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
redisReply *reply = (redisReply *)redisCommand(conn->ctx, "PING");
|
||||
if (!reply) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int result = (reply->type == REDIS_REPLY_STATUS &&
|
||||
strcmp(reply->str, "PONG") == 0) ? 0 : -1;
|
||||
|
||||
freeReplyObject(reply);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Free event list */
|
||||
void redis_free_events(char **events, int count)
|
||||
{
|
||||
if (!events) return;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
free(events[i]);
|
||||
}
|
||||
free(events);
|
||||
}
|
||||
38
src/redis_client.h
Normal file
38
src/redis_client.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef FSYNC_REDIS_CLIENT_H
|
||||
#define FSYNC_REDIS_CLIENT_H
|
||||
|
||||
#include "monitor.h"
|
||||
|
||||
/* Redis connection structure */
|
||||
typedef struct redis_conn redis_conn_t;
|
||||
|
||||
/* Connect to Redis */
|
||||
redis_conn_t* redis_connect(const char *host, int port);
|
||||
|
||||
/* Disconnect from Redis */
|
||||
void redis_disconnect(redis_conn_t *conn);
|
||||
|
||||
/* Store a file system event */
|
||||
int redis_store_event(redis_conn_t *conn, const fs_event_t *event);
|
||||
|
||||
/* Retrieve events */
|
||||
int redis_get_events(redis_conn_t *conn, char ***events, int *count);
|
||||
|
||||
/* Get events for a specific host */
|
||||
int redis_get_events_by_host(redis_conn_t *conn, const char *hostname,
|
||||
char ***events, int *count);
|
||||
|
||||
/* Get events for a specific path */
|
||||
int redis_get_events_by_path(redis_conn_t *conn, const char *path,
|
||||
char ***events, int *count);
|
||||
|
||||
/* Clear old events */
|
||||
int redis_cleanup_old_events(redis_conn_t *conn, int days);
|
||||
|
||||
/* Health check */
|
||||
int redis_ping(redis_conn_t *conn);
|
||||
|
||||
/* Free event list */
|
||||
void redis_free_events(char **events, int count);
|
||||
|
||||
#endif /* FSYNC_REDIS_CLIENT_H */
|
||||
Reference in New Issue
Block a user