initial commit
This commit is contained in:
1432
scripts/cassandra-migrate/Cargo.lock
generated
Normal file
1432
scripts/cassandra-migrate/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
scripts/cassandra-migrate/Cargo.toml
Normal file
22
scripts/cassandra-migrate/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "cassandra-migrate"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
scylla = "0.9"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
anyhow = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
md5 = "0.7"
|
||||
regex = "1.10"
|
||||
lazy_static = "1.4"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
opt-level = "z"
|
||||
21
scripts/cassandra-migrate/Dockerfile
Normal file
21
scripts/cassandra-migrate/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM rustlang/rust:nightly as builder
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY scripts/cassandra-migrate/Cargo.toml scripts/cassandra-migrate/Cargo.toml
|
||||
COPY scripts/cassandra-migrate/Cargo.lock scripts/cassandra-migrate/Cargo.lock
|
||||
COPY scripts/cassandra-migrate/src scripts/cassandra-migrate/src
|
||||
|
||||
WORKDIR /workspace/scripts/cassandra-migrate
|
||||
RUN cargo build --release
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /workspace/scripts/cassandra-migrate/target/release/cassandra-migrate /usr/local/bin/cassandra-migrate
|
||||
COPY fluxer_devops/cassandra/migrations fluxer_devops/cassandra/migrations
|
||||
|
||||
ENTRYPOINT ["cassandra-migrate"]
|
||||
110
scripts/cassandra-migrate/src/main.rs
Normal file
110
scripts/cassandra-migrate/src/main.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::env;
|
||||
|
||||
mod migrate;
|
||||
|
||||
fn get_env_or_default(key: &str, default: &str) -> String {
|
||||
env::var(key).unwrap_or_else(|_| default.to_string())
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "cassandra-migrate")]
|
||||
#[command(about = "Forward-only Cassandra migration tool for Fluxer", long_about = Some("A simple, forward-only migration tool for Cassandra.\nMigrations are stored in fluxer_devops/cassandra/migrations.\nMigration metadata is stored in the 'fluxer' keyspace."))]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
|
||||
#[arg(long, default_value_t = get_env_or_default("CASSANDRA_HOST", "localhost"))]
|
||||
host: String,
|
||||
|
||||
#[arg(long, default_value = "9042")]
|
||||
port: u16,
|
||||
|
||||
#[arg(long, default_value_t = get_env_or_default("CASSANDRA_USERNAME", "cassandra"))]
|
||||
username: String,
|
||||
|
||||
#[arg(long, default_value_t = get_env_or_default("CASSANDRA_PASSWORD", "cassandra"))]
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Create a new migration file
|
||||
Create {
|
||||
/// Name of the migration
|
||||
name: String,
|
||||
},
|
||||
/// Validate all migration files
|
||||
Check,
|
||||
/// Run pending migrations
|
||||
Up,
|
||||
/// Acknowledge a failed migration to skip it
|
||||
Ack {
|
||||
/// Filename of the migration to acknowledge
|
||||
filename: String,
|
||||
},
|
||||
/// Show migration status
|
||||
Status,
|
||||
/// Test Cassandra connection
|
||||
Test,
|
||||
/// Debug Cassandra connection
|
||||
Debug,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Create { name } => {
|
||||
migrate::create_migration(&name)?;
|
||||
}
|
||||
Commands::Check => {
|
||||
migrate::check_migrations()?;
|
||||
}
|
||||
Commands::Up => {
|
||||
migrate::run_migrations(&cli.host, cli.port, &cli.username, &cli.password).await?;
|
||||
}
|
||||
Commands::Ack { filename } => {
|
||||
migrate::acknowledge_migration(
|
||||
&cli.host,
|
||||
cli.port,
|
||||
&cli.username,
|
||||
&cli.password,
|
||||
&filename,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Commands::Status => {
|
||||
migrate::show_status(&cli.host, cli.port, &cli.username, &cli.password).await?;
|
||||
}
|
||||
Commands::Test => {
|
||||
migrate::test_connection(&cli.host, cli.port, &cli.username, &cli.password).await?;
|
||||
}
|
||||
Commands::Debug => {
|
||||
migrate::debug_connection(&cli.host, cli.port, &cli.username, &cli.password).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
682
scripts/cassandra-migrate/src/migrate.rs
Normal file
682
scripts/cassandra-migrate/src/migrate.rs
Normal file
@@ -0,0 +1,682 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use regex::Regex;
|
||||
use scylla::Session;
|
||||
use scylla::SessionBuilder;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const MIGRATION_TABLE: &str = "schema_migrations";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref MIGRATION_KEYSPACE: String = env::var("CASSANDRA_KEYSPACE").unwrap_or_else(|_| "fluxer".to_string());
|
||||
}
|
||||
|
||||
fn migration_keyspace() -> &'static str {
|
||||
MIGRATION_KEYSPACE.as_str()
|
||||
}
|
||||
|
||||
const MIGRATION_TEMPLATE: &str = "";
|
||||
|
||||
struct ForbiddenPattern {
|
||||
pattern: Regex,
|
||||
message: &'static str,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref FORBIDDEN_PATTERNS: Vec<ForbiddenPattern> = vec![
|
||||
ForbiddenPattern {
|
||||
pattern: Regex::new(r"(?i)\bCREATE\s+INDEX\b").unwrap(),
|
||||
message: "Secondary indexes are forbidden (CREATE INDEX)",
|
||||
},
|
||||
ForbiddenPattern {
|
||||
pattern: Regex::new(r"(?i)\bCREATE\s+CUSTOM\s+INDEX\b").unwrap(),
|
||||
message: "Custom indexes are forbidden (CREATE CUSTOM INDEX)",
|
||||
},
|
||||
ForbiddenPattern {
|
||||
pattern: Regex::new(r"(?i)\bCREATE\s+MATERIALIZED\s+VIEW\b").unwrap(),
|
||||
message: "Materialized views are forbidden (CREATE MATERIALIZED VIEW)",
|
||||
},
|
||||
ForbiddenPattern {
|
||||
pattern: Regex::new(r"(?i)\bDROP\s+TABLE\b").unwrap(),
|
||||
message: "DROP TABLE is forbidden",
|
||||
},
|
||||
ForbiddenPattern {
|
||||
pattern: Regex::new(r"(?i)\bDROP\s+KEYSPACE\b").unwrap(),
|
||||
message: "DROP KEYSPACE is forbidden",
|
||||
},
|
||||
ForbiddenPattern {
|
||||
pattern: Regex::new(r"(?i)\bDROP\s+TYPE\b").unwrap(),
|
||||
message: "DROP TYPE is forbidden",
|
||||
},
|
||||
ForbiddenPattern {
|
||||
pattern: Regex::new(r"(?i)\bDROP\s+INDEX\b").unwrap(),
|
||||
message: "DROP INDEX is forbidden",
|
||||
},
|
||||
ForbiddenPattern {
|
||||
pattern: Regex::new(r"(?i)\bDROP\s+MATERIALIZED\s+VIEW\b").unwrap(),
|
||||
message: "DROP MATERIALIZED VIEW is forbidden",
|
||||
},
|
||||
ForbiddenPattern {
|
||||
pattern: Regex::new(r"(?i)\bDROP\s+COLUMN\b").unwrap(),
|
||||
message: "DROP COLUMN is forbidden (use ALTER TABLE ... DROP ...)",
|
||||
},
|
||||
ForbiddenPattern {
|
||||
pattern: Regex::new(r"(?i)\bTRUNCATE\b").unwrap(),
|
||||
message: "TRUNCATE is forbidden",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
pub async fn test_connection(host: &str, port: u16, username: &str, password: &str) -> Result<()> {
|
||||
println!("Testing Cassandra connection to {host}:{port}...");
|
||||
|
||||
let session = create_session(host, port, username, password).await?;
|
||||
|
||||
let result = session
|
||||
.query("SELECT release_version FROM system.local", &[])
|
||||
.await?;
|
||||
|
||||
if let Some(rows) = result.rows
|
||||
&& let Some(row) = rows.into_iter().next()
|
||||
&& let Some(cql_value) = &row.columns[0]
|
||||
&& let Some(version) = cql_value.as_text()
|
||||
{
|
||||
println!("✓ Connection successful - Cassandra version: {version}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("✓ Connection successful");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_migration(name: &str) -> Result<()> {
|
||||
let sanitized = sanitize_name(name);
|
||||
if sanitized.is_empty() {
|
||||
return Err(anyhow!("Invalid migration name: {name}"));
|
||||
}
|
||||
|
||||
let timestamp = Utc::now().format("%Y%m%d%H%M%S");
|
||||
let filename = format!("{timestamp}_{sanitized}.cql");
|
||||
|
||||
let filepath = get_migration_path(&filename);
|
||||
|
||||
if filepath.exists() {
|
||||
return Err(anyhow!("Migration file already exists: {filename}"));
|
||||
}
|
||||
|
||||
fs::write(&filepath, MIGRATION_TEMPLATE)?;
|
||||
|
||||
println!("✓ Created migration: {filename}");
|
||||
println!(" Path: {}", filepath.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_migrations() -> Result<()> {
|
||||
let migrations = get_migration_files()?;
|
||||
|
||||
if migrations.is_empty() {
|
||||
println!("No migration files found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Checking {} migration file(s)...\n", migrations.len());
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut valid_count = 0;
|
||||
|
||||
for migration in &migrations {
|
||||
let content = fs::read_to_string(get_migration_path(migration))?;
|
||||
let file_errors = validate_migration_content(migration, &content);
|
||||
|
||||
if file_errors.is_empty() {
|
||||
valid_count += 1;
|
||||
println!("✓ {migration}");
|
||||
} else {
|
||||
errors.extend(file_errors);
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
println!("\nValidation errors:");
|
||||
for error in &errors {
|
||||
println!("✗ {error}");
|
||||
}
|
||||
return Err(anyhow!("Validation failed with {} error(s)", errors.len()));
|
||||
}
|
||||
|
||||
println!("\n✓ All {valid_count} migration(s) are valid!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_migrations(host: &str, port: u16, username: &str, password: &str) -> Result<()> {
|
||||
println!("Starting Cassandra migration process...");
|
||||
println!("Host: {host}, Port: {port}");
|
||||
|
||||
let session = create_session(host, port, username, password).await?;
|
||||
|
||||
setup_migration_infrastructure(&session).await?;
|
||||
|
||||
let migrations = get_migration_files()?;
|
||||
let applied = get_applied_migrations(&session).await?;
|
||||
|
||||
if migrations.is_empty() {
|
||||
println!("No migration files found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut pending = Vec::new();
|
||||
let mut skipped = Vec::new();
|
||||
|
||||
for migration in migrations {
|
||||
if !applied.contains_key(&migration) {
|
||||
if has_skip_ci(&migration)? {
|
||||
skipped.push(migration);
|
||||
} else {
|
||||
pending.push(migration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !skipped.is_empty() {
|
||||
println!(
|
||||
"Found {} migration(s) with '-- skip ci' annotation:",
|
||||
skipped.len()
|
||||
);
|
||||
for migration in &skipped {
|
||||
println!(" - {migration}");
|
||||
}
|
||||
println!("\nAuto-acknowledging skipped migrations...");
|
||||
|
||||
for migration in &skipped {
|
||||
auto_acknowledge_migration(&session, migration).await?;
|
||||
println!(" ✓ Acknowledged: {migration}");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
if pending.is_empty() {
|
||||
println!("✓ No pending migrations");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Found {} pending migration(s) to apply:", pending.len());
|
||||
for migration in &pending {
|
||||
println!(" - {migration}");
|
||||
}
|
||||
println!();
|
||||
|
||||
let pending_count = pending.len();
|
||||
for migration in pending {
|
||||
apply_migration(&session, &migration).await?;
|
||||
}
|
||||
|
||||
println!("✓ Successfully applied {pending_count} migration(s)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn show_status(host: &str, port: u16, username: &str, password: &str) -> Result<()> {
|
||||
let session = create_session(host, port, username, password).await?;
|
||||
|
||||
let migrations = get_migration_files()?;
|
||||
let applied = get_applied_migrations(&session).await?;
|
||||
|
||||
println!("Migration Status");
|
||||
println!("================\n");
|
||||
println!("Total migrations: {}", migrations.len());
|
||||
println!("Applied: {}", applied.len());
|
||||
println!("Pending: {}\n", migrations.len() - applied.len());
|
||||
|
||||
if !migrations.is_empty() {
|
||||
println!("Migrations:");
|
||||
for migration in migrations {
|
||||
let status = if applied.contains_key(&migration) {
|
||||
"[✓]"
|
||||
} else {
|
||||
"[ ]"
|
||||
};
|
||||
|
||||
let suffix = if has_skip_ci(&migration)? {
|
||||
" (skip ci)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
println!(" {status} {migration}{suffix}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn acknowledge_migration(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: &str,
|
||||
filename: &str,
|
||||
) -> Result<()> {
|
||||
let session = create_session(host, port, username, password).await?;
|
||||
|
||||
let applied = get_applied_migrations(&session).await?;
|
||||
if applied.contains_key(filename) {
|
||||
return Err(anyhow!("Migration {filename} is already applied"));
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(get_migration_path(filename))?;
|
||||
let checksum = calculate_checksum(&content);
|
||||
|
||||
session
|
||||
.query(
|
||||
format!(
|
||||
"INSERT INTO {}.{} (filename, applied_at, checksum) VALUES (?, ?, ?)",
|
||||
migration_keyspace(),
|
||||
MIGRATION_TABLE
|
||||
),
|
||||
(filename, Utc::now(), checksum),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("✓ Migration acknowledged: {filename}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_session(host: &str, port: u16, username: &str, password: &str) -> Result<Session> {
|
||||
let max_retries = 5;
|
||||
let retry_delay = Duration::from_secs(10);
|
||||
|
||||
let mut last_error = None;
|
||||
|
||||
for attempt in 1..=max_retries {
|
||||
if attempt > 1 {
|
||||
println!("Retrying connection (attempt {attempt}/{max_retries})...");
|
||||
}
|
||||
|
||||
let result = SessionBuilder::new()
|
||||
.known_node(format!("{host}:{port}"))
|
||||
.user(username, password)
|
||||
.connection_timeout(Duration::from_secs(60))
|
||||
.build()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(session) => {
|
||||
let _ = session
|
||||
.query(
|
||||
format!(
|
||||
"CREATE KEYSPACE IF NOT EXISTS {} WITH REPLICATION = {{'class': 'SimpleStrategy', 'replication_factor': 1}}",
|
||||
migration_keyspace()
|
||||
),
|
||||
&[],
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(session);
|
||||
}
|
||||
Err(e) => {
|
||||
last_error = Some(e);
|
||||
|
||||
if attempt < max_retries {
|
||||
tokio::time::sleep(retry_delay).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"Failed to connect to Cassandra after {} attempts: {}",
|
||||
max_retries,
|
||||
last_error.unwrap()
|
||||
))
|
||||
}
|
||||
|
||||
async fn setup_migration_infrastructure(session: &Session) -> Result<()> {
|
||||
session
|
||||
.query(
|
||||
format!(
|
||||
"CREATE TABLE IF NOT EXISTS {}.{} (filename text PRIMARY KEY, applied_at timestamp, checksum text)",
|
||||
migration_keyspace(),
|
||||
MIGRATION_TABLE
|
||||
),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create migrations table: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_migration(session: &Session, filename: &str) -> Result<()> {
|
||||
println!("Applying migration: {filename}");
|
||||
|
||||
let content = fs::read_to_string(get_migration_path(filename))?;
|
||||
let statements = parse_statements(&content);
|
||||
|
||||
if statements.is_empty() {
|
||||
return Err(anyhow!("No valid statements found in migration"));
|
||||
}
|
||||
|
||||
println!(" Executing {} statement(s)...", statements.len());
|
||||
|
||||
for (i, statement) in statements.iter().enumerate() {
|
||||
println!(" [{}/{}] Executing...", i + 1, statements.len());
|
||||
|
||||
session
|
||||
.query(statement.as_str(), &[])
|
||||
.await
|
||||
.map_err(|e| anyhow!("Statement {} failed: {}\n{}", i + 1, e, statement))?;
|
||||
}
|
||||
|
||||
let checksum = calculate_checksum(&content);
|
||||
session
|
||||
.query(
|
||||
format!(
|
||||
"INSERT INTO {}.{} (filename, applied_at, checksum) VALUES (?, ?, ?)",
|
||||
migration_keyspace(),
|
||||
MIGRATION_TABLE
|
||||
),
|
||||
(filename, Utc::now(), checksum),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to record migration: {e}"))?;
|
||||
|
||||
println!(" ✓ Migration applied successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn auto_acknowledge_migration(session: &Session, filename: &str) -> Result<()> {
|
||||
let content = fs::read_to_string(get_migration_path(filename))?;
|
||||
let checksum = calculate_checksum(&content);
|
||||
|
||||
session
|
||||
.query(
|
||||
format!(
|
||||
"INSERT INTO {}.{} (filename, applied_at, checksum) VALUES (?, ?, ?)",
|
||||
migration_keyspace(),
|
||||
MIGRATION_TABLE
|
||||
),
|
||||
(filename, Utc::now(), checksum),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to record migration: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_applied_migrations(session: &Session) -> Result<HashMap<String, DateTime<Utc>>> {
|
||||
let mut applied = HashMap::new();
|
||||
|
||||
let result = session
|
||||
.query(
|
||||
format!(
|
||||
"SELECT filename, applied_at FROM {}.{}",
|
||||
migration_keyspace(),
|
||||
MIGRATION_TABLE
|
||||
),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(rows) = result.rows {
|
||||
for row in rows {
|
||||
if let Some(filename_cql) = &row.columns[0]
|
||||
&& let Some(filename) = filename_cql.as_text()
|
||||
&& let Some(applied_at_cql) = &row.columns[1]
|
||||
{
|
||||
use scylla::frame::response::result::CqlValue;
|
||||
if let CqlValue::Timestamp(duration) = applied_at_cql {
|
||||
let millis = duration.num_milliseconds();
|
||||
let applied_at: DateTime<Utc> = DateTime::from_timestamp(
|
||||
millis / 1000,
|
||||
((millis % 1000) * 1_000_000) as u32,
|
||||
)
|
||||
.unwrap_or(Utc::now());
|
||||
applied.insert(filename.to_string(), applied_at);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(applied)
|
||||
}
|
||||
|
||||
fn get_migration_files() -> Result<Vec<String>> {
|
||||
let migrations_dir = get_migrations_dir();
|
||||
let mut migrations = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(migrations_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file()
|
||||
&& path.extension().is_some_and(|ext| ext == "cql")
|
||||
&& let Some(filename) = path.file_name().and_then(|n| n.to_str())
|
||||
{
|
||||
migrations.push(filename.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
migrations.sort();
|
||||
Ok(migrations)
|
||||
}
|
||||
|
||||
fn get_migrations_dir() -> PathBuf {
|
||||
PathBuf::from("fluxer_devops/cassandra/migrations")
|
||||
}
|
||||
|
||||
fn get_migration_path(filename: &str) -> PathBuf {
|
||||
get_migrations_dir().join(filename)
|
||||
}
|
||||
|
||||
fn has_skip_ci(filename: &str) -> Result<bool> {
|
||||
let content = fs::read_to_string(get_migration_path(filename))?;
|
||||
let lines: Vec<&str> = content.lines().take(10).collect();
|
||||
|
||||
for line in lines {
|
||||
let line = line.trim().to_lowercase();
|
||||
if line.contains("-- skip ci") || line.contains("--skip ci") {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn parse_statements(content: &str) -> Vec<String> {
|
||||
let mut statements = Vec::new();
|
||||
let mut current_statement = String::new();
|
||||
|
||||
for line in content.lines() {
|
||||
let line = if let Some(idx) = line.find("--") {
|
||||
&line[..idx]
|
||||
} else {
|
||||
line
|
||||
};
|
||||
|
||||
let trimmed = line.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
current_statement.push_str(trimmed);
|
||||
current_statement.push(' ');
|
||||
|
||||
if trimmed.ends_with(';') {
|
||||
let statement = current_statement.trim().to_string();
|
||||
if !statement.is_empty() {
|
||||
statements.push(statement);
|
||||
}
|
||||
current_statement.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if !current_statement.trim().is_empty() {
|
||||
statements.push(current_statement.trim().to_string());
|
||||
}
|
||||
|
||||
statements
|
||||
}
|
||||
|
||||
fn calculate_checksum(content: &str) -> String {
|
||||
format!("{:x}", md5::compute(content.as_bytes()))
|
||||
}
|
||||
|
||||
fn sanitize_name(name: &str) -> String {
|
||||
let mut name = name.replace(' ', "_");
|
||||
name = name.replace('-', "_");
|
||||
name = name.to_lowercase();
|
||||
|
||||
let result: String = name
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric() || *c == '_')
|
||||
.collect();
|
||||
|
||||
let mut result = result.replace("__", "_");
|
||||
while result.contains("__") {
|
||||
result = result.replace("__", "_");
|
||||
}
|
||||
|
||||
result.trim_matches('_').to_string()
|
||||
}
|
||||
|
||||
fn validate_migration_content(filename: &str, content: &str) -> Vec<String> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
let clean_content = remove_comments(content);
|
||||
|
||||
for forbidden in FORBIDDEN_PATTERNS.iter() {
|
||||
if forbidden.pattern.is_match(&clean_content) {
|
||||
errors.push(format!(" {}: {}", filename, forbidden.message));
|
||||
}
|
||||
}
|
||||
|
||||
if clean_content.trim().is_empty() {
|
||||
errors.push(format!(" {filename}: migration file is empty"));
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
|
||||
fn remove_comments(content: &str) -> String {
|
||||
content
|
||||
.lines()
|
||||
.map(|line| {
|
||||
if let Some(idx) = line.find("--") {
|
||||
&line[..idx]
|
||||
} else {
|
||||
line
|
||||
}
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub async fn debug_connection(host: &str, port: u16, username: &str, password: &str) -> Result<()> {
|
||||
println!("=== Cassandra Connection Debug ===");
|
||||
println!("Host: {host}:{port}");
|
||||
println!("Username: {username}");
|
||||
|
||||
println!("\n[1/3] Testing TCP connectivity...");
|
||||
let start = Instant::now();
|
||||
match tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
tokio::net::TcpStream::connect(format!("{host}:{port}")),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => {
|
||||
println!(
|
||||
" ✓ TCP connection successful ({:.2}s)",
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!(" ✗ TCP connection failed: {e}");
|
||||
return Err(anyhow!("TCP connection failed: {e}"));
|
||||
}
|
||||
Err(_) => {
|
||||
println!(" ✗ TCP connection timed out");
|
||||
return Err(anyhow!("TCP connection timed out"));
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n[2/3] Creating Cassandra session...");
|
||||
let start = Instant::now();
|
||||
let session = match tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
SessionBuilder::new()
|
||||
.known_node(format!("{host}:{port}"))
|
||||
.user(username, password)
|
||||
.connection_timeout(Duration::from_secs(20))
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(session)) => {
|
||||
println!(
|
||||
" ✓ Session created ({:.2}s)",
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
session
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!(" ✗ Session creation failed: {e}");
|
||||
return Err(anyhow!("Failed to create session: {e}"));
|
||||
}
|
||||
Err(_) => {
|
||||
println!(" ✗ Session creation timed out");
|
||||
return Err(anyhow!("Session creation timed out"));
|
||||
}
|
||||
};
|
||||
|
||||
println!("\n[3/3] Testing queries...");
|
||||
let start = Instant::now();
|
||||
let result = session
|
||||
.query("SELECT release_version FROM system.local", &[])
|
||||
.await?;
|
||||
|
||||
if let Some(rows) = result.rows
|
||||
&& let Some(row) = rows.into_iter().next()
|
||||
&& let Some(version_col) = &row.columns[0]
|
||||
&& let Some(version) = version_col.as_text()
|
||||
{
|
||||
println!(
|
||||
" ✓ Cassandra version: {} ({:.2}s)",
|
||||
version,
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
" ✓ Query successful ({:.2}s)",
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n✓ All debug checks passed");
|
||||
Ok(())
|
||||
}
|
||||
381
scripts/license-enforcer/Cargo.lock
generated
Normal file
381
scripts/license-enforcer/Cargo.lock
generated
Normal file
@@ -0,0 +1,381 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "license-enforcer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"regex",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
15
scripts/license-enforcer/Cargo.toml
Normal file
15
scripts/license-enforcer/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "license-enforcer"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
walkdir = "2.4"
|
||||
regex = "1.10"
|
||||
chrono = "0.4"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
opt-level = "z"
|
||||
426
scripts/license-enforcer/src/main.rs
Normal file
426
scripts/license-enforcer/src/main.rs
Normal file
@@ -0,0 +1,426 @@
|
||||
#![allow(clippy::four_forward_slashes)]
|
||||
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use regex::Regex;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const TS_LICENSE_HEADER: &str = r"/*
|
||||
* Copyright (C) {year} Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/";
|
||||
|
||||
const ERLANG_LICENSE_HEADER: &str = r"%% Copyright (C) {year} Fluxer Contributors
|
||||
%%
|
||||
%% This file is part of Fluxer.
|
||||
%%
|
||||
%% Fluxer is free software: you can redistribute it and/or modify
|
||||
%% it under the terms of the GNU Affero General Public License as published by
|
||||
%% the Free Software Foundation, either version 3 of the License, or
|
||||
%% (at your option) any later version.
|
||||
%%
|
||||
%% Fluxer is distributed in the hope that it will be useful,
|
||||
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
%% GNU Affero General Public License for more details.
|
||||
%%
|
||||
%% You should have received a copy of the GNU Affero General Public License
|
||||
%% along with Fluxer. If not, see <https://www.gnu.org/licenses/>.";
|
||||
|
||||
const GLEAM_LICENSE_HEADER: &str = r"//// Copyright (C) {year} Fluxer Contributors
|
||||
////
|
||||
//// This file is part of Fluxer.
|
||||
////
|
||||
//// Fluxer is free software: you can redistribute it and/or modify
|
||||
//// it under the terms of the GNU Affero General Public License as published by
|
||||
//// the Free Software Foundation, either version 3 of the License, or
|
||||
//// (at your option) any later version.
|
||||
////
|
||||
//// Fluxer is distributed in the hope that it will be useful,
|
||||
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//// GNU Affero General Public License for more details.
|
||||
////
|
||||
//// You should have received a copy of the GNU Affero General Public License
|
||||
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.";
|
||||
|
||||
const SHELL_LICENSE_HEADER: &str = r"# Copyright (C) {year} Fluxer Contributors
|
||||
#
|
||||
# This file is part of Fluxer.
|
||||
#
|
||||
# Fluxer is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fluxer is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Fluxer. If not, see <https://www.gnu.org/licenses/>.";
|
||||
|
||||
const BLOCK_COMMENT_EXTS: &[&str] = &[
|
||||
"ts", "tsx", "js", "jsx", "mjs", "cjs", "css", "go", "rs", "c", "cc", "cpp", "cxx", "h", "hh",
|
||||
"hpp", "hxx", "mm", "m", "java", "kt", "kts", "swift", "scala", "dart", "cs", "fs",
|
||||
];
|
||||
|
||||
const HASH_LINE_EXTS: &[&str] = &[
|
||||
"sh", "bash", "zsh", "py", "rb", "ps1", "psm1", "psd1", "ksh", "fish",
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum HeaderStyle {
|
||||
Block,
|
||||
Line(&'static str),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct FileTemplate {
|
||||
header: &'static str,
|
||||
style: HeaderStyle,
|
||||
}
|
||||
|
||||
impl FileTemplate {
|
||||
const fn new(header: &'static str, style: HeaderStyle) -> Self {
|
||||
Self { header, style }
|
||||
}
|
||||
}
|
||||
|
||||
struct Processor {
|
||||
current_year: i32,
|
||||
updated: usize,
|
||||
ignore_patterns: Vec<String>,
|
||||
}
|
||||
|
||||
impl Processor {
|
||||
fn new() -> Self {
|
||||
let current_year = chrono::Datelike::year(&chrono::Utc::now());
|
||||
let mut processor = Processor {
|
||||
current_year,
|
||||
updated: 0,
|
||||
ignore_patterns: Vec::new(),
|
||||
};
|
||||
processor.load_gitignore();
|
||||
processor
|
||||
}
|
||||
|
||||
fn load_gitignore(&mut self) {
|
||||
if let Ok(file) = fs::File::open(".gitignore") {
|
||||
let reader = BufReader::new(file);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() && !trimmed.starts_with('#') {
|
||||
self.ignore_patterns.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn should_ignore(&self, path: &str) -> bool {
|
||||
if path.contains("fluxer_static") {
|
||||
return true;
|
||||
}
|
||||
for pattern in &self.ignore_patterns {
|
||||
if self.match_pattern(pattern, path) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn match_pattern(&self, pattern: &str, path: &str) -> bool {
|
||||
if let Some(sub_pattern) = pattern.strip_prefix("**/") {
|
||||
if sub_pattern.ends_with('/') {
|
||||
let dir_name = sub_pattern.trim_end_matches('/');
|
||||
return path
|
||||
.split(std::path::MAIN_SEPARATOR)
|
||||
.any(|part| part == dir_name);
|
||||
}
|
||||
return path
|
||||
.split(std::path::MAIN_SEPARATOR)
|
||||
.any(|part| part == sub_pattern);
|
||||
}
|
||||
|
||||
if pattern.ends_with('/') {
|
||||
let dir_pattern = pattern.trim_end_matches('/');
|
||||
return path
|
||||
.split(std::path::MAIN_SEPARATOR)
|
||||
.any(|part| part == dir_pattern)
|
||||
|| path.starts_with(&format!("{dir_pattern}/"));
|
||||
}
|
||||
|
||||
if let Some(p) = pattern.strip_prefix('/') {
|
||||
return path == p;
|
||||
}
|
||||
|
||||
path.split(std::path::MAIN_SEPARATOR)
|
||||
.any(|part| part == pattern)
|
||||
|| Path::new(path).file_name().and_then(|f| f.to_str()) == Some(pattern)
|
||||
}
|
||||
|
||||
fn is_target_file(&self, path: &Path) -> bool {
|
||||
self.get_template(path).is_some()
|
||||
}
|
||||
|
||||
fn get_template(&self, path: &Path) -> Option<FileTemplate> {
|
||||
path.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.and_then(Self::template_for_extension)
|
||||
}
|
||||
|
||||
fn template_for_extension(ext: &str) -> Option<FileTemplate> {
|
||||
let normalized = ext.to_ascii_lowercase();
|
||||
if BLOCK_COMMENT_EXTS.contains(&normalized.as_str()) {
|
||||
Some(FileTemplate::new(TS_LICENSE_HEADER, HeaderStyle::Block))
|
||||
} else if HASH_LINE_EXTS.contains(&normalized.as_str()) {
|
||||
Some(FileTemplate::new(
|
||||
SHELL_LICENSE_HEADER,
|
||||
HeaderStyle::Line("#"),
|
||||
))
|
||||
} else {
|
||||
match normalized.as_str() {
|
||||
"gleam" => Some(FileTemplate::new(
|
||||
GLEAM_LICENSE_HEADER,
|
||||
HeaderStyle::Line("////"),
|
||||
)),
|
||||
"erl" | "hrl" => Some(FileTemplate::new(
|
||||
ERLANG_LICENSE_HEADER,
|
||||
HeaderStyle::Line("%%"),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_license(&self, content: &str) -> (bool, Option<i32>) {
|
||||
let lines: Vec<&str> = content.lines().take(25).collect();
|
||||
let mut has_agpl = false;
|
||||
let mut has_fluxer = false;
|
||||
let mut detected_year = None;
|
||||
|
||||
let year_regex = Regex::new(r"\b(20\d{2})\b").unwrap();
|
||||
|
||||
for line in lines {
|
||||
let lower = line.to_lowercase();
|
||||
if lower.contains("gnu affero general public license") || lower.contains("agpl") {
|
||||
has_agpl = true;
|
||||
}
|
||||
if lower.contains("fluxer") {
|
||||
has_fluxer = true;
|
||||
}
|
||||
if lower.contains("copyright")
|
||||
&& lower.contains("fluxer")
|
||||
&& detected_year.is_none()
|
||||
&& let Some(cap) = year_regex.captures(line)
|
||||
&& let Ok(year) = cap[1].parse::<i32>()
|
||||
&& (1900..3000).contains(&year)
|
||||
{
|
||||
detected_year = Some(year);
|
||||
}
|
||||
}
|
||||
|
||||
(has_agpl && has_fluxer, detected_year)
|
||||
}
|
||||
|
||||
fn update_year(&self, content: &str, old_year: i32) -> String {
|
||||
content.replacen(&old_year.to_string(), &self.current_year.to_string(), 1)
|
||||
}
|
||||
|
||||
fn strip_license_header(&self, content: &str, style: HeaderStyle) -> (String, bool) {
|
||||
let lines: Vec<&str> = content.split('\n').collect();
|
||||
if lines.is_empty() {
|
||||
return (content.to_string(), false);
|
||||
}
|
||||
|
||||
let mut prefix_end = 0;
|
||||
if let Some(first) = lines.get(0) {
|
||||
if first.starts_with("#!") {
|
||||
prefix_end = 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut header_start = prefix_end;
|
||||
while header_start < lines.len() && lines[header_start].trim().is_empty() {
|
||||
header_start += 1;
|
||||
}
|
||||
|
||||
if header_start >= lines.len() {
|
||||
return (content.to_string(), false);
|
||||
}
|
||||
|
||||
let original_ending = content.ends_with('\n');
|
||||
|
||||
let after_idx = match style {
|
||||
HeaderStyle::Block => {
|
||||
let first = lines[header_start].trim_start();
|
||||
if !first.starts_with("/*") {
|
||||
return (content.to_string(), false);
|
||||
}
|
||||
let mut header_end = header_start;
|
||||
let mut found_end = false;
|
||||
for i in header_start..lines.len() {
|
||||
if lines[i].contains("*/") {
|
||||
header_end = i;
|
||||
found_end = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found_end {
|
||||
return (content.to_string(), false);
|
||||
}
|
||||
let mut after = header_end + 1;
|
||||
while after < lines.len() && lines[after].trim().is_empty() {
|
||||
after += 1;
|
||||
}
|
||||
after
|
||||
}
|
||||
HeaderStyle::Line(prefix) => {
|
||||
let first = lines[header_start].trim_start();
|
||||
if !first.starts_with(prefix) {
|
||||
return (content.to_string(), false);
|
||||
}
|
||||
let mut header_end = header_start;
|
||||
while header_end < lines.len() {
|
||||
let trimmed = lines[header_end].trim_start();
|
||||
if trimmed.is_empty() {
|
||||
break;
|
||||
}
|
||||
if trimmed.starts_with(prefix) {
|
||||
header_end += 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
let mut after = header_end;
|
||||
while after < lines.len() && lines[after].trim().is_empty() {
|
||||
after += 1;
|
||||
}
|
||||
after
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_lines = Vec::new();
|
||||
new_lines.extend_from_slice(&lines[..prefix_end]);
|
||||
new_lines.extend_from_slice(&lines[after_idx..]);
|
||||
|
||||
let mut result = new_lines.join("\n");
|
||||
if original_ending && !result.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
|
||||
(result, true)
|
||||
}
|
||||
|
||||
fn add_header(&self, content: &str, template: FileTemplate) -> String {
|
||||
let header = template
|
||||
.header
|
||||
.replace("{year}", &self.current_year.to_string());
|
||||
|
||||
if let Some(first_line) = content.lines().next()
|
||||
&& first_line.starts_with("#!")
|
||||
{
|
||||
let rest = content.lines().skip(1).collect::<Vec<_>>().join("\n");
|
||||
return format!("{first_line}\n\n{header}\n\n{rest}");
|
||||
}
|
||||
|
||||
format!("{header}\n\n{content}")
|
||||
}
|
||||
|
||||
fn process_file(&mut self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
let template = self.get_template(path).ok_or("Unknown file type")?;
|
||||
let (has_header, detected_year) = self.detect_license(&content);
|
||||
|
||||
let (new_content, action) = if !has_header {
|
||||
(self.add_header(&content, template), "Added header")
|
||||
} else {
|
||||
let (stripped, stripped_ok) = self.strip_license_header(&content, template.style);
|
||||
if stripped_ok {
|
||||
(self.add_header(&stripped, template), "Normalized header")
|
||||
} else if let Some(old_year) = detected_year {
|
||||
if old_year == self.current_year {
|
||||
return Ok(());
|
||||
}
|
||||
(
|
||||
self.update_year(&content, old_year),
|
||||
&format!("Updated year {} → {}", old_year, self.current_year) as &str,
|
||||
)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
fs::write(path, new_content)?;
|
||||
self.updated += 1;
|
||||
println!("{}: {}", action, path.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let paths: Vec<PathBuf> = WalkDir::new(".")
|
||||
.into_iter()
|
||||
.filter_map(std::result::Result::ok)
|
||||
.filter(|e| {
|
||||
let path = e.path();
|
||||
let path_str = path.to_string_lossy();
|
||||
!self.should_ignore(&path_str)
|
||||
&& e.file_type().is_file()
|
||||
&& self.is_target_file(path)
|
||||
})
|
||||
.map(|e| e.path().to_path_buf())
|
||||
.collect();
|
||||
|
||||
for path in paths {
|
||||
if let Err(e) = self.process_file(&path) {
|
||||
eprintln!("Error processing {path:?}: {e}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut processor = Processor::new();
|
||||
if let Err(e) = processor.walk() {
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("Updated {} files", processor.updated);
|
||||
}
|
||||
251
scripts/snowflake-generator/Cargo.lock
generated
Normal file
251
scripts/snowflake-generator/Cargo.lock
generated
Normal file
@@ -0,0 +1,251 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snowflake-generator"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
13
scripts/snowflake-generator/Cargo.toml
Normal file
13
scripts/snowflake-generator/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "snowflake-generator"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
opt-level = "z"
|
||||
72
scripts/snowflake-generator/src/main.rs
Normal file
72
scripts/snowflake-generator/src/main.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use clap::Parser;
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
const FLUXER_EPOCH: i64 = 1420070400000;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "snowflake-generator")]
|
||||
#[command(about = "Generates unique Fluxer snowflake IDs", long_about = None)]
|
||||
struct Cli {
|
||||
/// Number of snowflakes to generate
|
||||
#[arg(long, short, default_value = "1")]
|
||||
count: usize,
|
||||
|
||||
/// Optional Unix timestamp in milliseconds (defaults to current time)
|
||||
#[arg(long, short)]
|
||||
timestamp: Option<i64>,
|
||||
}
|
||||
|
||||
fn generate_snowflake(timestamp: Option<i64>) -> i64 {
|
||||
let ts = match timestamp {
|
||||
Some(t) => t,
|
||||
None => SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as i64,
|
||||
};
|
||||
|
||||
(ts - FLUXER_EPOCH) << 22
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.count < 1 {
|
||||
eprintln!("Error: count must be at least 1");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if cli.count == 1 {
|
||||
let snowflake = generate_snowflake(cli.timestamp);
|
||||
println!("{snowflake}");
|
||||
} else {
|
||||
for _ in 0..cli.count {
|
||||
let snowflake = generate_snowflake(cli.timestamp);
|
||||
println!("{snowflake}");
|
||||
|
||||
if cli.timestamp.is_none() {
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user