diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6f34100 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +target/ +.git/ +.github/ +*.md +.gitignore +.dockerignore +docker-compose.yml \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6e169b1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,105 @@ +name: CI + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + rust: [stable, beta] + + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev pkg-config libssl-dev + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Check formatting + run: cargo fmt -- --check + + - name: Run clippy + run: cargo clippy -- -D warnings + + - name: Build + run: cargo build --verbose + + - name: Run tests + run: cargo test --verbose + + build: + name: Build Release + runs-on: ubuntu-latest + needs: test + + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev pkg-config libssl-dev + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Build release + run: cargo build --release --verbose + + - name: Upload binary + uses: actions/upload-artifact@v4 + with: + name: torn-binary + path: target/release/torn \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..7fd092d --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,66 @@ +name: Docker Build and Deploy + +on: + push: + branches: [ master, main ] + tags: [ 'v*' ] + pull_request: + branches: [ master, main ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Generate artifact attestation + if: github.event_name != 'pull_request' + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..14ef48e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# Build stage +FROM rust:1.88.0-slim as builder + +WORKDIR /usr/src/app + +# Install required system dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libssl-dev \ + libudev-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml Cargo.lock ./ + +# Copy source code +COPY src ./src +COPY config ./config +COPY style ./style +COPY templates ./templates + +# Build the application +RUN cargo build --release + +# Runtime stage +FROM debian:bookworm-slim + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libudev1 \ + libssl3 \ + wget \ + gnupg \ + && rm -rf /var/lib/apt/lists/* + +# Install HandBrake and MakeMKV from PPAs +RUN apt-get update && apt-get install -y \ + software-properties-common \ + && add-apt-repository ppa:stebbins/handbrake-releases \ + && add-apt-repository ppa:heyarje/makemkv-beta \ + && apt-get update && apt-get install -y \ + handbrake-cli \ + makemkv-bin \ + makemkv-oss \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy the binary from builder stage +COPY --from=builder /usr/src/app/target/release/torn /app/torn +COPY --from=builder /usr/src/app/config /app/config +COPY --from=builder /usr/src/app/style /app/style +COPY --from=builder /usr/src/app/templates /app/templates + +# Create directories for data +RUN mkdir -p /app/data/raw /app/data/output + +# Expose the web interface port +EXPOSE 8080 + +# Run the application +CMD ["./torn", "rip"] diff --git a/src/config.rs b/src/config.rs index 46a7216..086578b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,6 @@ pub struct Options { #[derive(Clone, Debug, Deserialize)] pub struct Directory { - pub logs: String, pub raw: String, pub output: String, } diff --git a/src/disc.rs b/src/disc.rs index 514748c..94e53d2 100644 --- a/src/disc.rs +++ b/src/disc.rs @@ -40,10 +40,10 @@ impl Disc { } pub fn title(&self) -> String { - if let Some(val) = self.properties.get("ID_FS_LABEL") { - if val != "iso9660" { - return val.to_title_case(); - } + if let Some(val) = self.properties.get("ID_FS_LABEL") + && val != "iso9660" + { + return val.to_title_case(); } if let Some(val) = self.properties.get("ID_FS_UUID") { @@ -78,10 +78,10 @@ fn get_device_proprties(device: &str) -> HashMap { } fn get_device_type(properties: &HashMap) -> Option { - if let Some(val) = properties.get("ID_FS_LABEL") { - if val == "iso9660" { - return Some(DiscType::Data); - } + if let Some(val) = properties.get("ID_FS_LABEL") + && val == "iso9660" + { + return Some(DiscType::Data); } if properties.get("ID_CDROM_MEDIA_BD").is_some() { diff --git a/src/handbrake.rs b/src/handbrake.rs index 96ae551..2fa295e 100644 --- a/src/handbrake.rs +++ b/src/handbrake.rs @@ -70,7 +70,7 @@ impl HandbrakeProcess { // Mark job as failed let mut jobs_map = jobs_clone.write().await; if let Some(job_status) = jobs_map.get_mut(&job.id) { - job_status.status = format!("Failed: {}", e); + job_status.status = format!("Failed: {e}"); job_status.progress = 0.0; } } @@ -180,13 +180,13 @@ async fn handbrake( { let mut jobs_map = jobs.write().await; if let Some(job_status) = jobs_map.get_mut(job_id) { - job_status.status = format!("Processing: {}", source_file); + job_status.status = format!("Processing: {source_file}"); job_status.progress = 0.5; // Rough estimate } } let mut child = Command::new("HandBrakeCLI") - .args(&[ + .args([ "-i", source_file, "-o", diff --git a/src/main.rs b/src/main.rs index d3c39af..aa97894 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,12 +38,12 @@ async fn main() -> Result<(), Error> { rip(settings).await?; } Command::Debug(_) => { - println!("Settings: {:#?}", settings); + println!("Settings: {settings:#?}"); for device in settings.options.devices { let disc = Disc::new(&device); - println!("{:#?}", disc); + println!("{disc:#?}"); } } } @@ -65,7 +65,7 @@ async fn rip(settings: Settings) -> Result<(), Error> { let web_hb_process = hb_process.clone(); let web_handle = tokio::spawn(async move { if let Err(e) = web::run_web_server(web_settings, web_hb_process).await { - warn!("Web interface error: {}", e); + warn!("Web interface error: {e}"); } Ok(()) }); @@ -86,7 +86,7 @@ async fn rip(settings: Settings) -> Result<(), Error> { for res in results { if let Err(err) = res { - error!("Error: {}", err); + error!("Error: {err}"); } } @@ -121,7 +121,7 @@ fn spawn_rip_process( disc::eject(&disc).await; } Some(t) => { - warn!("Disc type {:?} currently unsupported", t); + warn!("Disc type {t:?} currently unsupported"); disc::eject(&disc).await; } None => { diff --git a/src/makemkv.rs b/src/makemkv.rs index 619ef75..d00b310 100644 --- a/src/makemkv.rs +++ b/src/makemkv.rs @@ -3,7 +3,7 @@ use std::{ time::SystemTime, }; -use failure::{format_err, Error}; +use failure::{Error, format_err}; use tokio::{fs, process::Command}; use crate::config::MakeMKV; @@ -25,7 +25,7 @@ pub async fn rip(config: &MakeMKV, disc: &Disc, target_folder: &Path) -> Result< fs::create_dir_all(&target_folder).await?; let mut child = Command::new("makemkvcon") - .args(&[ + .args([ "mkv", "-r", &format!("dev:{}", disc.name), diff --git a/src/web.rs b/src/web.rs index 3ebc630..ae3e687 100644 --- a/src/web.rs +++ b/src/web.rs @@ -73,7 +73,7 @@ pub async fn run_web_server( // Check if port is already in use if let Err(e) = tokio::net::TcpListener::bind(&addr).await { if e.kind() == std::io::ErrorKind::AddrInUse { - info!("Web interface already running on {}", addr); + info!("Web interface already running on {addr}"); return Ok(()); } return Err(failure::format_err!( @@ -94,7 +94,7 @@ pub async fn run_web_server( .await .map_err(|e| failure::format_err!("Failed to bind to address {}: {}", addr, e))?; - info!("Web interface available at http://{}", addr); + info!("Web interface available at http://{addr}"); // Run the server with graceful shutdown handling let server_result = axum::serve(listener, app).await; @@ -104,7 +104,7 @@ pub async fn run_web_server( match server_result { Ok(_) => Ok(()), Err(e) => { - warn!("Web server stopped: {}", e); + warn!("Web server stopped: {e}"); Ok(()) // Don't fail the entire rip process if web server stops } } @@ -182,7 +182,7 @@ async fn eject_disc( axum::extract::Path(device): axum::extract::Path, State(_app_state): State, ) -> Json { - let device_path = format!("/dev/{}", device); + let device_path = format!("/dev/{device}"); let disc = Disc::new(&device_path); crate::disc::eject(&disc).await;