Compare commits
8 Commits
58d8ef050c
...
dbada3c509
| Author | SHA1 | Date | |
|---|---|---|---|
| dbada3c509 | |||
| c8be707420 | |||
| fdba2c75c7 | |||
| 55d600c0c0 | |||
| 71c5bd5e6c | |||
| 998f04713f | |||
| 6935fbea8b | |||
| 301e090251 |
@@ -16,6 +16,7 @@ keys:
|
|||||||
- &gpd-win4 age1ceg548u5ma6rgu3xgvd254y5xefqrdqfqhcjsjp3255q976fgd2qaua53d
|
- &gpd-win4 age1ceg548u5ma6rgu3xgvd254y5xefqrdqfqhcjsjp3255q976fgd2qaua53d
|
||||||
- &nb age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
|
- &nb age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
|
||||||
- &amzebs-01 age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w
|
- &amzebs-01 age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w
|
||||||
|
- &nas age1x3elhtccp4u8ha5ry32juj9fkpg0qg7qqx4gduuehgwwnnhcxp8s892hek
|
||||||
|
|
||||||
creation_rules:
|
creation_rules:
|
||||||
- path_regex: ^[^/]+\.yaml$
|
- path_regex: ^[^/]+\.yaml$
|
||||||
@@ -80,6 +81,14 @@ creation_rules:
|
|||||||
- *dominik2
|
- *dominik2
|
||||||
- *nb
|
- *nb
|
||||||
- *amzebs-01
|
- *amzebs-01
|
||||||
|
- path_regex: hosts/nas/[^/]+\.yaml$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *bitwarden
|
||||||
|
- *dominik
|
||||||
|
- *dominik2
|
||||||
|
- *nb
|
||||||
|
- *nas
|
||||||
- path_regex: hosts/mail/[^/]+\.yaml$
|
- path_regex: hosts/mail/[^/]+\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
|
|||||||
92
CLAUDE.md
Normal file
92
CLAUDE.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Repository Overview
|
||||||
|
|
||||||
|
This is a NixOS infrastructure repository managing multiple hosts (servers and personal machines) using a modular Nix configuration approach with SOPS for secrets management and Bento for deployment.
|
||||||
|
|
||||||
|
## Build and Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enter development shell (sets up MCP configs)
|
||||||
|
nix-shell
|
||||||
|
|
||||||
|
# Test configuration before deployment (required before PRs)
|
||||||
|
./scripts/test-configuration <hostname>
|
||||||
|
./scripts/test-configuration -v <hostname> # with --show-trace
|
||||||
|
|
||||||
|
# Edit encrypted secrets
|
||||||
|
nix-shell -p sops --run 'sops hosts/<hostname>/secrets.yaml'
|
||||||
|
|
||||||
|
# Update secrets keys after adding new age keys
|
||||||
|
./scripts/update-secrets-keys
|
||||||
|
|
||||||
|
# Format Nix files
|
||||||
|
nix run nixpkgs#nixpkgs-fmt .
|
||||||
|
|
||||||
|
# Compute hash for new packages
|
||||||
|
nix hash to-sri --type sha256 $(nix-prefetch-url https://example.com/file.tar.gz)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Host Structure
|
||||||
|
Each host in `hosts/<hostname>/` contains:
|
||||||
|
- `configuration.nix` - Main entry point importing modules
|
||||||
|
- `hardware-configuration.nix` - Machine-specific hardware config
|
||||||
|
- `secrets.yaml` - SOPS-encrypted secrets
|
||||||
|
- `modules/` - Host-specific service configurations
|
||||||
|
- `fleet.nix` → symlink to root `fleet.nix` (SFTP user provisioning)
|
||||||
|
- `utils/` → symlink to root `utils/` (shared modules)
|
||||||
|
|
||||||
|
Current hosts: `fw` (firewall/router), `nb` (notebook), `web-arm`, `mail`, `amzebs-01`
|
||||||
|
|
||||||
|
### Shared Components (`utils/`)
|
||||||
|
- `modules/` - Reusable NixOS modules (nginx, sops, borgbackup, lego, promtail, etc.)
|
||||||
|
- `overlays/` - Nixpkgs overlays
|
||||||
|
- `pkgs/` - Custom package derivations
|
||||||
|
- `bento.nix` - Deployment helper module
|
||||||
|
|
||||||
|
### Secrets Management
|
||||||
|
- SOPS with age encryption; keys defined in `.sops.yaml`
|
||||||
|
- Each host has its own age key derived from SSH host key
|
||||||
|
- Host secrets in `hosts/<hostname>/secrets.yaml`
|
||||||
|
- Shared module secrets in `utils/modules/<module>/secrets.yaml`
|
||||||
|
|
||||||
|
**IMPORTANT: Never modify secrets files directly.** Instead, tell the user which secrets need to be added and where, so they can edit the encrypted files themselves using:
|
||||||
|
```bash
|
||||||
|
nix-shell -p sops --run 'sops hosts/<hostname>/secrets.yaml'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
The Git runner handles deployment automatically when changes merge to main. A successful `./scripts/test-configuration <host>` dry-build is the gate before pushing.
|
||||||
|
|
||||||
|
## Custom Packages
|
||||||
|
|
||||||
|
When creating a new package in `utils/pkgs/`, always include an `update.sh` script to automate version updates. See `utils/pkgs/claude-code/update.sh` for the pattern:
|
||||||
|
|
||||||
|
1. Fetch latest version from upstream (npm, GitHub, etc.)
|
||||||
|
2. Update version string in `default.nix`
|
||||||
|
3. Update source hash using `nix-prefetch-url`
|
||||||
|
4. Update dependency hashes (e.g., `npmDepsHash`) by triggering a build with a fake hash
|
||||||
|
5. Verify the final build succeeds
|
||||||
|
|
||||||
|
Example structure:
|
||||||
|
```
|
||||||
|
utils/pkgs/<package-name>/
|
||||||
|
├── default.nix
|
||||||
|
├── update.sh # Always include this
|
||||||
|
└── (other files like patches, lock files)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
**IMPORTANT: Always run `./scripts/test-configuration <hostname>` after making any changes** to verify the NixOS configuration builds successfully. This is required before committing.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Nix files: two-space indentation, lower kebab-case naming
|
||||||
|
- Commits: Conventional Commits format (`fix:`, `feat:`, `chore:`), scope by host when relevant (`fix(mail):`)
|
||||||
|
- Modules import via explicit paths, not wildcards
|
||||||
|
- Comments explain non-obvious decisions (open ports, unusual service options)
|
||||||
@@ -39,7 +39,7 @@ cat ~/.ssh/id_rsa.pub | ssh -p23 u149513-subx@u149513-subx.your-backup.de instal
|
|||||||
|
|
||||||
# 4. Add new Host
|
# 4. Add new Host
|
||||||
```console
|
```console
|
||||||
sftp host.cloonar.com@git.cloonar.com:/config/bootstrap.sh ./
|
sftp host@git.cloonar.com:/config/bootstrap.sh ./
|
||||||
```
|
```
|
||||||
|
|
||||||
# 5. Yubikey
|
# 5. Yubikey
|
||||||
|
|||||||
@@ -47,6 +47,11 @@
|
|||||||
username = "gpd-win4";
|
username = "gpd-win4";
|
||||||
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjfS2DtS8PQgkf86dU+EVu5t+r/QlCWmY7+RPYprQrO";
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjfS2DtS8PQgkf86dU+EVu5t+r/QlCWmY7+RPYprQrO";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
username = "nas";
|
||||||
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICS6b97LPUpr7/kWvOcI40s5e+gfbfz0I2/hAPL6zTmU";
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
username = "amzebs-01";
|
username = "amzebs-01";
|
||||||
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMkFZ60SPl8pzEtGrFq1+n6ZkDuNe3xJaccJMjr3y/q";
|
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMkFZ60SPl8pzEtGrFq1+n6ZkDuNe3xJaccJMjr3y/q";
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
./modules/firefox-sync.nix
|
./modules/firefox-sync.nix
|
||||||
./modules/fivefilters.nix
|
./modules/fivefilters.nix
|
||||||
./modules/pyload
|
# ./modules/pyload
|
||||||
|
|
||||||
# home assistant
|
# home assistant
|
||||||
./modules/home-assistant
|
./modules/home-assistant
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
"02:00:00:00:00:04,${config.networkPrefix}.97.6,matrix"
|
"02:00:00:00:00:04,${config.networkPrefix}.97.6,matrix"
|
||||||
"ea:db:d4:c1:18:ba,${config.networkPrefix}.97.50,git"
|
"ea:db:d4:c1:18:ba,${config.networkPrefix}.97.50,git"
|
||||||
"c2:4f:64:dd:13:0c,${config.networkPrefix}.97.20,home-assistant"
|
"c2:4f:64:dd:13:0c,${config.networkPrefix}.97.20,home-assistant"
|
||||||
|
"6c:1f:f7:8e:a9:86,${config.networkPrefix}.97.11,nas"
|
||||||
"1a:c4:04:6e:29:02,${config.networkPrefix}.101.25,deconz"
|
"1a:c4:04:6e:29:02,${config.networkPrefix}.101.25,deconz"
|
||||||
|
|
||||||
"c4:a7:2b:c7:ea:30,${config.networkPrefix}.99.10,metz"
|
"c4:a7:2b:c7:ea:30,${config.networkPrefix}.99.10,metz"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
# Restrict to internal LAN only
|
# Restrict to internal LAN only
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
allow ${config.networkPrefix}.96.0/24;
|
allow ${config.networkPrefix}.96.0/24;
|
||||||
|
allow ${config.networkPrefix}.97.0/24;
|
||||||
allow ${config.networkPrefix}.98.0/24;
|
allow ${config.networkPrefix}.98.0/24;
|
||||||
deny all;
|
deny all;
|
||||||
'';
|
'';
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
# Restrict to internal LAN only
|
# Restrict to internal LAN only
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
allow ${config.networkPrefix}.96.0/24;
|
allow ${config.networkPrefix}.96.0/24;
|
||||||
|
allow ${config.networkPrefix}.97.0/24;
|
||||||
allow ${config.networkPrefix}.98.0/24;
|
allow ${config.networkPrefix}.98.0/24;
|
||||||
allow ${config.networkPrefix}.99.0/24;
|
allow ${config.networkPrefix}.99.0/24;
|
||||||
deny all;
|
deny all;
|
||||||
|
|||||||
87
hosts/nas/configuration.nix
Normal file
87
hosts/nas/configuration.nix
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# NAS host configuration
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
impermanence = builtins.fetchTarball "https://github.com/nix-community/impermanence/archive/master.tar.gz";
|
||||||
|
in {
|
||||||
|
nixpkgs.config.allowUnfree = true;
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
"${impermanence}/nixos.nix"
|
||||||
|
./utils/bento.nix
|
||||||
|
./utils/modules/sops.nix
|
||||||
|
|
||||||
|
./modules/pyload.nix
|
||||||
|
./modules/jellyfin.nix
|
||||||
|
|
||||||
|
./hardware-configuration.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
nixpkgs.overlays = [
|
||||||
|
(import ./utils/overlays/packages.nix)
|
||||||
|
];
|
||||||
|
|
||||||
|
# Hostname
|
||||||
|
networking.hostName = "nas";
|
||||||
|
|
||||||
|
# Timezone
|
||||||
|
time.timeZone = "Europe/Vienna";
|
||||||
|
console.keyMap = "de";
|
||||||
|
|
||||||
|
# SSH server
|
||||||
|
services.openssh.enable = true;
|
||||||
|
users.users.root.openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7"
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRQuPqH5fdX3KEw7DXzWEdO3AlUn1oSmtJtHB71ICoH Generated By Termius"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Firewall
|
||||||
|
networking.firewall.enable = true;
|
||||||
|
networking.firewall.allowedTCPPorts = [ 22 ];
|
||||||
|
|
||||||
|
# SOPS configuration
|
||||||
|
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||||
|
sops.defaultSopsFile = ./secrets.yaml;
|
||||||
|
|
||||||
|
# Btrfs maintenance
|
||||||
|
services.btrfs.autoScrub = {
|
||||||
|
enable = true;
|
||||||
|
interval = "monthly";
|
||||||
|
fileSystems = [ "/nix" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Impermanence - persist important directories
|
||||||
|
# Note: /var/lib/downloads and /var/lib/multimedia are mounted from LVM on RAID
|
||||||
|
environment.persistence."/nix/persist/system" = {
|
||||||
|
hideMounts = true;
|
||||||
|
directories = [
|
||||||
|
"/var/lib/pyload"
|
||||||
|
"/var/lib/jellyfin"
|
||||||
|
"/var/log"
|
||||||
|
"/var/lib/nixos"
|
||||||
|
"/var/bento"
|
||||||
|
"/root/.ssh"
|
||||||
|
];
|
||||||
|
files = [
|
||||||
|
"/etc/machine-id"
|
||||||
|
{ file = "/etc/ssh/ssh_host_ed25519_key"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
|
||||||
|
{ file = "/etc/ssh/ssh_host_ed25519_key.pub"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
|
||||||
|
{ file = "/etc/ssh/ssh_host_rsa_key"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
|
||||||
|
{ file = "/etc/ssh/ssh_host_rsa_key.pub"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Nix settings
|
||||||
|
nix = {
|
||||||
|
settings = {
|
||||||
|
auto-optimise-store = true;
|
||||||
|
experimental-features = [ "nix-command" "flakes" ];
|
||||||
|
};
|
||||||
|
gc = {
|
||||||
|
automatic = true;
|
||||||
|
dates = "weekly";
|
||||||
|
options = "--delete-older-than 14d";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
system.stateVersion = "24.05";
|
||||||
|
}
|
||||||
1
hosts/nas/fleet.nix
Symbolic link
1
hosts/nas/fleet.nix
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../fleet.nix
|
||||||
92
hosts/nas/hardware-configuration.nix
Normal file
92
hosts/nas/hardware-configuration.nix
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Hardware configuration for NAS
|
||||||
|
# TODO: Update disk labels/UUIDs after installation
|
||||||
|
{ config, lib, pkgs, modulesPath, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.loader.systemd-boot = {
|
||||||
|
enable = true;
|
||||||
|
configurationLimit = 5;
|
||||||
|
};
|
||||||
|
boot.loader.efi.canTouchEfiVariables = true;
|
||||||
|
boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usb_storage" "sd_mod" ];
|
||||||
|
boot.initrd.kernelModules = [ "dm-snapshot" ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
# RAID 1 array for data storage
|
||||||
|
boot.swraid = {
|
||||||
|
enable = true;
|
||||||
|
mdadmConf = ''
|
||||||
|
DEVICE /dev/disk/by-id/ata-ST18000NM000J-2TV103_ZR52TBSB-part1
|
||||||
|
DEVICE /dev/disk/by-id/ata-ST18000NM000J-2TV103_ZR52V9QX-part1
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Tmpfs root filesystem (ephemeral - resets on reboot)
|
||||||
|
fileSystems."/" = {
|
||||||
|
device = "none";
|
||||||
|
fsType = "tmpfs";
|
||||||
|
options = [ "size=8G" "mode=755" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Boot partition - EFI
|
||||||
|
fileSystems."/boot" = {
|
||||||
|
device = "/dev/disk/by-partlabel/BOOT";
|
||||||
|
fsType = "vfat";
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems."/nix" = {
|
||||||
|
device = "/dev/disk/by-partlabel/NIXOS";
|
||||||
|
fsType = "btrfs";
|
||||||
|
neededForBoot = true;
|
||||||
|
options = [
|
||||||
|
"subvol=@"
|
||||||
|
"compress=zstd:1"
|
||||||
|
"noatime"
|
||||||
|
"commit=120"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems."/nix/store" = {
|
||||||
|
device = "/dev/disk/by-partlabel/NIXOS";
|
||||||
|
fsType = "btrfs";
|
||||||
|
neededForBoot = true;
|
||||||
|
options = [
|
||||||
|
"subvol=@nix-store"
|
||||||
|
"compress=zstd:1"
|
||||||
|
"noatime"
|
||||||
|
"commit=120"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems."/nix/persist" = {
|
||||||
|
device = "/dev/disk/by-partlabel/NIXOS";
|
||||||
|
fsType = "btrfs";
|
||||||
|
neededForBoot = true;
|
||||||
|
options = [
|
||||||
|
"subvol=@nix-persist"
|
||||||
|
"compress=zstd:1"
|
||||||
|
"noatime"
|
||||||
|
"commit=120"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# LVM volumes on RAID array
|
||||||
|
fileSystems."/var/lib/downloads" = {
|
||||||
|
device = "/dev/vg-data/lv-downloads";
|
||||||
|
fsType = "xfs";
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems."/var/lib/multimedia" = {
|
||||||
|
device = "/dev/vg-data/lv-multimedia";
|
||||||
|
fsType = "xfs";
|
||||||
|
};
|
||||||
|
|
||||||
|
# DHCP networking
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
89
hosts/nas/modules/filebot-process.nix
Normal file
89
hosts/nas/modules/filebot-process.nix
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{ pkgs }:
|
||||||
|
|
||||||
|
pkgs.writeShellScriptBin "filebot-process" ''
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# FileBot AMC script for automated media organization
|
||||||
|
# Called by PyLoad's package_extracted hook with parameters:
|
||||||
|
# $1 = package_id
|
||||||
|
# $2 = package_name
|
||||||
|
# $3 = download_folder (actual path to extracted files)
|
||||||
|
# $4 = password (optional)
|
||||||
|
|
||||||
|
PACKAGE_ID="''${1:-}"
|
||||||
|
PACKAGE_NAME="''${2:-unknown}"
|
||||||
|
DOWNLOAD_DIR="''${3:-/var/lib/downloads}"
|
||||||
|
PASSWORD="''${4:-}"
|
||||||
|
|
||||||
|
OUTPUT_DIR="/var/lib/multimedia"
|
||||||
|
LOG_FILE="/var/lib/pyload/filebot-amc.log"
|
||||||
|
EXCLUDE_LIST="/var/lib/pyload/filebot-exclude-list.txt"
|
||||||
|
|
||||||
|
# Ensure FileBot data directory exists
|
||||||
|
mkdir -p /var/lib/pyload/.local/share/filebot/data
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
touch "$EXCLUDE_LIST"
|
||||||
|
|
||||||
|
# Install FileBot license if not already installed
|
||||||
|
if [ ! -f /var/lib/pyload/.local/share/filebot/data/.license ]; then
|
||||||
|
echo "$(date): Installing FileBot license..." >> "$LOG_FILE"
|
||||||
|
${pkgs.filebot}/bin/filebot --license /var/lib/pyload/filebot-license.psm || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "===========================================" >> "$LOG_FILE"
|
||||||
|
echo "$(date): PyLoad package extracted hook triggered" >> "$LOG_FILE"
|
||||||
|
echo "Package ID: $PACKAGE_ID" >> "$LOG_FILE"
|
||||||
|
echo "Package Name: $PACKAGE_NAME" >> "$LOG_FILE"
|
||||||
|
echo "Download Directory: $DOWNLOAD_DIR" >> "$LOG_FILE"
|
||||||
|
echo "===========================================" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Check if download directory exists and has media files
|
||||||
|
if [ ! -d "$DOWNLOAD_DIR" ]; then
|
||||||
|
echo "$(date): Download directory does not exist: $DOWNLOAD_DIR" >> "$LOG_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if there are any video/media files to process
|
||||||
|
if ! find "$DOWNLOAD_DIR" -type f \( -iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" -o -iname "*.m4v" -o -iname "*.mov" \) -print -quit | grep -q .; then
|
||||||
|
echo "$(date): No media files found in: $DOWNLOAD_DIR" >> "$LOG_FILE"
|
||||||
|
echo "$(date): Skipping FileBot processing" >> "$LOG_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$(date): Starting FileBot processing" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Run FileBot AMC script
|
||||||
|
set +e # Temporarily disable exit on error to capture exit code
|
||||||
|
${pkgs.filebot}/bin/filebot \
|
||||||
|
-script fn:amc \
|
||||||
|
--output "$OUTPUT_DIR" \
|
||||||
|
--action move \
|
||||||
|
--conflict auto \
|
||||||
|
-non-strict \
|
||||||
|
--log-file "$LOG_FILE" \
|
||||||
|
--def \
|
||||||
|
excludeList="$EXCLUDE_LIST" \
|
||||||
|
movieFormat="$OUTPUT_DIR/movies/{n} ({y})/{n} ({y}) - {vf}" \
|
||||||
|
seriesFormat="$OUTPUT_DIR/tv-shows/{n}/Season {s.pad(2)}/{n} - {s00e00} - {t}" \
|
||||||
|
ut_dir="$DOWNLOAD_DIR" \
|
||||||
|
ut_kind=multi \
|
||||||
|
clean=y \
|
||||||
|
skipExtract=y
|
||||||
|
|
||||||
|
FILEBOT_EXIT_CODE=$?
|
||||||
|
set -e # Re-enable exit on error
|
||||||
|
|
||||||
|
if [ $FILEBOT_EXIT_CODE -ne 0 ]; then
|
||||||
|
echo "$(date): FileBot processing failed with exit code $FILEBOT_EXIT_CODE" >> "$LOG_FILE"
|
||||||
|
exit 0 # Don't fail the hook even if FileBot fails
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$(date): FileBot processing completed successfully" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Clean up any remaining empty directories
|
||||||
|
find "$DOWNLOAD_DIR" -type d -empty -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "$(date): All processing completed" >> "$LOG_FILE"
|
||||||
|
exit 0
|
||||||
|
''
|
||||||
51
hosts/nas/modules/jellyfin.nix
Normal file
51
hosts/nas/modules/jellyfin.nix
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{ lib, pkgs, ... }: {
|
||||||
|
# Intel graphics support for hardware transcoding
|
||||||
|
hardware.graphics = {
|
||||||
|
enable = true;
|
||||||
|
extraPackages = with pkgs; [
|
||||||
|
intel-media-driver
|
||||||
|
vpl-gpu-rt
|
||||||
|
intel-compute-runtime
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Set VA-API driver to iHD (modern Intel driver)
|
||||||
|
environment.sessionVariables = {
|
||||||
|
LIBVA_DRIVER_NAME = "iHD";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Jellyfin user with render/video groups for GPU access
|
||||||
|
users.users.jellyfin = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "jellyfin";
|
||||||
|
home = "/var/lib/jellyfin";
|
||||||
|
createHome = true;
|
||||||
|
extraGroups = [ "render" "video" ];
|
||||||
|
};
|
||||||
|
users.groups.jellyfin = {};
|
||||||
|
|
||||||
|
# Create jellyfin directory
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/jellyfin 0755 jellyfin jellyfin - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
services.jellyfin = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Override systemd hardening for GPU access
|
||||||
|
systemd.services.jellyfin = {
|
||||||
|
serviceConfig = {
|
||||||
|
PrivateUsers = lib.mkForce false; # Disable user namespacing - breaks GPU device access
|
||||||
|
DeviceAllow = [
|
||||||
|
"/dev/dri/card0 rw"
|
||||||
|
"/dev/dri/renderD128 rw"
|
||||||
|
];
|
||||||
|
SupplementaryGroups = [ "render" "video" ]; # Critical: Explicit group membership for GPU access
|
||||||
|
};
|
||||||
|
environment = {
|
||||||
|
LIBVA_DRIVER_NAME = "iHD"; # Ensure service sees this variable
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
104
hosts/nas/modules/pyload.nix
Normal file
104
hosts/nas/modules/pyload.nix
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
let
|
||||||
|
filebotScript = pkgs.callPackage ./filebot-process.nix {};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
|
||||||
|
"unrar"
|
||||||
|
"filebot"
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
unrar # Required for RAR archive extraction
|
||||||
|
p7zip # Required for 7z and other archive formats
|
||||||
|
];
|
||||||
|
|
||||||
|
# Create directory structure
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/downloads 0755 pyload pyload - -"
|
||||||
|
"d /var/lib/multimedia 0775 root jellyfin - -"
|
||||||
|
"d /var/lib/multimedia/movies 0775 jellyfin jellyfin - -"
|
||||||
|
"d /var/lib/multimedia/tv-shows 0775 jellyfin jellyfin - -"
|
||||||
|
"d /var/lib/multimedia/music 0755 jellyfin jellyfin - -"
|
||||||
|
|
||||||
|
# PyLoad hook scripts directory
|
||||||
|
"d /var/lib/pyload/config 0755 pyload pyload - -"
|
||||||
|
"d /var/lib/pyload/config/scripts 0755 pyload pyload - -"
|
||||||
|
"d /var/lib/pyload/config/scripts/package_extracted 0755 pyload pyload - -"
|
||||||
|
"L+ /var/lib/pyload/config/scripts/package_extracted/filebot-process.sh - - - - ${filebotScript}/bin/filebot-process"
|
||||||
|
];
|
||||||
|
|
||||||
|
# FileBot license secret (only if secrets.yaml exists)
|
||||||
|
sops.secrets.filebot-license = {
|
||||||
|
mode = "0440";
|
||||||
|
owner = "pyload";
|
||||||
|
group = "pyload";
|
||||||
|
path = "/var/lib/pyload/filebot-license.psm";
|
||||||
|
};
|
||||||
|
|
||||||
|
# PyLoad user with jellyfin group membership for multimedia access
|
||||||
|
users.users.pyload = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "pyload";
|
||||||
|
home = "/var/lib/pyload";
|
||||||
|
createHome = true;
|
||||||
|
extraGroups = [ "jellyfin" ];
|
||||||
|
};
|
||||||
|
users.groups.pyload = {};
|
||||||
|
|
||||||
|
services.pyload = {
|
||||||
|
enable = true;
|
||||||
|
downloadDirectory = "/var/lib/downloads";
|
||||||
|
listenAddress = "0.0.0.0";
|
||||||
|
port = 8000;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Configure pyload service
|
||||||
|
systemd.services.pyload = {
|
||||||
|
# Add extraction tools to service PATH
|
||||||
|
path = with pkgs; [
|
||||||
|
unrar # For RAR extraction
|
||||||
|
p7zip # For 7z extraction
|
||||||
|
];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
# Disable SSL certificate verification
|
||||||
|
PYLOAD__GENERAL__SSL_VERIFY = "0";
|
||||||
|
|
||||||
|
# Download speed limiting (150 Mbit/s = 19200 KiB/s)
|
||||||
|
PYLOAD__DOWNLOAD__LIMIT_SPEED = "1";
|
||||||
|
PYLOAD__DOWNLOAD__MAX_SPEED = "19200";
|
||||||
|
|
||||||
|
# Enable ExtractArchive plugin
|
||||||
|
PYLOAD__EXTRACTARCHIVE__ENABLED = "1";
|
||||||
|
PYLOAD__EXTRACTARCHIVE__DELETE = "1";
|
||||||
|
PYLOAD__EXTRACTARCHIVE__DELTOTRASH = "0";
|
||||||
|
PYLOAD__EXTRACTARCHIVE__REPAIR = "1";
|
||||||
|
PYLOAD__EXTRACTARCHIVE__RECURSIVE = "1";
|
||||||
|
PYLOAD__EXTRACTARCHIVE__FULLPATH = "1";
|
||||||
|
|
||||||
|
# Enable ExternalScripts plugin for hooks
|
||||||
|
PYLOAD__EXTERNALSCRIPTS__ENABLED = "1";
|
||||||
|
PYLOAD__EXTERNALSCRIPTS__UNLOCK = "1"; # Run hooks asynchronously
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
# Bind mount multimedia directory as writable for FileBot hook scripts
|
||||||
|
BindPaths = [ "/var/lib/multimedia" ];
|
||||||
|
|
||||||
|
# Override SystemCallFilter to allow @resources syscalls
|
||||||
|
# FileBot (Java) needs resource management syscalls like setpriority
|
||||||
|
# during cleanup operations. Still block privileged syscalls for security.
|
||||||
|
SystemCallFilter = lib.mkForce [
|
||||||
|
"@system-service"
|
||||||
|
"@resources" # Explicitly allow resource management syscalls
|
||||||
|
"~@privileged" # Still block privileged operations
|
||||||
|
"fchown" # Re-allow fchown for FileBot file operations
|
||||||
|
"fchown32" # 32-bit compatibility
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Open firewall for PyLoad web interface
|
||||||
|
networking.firewall.allowedTCPPorts = [ 8000 ];
|
||||||
|
}
|
||||||
43
hosts/nas/secrets.yaml
Normal file
43
hosts/nas/secrets.yaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
filebot-license: ENC[AES256_GCM,data:7fmczgcLOp/tkdoMVGfwESam9NwhZrNJXNcnebMKfilEKc81V4fdzS3/hxlueun2At6047UAdDi+6jAO3FvTmzmpmOFnrQxhojUKX2kj9SMSuDlskqxNt04O8wpGy69T8uOeV3ZUcRASzmfzQmuVU+zPKoIV2nq34mhXeIl17V22ZvoTDtFLs2w3IzxtKMZqLtDH4GZ4dkRcQBFcGY96T9R4CLcBqyZeBYb15ZaVLN9dHpQfExtpsBMVB3NikdWgUK9IAbjff1Xtkn+RT9gpQggCyQUl6UbtGqienbCq8ATORe1f89s+q6x/KneC5bdVZKQ4IqZrzDcuQ5aEJUEslc3aVITI3THEBWLOhYANy/4mMyw7R2KRIiHLrXAiiuxi3JumBvev27S6ilKdNo2By8OsbAB/Oj2YvDc5EBTePLCU0OloHIMSxWv5VkLQ2rM94bYtqXN269Lv6w4zmpOF0QblrvtoXnhhf4BQyf9owRDuNMrbp3Iu9Xnj87GoQVnulpwsMSifFxIibd94sRxS/N/5JTTih3Twj/Y8ZpmvoR8t9vCJ14vwBbMXheBpIYzxTk1phZKK1jLfi49DpWudop6iKNOCZdMeXxVRyAfTpOZSGqmdmsqOEDZESCnOAcNYlTxXmLmTdeqJfvTzsnWUi6kbbMYzaSaUlG7UB+RYnt97mcT5NtmbwCRDuFEOdqKGiR/vi7natH8VXe6nlY7xZxei0kkKAhIwyXV+mj/xurbQP3SuyYZriO2luCfuazxmVR0FpBqv+UZ6LwldmQ5e+O7hveYd+o1IjqjT67pVgLyFo1XY95Phq4/vFNvcZKhP7dqKlddkO5RCqjxHCmLowPSUfO2G4k7NV9/UioudATZnWGBpMfSp0pSuD9GXrPg=,iv:5BP77BRudjLiIKI5973BWbQlftupAdfd/aqFeN7DYLM=,tag:q2CAzGZ8lXPS41Uf2NjJTg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQWUI4YUsvODJtdTN4NDNn
|
||||||
|
QkFDVmFSV2Q1Q04rcWtiazZEMzFCYzRJMEJrCjM5RE41TjE0eURrNi9iQnBTR1Fy
|
||||||
|
SENYYmloSjI1c25pck5CSTJZTDhCeTQKLS0tIEs0SnFSNUdsdzZWS0loTEdBN1RD
|
||||||
|
ZnhBREtlR3o4VTVMZ1RtY1lVbG40YkUK2isPCoJSTQ6CUbHftSDoUZC8MMTqr512
|
||||||
|
lCoeGQqnArTO8CWDJxIxRczooTo4mW7vDqD7idWdPgOdWZI8hWPE5Q==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwdnJIeWh1RkhvLzF2QUlm
|
||||||
|
ZmNHaFpzL0M3eUdCdk1GL1g2MTdtdTdjVmdjCmVRTWJsajhKT0E5STN2SEUzWHFa
|
||||||
|
ak1NelloQnNiY3FaUm9oVGg5eit2eTAKLS0tIEoxcURjUkJsRENtblZpKy9QT0gx
|
||||||
|
ME5kM1EwYUFNMVFkT3VWZmpGSzRoWFUKzGNK5FzRWiY+E1Je6l0veoN5Z3K2TFMY
|
||||||
|
pm9+FGuYs+wxSrhLwajITj+NuH0+zK81mrYsugH+6OTNb7cDbLgh/g==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlWCs0a3EvZC9sWmI5S2tU
|
||||||
|
aFpRMVlrZG1zL1ZJSklJMkNQY1RLMW53YUhZCjNpQ1pHUE0yVUxleVovcVRLMFNh
|
||||||
|
amVFTnJteW8xRjlFY29HWkJrcVJiQzgKLS0tIFJrWWloc2ZWdGdPNlNQM2szTkZI
|
||||||
|
Zk42dFgrcUJOa3UwSDB3MnpMcVRLdmsKOKbF18HnowVhiEHO2B+BZqpM8Oc8vbDh
|
||||||
|
hczIpcezwMvv96L2/seX86Hv5mEAQvwN2CVA+sknnDL1XNA/2Ng9cw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
- recipient: age1x3elhtccp4u8ha5ry32juj9fkpg0qg7qqx4gduuehgwwnnhcxp8s892hek
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6L1pVc2lRYVVnMU1uR1hn
|
||||||
|
aldPODkvTVFzNDRuWWFTTU5jU0dyOHFMNkZBCnQ0ZGxUcGR5d0FqK2pOenJzSEN4
|
||||||
|
ak12VXhQSnZlbSs1V3BxZnBIQ0xKV1EKLS0tIEkrc00wTzJzVjVDd0o4WHNQVDV6
|
||||||
|
WGlpR1kvdXFnMkxOQVVuL3pIckdLRGcK+xoZE63l+9mlR5ufN9kEtgKEHdIUcGbI
|
||||||
|
CpNhd8RE23tPKaVa0XbQA3bMqc1J9jST3vSWWewexwdLvfjrooSFZw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
lastmodified: "2025-11-28T18:05:54Z"
|
||||||
|
mac: ENC[AES256_GCM,data:rmGDt0ZvZ8S//X1sqzkM9GdsoLBTB9dUprWdVN5M9F4/Zpq6Mpyk04VdGxYz421Gi+AsvhAkBaKi+XJjiEjRf9dYON/N18bWeRe3mJMLVOOoxGz+PQOeAuCyphZEKsCkae79WtbRZqONkU+kSqT5ED6iLjhOpLn1h6Cuw4wV1Xc=,iv:XWjRyxlGP4a14eUaJvZpizy2UiCSIi/PIUyaZg6GCJY=,tag:ZNp/U1O3wkcb8o5s1USrsw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.11.0
|
||||||
1
hosts/nas/utils
Symbolic link
1
hosts/nas/utils
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../utils
|
||||||
@@ -44,7 +44,7 @@ vim.api.nvim_create_autocmd("BufReadPre", {
|
|||||||
pattern = secrets_patterns,
|
pattern = secrets_patterns,
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
-- Set filetype to yaml before the file is read so syntax highlighting works
|
-- Set filetype to yaml before the file is read so syntax highlighting works
|
||||||
vim.bo.filetype = "yaml"
|
vim.bo[args.buf].filetype = "yaml"
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ vim.api.nvim_create_autocmd("BufReadPost", {
|
|||||||
group = sops_group,
|
group = sops_group,
|
||||||
pattern = secrets_patterns,
|
pattern = secrets_patterns,
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
local filepath = vim.fn.expand("%:p")
|
local filepath = vim.api.nvim_buf_get_name(args.buf)
|
||||||
|
|
||||||
-- Only decrypt if file exists and has content
|
-- Only decrypt if file exists and has content
|
||||||
if vim.fn.filereadable(filepath) == 1 and vim.fn.getfsize(filepath) > 0 then
|
if vim.fn.filereadable(filepath) == 1 and vim.fn.getfsize(filepath) > 0 then
|
||||||
@@ -98,26 +98,27 @@ vim.api.nvim_create_autocmd("BufReadPost", {
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Detach LSP clients BEFORE replacing buffer to prevent sync errors
|
||||||
|
detach_lsp_clients(args.buf)
|
||||||
|
|
||||||
-- Replace buffer content with decrypted content
|
-- Replace buffer content with decrypted content
|
||||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(result, "\n"))
|
-- Strip trailing newline to avoid adding extra empty line
|
||||||
|
vim.api.nvim_buf_set_lines(args.buf, 0, -1, false, vim.split(result:gsub("\n$", ""), "\n"))
|
||||||
|
|
||||||
-- Mark buffer as not modified (since we just loaded it)
|
-- Mark buffer as not modified (since we just loaded it)
|
||||||
vim.bo.modified = false
|
vim.bo[args.buf].modified = false
|
||||||
|
|
||||||
-- Restore cursor position
|
-- Restore cursor position
|
||||||
pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos)
|
pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos)
|
||||||
|
|
||||||
-- Disable swap, backup, and undo files for security
|
-- Disable swap, backup, and undo files for security
|
||||||
vim.bo.swapfile = false
|
vim.bo[args.buf].swapfile = false
|
||||||
vim.bo.backup = false
|
vim.bo[args.buf].backup = false
|
||||||
vim.bo.writebackup = false
|
vim.bo[args.buf].writebackup = false
|
||||||
vim.bo.undofile = false
|
vim.bo[args.buf].undofile = false
|
||||||
|
|
||||||
-- Ensure filetype is set to yaml for syntax highlighting
|
-- Ensure filetype is set to yaml for syntax highlighting
|
||||||
vim.bo.filetype = "yaml"
|
vim.bo[args.buf].filetype = "yaml"
|
||||||
|
|
||||||
-- Detach LSP clients to prevent sync errors when buffer content is replaced
|
|
||||||
detach_lsp_clients(0)
|
|
||||||
|
|
||||||
vim.notify("SOPS: File decrypted successfully", vim.log.levels.INFO)
|
vim.notify("SOPS: File decrypted successfully", vim.log.levels.INFO)
|
||||||
else
|
else
|
||||||
@@ -132,17 +133,22 @@ vim.api.nvim_create_autocmd("BufWriteCmd", {
|
|||||||
group = sops_group,
|
group = sops_group,
|
||||||
pattern = secrets_patterns,
|
pattern = secrets_patterns,
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
local filepath = vim.fn.expand("%:p")
|
local filepath = vim.api.nvim_buf_get_name(args.buf)
|
||||||
|
|
||||||
if is_secrets_file(filepath) then
|
if not is_secrets_file(filepath) then
|
||||||
-- Guard against double-execution
|
return
|
||||||
if currently_saving[filepath] then
|
end
|
||||||
return
|
|
||||||
end
|
|
||||||
currently_saving[filepath] = true
|
|
||||||
|
|
||||||
|
-- Guard against double-execution
|
||||||
|
if currently_saving[filepath] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
currently_saving[filepath] = true
|
||||||
|
|
||||||
|
-- Use pcall to ensure guard is always cleared, even on unexpected errors
|
||||||
|
local ok, err = pcall(function()
|
||||||
-- Get current buffer content
|
-- Get current buffer content
|
||||||
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
local lines = vim.api.nvim_buf_get_lines(args.buf, 0, -1, false)
|
||||||
local content = table.concat(lines, "\n")
|
local content = table.concat(lines, "\n")
|
||||||
|
|
||||||
-- Check buffer content size before encrypting
|
-- Check buffer content size before encrypting
|
||||||
@@ -153,8 +159,6 @@ vim.api.nvim_create_autocmd("BufWriteCmd", {
|
|||||||
string.format("SOPS: Buffer content too large (%sMB > %sMB limit). Cannot encrypt.", size_mb, limit_mb),
|
string.format("SOPS: Buffer content too large (%sMB > %sMB limit). Cannot encrypt.", size_mb, limit_mb),
|
||||||
vim.log.levels.ERROR
|
vim.log.levels.ERROR
|
||||||
)
|
)
|
||||||
-- Don't write anything, leave buffer marked as modified
|
|
||||||
currently_saving[filepath] = nil
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -162,22 +166,21 @@ vim.api.nvim_create_autocmd("BufWriteCmd", {
|
|||||||
-- This avoids /dev/stdin issues while keeping secrets secure (not in /tmp)
|
-- This avoids /dev/stdin issues while keeping secrets secure (not in /tmp)
|
||||||
local dir = vim.fn.fnamemodify(filepath, ":h")
|
local dir = vim.fn.fnamemodify(filepath, ":h")
|
||||||
local filename = vim.fn.fnamemodify(filepath, ":t")
|
local filename = vim.fn.fnamemodify(filepath, ":t")
|
||||||
local temp_file = string.format("%s/.%s.sops_tmp_%d", dir, filename, os.time())
|
local temp_file = string.format("%s/.%s.sops_tmp_%d_%d", dir, filename, os.time(), vim.loop.hrtime() % 1000000)
|
||||||
|
|
||||||
-- Write plaintext content to temp file
|
-- Write plaintext content to temp file
|
||||||
local temp_f, temp_err = io.open(temp_file, "w")
|
local temp_f, temp_err = io.open(temp_file, "w")
|
||||||
if not temp_f then
|
if not temp_f then
|
||||||
vim.notify("SOPS: Failed to create temp file: " .. (temp_err or "unknown error"), vim.log.levels.ERROR)
|
vim.notify("SOPS: Failed to create temp file: " .. (temp_err or "unknown error"), vim.log.levels.ERROR)
|
||||||
-- Don't write anything, leave buffer marked as modified
|
|
||||||
currently_saving[filepath] = nil
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
temp_f:write(content)
|
temp_f:write(content .. "\n")
|
||||||
temp_f:close()
|
temp_f:close()
|
||||||
|
|
||||||
-- Encrypt temp file with filename override so SOPS matches .sops.yaml rules
|
-- Encrypt temp file with filename override so SOPS matches .sops.yaml rules
|
||||||
-- Uses real filepath for rule matching, temp file for content
|
-- Uses real filepath for rule matching, temp file for content
|
||||||
local cmd = string.format("sops --encrypt --filename-override %s %s",
|
local cmd = string.format("timeout %d sops --encrypt --filename-override %s %s",
|
||||||
|
SOPS_TIMEOUT,
|
||||||
vim.fn.shellescape(filepath),
|
vim.fn.shellescape(filepath),
|
||||||
vim.fn.shellescape(temp_file))
|
vim.fn.shellescape(temp_file))
|
||||||
local encrypted = vim.fn.system(cmd)
|
local encrypted = vim.fn.system(cmd)
|
||||||
@@ -186,16 +189,25 @@ vim.api.nvim_create_autocmd("BufWriteCmd", {
|
|||||||
-- Always clean up temp file, even on error
|
-- Always clean up temp file, even on error
|
||||||
os.remove(temp_file)
|
os.remove(temp_file)
|
||||||
|
|
||||||
|
-- Check for timeout (exit code 124 from timeout command)
|
||||||
|
if sops_exit_code == 124 then
|
||||||
|
vim.notify(
|
||||||
|
string.format("SOPS: Encryption timed out after %d seconds.", SOPS_TIMEOUT),
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if sops_exit_code == 0 then
|
if sops_exit_code == 0 then
|
||||||
-- Write encrypted content directly to file
|
-- Write encrypted content directly to file
|
||||||
local file, err = io.open(filepath, "w")
|
local file, file_err = io.open(filepath, "w")
|
||||||
if file then
|
if file then
|
||||||
local success, write_err = file:write(encrypted)
|
local success, write_err = file:write(encrypted)
|
||||||
file:close()
|
file:close()
|
||||||
|
|
||||||
if success then
|
if success then
|
||||||
-- Mark buffer as saved
|
-- Mark buffer as saved
|
||||||
vim.bo.modified = false
|
vim.bo[args.buf].modified = false
|
||||||
vim.notify("SOPS: File encrypted and saved successfully", vim.log.levels.INFO)
|
vim.notify("SOPS: File encrypted and saved successfully", vim.log.levels.INFO)
|
||||||
|
|
||||||
-- Re-decrypt to show plaintext in buffer
|
-- Re-decrypt to show plaintext in buffer
|
||||||
@@ -207,37 +219,38 @@ vim.api.nvim_create_autocmd("BufWriteCmd", {
|
|||||||
-- Save cursor position
|
-- Save cursor position
|
||||||
local cursor_pos = vim.api.nvim_win_get_cursor(0)
|
local cursor_pos = vim.api.nvim_win_get_cursor(0)
|
||||||
|
|
||||||
|
-- Detach LSP clients BEFORE replacing buffer to prevent sync errors
|
||||||
|
detach_lsp_clients(args.buf)
|
||||||
|
|
||||||
-- Replace buffer with decrypted content
|
-- Replace buffer with decrypted content
|
||||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(decrypted, "\n"))
|
-- Strip trailing newline to avoid adding extra empty line
|
||||||
|
vim.api.nvim_buf_set_lines(args.buf, 0, -1, false, vim.split(decrypted:gsub("\n$", ""), "\n"))
|
||||||
|
|
||||||
-- Mark as not modified since we just saved
|
-- Mark as not modified since we just saved
|
||||||
vim.bo.modified = false
|
vim.bo[args.buf].modified = false
|
||||||
|
|
||||||
-- Restore cursor position
|
-- Restore cursor position
|
||||||
pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos)
|
pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos)
|
||||||
|
|
||||||
-- Detach LSP clients to prevent sync errors when buffer content is replaced
|
|
||||||
detach_lsp_clients(0)
|
|
||||||
else
|
else
|
||||||
vim.notify("SOPS: Could not re-decrypt after save. Buffer may show encrypted content.", vim.log.levels.WARN)
|
vim.notify("SOPS: Could not re-decrypt after save. Buffer may show encrypted content.", vim.log.levels.WARN)
|
||||||
end
|
end
|
||||||
-- Clear guard after successful save
|
|
||||||
currently_saving[filepath] = nil
|
|
||||||
else
|
else
|
||||||
vim.notify("SOPS: Failed to write encrypted content: " .. (write_err or "unknown error"), vim.log.levels.ERROR)
|
vim.notify("SOPS: Failed to write encrypted content: " .. (write_err or "unknown error"), vim.log.levels.ERROR)
|
||||||
-- Don't mark as saved, keep buffer marked as modified
|
|
||||||
currently_saving[filepath] = nil
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
vim.notify("SOPS: Failed to open file for writing: " .. (err or "unknown error"), vim.log.levels.ERROR)
|
vim.notify("SOPS: Failed to open file for writing: " .. (file_err or "unknown error"), vim.log.levels.ERROR)
|
||||||
-- Don't mark as saved, keep buffer marked as modified
|
|
||||||
currently_saving[filepath] = nil
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
vim.notify("SOPS: Failed to encrypt file - NOT SAVED! Error: " .. encrypted, vim.log.levels.ERROR)
|
vim.notify("SOPS: Failed to encrypt file - NOT SAVED! Error: " .. encrypted, vim.log.levels.ERROR)
|
||||||
-- Don't write anything, leave buffer marked as modified
|
|
||||||
currently_saving[filepath] = nil
|
|
||||||
end
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Always clear guard, even if pcall caught an error
|
||||||
|
currently_saving[filepath] = nil
|
||||||
|
|
||||||
|
-- Re-throw unexpected errors so they're visible
|
||||||
|
if not ok then
|
||||||
|
vim.notify("SOPS: Unexpected error during save: " .. tostring(err), vim.log.levels.ERROR)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
@@ -247,7 +260,7 @@ vim.api.nvim_create_autocmd("BufLeave", {
|
|||||||
group = sops_group,
|
group = sops_group,
|
||||||
pattern = secrets_patterns,
|
pattern = secrets_patterns,
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
if vim.bo.modified then
|
if vim.bo[args.buf].modified then
|
||||||
vim.notify("Warning: Unsaved changes in secrets file!", vim.log.levels.WARN)
|
vim.notify("Warning: Unsaved changes in secrets file!", vim.log.levels.WARN)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|||||||
Reference in New Issue
Block a user