Compare commits
14 Commits
2ccdfaf1c7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c38ec59815 | |||
| 9a5f80974a | |||
| e81211c88e | |||
| bae8712f3b | |||
| fc3b2ef309 | |||
| 8416b92a8d | |||
| 5761deb298 | |||
| 1fbe319a10 | |||
| b1572c029e | |||
| 52c93d2cd5 | |||
| fd410f8109 | |||
| 32ccdbec83 | |||
| 29dfcf169c | |||
| 0fc1473a6f |
@@ -6,6 +6,5 @@ prisma/dev.db
|
||||
prisma/dev.db-journal
|
||||
dist
|
||||
target
|
||||
*.csv
|
||||
*.sql
|
||||
dump
|
||||
Generated
+30
@@ -64,6 +64,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dotenvy",
|
||||
"terminal_size",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
]
|
||||
@@ -255,6 +256,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
@@ -454,6 +461,19 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
@@ -566,6 +586,16 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.11.0"
|
||||
|
||||
@@ -8,3 +8,4 @@ anyhow = "1"
|
||||
dotenvy = "0.15"
|
||||
tokio = { version = "1", features = ["io-util", "macros", "net", "process", "rt-multi-thread", "signal", "time"] }
|
||||
tokio-postgres = "0.7"
|
||||
terminal_size = "0.4"
|
||||
|
||||
+7
-1
@@ -3,17 +3,23 @@ WORKDIR /app
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src ./src
|
||||
COPY cidrs.txt ./cidrs.txt
|
||||
COPY camfinder-open-rtsp.csv ./camfinder-open-rtsp.csv
|
||||
COPY rtsp_paths.txt ./rtsp_paths.txt
|
||||
COPY main_ids.json ./main_ids.json
|
||||
|
||||
RUN cargo build --release
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates ffmpeg redis-tools sqlite3 \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates ffmpeg redis-tools \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/target/release/camfinder /usr/local/bin/camfinder
|
||||
COPY cidrs.txt /app/cidrs.txt
|
||||
COPY camfinder-open-rtsp.csv /app/camfinder-open-rtsp.csv
|
||||
COPY rtsp_paths.txt /app/rtsp_paths.txt
|
||||
COPY main_ids.json /app/main_ids.json
|
||||
|
||||
ENTRYPOINT ["camfinder"]
|
||||
|
||||
@@ -28,14 +28,18 @@ O `docker-compose.yml` já sobe:
|
||||
|
||||
Para o fluxo RTSP com Redis, use `docker-compose.redis-rtsp.yml`:
|
||||
|
||||
- `redis` como fila.
|
||||
- `coordinator` para ler `camfinder-open-rtsp.csv` e alimentar a fila.
|
||||
- `worker-1` e `worker-2` para consumir IPs, testar paths RTSP e salvar screenshots e SQLite.
|
||||
- `redis` como fila. Ele publica a porta `6379`, então workers externos podem apontar `REDIS_URL` para esse host.
|
||||
- `coordinator` para ler `camfinder-open-rtsp.csv` e alimentar a fila com blocos de IPs. No modo simples/local, monte o repositório inteiro em `/app`.
|
||||
- `worker-1` e `worker-2` para consumir blocos, testar paths RTSP e salvar screenshots e SQLite.
|
||||
|
||||
Para escalar workers:
|
||||
|
||||
- `docker compose up --scale worker=3`
|
||||
|
||||
Para um worker em outra máquina, aponte `REDIS_URL` para o Redis do coordenador, por exemplo `redis://<host>:6379/0`, e use o mesmo `RTSP_QUEUE_NAME`.
|
||||
|
||||
Se estiver usando o compose local, monte `./:/app` para coordinator e workers RTSP. O coordinator lê `camfinder-open-rtsp.csv` e `rtsp_paths.txt` da raiz do repo.
|
||||
|
||||
Para workers de outras máquinas, use `vps.valmo.dev:666` como endereço do coordenador.
|
||||
|
||||
## Scripts
|
||||
@@ -67,6 +71,7 @@ Para workers de outras máquinas, use `vps.valmo.dev:666` como endereço do coor
|
||||
- `RTSP_QUEUE_NAME`
|
||||
- `RTSP_INPUT_CSV`
|
||||
- `RTSP_WORKER_COUNT`
|
||||
- `RTSP_QUEUE_BLOCK_SIZE`
|
||||
- `RTSP_SNAPSHOT_DIR`
|
||||
- `RTSP_SQLITE_PATH`
|
||||
- `RTSP_CAPTURE_TIMEOUT_MS`
|
||||
@@ -74,7 +79,8 @@ Para workers de outras máquinas, use `vps.valmo.dev:666` como endereço do coor
|
||||
## Modo RTSP
|
||||
|
||||
O modo `rtsp` monta URLs no formato `rtsp://<host>:554/<path>` e testa a conexão
|
||||
via `DESCRIBE`, sem tentar usuário, senha ou variações de autenticação.
|
||||
via `DESCRIBE`, tentando primeiro sem auth e depois cruzando os usuários e
|
||||
senhas padrão definidos em `main_ids.json`.
|
||||
|
||||
Por padrão, ele lê os paths de [rtsp_paths.txt](/home/valmo/Documents/CamFinder/rtsp_paths.txt).
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,41 @@
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
command: ["redis-server", "--appendonly", "yes", "--bind", "0.0.0.0", "--protected-mode", "no"]
|
||||
volumes:
|
||||
- camfinder-redis:/data
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: camfinder
|
||||
POSTGRES_USER: camfinder
|
||||
POSTGRES_PASSWORD: camfinder
|
||||
volumes:
|
||||
- camfinder-postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U camfinder -d camfinder"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
|
||||
coordinator:
|
||||
build: .
|
||||
command: ["redis-coordinator"]
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
DATABASE_URL: postgres://camfinder:camfinder@postgres:5432/camfinder
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_INPUT_CSV: /app/camfinder-open-rtsp.csv
|
||||
RTSP_WORKER_COUNT: 2
|
||||
depends_on:
|
||||
- redis
|
||||
redis:
|
||||
condition: service_started
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./camfinder-open-rtsp.csv:/app/camfinder-open-rtsp.csv:ro
|
||||
- ./dump:/app/dump
|
||||
- ./:/app
|
||||
|
||||
worker-1:
|
||||
build: .
|
||||
@@ -26,13 +44,12 @@ services:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_SQLITE_PATH: /app/dump/rtsp-results.sqlite
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
depends_on:
|
||||
- redis
|
||||
- coordinator
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
- ./:/app
|
||||
|
||||
worker-2:
|
||||
build: .
|
||||
@@ -41,13 +58,13 @@ services:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_SQLITE_PATH: /app/dump/rtsp-results.sqlite
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
depends_on:
|
||||
- redis
|
||||
- coordinator
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
- ./:/app
|
||||
|
||||
volumes:
|
||||
camfinder-redis:
|
||||
camfinder-postgres:
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: ["redis-server", "--appendonly", "yes", "--bind", "0.0.0.0", "--protected-mode", "no"]
|
||||
volumes:
|
||||
- camfinder-redis:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: camfinder
|
||||
POSTGRES_USER: camfinder
|
||||
POSTGRES_PASSWORD: camfinder
|
||||
volumes:
|
||||
- camfinder-postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U camfinder -d camfinder"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
|
||||
coordinator:
|
||||
build: .
|
||||
command: ["redis-coordinator"]
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
DATABASE_URL: postgres://camfinder:camfinder@postgres:5432/camfinder
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_INPUT_CSV: /app/camfinder-open-rtsp.csv
|
||||
RTSP_WORKER_COUNT: 8
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./:/app:ro
|
||||
|
||||
worker-1:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
depends_on:
|
||||
- redis
|
||||
- coordinator
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-2:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
depends_on:
|
||||
- worker-1
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-3:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
depends_on:
|
||||
- worker-2
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-4:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
depends_on:
|
||||
- worker-3
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-5:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
depends_on:
|
||||
- worker-4
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-6:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
depends_on:
|
||||
- worker-5
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-7:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
depends_on:
|
||||
- worker-6
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-8:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
depends_on:
|
||||
- worker-7
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
volumes:
|
||||
camfinder-redis:
|
||||
camfinder-postgres:
|
||||
@@ -0,0 +1,96 @@
|
||||
services:
|
||||
worker-1:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://192.168.15.100:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-2:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://192.168.15.100:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-3:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://192.168.15.100:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-4:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://192.168.15.100:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-5:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://192.168.15.100:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-6:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://192.168.15.100:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-7:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://192.168.15.100:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
|
||||
worker-8:
|
||||
build: .
|
||||
command: ["redis-worker"]
|
||||
environment:
|
||||
REDIS_URL: redis://192.168.15.100:6379/0
|
||||
RTSP_QUEUE_NAME: camfinder:rtsp:queue
|
||||
RTSP_SNAPSHOT_DIR: /app/dump/feeds
|
||||
RTSP_CAPTURE_TIMEOUT_MS: 15000
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./dump:/app/dump
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"usernames": [
|
||||
"",
|
||||
"admin"
|
||||
],
|
||||
"passwords" : [
|
||||
"",
|
||||
"admin",
|
||||
"8888",
|
||||
"9999",
|
||||
"1234",
|
||||
"12345",
|
||||
"123456",
|
||||
"123456789",
|
||||
"1234567890",
|
||||
"pass",
|
||||
"password"
|
||||
]
|
||||
}
|
||||
+87
-18
@@ -25,8 +25,8 @@ pub struct Config {
|
||||
pub redis_queue_name: String,
|
||||
pub redis_input_csv: String,
|
||||
pub redis_worker_count: usize,
|
||||
pub redis_queue_block_size: usize,
|
||||
pub snapshot_dir: PathBuf,
|
||||
pub sqlite_path: PathBuf,
|
||||
pub capture_timeout_ms: u64,
|
||||
pub port: u16,
|
||||
pub timeout_ms: u64,
|
||||
@@ -60,17 +60,18 @@ impl Config {
|
||||
.unwrap_or_else(|| read_env("RTSP_QUEUE_NAME", "camfinder:rtsp:queue")),
|
||||
redis_input_csv: cli
|
||||
.redis_input_csv
|
||||
.unwrap_or_else(|| read_env("RTSP_INPUT_CSV", "camfinder-open-rtsp.csv")),
|
||||
.unwrap_or_else(|| read_env("RTSP_INPUT_CSV", "/app/camfinder-open-rtsp.csv")),
|
||||
redis_worker_count: cli
|
||||
.redis_worker_count
|
||||
.or_else(|| parse_usize("RTSP_WORKER_COUNT", 2).ok())
|
||||
.unwrap_or(2),
|
||||
redis_queue_block_size: cli
|
||||
.redis_queue_block_size
|
||||
.or_else(|| parse_usize("RTSP_QUEUE_BLOCK_SIZE", 128).ok())
|
||||
.unwrap_or(128),
|
||||
snapshot_dir: cli
|
||||
.snapshot_dir
|
||||
.unwrap_or_else(|| PathBuf::from(read_env("RTSP_SNAPSHOT_DIR", "dump/feeds"))),
|
||||
sqlite_path: cli
|
||||
.sqlite_path
|
||||
.unwrap_or_else(|| PathBuf::from(read_env("RTSP_SQLITE_PATH", "dump/rtsp-results.sqlite"))),
|
||||
capture_timeout_ms: cli
|
||||
.capture_timeout_ms
|
||||
.or_else(|| parse_u64("RTSP_CAPTURE_TIMEOUT_MS", 15000).ok())
|
||||
@@ -116,8 +117,8 @@ struct CliArgs {
|
||||
redis_queue_name: Option<String>,
|
||||
redis_input_csv: Option<String>,
|
||||
redis_worker_count: Option<usize>,
|
||||
redis_queue_block_size: Option<usize>,
|
||||
snapshot_dir: Option<PathBuf>,
|
||||
sqlite_path: Option<PathBuf>,
|
||||
capture_timeout_ms: Option<u64>,
|
||||
database_url: Option<String>,
|
||||
coordinator_bind: Option<SocketAddr>,
|
||||
@@ -177,13 +178,19 @@ impl CliArgs {
|
||||
"--worker-count",
|
||||
)?)?);
|
||||
}
|
||||
"--snapshot-dir" => {
|
||||
cli.snapshot_dir =
|
||||
Some(PathBuf::from(next_value(args, &mut index, "--snapshot-dir")?));
|
||||
"--queue-block-size" => {
|
||||
cli.redis_queue_block_size = Some(parse_usize_literal(&next_value(
|
||||
args,
|
||||
&mut index,
|
||||
"--queue-block-size",
|
||||
)?)?);
|
||||
}
|
||||
"--sqlite-path" => {
|
||||
cli.sqlite_path =
|
||||
Some(PathBuf::from(next_value(args, &mut index, "--sqlite-path")?));
|
||||
"--snapshot-dir" => {
|
||||
cli.snapshot_dir = Some(PathBuf::from(next_value(
|
||||
args,
|
||||
&mut index,
|
||||
"--snapshot-dir",
|
||||
)?));
|
||||
}
|
||||
"--capture-timeout-ms" => {
|
||||
cli.capture_timeout_ms = Some(parse_u64_literal(&next_value(
|
||||
@@ -348,26 +355,55 @@ fn load_csv_targets(file: Option<&Path>, inline: Option<&str>) -> Result<Vec<Str
|
||||
}
|
||||
|
||||
if let Some(file) = file {
|
||||
let content = fs::read_to_string(file)
|
||||
.with_context(|| format!("failed to read CSV file {}", file.display()))?;
|
||||
match fs::read_to_string(file) {
|
||||
Ok(content) => {
|
||||
let targets = parse_csv_targets(&content);
|
||||
if !targets.is_empty() {
|
||||
return Ok(targets);
|
||||
}
|
||||
return Err(anyhow!("arquivo CSV vazio: {}", file.display()));
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
let targets = parse_csv_targets(include_str!("../camfinder-open-rtsp.csv"));
|
||||
if !targets.is_empty() {
|
||||
return Ok(targets);
|
||||
}
|
||||
return Err(anyhow!(
|
||||
"arquivo CSV padrão embutido vazio: ../camfinder-open-rtsp.csv"
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(anyhow!(err))
|
||||
.with_context(|| format!("failed to read CSV file {}", file.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(path) = env::var("RTSP_TARGETS_FILE") {
|
||||
let path = path.trim();
|
||||
if !path.is_empty() {
|
||||
let content = fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read CSV file {path}"))?;
|
||||
match fs::read_to_string(path) {
|
||||
Ok(content) => {
|
||||
let targets = parse_csv_targets(&content);
|
||||
if !targets.is_empty() {
|
||||
return Ok(targets);
|
||||
}
|
||||
return Err(anyhow!("arquivo CSV vazio: {path}"));
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
let targets = parse_csv_targets(include_str!("../camfinder-open-rtsp.csv"));
|
||||
if !targets.is_empty() {
|
||||
return Ok(targets);
|
||||
}
|
||||
return Err(anyhow!(
|
||||
"arquivo CSV padrão embutido vazio: ../camfinder-open-rtsp.csv"
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err).with_context(|| format!("failed to read CSV file {path}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(value) = env::var("RTSP_TARGETS") {
|
||||
@@ -430,7 +466,20 @@ fn load_rtsp_paths(file: Option<&Path>, inline: Option<&str>) -> Result<Vec<Stri
|
||||
if let Ok(path) = env::var("RTSP_PATHS_FILE") {
|
||||
let path = path.trim();
|
||||
if !path.is_empty() {
|
||||
return load_rtsp_paths_from_file(Path::new(path));
|
||||
return match load_rtsp_paths_from_file(Path::new(path)) {
|
||||
Ok(paths) => Ok(paths),
|
||||
Err(err) if is_not_found(&err) => {
|
||||
let paths = parse_rtsp_paths(include_str!("../rtsp_paths.txt"));
|
||||
if !paths.is_empty() {
|
||||
Ok(paths)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"arquivo de paths RTSP padrão embutido vazio: ../rtsp_paths.txt"
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +493,20 @@ fn load_rtsp_paths(file: Option<&Path>, inline: Option<&str>) -> Result<Vec<Stri
|
||||
}
|
||||
}
|
||||
|
||||
load_rtsp_paths_from_file(Path::new("rtsp_paths.txt"))
|
||||
match load_rtsp_paths_from_file(Path::new("/app/rtsp_paths.txt")) {
|
||||
Ok(paths) => Ok(paths),
|
||||
Err(err) if is_not_found(&err) => {
|
||||
let paths = parse_rtsp_paths(include_str!("../rtsp_paths.txt"));
|
||||
if !paths.is_empty() {
|
||||
Ok(paths)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"arquivo de paths RTSP padrão embutido vazio: ../rtsp_paths.txt"
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_rtsp_paths_from_file(file: &Path) -> Result<Vec<String>> {
|
||||
@@ -457,6 +519,13 @@ fn load_rtsp_paths_from_file(file: &Path) -> Result<Vec<String>> {
|
||||
Err(anyhow!("arquivo de paths RTSP vazio: {}", file.display()))
|
||||
}
|
||||
|
||||
fn is_not_found(err: &anyhow::Error) -> bool {
|
||||
err.chain()
|
||||
.find_map(|cause| cause.downcast_ref::<std::io::Error>())
|
||||
.map(|err| err.kind() == std::io::ErrorKind::NotFound)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn parse_rtsp_paths(input: &str) -> Vec<String> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
input
|
||||
|
||||
@@ -69,6 +69,29 @@ impl Database {
|
||||
|
||||
CREATE INDEX IF NOT EXISTS rtsp_probe_results_status_idx
|
||||
ON rtsp_probe_results (status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rtsp_fetch_results (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
checked_at TIMESTAMPTZ NOT NULL,
|
||||
checked_at_ms BIGINT NOT NULL,
|
||||
target_ip TEXT NOT NULL,
|
||||
target_port INTEGER NOT NULL,
|
||||
rtsp_path TEXT NOT NULL,
|
||||
rtsp_url TEXT NOT NULL,
|
||||
screenshot_path TEXT,
|
||||
status TEXT NOT NULL,
|
||||
error TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS rtsp_fetch_results_checked_at_idx
|
||||
ON rtsp_fetch_results (checked_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS rtsp_fetch_results_target_ip_idx
|
||||
ON rtsp_fetch_results (target_ip);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS rtsp_fetch_results_status_idx
|
||||
ON rtsp_fetch_results (status);
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
@@ -252,6 +275,62 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_rtsp_fetch_result(
|
||||
&self,
|
||||
checked_at_ms: i64,
|
||||
target_ip: &str,
|
||||
target_port: u16,
|
||||
rtsp_path: &str,
|
||||
rtsp_url: &str,
|
||||
screenshot_path: Option<&str>,
|
||||
status: &str,
|
||||
error: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let target_port = i32::from(target_port);
|
||||
|
||||
self.client
|
||||
.execute(
|
||||
r#"
|
||||
INSERT INTO rtsp_fetch_results (
|
||||
checked_at,
|
||||
checked_at_ms,
|
||||
target_ip,
|
||||
target_port,
|
||||
rtsp_path,
|
||||
rtsp_url,
|
||||
screenshot_path,
|
||||
status,
|
||||
error
|
||||
)
|
||||
VALUES (
|
||||
TO_TIMESTAMP(($1::BIGINT)::DOUBLE PRECISION / 1000.0),
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8
|
||||
)
|
||||
"#,
|
||||
&[
|
||||
&checked_at_ms,
|
||||
&target_ip,
|
||||
&target_port,
|
||||
&rtsp_path,
|
||||
&rtsp_url,
|
||||
&screenshot_path,
|
||||
&status,
|
||||
&error,
|
||||
],
|
||||
)
|
||||
.await
|
||||
.context("failed to insert RTSP fetch result")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn summarize_chunks(&self) -> Result<(usize, usize)> {
|
||||
let row = self
|
||||
.client
|
||||
|
||||
+1
-1
@@ -3,8 +3,8 @@ mod coordinator;
|
||||
mod db;
|
||||
mod ip;
|
||||
mod models;
|
||||
mod rtsp;
|
||||
mod redis_rtsp;
|
||||
mod rtsp;
|
||||
mod scan;
|
||||
mod terminal;
|
||||
mod worker;
|
||||
|
||||
+982
-132
File diff suppressed because it is too large
Load Diff
+165
@@ -1,5 +1,7 @@
|
||||
use std::io::{self, IsTerminal, Write};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use terminal_size::{terminal_size, Width};
|
||||
|
||||
pub const BANNER: [&str; 9] = [
|
||||
" ░██████ ░██████ ",
|
||||
@@ -25,6 +27,23 @@ pub struct TerminalUi {
|
||||
lock: Mutex<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RedisWorkerScreenState {
|
||||
pub worker_name: String,
|
||||
pub queue_name: String,
|
||||
pub current_ip: String,
|
||||
pub current_path: String,
|
||||
pub current_status: String,
|
||||
pub processed: usize,
|
||||
pub captured: usize,
|
||||
pub global_done: usize,
|
||||
pub global_total: usize,
|
||||
pub queue_remaining: Option<usize>,
|
||||
pub block_done: usize,
|
||||
pub block_total: usize,
|
||||
pub elapsed: Duration,
|
||||
}
|
||||
|
||||
impl TerminalUi {
|
||||
pub fn new(enabled: bool) -> Self {
|
||||
Self {
|
||||
@@ -56,6 +75,104 @@ impl TerminalUi {
|
||||
let _ = stdout.flush();
|
||||
}
|
||||
|
||||
pub fn render_redis_worker(&self, state: &RedisWorkerScreenState) {
|
||||
if !self.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let _guard = self.lock.lock().expect("terminal mutex poisoned");
|
||||
let mut stdout = io::stdout();
|
||||
let width = terminal_width();
|
||||
let content_width = width.saturating_sub(2).max(32);
|
||||
let global_percent = percent(state.global_done, state.global_total);
|
||||
let block_percent = percent(state.block_done, state.block_total);
|
||||
let global_bar = progress_bar(global_percent, content_width.saturating_sub(24));
|
||||
let block_bar = progress_bar(block_percent, content_width.saturating_sub(24));
|
||||
let queue_remaining = state
|
||||
.queue_remaining
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "?".to_string());
|
||||
|
||||
let _ = write!(stdout, "\x1b[2J\x1b[H\x1b[?25l");
|
||||
let _ = writeln!(
|
||||
stdout,
|
||||
"{}",
|
||||
fit_line(
|
||||
&format!(
|
||||
"CamFinder Redis Worker {} pid:{}",
|
||||
state.worker_name,
|
||||
std::process::id()
|
||||
),
|
||||
content_width
|
||||
)
|
||||
);
|
||||
let _ = writeln!(stdout, "{}", "─".repeat(content_width));
|
||||
let _ = writeln!(
|
||||
stdout,
|
||||
"{}",
|
||||
fit_line(&format!("Fila: {}", state.queue_name), content_width)
|
||||
);
|
||||
let _ = writeln!(
|
||||
stdout,
|
||||
"{}",
|
||||
fit_line(
|
||||
&format!(
|
||||
"Global: [{}] {:>5.1}% {}/{} IPs redis:{}",
|
||||
global_bar,
|
||||
global_percent,
|
||||
state.global_done,
|
||||
state.global_total,
|
||||
queue_remaining
|
||||
),
|
||||
content_width
|
||||
)
|
||||
);
|
||||
let _ = writeln!(
|
||||
stdout,
|
||||
"{}",
|
||||
fit_line(
|
||||
&format!(
|
||||
"Bloco : [{}] {:>5.1}% {}/{} tentativas",
|
||||
block_bar, block_percent, state.block_done, state.block_total
|
||||
),
|
||||
content_width
|
||||
)
|
||||
);
|
||||
let _ = writeln!(stdout, "{}", "─".repeat(content_width));
|
||||
let _ = writeln!(
|
||||
stdout,
|
||||
"{}",
|
||||
fit_line(&format!("IP atual : {}", state.current_ip), content_width)
|
||||
);
|
||||
let _ = writeln!(
|
||||
stdout,
|
||||
"{}",
|
||||
fit_line(&format!("Path : {}", state.current_path), content_width)
|
||||
);
|
||||
let _ = writeln!(
|
||||
stdout,
|
||||
"{}",
|
||||
fit_line(
|
||||
&format!("Status : {}", state.current_status),
|
||||
content_width
|
||||
)
|
||||
);
|
||||
let _ = writeln!(
|
||||
stdout,
|
||||
"{}",
|
||||
fit_line(
|
||||
&format!(
|
||||
"Tentativas: {} Capturas: {} Tempo: {}",
|
||||
state.processed,
|
||||
state.captured,
|
||||
format_duration(state.elapsed)
|
||||
),
|
||||
content_width
|
||||
)
|
||||
);
|
||||
let _ = stdout.flush();
|
||||
}
|
||||
|
||||
pub fn render(&self, state: &ScreenState) {
|
||||
if !self.enabled {
|
||||
return;
|
||||
@@ -97,3 +214,51 @@ impl TerminalUi {
|
||||
pub fn stdout_is_terminal() -> bool {
|
||||
io::stdout().is_terminal()
|
||||
}
|
||||
|
||||
fn terminal_width() -> usize {
|
||||
terminal_size()
|
||||
.map(|(Width(width), _)| usize::from(width))
|
||||
.unwrap_or(80)
|
||||
}
|
||||
|
||||
fn percent(done: usize, total: usize) -> f64 {
|
||||
if total == 0 {
|
||||
0.0
|
||||
} else {
|
||||
((done as f64 / total as f64) * 100.0).clamp(0.0, 100.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn progress_bar(percent: f64, width: usize) -> String {
|
||||
let width = width.clamp(8, 80);
|
||||
let filled = ((percent / 100.0) * width as f64).round() as usize;
|
||||
let empty = width.saturating_sub(filled);
|
||||
format!("{}{}", "█".repeat(filled), "░".repeat(empty))
|
||||
}
|
||||
|
||||
fn fit_line(value: &str, width: usize) -> String {
|
||||
let chars = value.chars().collect::<Vec<_>>();
|
||||
if chars.len() <= width {
|
||||
return value.to_string();
|
||||
}
|
||||
if width <= 1 {
|
||||
return "…".to_string();
|
||||
}
|
||||
chars
|
||||
.into_iter()
|
||||
.take(width - 1)
|
||||
.chain(std::iter::once('…'))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn format_duration(duration: Duration) -> String {
|
||||
let total = duration.as_secs();
|
||||
let hours = total / 3600;
|
||||
let minutes = (total % 3600) / 60;
|
||||
let seconds = total % 60;
|
||||
if hours > 0 {
|
||||
format!("{hours:02}:{minutes:02}:{seconds:02}")
|
||||
} else {
|
||||
format!("{minutes:02}:{seconds:02}")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user