Initial commit: Ultimate Notetaking, Sync & Backup System
A NixOS-based system for managing personal data across three tiers: - Tier 1: Configuration (shareable via git) - Tier 2: Syncable data (nb + Syncthing) - Tier 3: Large data (self-hosted services + backup) Includes: - NixOS modules for nb, Syncthing, backup (restic) - Server modules for Forgejo, Immich, Jellyfin - Helper scripts (usync, ustatus) - Comprehensive documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Nix
|
||||
result
|
||||
result-*
|
||||
.direnv/
|
||||
|
||||
# Secrets (NEVER commit unencrypted secrets)
|
||||
secrets/*.yaml
|
||||
!secrets/secrets.yaml.example
|
||||
*.key
|
||||
*.pem
|
||||
.age
|
||||
.sops
|
||||
|
||||
# Editor
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.vim/
|
||||
.nvim/
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Hardware configs (machine-specific)
|
||||
hosts/*/hardware-configuration.nix
|
||||
292
README.md
Normal file
292
README.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Ultimate Notetaking, Sync & Backup System
|
||||
|
||||
A NixOS-based system for managing the three types of data in a computer:
|
||||
|
||||
| Tier | Type | Examples | Sync Model |
|
||||
|------|------|----------|------------|
|
||||
| **1** | Configuration | flake.nix, dotfiles | Git (public, shareable) |
|
||||
| **2** | Syncable Data | Notes, documents | Git (nb) + Syncthing |
|
||||
| **3** | Large Data | Photos, videos, repos | Central backup, on-demand |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone this repo
|
||||
git clone https://github.com/YOUR_USERNAME/ultimate-notetaking-sync-backup-system.git
|
||||
cd ultimate-notetaking-sync-backup-system
|
||||
|
||||
# Option 1: Just try the tools (no system changes)
|
||||
nix develop
|
||||
|
||||
# Option 2: Apply as a NixOS module
|
||||
# Add to your flake.nix inputs, then import the module
|
||||
```
|
||||
|
||||
## Philosophy
|
||||
|
||||
1. **Config is code**: Your system configuration should be version-controlled and shareable
|
||||
2. **Notes deserve versioning**: Use git-backed tools (nb) for notes, not just file sync
|
||||
3. **Sync ≠ Backup**: Syncthing syncs; restic backs up. Use both.
|
||||
4. **Self-host when practical**: Forgejo, Immich, Jellyfin over proprietary clouds
|
||||
5. **Open source only**: Every tool in this stack is FOSS
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ YOUR MACHINES │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ Desktop │ │ Laptop │ │ Phone │ │
|
||||
│ └────┬────┘ └────┬────┘ └────┬────┘ │
|
||||
│ │ │ │ │
|
||||
│ └──────────────┼──────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────▼───────┐ │
|
||||
│ │ TIER 2 │ │
|
||||
│ │ nb (notes) │◄── git push/pull │
|
||||
│ │ Syncthing │◄── P2P sync (optional) │
|
||||
│ └───────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ YOUR SERVER │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Forgejo │ │ Immich │ │ Jellyfin │ │
|
||||
│ │ (git/nb) │ │ (photos) │ │ (media) │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ └────────────────┼────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────▼──────┐ │
|
||||
│ │ restic │ │
|
||||
│ │ (backup) │ │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ ┌──────▼──────┐ │
|
||||
│ │ B2 / NAS │ │
|
||||
│ │ (offsite) │ │
|
||||
│ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## What's Included
|
||||
|
||||
### NixOS Modules
|
||||
|
||||
- `modules/nb.nix` - nb notebook CLI installation and configuration
|
||||
- `modules/syncthing.nix` - Declarative Syncthing setup
|
||||
- `modules/neovim.nix` - Neovim with nb integration
|
||||
- `modules/backup.nix` - restic backup timers
|
||||
- `modules/server/` - Forgejo, Immich, Jellyfin configurations
|
||||
|
||||
### Development Shell
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
Provides: `nb`, `syncthing`, `restic`, `rclone`, `jq`, and helper scripts.
|
||||
|
||||
### Helper Scripts
|
||||
|
||||
- `usync` - Unified sync command (syncs nb + triggers Syncthing scan)
|
||||
- `ubackup` - Run restic backup manually
|
||||
- `ustatus` - Show sync/backup status across all tiers
|
||||
|
||||
## Usage
|
||||
|
||||
### Tier 1: Configuration
|
||||
|
||||
Your system config lives in this repo. Fork it, customize it, push to your own GitHub.
|
||||
|
||||
```bash
|
||||
# Rebuild your system
|
||||
sudo nixos-rebuild switch --flake .#your-hostname
|
||||
|
||||
# Update flake inputs
|
||||
nix flake update
|
||||
```
|
||||
|
||||
### Tier 2: Notes with nb
|
||||
|
||||
```bash
|
||||
# Create a note
|
||||
nb add "My note title"
|
||||
|
||||
# Edit with neovim
|
||||
nb edit 1
|
||||
|
||||
# Sync to remote
|
||||
nb sync
|
||||
|
||||
# Search notes
|
||||
nb search "keyword"
|
||||
|
||||
# List notebooks
|
||||
nb notebooks
|
||||
```
|
||||
|
||||
Set up git remote for nb:
|
||||
```bash
|
||||
nb remote set git@your-forgejo:you/notes.git
|
||||
```
|
||||
|
||||
### Tier 2: Documents with Syncthing
|
||||
|
||||
Documents in `~/Documents/` sync automatically via Syncthing.
|
||||
|
||||
```bash
|
||||
# Check Syncthing status
|
||||
syncthing cli show system
|
||||
|
||||
# Force rescan
|
||||
syncthing cli scan --folder documents
|
||||
```
|
||||
|
||||
### Tier 3: Large Data
|
||||
|
||||
Photos upload to Immich (mobile app or web).
|
||||
Media is served via Jellyfin.
|
||||
Manual files can be uploaded with rclone:
|
||||
|
||||
```bash
|
||||
# Upload to your server
|
||||
rclone copy ~/Videos/project server:archive/videos/
|
||||
|
||||
# List remote files
|
||||
rclone ls server:archive/
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Adding Your Own Notebooks
|
||||
|
||||
Edit `modules/nb.nix`:
|
||||
|
||||
```nix
|
||||
{
|
||||
programs.nb = {
|
||||
notebooks = {
|
||||
personal = {
|
||||
remote = "git@forgejo:you/personal-notes.git";
|
||||
};
|
||||
work = {
|
||||
remote = "git@forgejo:you/work-notes.git";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Syncthing Folders
|
||||
|
||||
Edit `modules/syncthing.nix`:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.syncthing.settings.folders = {
|
||||
"documents" = {
|
||||
path = "~/Documents";
|
||||
devices = [ "laptop" "desktop" ];
|
||||
};
|
||||
"music" = {
|
||||
path = "~/Music";
|
||||
devices = [ "laptop" "desktop" "server" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Backup Paths
|
||||
|
||||
Edit `modules/backup.nix`:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.restic.backups.default = {
|
||||
paths = [
|
||||
"/home/you/Documents"
|
||||
"/home/you/notes"
|
||||
"/var/lib/important"
|
||||
];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why nb instead of Obsidian/Notion/etc?
|
||||
|
||||
- **Git-native**: Full version history, proper merges
|
||||
- **Plain text**: Markdown files, no vendor lock-in
|
||||
- **CLI-first**: Works over SSH, in tmux, everywhere
|
||||
- **Scriptable**: Integrates with shell workflows
|
||||
- **Encrypted option**: Built-in GPG encryption
|
||||
|
||||
### Why not just Syncthing for everything?
|
||||
|
||||
Syncthing is great for binary files but poor for text conflicts. When you edit notes on multiple devices, you want git-style merges, not `.sync-conflict` files scattered everywhere.
|
||||
|
||||
### Is Syncthing buggy?
|
||||
|
||||
No. See [our research](./docs/research/sync-tools-comparison.md). Common issues are:
|
||||
- Treating it as a backup (it's not)
|
||||
- Android battery killing the app (use Syncthing-Fork)
|
||||
- Expecting cloud-style behavior from P2P sync
|
||||
|
||||
### Why self-host?
|
||||
|
||||
- **Control**: Your data on your hardware
|
||||
- **Privacy**: No third-party access
|
||||
- **Cost**: One-time hardware vs. monthly subscriptions
|
||||
- **Learning**: Valuable sysadmin experience
|
||||
|
||||
But cloud is fine too. This system works with GitHub, Backblaze B2, etc.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── flake.nix # Entry point
|
||||
├── flake.lock
|
||||
├── README.md
|
||||
├── docs/
|
||||
│ ├── research/
|
||||
│ │ └── sync-tools-comparison.md
|
||||
│ ├── ARCHITECTURE.md
|
||||
│ └── LLM-CONTEXT.md # For AI assistants
|
||||
├── modules/
|
||||
│ ├── nb.nix
|
||||
│ ├── syncthing.nix
|
||||
│ ├── neovim.nix
|
||||
│ ├── backup.nix
|
||||
│ └── server/
|
||||
│ ├── forgejo.nix
|
||||
│ ├── immich.nix
|
||||
│ └── jellyfin.nix
|
||||
├── hosts/
|
||||
│ ├── desktop/
|
||||
│ │ └── configuration.nix
|
||||
│ ├── laptop/
|
||||
│ │ └── configuration.nix
|
||||
│ └── server/
|
||||
│ └── configuration.nix
|
||||
├── home/
|
||||
│ └── default.nix # home-manager config
|
||||
└── scripts/
|
||||
├── usync
|
||||
├── ubackup
|
||||
└── ustatus
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs welcome! Please read [ARCHITECTURE.md](./docs/ARCHITECTURE.md) first.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
---
|
||||
|
||||
*Built with Nix, because reproducibility matters.*
|
||||
267
docs/ARCHITECTURE.md
Normal file
267
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# System Architecture
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Declarative Everything
|
||||
|
||||
Every aspect of this system is declared in Nix. No imperative setup steps, no hidden state.
|
||||
|
||||
```
|
||||
Desired State (Nix) → nixos-rebuild → Actual State
|
||||
```
|
||||
|
||||
### 2. Separation of Concerns
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ CONFIGURATION LAYER │
|
||||
│ What: How machines should be configured │
|
||||
│ Where: This repo (flake.nix, modules/) │
|
||||
│ Managed by: Git + GitHub │
|
||||
│ Shareable: Yes (public) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ DATA LAYER │
|
||||
│ │
|
||||
│ ┌────────────────────┐ ┌────────────────────┐ │
|
||||
│ │ TIER 2: Sync │ │ TIER 3: Backup │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ~/notes/ (nb) │ │ Photos (Immich) │ │
|
||||
│ │ ~/Documents/ │ │ Media (Jellyfin) │ │
|
||||
│ │ │ │ Repos (Forgejo) │ │
|
||||
│ │ Managed by: │ │ │ │
|
||||
│ │ - nb (git sync) │ │ Managed by: │ │
|
||||
│ │ - Syncthing │ │ - Server apps │ │
|
||||
│ │ │ │ - restic backup │ │
|
||||
│ │ Multi-device: Yes │ │ Multi-device: No │ │
|
||||
│ │ Real-time: Yes │ │ (on-demand access)│ │
|
||||
│ └────────────────────┘ └────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3. No Vendor Lock-in
|
||||
|
||||
Every component can be replaced:
|
||||
|
||||
| Component | Could be replaced with |
|
||||
|-----------|----------------------|
|
||||
| nb | Obsidian + git, org-mode, plain vim + git |
|
||||
| Syncthing | Unison, rclone bisync |
|
||||
| Forgejo | Gitea, GitLab, bare git |
|
||||
| Immich | PhotoPrism, any DAM |
|
||||
| Jellyfin | Plex (non-FOSS), Kodi |
|
||||
| restic | borg, duplicity |
|
||||
|
||||
### 4. Offline-First
|
||||
|
||||
All Tier 2 data is fully available offline:
|
||||
- nb notebooks are local git repos
|
||||
- Syncthing folders are local directories
|
||||
- Sync happens when connectivity allows
|
||||
|
||||
Tier 3 data is available via caching or on-demand download.
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Tier 1: Configuration
|
||||
|
||||
```
|
||||
┌─────────┐ git push ┌─────────┐
|
||||
│ Local │ ───────────────► │ GitHub │
|
||||
│ Repo │ ◄─────────────── │ (pub) │
|
||||
└────┬────┘ git pull └─────────┘
|
||||
│
|
||||
│ nixos-rebuild switch
|
||||
▼
|
||||
┌─────────┐
|
||||
│ Machine │
|
||||
│ State │
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
### Tier 2: Notes (nb)
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐
|
||||
│ Device │ nb sync │ Forgejo │
|
||||
│ A │ ◄────────────────► │ (priv) │
|
||||
└─────────┘ └────┬────┘
|
||||
│
|
||||
┌─────────┐ nb sync ┌────▼────┐
|
||||
│ Device │ ◄────────────────► │ Forgejo │
|
||||
│ B │ │ (priv) │
|
||||
└─────────┘ └─────────┘
|
||||
|
||||
Conflict Resolution: Git 3-way merge
|
||||
History: Full git history preserved
|
||||
```
|
||||
|
||||
### Tier 2: Documents (Syncthing)
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐
|
||||
│ Device │ ◄──── P2P ───────► │ Device │
|
||||
│ A │ │ B │
|
||||
└────┬────┘ └────┬────┘
|
||||
│ │
|
||||
│ ┌─────────┐ │
|
||||
└────────►│ Device │◄─────────┘
|
||||
│ C │
|
||||
└─────────┘
|
||||
|
||||
Conflict Resolution: .sync-conflict files (manual)
|
||||
History: Syncthing versioning (configurable)
|
||||
```
|
||||
|
||||
### Tier 3: Large Data
|
||||
|
||||
```
|
||||
┌─────────┐ upload ┌─────────┐
|
||||
│ Device │ ──────────────► │ Server │
|
||||
│ │ ◄────────────── │ Immich/ │
|
||||
└─────────┘ on-demand │ Jellyfin│
|
||||
└────┬────┘
|
||||
│ restic
|
||||
┌────▼────┐
|
||||
│ Backup │
|
||||
│ B2/NAS │
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
## Module Dependency Graph
|
||||
|
||||
```
|
||||
flake.nix
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
hosts/ modules/ home/
|
||||
desktop (shared) default.nix
|
||||
laptop │
|
||||
server │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────────────────┐
|
||||
│ modules/ │
|
||||
│ ┌──────┐ ┌───────────┐ ┌─────┐ │
|
||||
│ │ nb │ │ syncthing │ │nvim │ │
|
||||
│ └──────┘ └───────────┘ └─────┘ │
|
||||
│ ┌────────┐ │
|
||||
│ │ backup │ │
|
||||
│ └────────┘ │
|
||||
│ │
|
||||
│ ┌─────────── server/ ────────┐ │
|
||||
│ │ forgejo immich jellyfin │ │
|
||||
│ └───────────────────────────┘ │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Security Model
|
||||
|
||||
### Secrets
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ sops-nix │
|
||||
│ │
|
||||
│ secrets.yaml (encrypted in repo) │
|
||||
│ │ │
|
||||
│ │ age/gpg key (not in repo) │
|
||||
│ ▼ │
|
||||
│ Decrypted at activation time │
|
||||
│ Placed in /run/secrets/ │
|
||||
│ Permissions set per-secret │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Network Security
|
||||
|
||||
- All sync over TLS (Syncthing) or SSH (nb, Forgejo)
|
||||
- Server services behind reverse proxy (Caddy/nginx)
|
||||
- No ports exposed except 443 (HTTPS)
|
||||
- Tailscale/WireGuard for private network (optional)
|
||||
|
||||
## Backup Strategy
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ 3-2-1 Backup Rule │
|
||||
│ │
|
||||
│ 3 copies: │
|
||||
│ ├── Primary: Local device │
|
||||
│ ├── Secondary: Home server │
|
||||
│ └── Tertiary: Offsite (B2/cloud) │
|
||||
│ │
|
||||
│ 2 media types: │
|
||||
│ ├── SSD (local) │
|
||||
│ └── Cloud object storage (offsite) │
|
||||
│ │
|
||||
│ 1 offsite: │
|
||||
│ └── Backblaze B2 / AWS S3 / etc. │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
|
||||
Backup Schedule (systemd timers):
|
||||
├── Tier 2 (notes/docs): Daily, keep 30 days
|
||||
├── Tier 3 (media): Weekly, keep 90 days
|
||||
└── Server state: Daily, keep 14 days
|
||||
```
|
||||
|
||||
## Scaling Considerations
|
||||
|
||||
### Adding a New Machine
|
||||
|
||||
1. Create `hosts/<hostname>/configuration.nix`
|
||||
2. Add hardware-configuration.nix
|
||||
3. Add to flake.nix outputs
|
||||
4. Run `nixos-rebuild switch --flake .#<hostname>`
|
||||
5. nb automatically syncs when `nb sync` is run
|
||||
6. Syncthing auto-discovers via device IDs in config
|
||||
|
||||
### Adding a New User
|
||||
|
||||
1. Add user to `hosts/<hostname>/configuration.nix`
|
||||
2. Create home-manager config
|
||||
3. Set up nb notebooks for user
|
||||
4. Add Syncthing device ID
|
||||
|
||||
### Multiple Servers
|
||||
|
||||
The architecture supports multiple servers:
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Server │ │ Server │ │ Server │
|
||||
│ Home │ │ VPS │ │ NAS │
|
||||
│ (media) │ │ (Forgejo│ │(backup) │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
Each server has its own host config in `hosts/`.
|
||||
|
||||
## Failure Modes
|
||||
|
||||
| Failure | Impact | Recovery |
|
||||
|---------|--------|----------|
|
||||
| Device lost | Tier 2 data unavailable locally | Sync from other devices or Forgejo |
|
||||
| Server down | Tier 3 unavailable, nb sync fails | Use local cache, fix server |
|
||||
| Backup failure | No new backups | Alert (systemd OnFailure), investigate |
|
||||
| Sync conflict | Manual resolution needed | Resolve .sync-conflict or git merge |
|
||||
| Internet down | Can't sync | Work offline, sync when back |
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Potential Additions
|
||||
|
||||
1. **Encrypted Tier 2**: Full-disk encryption + nb's GPG encryption
|
||||
2. **Mobile support**: Termux + Unison, or Syncthing-Fork
|
||||
3. **Real-time collaboration**: Automerge/CRDT-based notes
|
||||
4. **Monitoring**: Prometheus + Grafana for sync/backup status
|
||||
|
||||
### Not In Scope
|
||||
|
||||
1. Windows support (NixOS only)
|
||||
2. Proprietary services integration
|
||||
3. Enterprise/team features
|
||||
4. Real-time streaming replication
|
||||
240
docs/LLM-CONTEXT.md
Normal file
240
docs/LLM-CONTEXT.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# LLM Context Document
|
||||
|
||||
This document provides context for AI assistants (Claude, GPT, Copilot, etc.) working with this codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Name**: Ultimate Notetaking, Sync & Backup System
|
||||
**Purpose**: NixOS-based declarative system for managing personal data across multiple machines
|
||||
**Primary User**: Linux users, especially NixOS, who want full control over their data
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### The Three-Tier Data Model
|
||||
|
||||
```
|
||||
TIER 1: Configuration (Shareable)
|
||||
├── What: System config, dotfiles, flake.nix
|
||||
├── Where: This git repo (public on GitHub)
|
||||
├── How: NixOS + home-manager + sops-nix
|
||||
└── Philosophy: Anyone can clone and use this config
|
||||
|
||||
TIER 2: Syncable Data (Private, Multi-device)
|
||||
├── What: Notes, documents, writings
|
||||
├── Where: ~/notes/ (nb), ~/Documents/ (Syncthing)
|
||||
├── How: nb (git-backed) for text, Syncthing for binary
|
||||
└── Philosophy: Edit anywhere, sync automatically
|
||||
|
||||
TIER 3: Large Data (Backed up, On-demand)
|
||||
├── What: Photos, videos, large repos
|
||||
├── Where: Self-hosted servers (Immich, Jellyfin, Forgejo)
|
||||
├── How: Upload to server, access on-demand, restic backup
|
||||
└── Philosophy: Don't sync everything; backup everything
|
||||
```
|
||||
|
||||
### Key Tools
|
||||
|
||||
| Tool | Purpose | Tier |
|
||||
|------|---------|------|
|
||||
| NixOS | Declarative system configuration | 1 |
|
||||
| home-manager | Declarative user configuration | 1 |
|
||||
| sops-nix | Encrypted secrets in git | 1 |
|
||||
| nb | CLI notebook with git sync | 2 |
|
||||
| Syncthing | P2P file synchronization | 2 |
|
||||
| Neovim | Primary editor | 1, 2 |
|
||||
| Forgejo | Self-hosted git (GitHub alternative) | 3 |
|
||||
| Immich | Self-hosted photos (Google Photos alternative) | 3 |
|
||||
| Jellyfin | Self-hosted media (Plex alternative) | 3 |
|
||||
| restic | Encrypted, deduplicated backups | 3 |
|
||||
| rclone | Cloud storage Swiss Army knife | 3 |
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
flake.nix # Nix flake entry point
|
||||
├── inputs: nixpkgs, home-manager, sops-nix
|
||||
├── outputs: nixosConfigurations, homeConfigurations, devShells
|
||||
└── imports modules for each host
|
||||
|
||||
modules/
|
||||
├── nb.nix # nb installation and config
|
||||
├── syncthing.nix # Declarative Syncthing
|
||||
├── neovim.nix # Neovim with nb integration
|
||||
├── backup.nix # restic systemd timers
|
||||
└── server/ # Server-only modules
|
||||
├── forgejo.nix
|
||||
├── immich.nix
|
||||
└── jellyfin.nix
|
||||
|
||||
hosts/
|
||||
├── desktop/ # Desktop-specific config
|
||||
├── laptop/ # Laptop-specific config
|
||||
└── server/ # Server-specific config
|
||||
|
||||
home/
|
||||
└── default.nix # home-manager user config
|
||||
|
||||
scripts/
|
||||
├── usync # Unified sync (nb + Syncthing)
|
||||
├── ubackup # Manual backup trigger
|
||||
└── ustatus # Status dashboard
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### User wants to add a new notebook
|
||||
|
||||
1. Edit `modules/nb.nix`
|
||||
2. Add notebook to `programs.nb.notebooks`
|
||||
3. Run `nixos-rebuild switch`
|
||||
4. Run `nb notebooks` to verify
|
||||
|
||||
### User wants to add a Syncthing folder
|
||||
|
||||
1. Edit `modules/syncthing.nix`
|
||||
2. Add folder to `services.syncthing.settings.folders`
|
||||
3. Add device IDs if new devices
|
||||
4. Run `nixos-rebuild switch`
|
||||
|
||||
### User wants to change backup paths
|
||||
|
||||
1. Edit `modules/backup.nix`
|
||||
2. Modify `services.restic.backups.default.paths`
|
||||
3. Run `nixos-rebuild switch`
|
||||
|
||||
### User wants to add a new host
|
||||
|
||||
1. Create `hosts/<hostname>/configuration.nix`
|
||||
2. Add to `flake.nix` outputs
|
||||
3. Configure host-specific settings
|
||||
4. Run `nixos-rebuild switch --flake .#<hostname>`
|
||||
|
||||
## Code Patterns
|
||||
|
||||
### Module Structure
|
||||
|
||||
All modules follow this pattern:
|
||||
|
||||
```nix
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.ourService;
|
||||
in {
|
||||
options.services.ourService = {
|
||||
enable = mkEnableOption "our service";
|
||||
|
||||
someOption = mkOption {
|
||||
type = types.str;
|
||||
default = "default-value";
|
||||
description = "Description of the option";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# Implementation
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Home-manager Integration
|
||||
|
||||
User-level config uses home-manager:
|
||||
|
||||
```nix
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
programs.git = {
|
||||
enable = true;
|
||||
userName = "User Name";
|
||||
userEmail = "user@example.com";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Secrets Management
|
||||
|
||||
Secrets use sops-nix:
|
||||
|
||||
```nix
|
||||
{
|
||||
sops.secrets.syncthing-key = {
|
||||
sopsFile = ./secrets/secrets.yaml;
|
||||
owner = "user";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
1. **Open source only**: No proprietary tools
|
||||
2. **NixOS-centric**: Must work with `nixos-rebuild`
|
||||
3. **Flakes required**: All Nix code uses flakes
|
||||
4. **No cloud dependencies**: Must work fully self-hosted (cloud optional)
|
||||
5. **CLI-first**: GUI is optional, never required
|
||||
6. **Markdown for notes**: Plain text, portable, version-control friendly
|
||||
|
||||
## When Helping Users
|
||||
|
||||
### DO:
|
||||
- Suggest declarative (Nix) solutions
|
||||
- Recommend nb for text/notes
|
||||
- Recommend Syncthing for binary files
|
||||
- Use systemd for services
|
||||
- Use home-manager for user config
|
||||
- Keep modules composable
|
||||
|
||||
### DON'T:
|
||||
- Suggest proprietary alternatives
|
||||
- Hardcode paths (use options)
|
||||
- Mix system and user config inappropriately
|
||||
- Ignore the three-tier model
|
||||
- Suggest Syncthing for notes (use nb instead)
|
||||
|
||||
## Debugging
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Symptom | Likely Cause | Solution |
|
||||
|---------|--------------|----------|
|
||||
| nb sync fails | SSH key not available | Check `ssh-add -l` |
|
||||
| Syncthing not starting | Declarative config conflict | Check for GUI-made changes |
|
||||
| Backup fails | Repository not initialized | Run `restic init` |
|
||||
| Module not found | Not imported in flake.nix | Add to imports |
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# Check Nix config
|
||||
nix flake check
|
||||
|
||||
# Show Syncthing status
|
||||
syncthing cli show system
|
||||
|
||||
# nb sync status
|
||||
nb sync status
|
||||
|
||||
# restic backup status
|
||||
restic -r <repo> snapshots
|
||||
```
|
||||
|
||||
## Extension Points
|
||||
|
||||
Users commonly want to extend:
|
||||
|
||||
1. **Additional notebooks**: Add to `modules/nb.nix`
|
||||
2. **New Syncthing folders**: Add to `modules/syncthing.nix`
|
||||
3. **Backup exclusions**: Modify `modules/backup.nix`
|
||||
4. **Server services**: Add new modules under `modules/server/`
|
||||
5. **Editor config**: Modify `modules/neovim.nix`
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Architecture](./ARCHITECTURE.md) - Detailed system design
|
||||
- [Sync Tools Comparison](./research/sync-tools-comparison.md) - Why we chose these tools
|
||||
- [nb documentation](https://xwmx.github.io/nb/)
|
||||
- [NixOS manual](https://nixos.org/manual/nixos/stable/)
|
||||
- [home-manager manual](https://nix-community.github.io/home-manager/)
|
||||
271
docs/research/sync-tools-comparison.md
Normal file
271
docs/research/sync-tools-comparison.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# File Synchronization Tools: Comprehensive Comparison
|
||||
|
||||
*Research conducted: February 2026*
|
||||
|
||||
## Executive Summary
|
||||
|
||||
After extensive research, **Syncthing is not buggy—but it is complex and unforgiving**. Most reported "bugs" are actually:
|
||||
1. Misunderstanding of how P2P sync works
|
||||
2. Conflict handling that differs from user expectations
|
||||
3. Android-specific issues (now largely resolved with forks)
|
||||
4. Users treating it as a backup tool (it's not)
|
||||
|
||||
For a NixOS-based notes/data sync system, the recommended approach is:
|
||||
- **Tier 2 (Notes)**: `nb` with git sync to private Forgejo (explicit, versioned)
|
||||
- **Tier 2 (Documents)**: Syncthing OR Unison (based on preference)
|
||||
- **Tier 3 (Large files)**: rclone + restic to self-hosted or cloud storage
|
||||
|
||||
---
|
||||
|
||||
## Tool-by-Tool Analysis
|
||||
|
||||
### Syncthing
|
||||
|
||||
**What it is**: Continuous, real-time, P2P file synchronization
|
||||
|
||||
**Strengths**:
|
||||
- Truly decentralized (no server required)
|
||||
- Real-time sync via filesystem watching
|
||||
- Handles file renames/moves efficiently
|
||||
- Cross-platform with good GUI
|
||||
- Battle-tested with large user base
|
||||
- Declarative NixOS module available
|
||||
|
||||
**Weaknesses**:
|
||||
- **Conflict handling is file-level**: Creates `.sync-conflict-*` files that must be manually resolved
|
||||
- **Not a backup**: Deletions propagate immediately; if you delete on one device, it's gone everywhere
|
||||
- **Android app discontinued** (Dec 2024): Use [Syncthing-Fork](https://github.com/catfriend1/syncthing-android) instead
|
||||
- **Complexity ceiling**: Multi-device topologies can create unexpected sync loops
|
||||
- **No ownership/permission sync**: By design, to avoid requiring root
|
||||
- **OOM issues in v2.0**: On low-memory devices (1GB RAM) with large file sets
|
||||
|
||||
**Common Misconceptions**:
|
||||
| Issue Reported | Actual Cause |
|
||||
|----------------|--------------|
|
||||
| "Files disappearing" | Deletions syncing correctly; user expected backup behavior |
|
||||
| "Sync stuck at 99%" | Large files still transferring, or ignored files being counted |
|
||||
| "Random conflicts" | Multiple devices modifying same file; Syncthing is doing its job |
|
||||
| "Ignoring read-only" | Misconfigured folder type; "Receive Only" ≠ read-only filesystem |
|
||||
|
||||
**Verdict**: Syncthing works well if you understand that it's **sync, not backup**. For notes that change frequently on multiple devices, conflicts will happen.
|
||||
|
||||
**Sources**:
|
||||
- [Syncthing GitHub Issues](https://github.com/syncthing/syncthing/issues)
|
||||
- [Syncthing 2.0 OOM Discussion](https://forum.syncthing.net/t/syncthing-2-0-1-oom/24824)
|
||||
- [Lobsters: "Do not use Syncthing"](https://lobste.rs/s/pacmpc/do_not_use_syncthing)
|
||||
- [Syncthing Android Discontinuation](https://www.ghacks.net/2024/10/21/syncthing-for-android-is-being-discontinued-but-theres-an-alternative-app-you-can-switch-to/)
|
||||
|
||||
---
|
||||
|
||||
### Unison
|
||||
|
||||
**What it is**: Bidirectional file synchronizer, runs on-demand (not continuous)
|
||||
|
||||
**Strengths**:
|
||||
- **Explicit sync**: You run it when you want, see exactly what will happen
|
||||
- **Proper conflict detection**: Shows both versions, lets you choose
|
||||
- **SSH-native**: Works over SSH, no extra protocols
|
||||
- **Version 2.52+ is OCaml-independent**: No more version matching hell
|
||||
- **ACL/xattr support** (v2.53+): Can sync permissions and extended attributes
|
||||
- **Hub-and-spoke works great**: Sync all devices to a NAS/server
|
||||
- **Lower battery usage** than continuous sync
|
||||
|
||||
**Weaknesses**:
|
||||
- **Manual invocation**: No background daemon (by design, but requires discipline)
|
||||
- **File rename detection is poor**: Rename = delete + create, potentially losing history
|
||||
- **GUI is deprecated**: lablgtk maintenance uncertain; CLI is the future
|
||||
- **Small maintainer base**: ~2.5 people, 0.1 FTE
|
||||
- **Learning curve**: Profile configuration can be confusing
|
||||
|
||||
**Best for**: Users who want explicit control over sync timing and conflict resolution
|
||||
|
||||
**Sources**:
|
||||
- [Unison GitHub](https://github.com/bcpierce00/unison)
|
||||
- [Migration from Syncthing to Unison](https://codelearn.me/2025/12/29/from-syncthing-to-unison.html)
|
||||
- [Syncthing Forum Comparison](https://forum.syncthing.net/t/syncthing-vs-rsync-vs-unison/6298)
|
||||
|
||||
---
|
||||
|
||||
### rsync
|
||||
|
||||
**What it is**: One-way file copying/mirroring tool
|
||||
|
||||
**Strengths**:
|
||||
- Rock solid, ubiquitous
|
||||
- Efficient delta transfers
|
||||
- Perfect for backups
|
||||
|
||||
**Weaknesses**:
|
||||
- **Unidirectional only**: Not a sync tool
|
||||
- No conflict handling (overwrites target)
|
||||
|
||||
**Best for**: Backups, deployments, one-way mirroring. Not for bidirectional sync.
|
||||
|
||||
---
|
||||
|
||||
### rclone (with bisync)
|
||||
|
||||
**What it is**: Cloud storage Swiss Army knife, with experimental bidirectional sync
|
||||
|
||||
**Strengths**:
|
||||
- Supports 70+ cloud backends
|
||||
- `bisync` command provides bidirectional sync (as of v1.66)
|
||||
- Checksum-based comparison available
|
||||
- Lock files prevent concurrent corruption
|
||||
- Great for cloud ↔ local sync
|
||||
|
||||
**Weaknesses**:
|
||||
- **bisync is "advanced"**: Documentation warns of potential data loss
|
||||
- Designed for cloud storage, not LAN sync
|
||||
- More complex ignore pattern syntax than Syncthing
|
||||
- No real-time watching (cron-based)
|
||||
|
||||
**Best for**: Syncing with cloud storage (S3, B2, Drive, etc.), Tier 3 backup
|
||||
|
||||
**Sources**:
|
||||
- [rclone bisync documentation](https://rclone.org/bisync/)
|
||||
|
||||
---
|
||||
|
||||
### Mutagen
|
||||
|
||||
**What it is**: Fast bidirectional sync for remote development
|
||||
|
||||
**Strengths**:
|
||||
- Designed for code sync to remote containers/servers
|
||||
- Very fast (near-native Docker volume performance)
|
||||
- Real-time filesystem watching
|
||||
- Multiple sync modes (two-way-safe, one-way, etc.)
|
||||
- SSH and Docker native
|
||||
|
||||
**Weaknesses**:
|
||||
- **Ephemeral by design**: Not meant for long-term sync
|
||||
- Less configuration flexibility than Syncthing
|
||||
- Primarily for developer workflows
|
||||
|
||||
**Best for**: Remote development, Docker volumes, SSH workspaces. Overkill for notes.
|
||||
|
||||
**Sources**:
|
||||
- [Mutagen GitHub](https://github.com/mutagen-io/mutagen)
|
||||
- [Mutagen Documentation](https://mutagen.io/documentation/synchronization/)
|
||||
|
||||
---
|
||||
|
||||
### nb (Notebook CLI)
|
||||
|
||||
**What it is**: CLI note-taking with built-in git sync
|
||||
|
||||
**Strengths**:
|
||||
- **Git-native**: Every notebook is a git repo
|
||||
- Explicit sync via `nb sync`
|
||||
- Full history, branching, proper merges
|
||||
- Works with any git remote (GitHub, Gitea, Forgejo)
|
||||
- Markdown by default, encryption optional
|
||||
- Single portable shell script
|
||||
- `$EDITOR` integration (neovim-friendly)
|
||||
|
||||
**Weaknesses**:
|
||||
- **Explicit sync only**: Must run `nb sync` (or set up cron/systemd)
|
||||
- Git conflicts require git knowledge to resolve
|
||||
- No GUI (by design)
|
||||
|
||||
**Best for**: Notes and writing. This is the ideal Tier 2 solution for text content.
|
||||
|
||||
**Sources**:
|
||||
- [nb GitHub](https://github.com/xwmx/nb)
|
||||
- [nb Documentation](https://xwmx.github.io/nb/)
|
||||
|
||||
---
|
||||
|
||||
### CRDTs (Automerge, Yjs)
|
||||
|
||||
**What it is**: Conflict-free Replicated Data Types for automatic merging
|
||||
|
||||
**Strengths**:
|
||||
- **No conflicts by design**: Concurrent edits automatically merge
|
||||
- Real-time collaboration possible
|
||||
- Local-first architecture
|
||||
- Automerge 3.0 has 10x lower memory usage
|
||||
|
||||
**Weaknesses**:
|
||||
- **Application-level, not filesystem-level**: Requires app integration
|
||||
- Not a drop-in replacement for file sync
|
||||
- Larger file sizes than plain text
|
||||
- Requires specialized tooling
|
||||
|
||||
**Best for**: Future note-taking apps, real-time collaboration. Not ready for filesystem sync.
|
||||
|
||||
**Sources**:
|
||||
- [Automerge GitHub](https://github.com/automerge/automerge)
|
||||
- [Local, First, Forever](https://tonsky.me/blog/crdt-filesync/)
|
||||
|
||||
---
|
||||
|
||||
## Recommendation Matrix
|
||||
|
||||
| Use Case | Recommended Tool | Reasoning |
|
||||
|----------|------------------|-----------|
|
||||
| **Notes (Markdown)** | `nb` with git | Explicit sync, full history, proper conflict resolution |
|
||||
| **Documents (Office, misc)** | Syncthing OR Unison | Syncthing for "set and forget", Unison for control |
|
||||
| **Code/Dotfiles** | Git + home-manager | Version control is the right tool |
|
||||
| **Photos** | Immich + rclone | Specialized tool for media |
|
||||
| **Large files (video, etc.)** | rclone to B2/S3/NAS | Cloud storage with proper backup |
|
||||
| **Backup** | restic or borg | Deduplication, encryption, versioning |
|
||||
|
||||
---
|
||||
|
||||
## Why "Syncthing is Buggy" is Usually Wrong
|
||||
|
||||
The perception of bugginess typically comes from:
|
||||
|
||||
1. **Expectation mismatch**: Users expect Dropbox behavior (cloud-centric) but get P2P behavior
|
||||
2. **Conflict = failure to them**: Syncthing correctly identifies conflicts; users see this as a bug
|
||||
3. **Android battery drain**: Real issue, but fixed in forks with battery optimization
|
||||
4. **Propagated deletions**: Syncthing does what it's supposed to; users wanted a backup
|
||||
5. **Complex topologies**: 5+ devices with selective sync creates edge cases
|
||||
|
||||
**If you experienced "bugginess" with Syncthing, ask yourself**:
|
||||
- Did you have proper backups outside of Syncthing?
|
||||
- Were multiple devices editing the same files?
|
||||
- Did you understand the difference between "Send Only" / "Receive Only" / "Send & Receive"?
|
||||
- Was your Android device killing the app in the background?
|
||||
|
||||
---
|
||||
|
||||
## Final Recommendation for This Project
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TIER 1: Configuration │
|
||||
│ Tool: Git (public GitHub repo) │
|
||||
│ NixOS flake + home-manager, sops-nix for secrets │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TIER 2: Syncable Data │
|
||||
│ │
|
||||
│ Notes & Writing: │
|
||||
│ ├── Tool: nb (with git sync to private Forgejo) │
|
||||
│ ├── Why: Explicit sync, full history, proper merges │
|
||||
│ └── Conflict handling: Git-style (user resolves) │
|
||||
│ │
|
||||
│ Other Documents: │
|
||||
│ ├── Tool: Syncthing (if you want auto-sync) │
|
||||
│ │ OR Unison (if you want manual control) │
|
||||
│ ├── Why: Real-time for docs that change less frequently │
|
||||
│ └── Conflict handling: .sync-conflict files (Syncthing) │
|
||||
│ or interactive (Unison) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TIER 3: Large Data │
|
||||
│ │
|
||||
│ Photos: Immich (self-hosted) │
|
||||
│ Media: Jellyfin (self-hosted) │
|
||||
│ Repos: Forgejo (self-hosted) │
|
||||
│ Backup: restic → B2/Backblaze/NAS │
|
||||
│ Access: On-demand download via rclone or web UI │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The key insight: **Use git for text, file-sync for binary, and specialized tools for media**.
|
||||
141
flake.nix
Normal file
141
flake.nix
Normal file
@@ -0,0 +1,141 @@
|
||||
{
|
||||
description = "Ultimate Notetaking, Sync & Backup System - NixOS configuration for managing personal data";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
sops-nix = {
|
||||
url = "github:Mic92/sops-nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Optional: Neovim distribution
|
||||
# nixvim = {
|
||||
# url = "github:nix-community/nixvim";
|
||||
# inputs.nixpkgs.follows = "nixpkgs";
|
||||
# };
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, home-manager, sops-nix, ... }@inputs:
|
||||
let
|
||||
# Supported systems
|
||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
|
||||
|
||||
# Helper to generate outputs for all systems
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
|
||||
# Nixpkgs instantiated for each system
|
||||
nixpkgsFor = forAllSystems (system: import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = false; # FOSS only
|
||||
});
|
||||
|
||||
# Shared modules for all hosts
|
||||
sharedModules = [
|
||||
./modules/nb.nix
|
||||
./modules/syncthing.nix
|
||||
./modules/backup.nix
|
||||
sops-nix.nixosModules.sops
|
||||
];
|
||||
|
||||
# Create a NixOS configuration for a host
|
||||
mkHost = { hostname, system ? "x86_64-linux", extraModules ? [] }:
|
||||
nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
specialArgs = { inherit inputs; };
|
||||
modules = sharedModules ++ [
|
||||
./hosts/${hostname}/configuration.nix
|
||||
home-manager.nixosModules.home-manager
|
||||
{
|
||||
home-manager.useGlobalPkgs = true;
|
||||
home-manager.useUserPackages = true;
|
||||
# home-manager.users.YOUR_USERNAME = import ./home;
|
||||
}
|
||||
] ++ extraModules;
|
||||
};
|
||||
|
||||
in {
|
||||
# NixOS configurations
|
||||
# Uncomment and customize for your hosts:
|
||||
#
|
||||
# nixosConfigurations = {
|
||||
# desktop = mkHost { hostname = "desktop"; };
|
||||
# laptop = mkHost { hostname = "laptop"; };
|
||||
# server = mkHost {
|
||||
# hostname = "server";
|
||||
# extraModules = [
|
||||
# ./modules/server/forgejo.nix
|
||||
# ./modules/server/immich.nix
|
||||
# ./modules/server/jellyfin.nix
|
||||
# ];
|
||||
# };
|
||||
# };
|
||||
|
||||
# Development shell with all tools
|
||||
devShells = forAllSystems (system:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system};
|
||||
in {
|
||||
default = pkgs.mkShell {
|
||||
name = "unsbs-dev";
|
||||
|
||||
packages = with pkgs; [
|
||||
# Tier 2: Notes & Sync
|
||||
nb # Notebook CLI
|
||||
syncthing # File sync
|
||||
unison # Alternative sync
|
||||
|
||||
# Tier 3: Backup & Cloud
|
||||
restic # Backup
|
||||
rclone # Cloud storage
|
||||
|
||||
# Development
|
||||
git
|
||||
neovim
|
||||
jq
|
||||
ripgrep
|
||||
fd
|
||||
|
||||
# Nix tools
|
||||
nil # Nix LSP
|
||||
nixpkgs-fmt # Nix formatter
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "Ultimate Notetaking, Sync & Backup System"
|
||||
echo ""
|
||||
echo "Available tools:"
|
||||
echo " nb - Note-taking CLI"
|
||||
echo " syncthing - File synchronization"
|
||||
echo " unison - Alternative file sync"
|
||||
echo " restic - Backup"
|
||||
echo " rclone - Cloud storage"
|
||||
echo ""
|
||||
echo "Run 'nb help' to get started with notes."
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
# Export modules for use in other flakes
|
||||
nixosModules = {
|
||||
nb = import ./modules/nb.nix;
|
||||
syncthing = import ./modules/syncthing.nix;
|
||||
backup = import ./modules/backup.nix;
|
||||
default = { imports = sharedModules; };
|
||||
};
|
||||
|
||||
# Templates for creating new hosts
|
||||
templates = {
|
||||
default = {
|
||||
path = ./templates/host;
|
||||
description = "Template for a new host configuration";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
248
home/default.nix
Normal file
248
home/default.nix
Normal file
@@ -0,0 +1,248 @@
|
||||
# Home Manager Configuration
|
||||
#
|
||||
# User-level configuration for dotfiles and user services.
|
||||
# This is imported by home-manager in flake.nix.
|
||||
#
|
||||
# Customize for your user.
|
||||
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
# ============================================================================
|
||||
# USER INFO
|
||||
# ============================================================================
|
||||
|
||||
home.username = "youruser"; # Change this!
|
||||
home.homeDirectory = "/home/youruser"; # Change this!
|
||||
|
||||
# ============================================================================
|
||||
# PACKAGES
|
||||
# ============================================================================
|
||||
|
||||
home.packages = with pkgs; [
|
||||
# CLI tools
|
||||
ripgrep
|
||||
fd
|
||||
jq
|
||||
bat
|
||||
eza # ls replacement
|
||||
fzf
|
||||
htop
|
||||
|
||||
# Development
|
||||
git
|
||||
gh # GitHub CLI
|
||||
|
||||
# Notes & sync (also in system, but ensuring availability)
|
||||
nb
|
||||
];
|
||||
|
||||
# ============================================================================
|
||||
# GIT
|
||||
# ============================================================================
|
||||
|
||||
programs.git = {
|
||||
enable = true;
|
||||
userName = "Your Name"; # Change this!
|
||||
userEmail = "you@example.com"; # Change this!
|
||||
|
||||
extraConfig = {
|
||||
init.defaultBranch = "main";
|
||||
pull.rebase = true;
|
||||
push.autoSetupRemote = true;
|
||||
|
||||
# Sign commits (optional)
|
||||
# commit.gpgsign = true;
|
||||
# gpg.format = "ssh";
|
||||
# user.signingkey = "~/.ssh/id_ed25519.pub";
|
||||
};
|
||||
|
||||
# Useful aliases
|
||||
aliases = {
|
||||
st = "status";
|
||||
co = "checkout";
|
||||
br = "branch";
|
||||
ci = "commit";
|
||||
lg = "log --oneline --graph --decorate";
|
||||
};
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# SHELL
|
||||
# ============================================================================
|
||||
|
||||
programs.zsh = {
|
||||
enable = true;
|
||||
autosuggestion.enable = true;
|
||||
syntaxHighlighting.enable = true;
|
||||
|
||||
shellAliases = {
|
||||
# Nix
|
||||
rebuild = "sudo nixos-rebuild switch --flake ~/.config/nixos";
|
||||
update = "nix flake update ~/.config/nixos";
|
||||
|
||||
# Notes
|
||||
n = "nb";
|
||||
na = "nb add";
|
||||
ne = "nb edit";
|
||||
ns = "nb search";
|
||||
nsy = "nb sync";
|
||||
|
||||
# Files
|
||||
ls = "eza";
|
||||
ll = "eza -la";
|
||||
tree = "eza --tree";
|
||||
|
||||
# Sync
|
||||
sync = "nb sync && syncthing cli scan --all";
|
||||
};
|
||||
|
||||
initExtra = ''
|
||||
# nb completion
|
||||
eval "$(nb completions zsh)"
|
||||
|
||||
# FZF integration
|
||||
if command -v fzf &> /dev/null; then
|
||||
source ${pkgs.fzf}/share/fzf/key-bindings.zsh
|
||||
source ${pkgs.fzf}/share/fzf/completion.zsh
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# Alternative: Bash
|
||||
# programs.bash = {
|
||||
# enable = true;
|
||||
# shellAliases = { ... };
|
||||
# };
|
||||
|
||||
# ============================================================================
|
||||
# NEOVIM
|
||||
# ============================================================================
|
||||
|
||||
programs.neovim = {
|
||||
enable = true;
|
||||
defaultEditor = true;
|
||||
viAlias = true;
|
||||
vimAlias = true;
|
||||
|
||||
plugins = with pkgs.vimPlugins; [
|
||||
# Essentials
|
||||
plenary-nvim
|
||||
telescope-nvim
|
||||
nvim-treesitter.withAllGrammars
|
||||
|
||||
# Git
|
||||
vim-fugitive
|
||||
gitsigns-nvim
|
||||
|
||||
# LSP (for Nix editing)
|
||||
nvim-lspconfig
|
||||
|
||||
# Quality of life
|
||||
comment-nvim
|
||||
which-key-nvim
|
||||
nvim-autopairs
|
||||
|
||||
# Theme
|
||||
catppuccin-nvim
|
||||
];
|
||||
|
||||
extraLuaConfig = ''
|
||||
-- Basic settings
|
||||
vim.opt.number = true
|
||||
vim.opt.relativenumber = true
|
||||
vim.opt.expandtab = true
|
||||
vim.opt.tabstop = 2
|
||||
vim.opt.shiftwidth = 2
|
||||
vim.opt.smartindent = true
|
||||
vim.opt.wrap = false
|
||||
vim.opt.ignorecase = true
|
||||
vim.opt.smartcase = true
|
||||
|
||||
-- Theme
|
||||
vim.cmd.colorscheme "catppuccin"
|
||||
|
||||
-- Leader key
|
||||
vim.g.mapleader = " "
|
||||
|
||||
-- Telescope
|
||||
local telescope = require('telescope.builtin')
|
||||
vim.keymap.set('n', '<leader>ff', telescope.find_files, { desc = 'Find files' })
|
||||
vim.keymap.set('n', '<leader>fg', telescope.live_grep, { desc = 'Grep' })
|
||||
vim.keymap.set('n', '<leader>fb', telescope.buffers, { desc = 'Buffers' })
|
||||
|
||||
-- Git signs
|
||||
require('gitsigns').setup()
|
||||
|
||||
-- Comments
|
||||
require('Comment').setup()
|
||||
|
||||
-- Which-key
|
||||
require('which-key').setup()
|
||||
|
||||
-- Autopairs
|
||||
require('nvim-autopairs').setup()
|
||||
|
||||
-- LSP for Nix
|
||||
require('lspconfig').nil_ls.setup{}
|
||||
'';
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# TMUX
|
||||
# ============================================================================
|
||||
|
||||
programs.tmux = {
|
||||
enable = true;
|
||||
terminal = "tmux-256color";
|
||||
prefix = "C-a";
|
||||
mouse = true;
|
||||
|
||||
extraConfig = ''
|
||||
# Split panes with | and -
|
||||
bind | split-window -h
|
||||
bind - split-window -v
|
||||
|
||||
# Vim-style pane navigation
|
||||
bind h select-pane -L
|
||||
bind j select-pane -D
|
||||
bind k select-pane -U
|
||||
bind l select-pane -R
|
||||
|
||||
# Status bar
|
||||
set -g status-position top
|
||||
set -g status-style 'bg=default fg=white'
|
||||
'';
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# DIRECTORIES
|
||||
# ============================================================================
|
||||
|
||||
# Create standard directories
|
||||
home.file = {
|
||||
".local/bin/.keep".text = ""; # Ensure bin directory exists
|
||||
};
|
||||
|
||||
xdg = {
|
||||
enable = true;
|
||||
userDirs = {
|
||||
enable = true;
|
||||
createDirectories = true;
|
||||
documents = "$HOME/Documents";
|
||||
download = "$HOME/Downloads";
|
||||
music = "$HOME/Music";
|
||||
pictures = "$HOME/Pictures";
|
||||
videos = "$HOME/Videos";
|
||||
};
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# STATE VERSION
|
||||
# ============================================================================
|
||||
|
||||
home.stateVersion = "24.05"; # Don't change after initial setup
|
||||
|
||||
# Let home-manager manage itself
|
||||
programs.home-manager.enable = true;
|
||||
}
|
||||
202
hosts/example/configuration.nix
Normal file
202
hosts/example/configuration.nix
Normal file
@@ -0,0 +1,202 @@
|
||||
# Example Host Configuration
|
||||
#
|
||||
# This is a template for your machine's configuration.nix.
|
||||
# Copy this to hosts/your-hostname/configuration.nix and customize.
|
||||
#
|
||||
# To use:
|
||||
# 1. Copy this file to hosts/<your-hostname>/configuration.nix
|
||||
# 2. Generate hardware-configuration.nix: nixos-generate-config --show-hardware-config
|
||||
# 3. Customize the settings below
|
||||
# 4. Add to flake.nix outputs
|
||||
|
||||
{ config, pkgs, lib, inputs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./hardware-configuration.nix # Generate with nixos-generate-config
|
||||
];
|
||||
|
||||
# ============================================================================
|
||||
# SYSTEM BASICS
|
||||
# ============================================================================
|
||||
|
||||
networking.hostName = "example"; # Change this!
|
||||
|
||||
# Bootloader (adjust for your system)
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
# Networking
|
||||
networking.networkmanager.enable = true;
|
||||
|
||||
# Timezone and locale
|
||||
time.timeZone = "America/New_York"; # Change this!
|
||||
i18n.defaultLocale = "en_US.UTF-8";
|
||||
|
||||
# ============================================================================
|
||||
# USERS
|
||||
# ============================================================================
|
||||
|
||||
users.users.youruser = { # Change this!
|
||||
isNormalUser = true;
|
||||
description = "Your Name";
|
||||
extraGroups = [ "wheel" "networkmanager" ];
|
||||
shell = pkgs.zsh; # Or bash, fish, etc.
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# TIER 1: CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# Enable Nix flakes
|
||||
nix.settings = {
|
||||
experimental-features = [ "nix-command" "flakes" ];
|
||||
auto-optimise-store = true;
|
||||
};
|
||||
|
||||
# Garbage collection
|
||||
nix.gc = {
|
||||
automatic = true;
|
||||
dates = "weekly";
|
||||
options = "--delete-older-than 30d";
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# TIER 2: NOTES (nb)
|
||||
# ============================================================================
|
||||
|
||||
programs.nb = {
|
||||
enable = true;
|
||||
editor = "nvim";
|
||||
defaultExtension = "md";
|
||||
|
||||
# Configure your notebooks
|
||||
notebooks = {
|
||||
# personal = {
|
||||
# remote = "git@forgejo.yourdomain.com:youruser/personal-notes.git";
|
||||
# };
|
||||
# work = {
|
||||
# remote = "git@forgejo.yourdomain.com:youruser/work-notes.git";
|
||||
# };
|
||||
};
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# TIER 2: SYNC (Syncthing)
|
||||
# ============================================================================
|
||||
|
||||
services.syncthing-managed = {
|
||||
enable = true;
|
||||
user = "youruser"; # Change this!
|
||||
|
||||
# Add your device IDs here
|
||||
devices = {
|
||||
# laptop = {
|
||||
# id = "XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX";
|
||||
# };
|
||||
# desktop = {
|
||||
# id = "YYYYYYY-YYYYYYY-YYYYYYY-YYYYYYY-YYYYYYY-YYYYYYY-YYYYYYY-YYYYYYY";
|
||||
# };
|
||||
};
|
||||
|
||||
# Configure folders to sync
|
||||
folders = {
|
||||
# documents = {
|
||||
# path = "/home/youruser/Documents";
|
||||
# devices = [ "laptop" "desktop" ];
|
||||
# versioning = { type = "simple"; params.keep = "5"; };
|
||||
# };
|
||||
};
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# TIER 3: BACKUP
|
||||
# ============================================================================
|
||||
|
||||
services.backup = {
|
||||
enable = true;
|
||||
|
||||
# Configure your backup repository
|
||||
# repository = "b2:your-bucket:backup";
|
||||
repository = "/mnt/backup"; # Local example
|
||||
|
||||
# Paths to back up
|
||||
paths = [
|
||||
"/home/youruser/Documents"
|
||||
"/home/youruser/notes"
|
||||
# Add more paths
|
||||
];
|
||||
|
||||
# Password file (create this manually or use sops-nix)
|
||||
passwordFile = "/etc/restic-password"; # Create this!
|
||||
|
||||
# For cloud storage, set environment file
|
||||
# environmentFile = "/etc/restic-env";
|
||||
|
||||
# Schedule: 2 AM daily
|
||||
schedule = "*-*-* 02:00:00";
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# EDITOR
|
||||
# ============================================================================
|
||||
|
||||
programs.neovim = {
|
||||
enable = true;
|
||||
defaultEditor = true;
|
||||
viAlias = true;
|
||||
vimAlias = true;
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# PACKAGES
|
||||
# ============================================================================
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
# Shell essentials
|
||||
git
|
||||
ripgrep
|
||||
fd
|
||||
jq
|
||||
htop
|
||||
tmux
|
||||
|
||||
# Development
|
||||
neovim
|
||||
|
||||
# Sync & backup (installed by modules, but explicit is fine)
|
||||
syncthing
|
||||
restic
|
||||
rclone
|
||||
];
|
||||
|
||||
# ============================================================================
|
||||
# SECRETS (sops-nix)
|
||||
# ============================================================================
|
||||
|
||||
# Uncomment and configure when you set up sops-nix
|
||||
#
|
||||
# sops = {
|
||||
# defaultSopsFile = ../../secrets/secrets.yaml;
|
||||
# age.keyFile = "/home/youruser/.config/sops/age/keys.txt";
|
||||
#
|
||||
# secrets = {
|
||||
# "restic-password" = {};
|
||||
# "syncthing-key" = {
|
||||
# owner = "youruser";
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
|
||||
# ============================================================================
|
||||
# SERVICES
|
||||
# ============================================================================
|
||||
|
||||
services.openssh.enable = true;
|
||||
|
||||
# ============================================================================
|
||||
# SYSTEM
|
||||
# ============================================================================
|
||||
|
||||
system.stateVersion = "24.05"; # Don't change after initial install
|
||||
}
|
||||
297
modules/backup.nix
Normal file
297
modules/backup.nix
Normal file
@@ -0,0 +1,297 @@
|
||||
# Backup Module
|
||||
#
|
||||
# Declarative restic backup configuration with systemd timers.
|
||||
#
|
||||
# Usage:
|
||||
# services.backup.enable = true;
|
||||
# services.backup.paths = [ "/home/user/Documents" "/home/user/notes" ];
|
||||
# services.backup.repository = "b2:mybucket:backup";
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.backup;
|
||||
in {
|
||||
options.services.backup = {
|
||||
enable = mkEnableOption "automated backups with restic";
|
||||
|
||||
repository = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Restic repository location.
|
||||
Examples:
|
||||
- Local: /mnt/backup
|
||||
- SFTP: sftp:user@host:/path
|
||||
- B2: b2:bucket-name:path
|
||||
- S3: s3:s3.amazonaws.com/bucket-name
|
||||
- REST: rest:http://host:8000/
|
||||
'';
|
||||
example = "b2:mybucket:backup";
|
||||
};
|
||||
|
||||
paths = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Paths to back up.";
|
||||
example = [ "/home/user/Documents" "/home/user/notes" "/var/lib/important" ];
|
||||
};
|
||||
|
||||
exclude = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [
|
||||
"**/.git"
|
||||
"**/node_modules"
|
||||
"**/__pycache__"
|
||||
"**/.cache"
|
||||
"**/Cache"
|
||||
"**/.thumbnails"
|
||||
"**/Trash"
|
||||
"**/.local/share/Trash"
|
||||
];
|
||||
description = "Patterns to exclude from backup.";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.str;
|
||||
description = "Path to file containing restic repository password.";
|
||||
example = "/run/secrets/restic-password";
|
||||
};
|
||||
|
||||
environmentFile = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Path to file containing environment variables for cloud storage.
|
||||
Should contain variables like B2_ACCOUNT_ID, B2_ACCOUNT_KEY, etc.
|
||||
'';
|
||||
example = "/run/secrets/restic-env";
|
||||
};
|
||||
|
||||
schedule = mkOption {
|
||||
type = types.str;
|
||||
default = "*-*-* 02:00:00"; # 2 AM daily
|
||||
description = "When to run backups (systemd calendar format).";
|
||||
example = "*-*-* 04:00:00";
|
||||
};
|
||||
|
||||
pruneSchedule = mkOption {
|
||||
type = types.str;
|
||||
default = "Sun *-*-* 03:00:00"; # Sunday 3 AM
|
||||
description = "When to prune old backups.";
|
||||
};
|
||||
|
||||
retention = {
|
||||
daily = mkOption {
|
||||
type = types.int;
|
||||
default = 7;
|
||||
description = "Number of daily backups to keep.";
|
||||
};
|
||||
|
||||
weekly = mkOption {
|
||||
type = types.int;
|
||||
default = 4;
|
||||
description = "Number of weekly backups to keep.";
|
||||
};
|
||||
|
||||
monthly = mkOption {
|
||||
type = types.int;
|
||||
default = 6;
|
||||
description = "Number of monthly backups to keep.";
|
||||
};
|
||||
|
||||
yearly = mkOption {
|
||||
type = types.int;
|
||||
default = 2;
|
||||
description = "Number of yearly backups to keep.";
|
||||
};
|
||||
};
|
||||
|
||||
extraOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Extra options to pass to restic backup.";
|
||||
example = [ "--verbose" "--exclude-caches" ];
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "root";
|
||||
description = "User to run backups as.";
|
||||
};
|
||||
|
||||
notifyOnFailure = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Send notification on backup failure.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# Install restic
|
||||
environment.systemPackages = [ pkgs.restic ];
|
||||
|
||||
# Backup service
|
||||
systemd.services.restic-backup = {
|
||||
description = "Restic Backup";
|
||||
after = [ "network-online.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = cfg.user;
|
||||
ExecStart = let
|
||||
excludeArgs = concatMapStringsSep " " (e: "--exclude '${e}'") cfg.exclude;
|
||||
pathArgs = concatStringsSep " " cfg.paths;
|
||||
extraArgs = concatStringsSep " " cfg.extraOptions;
|
||||
in pkgs.writeShellScript "restic-backup" ''
|
||||
set -euo pipefail
|
||||
|
||||
export RESTIC_REPOSITORY="${cfg.repository}"
|
||||
export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
|
||||
${optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
|
||||
|
||||
echo "Starting backup at $(date)"
|
||||
|
||||
${pkgs.restic}/bin/restic backup \
|
||||
${excludeArgs} \
|
||||
${extraArgs} \
|
||||
${pathArgs}
|
||||
|
||||
echo "Backup completed at $(date)"
|
||||
'';
|
||||
|
||||
# Security hardening
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "strict";
|
||||
ReadWritePaths = [ "/tmp" ];
|
||||
NoNewPrivileges = true;
|
||||
};
|
||||
|
||||
# Retry on failure
|
||||
unitConfig = {
|
||||
OnFailure = mkIf cfg.notifyOnFailure [ "backup-notify-failure@%n.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
# Backup timer
|
||||
systemd.timers.restic-backup = {
|
||||
description = "Restic Backup Timer";
|
||||
wantedBy = [ "timers.target" ];
|
||||
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.schedule;
|
||||
Persistent = true; # Run missed backups
|
||||
RandomizedDelaySec = "30min"; # Spread load
|
||||
};
|
||||
};
|
||||
|
||||
# Prune service
|
||||
systemd.services.restic-prune = {
|
||||
description = "Restic Prune Old Backups";
|
||||
after = [ "network-online.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = cfg.user;
|
||||
ExecStart = pkgs.writeShellScript "restic-prune" ''
|
||||
set -euo pipefail
|
||||
|
||||
export RESTIC_REPOSITORY="${cfg.repository}"
|
||||
export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
|
||||
${optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
|
||||
|
||||
echo "Pruning old backups at $(date)"
|
||||
|
||||
${pkgs.restic}/bin/restic forget \
|
||||
--keep-daily ${toString cfg.retention.daily} \
|
||||
--keep-weekly ${toString cfg.retention.weekly} \
|
||||
--keep-monthly ${toString cfg.retention.monthly} \
|
||||
--keep-yearly ${toString cfg.retention.yearly} \
|
||||
--prune
|
||||
|
||||
echo "Prune completed at $(date)"
|
||||
'';
|
||||
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "strict";
|
||||
NoNewPrivileges = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Prune timer
|
||||
systemd.timers.restic-prune = {
|
||||
description = "Restic Prune Timer";
|
||||
wantedBy = [ "timers.target" ];
|
||||
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.pruneSchedule;
|
||||
Persistent = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Failure notification service template
|
||||
systemd.services."backup-notify-failure@" = mkIf cfg.notifyOnFailure {
|
||||
description = "Backup Failure Notification";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = pkgs.writeShellScript "backup-notify-failure" ''
|
||||
echo "Backup failed: %i at $(date)" | ${pkgs.util-linux}/bin/wall
|
||||
# Add your notification method here:
|
||||
# - Send email
|
||||
# - Push notification
|
||||
# - Slack webhook
|
||||
# - etc.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# Helper scripts
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeShellScriptBin "ubackup" ''
|
||||
# Unified backup helper
|
||||
case "''${1:-}" in
|
||||
now)
|
||||
echo "Starting backup..."
|
||||
sudo systemctl start restic-backup
|
||||
;;
|
||||
status)
|
||||
echo "=== Recent backups ==="
|
||||
export RESTIC_REPOSITORY="${cfg.repository}"
|
||||
export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
|
||||
${optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
|
||||
${pkgs.restic}/bin/restic snapshots --last 10
|
||||
;;
|
||||
restore)
|
||||
if [ -z "''${2:-}" ]; then
|
||||
echo "Usage: ubackup restore <snapshot-id> <target-path>"
|
||||
exit 1
|
||||
fi
|
||||
export RESTIC_REPOSITORY="${cfg.repository}"
|
||||
export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
|
||||
${optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
|
||||
${pkgs.restic}/bin/restic restore "$2" --target "''${3:-.}"
|
||||
;;
|
||||
check)
|
||||
echo "Checking backup integrity..."
|
||||
export RESTIC_REPOSITORY="${cfg.repository}"
|
||||
export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
|
||||
${optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
|
||||
${pkgs.restic}/bin/restic check
|
||||
;;
|
||||
*)
|
||||
echo "Usage: ubackup {now|status|restore|check}"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " now Run backup immediately"
|
||||
echo " status Show recent backups"
|
||||
echo " restore ID PATH Restore snapshot to path"
|
||||
echo " check Verify backup integrity"
|
||||
;;
|
||||
esac
|
||||
'')
|
||||
];
|
||||
};
|
||||
}
|
||||
146
modules/nb.nix
Normal file
146
modules/nb.nix
Normal file
@@ -0,0 +1,146 @@
|
||||
# nb - CLI Notebook Module
|
||||
#
|
||||
# This module installs and configures nb, a command-line notebook tool
|
||||
# with git-backed versioning and sync.
|
||||
#
|
||||
# Usage in your configuration.nix:
|
||||
# programs.nb.enable = true;
|
||||
# programs.nb.notebooks.personal.remote = "git@forgejo:you/notes.git";
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.programs.nb;
|
||||
in {
|
||||
options.programs.nb = {
|
||||
enable = mkEnableOption "nb notebook CLI";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.nb;
|
||||
defaultText = literalExpression "pkgs.nb";
|
||||
description = "The nb package to use.";
|
||||
};
|
||||
|
||||
defaultNotebook = mkOption {
|
||||
type = types.str;
|
||||
default = "home";
|
||||
description = "The default notebook name.";
|
||||
};
|
||||
|
||||
editor = mkOption {
|
||||
type = types.str;
|
||||
default = "nvim";
|
||||
description = "Editor to use for editing notes.";
|
||||
};
|
||||
|
||||
defaultExtension = mkOption {
|
||||
type = types.str;
|
||||
default = "md";
|
||||
description = "Default file extension for new notes.";
|
||||
};
|
||||
|
||||
autoSync = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to automatically sync notebooks after operations.
|
||||
Note: This can slow down operations if network is slow.
|
||||
'';
|
||||
};
|
||||
|
||||
colorTheme = mkOption {
|
||||
type = types.str;
|
||||
default = "blacklight";
|
||||
description = "Color theme for nb (see 'nb settings colors').";
|
||||
};
|
||||
|
||||
notebooks = mkOption {
|
||||
type = types.attrsOf (types.submodule {
|
||||
options = {
|
||||
remote = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Git remote URL for syncing this notebook.";
|
||||
example = "git@github.com:user/notes.git";
|
||||
};
|
||||
|
||||
encrypted = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to encrypt notes in this notebook.";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = {};
|
||||
description = "Notebooks to configure with their remotes.";
|
||||
example = literalExpression ''
|
||||
{
|
||||
personal = { remote = "git@forgejo:user/personal.git"; };
|
||||
work = { remote = "git@forgejo:user/work.git"; encrypted = true; };
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
environment.systemPackages = [
|
||||
cfg.package
|
||||
pkgs.git # nb requires git
|
||||
pkgs.bat # Optional: better file viewing
|
||||
pkgs.w3m # Optional: for nb browse
|
||||
];
|
||||
|
||||
# Set environment variables for nb
|
||||
environment.variables = {
|
||||
EDITOR = cfg.editor;
|
||||
NB_DEFAULT_EXTENSION = cfg.defaultExtension;
|
||||
NB_AUTO_SYNC = if cfg.autoSync then "1" else "0";
|
||||
NB_COLOR_THEME = cfg.colorTheme;
|
||||
};
|
||||
|
||||
# Create activation script to set up notebooks
|
||||
system.activationScripts.nb-setup = let
|
||||
notebookSetup = concatStringsSep "\n" (mapAttrsToList (name: opts: ''
|
||||
# Create notebook if it doesn't exist
|
||||
if ! ${cfg.package}/bin/nb notebooks | grep -q "^${name}$"; then
|
||||
echo "Creating nb notebook: ${name}"
|
||||
${cfg.package}/bin/nb notebooks add ${name}
|
||||
fi
|
||||
|
||||
${optionalString (opts.remote != null) ''
|
||||
# Set remote for notebook
|
||||
echo "Setting remote for ${name}: ${opts.remote}"
|
||||
${cfg.package}/bin/nb ${name}:remote set ${opts.remote} 2>/dev/null || true
|
||||
''}
|
||||
'') cfg.notebooks);
|
||||
in stringAfter [ "users" ] ''
|
||||
# Skip if running in a chroot or during initial install
|
||||
if [ -d /home ]; then
|
||||
${notebookSetup}
|
||||
fi
|
||||
'';
|
||||
|
||||
# Optional: systemd timer for periodic sync
|
||||
# Uncomment if you want automatic background sync
|
||||
#
|
||||
# systemd.user.services.nb-sync = {
|
||||
# description = "Sync nb notebooks";
|
||||
# serviceConfig = {
|
||||
# Type = "oneshot";
|
||||
# ExecStart = "${cfg.package}/bin/nb sync --all";
|
||||
# };
|
||||
# };
|
||||
#
|
||||
# systemd.user.timers.nb-sync = {
|
||||
# description = "Periodic nb sync";
|
||||
# wantedBy = [ "timers.target" ];
|
||||
# timerConfig = {
|
||||
# OnCalendar = "*:0/15"; # Every 15 minutes
|
||||
# Persistent = true;
|
||||
# };
|
||||
# };
|
||||
};
|
||||
}
|
||||
145
modules/server/forgejo.nix
Normal file
145
modules/server/forgejo.nix
Normal file
@@ -0,0 +1,145 @@
|
||||
# Forgejo Module
|
||||
#
|
||||
# Self-hosted Git forge (GitHub/Gitea alternative).
|
||||
# Used for hosting private nb notebook repositories.
|
||||
#
|
||||
# Usage:
|
||||
# services.forgejo-managed.enable = true;
|
||||
# services.forgejo-managed.domain = "git.yourdomain.com";
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.forgejo-managed;
|
||||
in {
|
||||
options.services.forgejo-managed = {
|
||||
enable = mkEnableOption "managed Forgejo git forge";
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
description = "Domain name for Forgejo.";
|
||||
example = "git.example.com";
|
||||
};
|
||||
|
||||
httpPort = mkOption {
|
||||
type = types.port;
|
||||
default = 3000;
|
||||
description = "HTTP port for Forgejo (behind reverse proxy).";
|
||||
};
|
||||
|
||||
sshPort = mkOption {
|
||||
type = types.port;
|
||||
default = 2222;
|
||||
description = "SSH port for git operations.";
|
||||
};
|
||||
|
||||
stateDir = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/forgejo";
|
||||
description = "State directory for Forgejo data.";
|
||||
};
|
||||
|
||||
enableLFS = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable Git LFS support.";
|
||||
};
|
||||
|
||||
disableRegistration = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Disable open user registration.";
|
||||
};
|
||||
|
||||
adminEmail = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Admin email address.";
|
||||
};
|
||||
|
||||
appName = mkOption {
|
||||
type = types.str;
|
||||
default = "Forgejo";
|
||||
description = "Application name shown in UI.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.forgejo = {
|
||||
enable = true;
|
||||
stateDir = cfg.stateDir;
|
||||
|
||||
settings = {
|
||||
DEFAULT = {
|
||||
APP_NAME = cfg.appName;
|
||||
};
|
||||
|
||||
server = {
|
||||
DOMAIN = cfg.domain;
|
||||
HTTP_PORT = cfg.httpPort;
|
||||
ROOT_URL = "https://${cfg.domain}/";
|
||||
SSH_PORT = cfg.sshPort;
|
||||
SSH_DOMAIN = cfg.domain;
|
||||
START_SSH_SERVER = true; # Built-in SSH server
|
||||
LFS_START_SERVER = cfg.enableLFS;
|
||||
};
|
||||
|
||||
service = {
|
||||
DISABLE_REGISTRATION = cfg.disableRegistration;
|
||||
REQUIRE_SIGNIN_VIEW = false; # Allow viewing public repos
|
||||
};
|
||||
|
||||
repository = {
|
||||
DEFAULT_PRIVATE = "private"; # New repos are private by default
|
||||
ENABLE_PUSH_CREATE_USER = true; # Allow creating repos via push
|
||||
};
|
||||
|
||||
"repository.upload" = {
|
||||
ENABLED = true;
|
||||
FILE_MAX_SIZE = 100; # MB
|
||||
MAX_FILES = 10;
|
||||
};
|
||||
|
||||
session = {
|
||||
PROVIDER = "file";
|
||||
COOKIE_SECURE = true; # Require HTTPS
|
||||
};
|
||||
|
||||
log = {
|
||||
LEVEL = "Info";
|
||||
};
|
||||
|
||||
# Security
|
||||
security = {
|
||||
INSTALL_LOCK = true; # Prevent web-based install
|
||||
MIN_PASSWORD_LENGTH = 12;
|
||||
};
|
||||
|
||||
mailer = mkIf (cfg.adminEmail != null) {
|
||||
ENABLED = true;
|
||||
FROM = cfg.adminEmail;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Open SSH port
|
||||
networking.firewall.allowedTCPPorts = [ cfg.sshPort ];
|
||||
|
||||
# Create backup for Forgejo data
|
||||
services.backup.paths = mkIf config.services.backup.enable [
|
||||
cfg.stateDir
|
||||
];
|
||||
|
||||
# Reverse proxy with Caddy (optional, can use nginx)
|
||||
# Uncomment if using Caddy:
|
||||
#
|
||||
# services.caddy = {
|
||||
# enable = true;
|
||||
# virtualHosts."${cfg.domain}".extraConfig = ''
|
||||
# reverse_proxy localhost:${toString cfg.httpPort}
|
||||
# '';
|
||||
# };
|
||||
};
|
||||
}
|
||||
93
modules/server/immich.nix
Normal file
93
modules/server/immich.nix
Normal file
@@ -0,0 +1,93 @@
|
||||
# Immich Module
|
||||
#
|
||||
# Self-hosted photo and video backup (Google Photos alternative).
|
||||
# For Tier 3 photo management.
|
||||
#
|
||||
# Note: Immich is complex and changes frequently. This module provides
|
||||
# a starting point but may need updates. Check NixOS options for latest.
|
||||
#
|
||||
# Usage:
|
||||
# services.immich-managed.enable = true;
|
||||
# services.immich-managed.domain = "photos.yourdomain.com";
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.immich-managed;
|
||||
in {
|
||||
options.services.immich-managed = {
|
||||
enable = mkEnableOption "managed Immich photo service";
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
description = "Domain name for Immich.";
|
||||
example = "photos.example.com";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 2283;
|
||||
description = "Port for Immich web interface.";
|
||||
};
|
||||
|
||||
mediaLocation = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/immich";
|
||||
description = "Location for storing photos and videos.";
|
||||
};
|
||||
|
||||
externalLibraryPaths = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Additional paths for external photo libraries.";
|
||||
example = [ "/mnt/photos/archive" ];
|
||||
};
|
||||
|
||||
enableMachineLearning = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable ML features (face recognition, search).";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# Immich service (NixOS 24.05+)
|
||||
services.immich = {
|
||||
enable = true;
|
||||
port = cfg.port;
|
||||
mediaLocation = cfg.mediaLocation;
|
||||
|
||||
# Machine learning (optional, resource-intensive)
|
||||
machine-learning.enable = cfg.enableMachineLearning;
|
||||
|
||||
# Settings
|
||||
settings = {
|
||||
# Add any Immich-specific settings here
|
||||
# Check Immich docs for available options
|
||||
};
|
||||
};
|
||||
|
||||
# Ensure media directory exists with correct permissions
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.mediaLocation} 0755 immich immich -"
|
||||
] ++ (map (path: "d ${path} 0755 immich immich -") cfg.externalLibraryPaths);
|
||||
|
||||
# Backup Immich data
|
||||
services.backup.paths = mkIf config.services.backup.enable [
|
||||
cfg.mediaLocation
|
||||
"/var/lib/immich" # Database and config
|
||||
];
|
||||
|
||||
# Memory recommendation
|
||||
warnings = mkIf (cfg.enableMachineLearning && config.hardware.cpu.intel.updateMicrocode or false) [
|
||||
"Immich ML features benefit from GPU acceleration. Consider enabling CUDA or OpenCL."
|
||||
];
|
||||
|
||||
# Reverse proxy example (Caddy)
|
||||
# services.caddy.virtualHosts."${cfg.domain}".extraConfig = ''
|
||||
# reverse_proxy localhost:${toString cfg.port}
|
||||
# '';
|
||||
};
|
||||
}
|
||||
91
modules/server/jellyfin.nix
Normal file
91
modules/server/jellyfin.nix
Normal file
@@ -0,0 +1,91 @@
|
||||
# Jellyfin Module
|
||||
#
|
||||
# Self-hosted media server (Plex alternative).
|
||||
# For Tier 3 media streaming.
|
||||
#
|
||||
# Usage:
|
||||
# services.jellyfin-managed.enable = true;
|
||||
# services.jellyfin-managed.domain = "media.yourdomain.com";
|
||||
# services.jellyfin-managed.mediaLibraries = [ "/mnt/media/movies" "/mnt/media/tv" ];
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.jellyfin-managed;
|
||||
in {
|
||||
options.services.jellyfin-managed = {
|
||||
enable = mkEnableOption "managed Jellyfin media server";
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
description = "Domain name for Jellyfin.";
|
||||
example = "media.example.com";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8096;
|
||||
description = "HTTP port for Jellyfin.";
|
||||
};
|
||||
|
||||
mediaLibraries = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Paths to media libraries.";
|
||||
example = [ "/mnt/media/movies" "/mnt/media/tv" "/mnt/media/music" ];
|
||||
};
|
||||
|
||||
enableHardwareAcceleration = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable hardware transcoding (requires compatible GPU).";
|
||||
};
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Open firewall for Jellyfin ports.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.jellyfin = {
|
||||
enable = true;
|
||||
openFirewall = cfg.openFirewall;
|
||||
};
|
||||
|
||||
# Give jellyfin access to media directories
|
||||
systemd.services.jellyfin.serviceConfig.SupplementaryGroups = [
|
||||
"render" # For hardware acceleration
|
||||
"video" # For hardware acceleration
|
||||
];
|
||||
|
||||
# Ensure media directories have correct permissions
|
||||
systemd.tmpfiles.rules = map (path:
|
||||
"d ${path} 0755 jellyfin jellyfin -"
|
||||
) cfg.mediaLibraries;
|
||||
|
||||
# Hardware acceleration (Intel VAAPI example)
|
||||
hardware.graphics = mkIf cfg.enableHardwareAcceleration {
|
||||
enable = true;
|
||||
extraPackages = with pkgs; [
|
||||
intel-media-driver # For Intel
|
||||
# nvidia-vaapi-driver # For NVIDIA
|
||||
libva
|
||||
];
|
||||
};
|
||||
|
||||
# Note: For hardware acceleration, jellyfin user needs access to /dev/dri
|
||||
users.users.jellyfin.extraGroups = mkIf cfg.enableHardwareAcceleration [
|
||||
"render"
|
||||
"video"
|
||||
];
|
||||
|
||||
# Reverse proxy example
|
||||
# services.caddy.virtualHosts."${cfg.domain}".extraConfig = ''
|
||||
# reverse_proxy localhost:${toString cfg.port}
|
||||
# '';
|
||||
};
|
||||
}
|
||||
269
modules/syncthing.nix
Normal file
269
modules/syncthing.nix
Normal file
@@ -0,0 +1,269 @@
|
||||
# Syncthing Module
|
||||
#
|
||||
# Declarative Syncthing configuration for NixOS.
|
||||
# This wraps the built-in syncthing module with sensible defaults.
|
||||
#
|
||||
# Usage:
|
||||
# services.syncthing-managed.enable = true;
|
||||
# services.syncthing-managed.user = "youruser";
|
||||
# services.syncthing-managed.devices.laptop.id = "DEVICE-ID-HERE";
|
||||
# services.syncthing-managed.folders.documents.path = "~/Documents";
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.syncthing-managed;
|
||||
in {
|
||||
options.services.syncthing-managed = {
|
||||
enable = mkEnableOption "managed Syncthing file synchronization";
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
description = "User to run Syncthing as.";
|
||||
example = "alice";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "users";
|
||||
description = "Group to run Syncthing as.";
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.str;
|
||||
default = "/home/${cfg.user}";
|
||||
defaultText = literalExpression ''"/home/''${cfg.user}"'';
|
||||
description = "Default directory for Syncthing data.";
|
||||
};
|
||||
|
||||
configDir = mkOption {
|
||||
type = types.str;
|
||||
default = "/home/${cfg.user}/.config/syncthing";
|
||||
defaultText = literalExpression ''"/home/''${cfg.user}/.config/syncthing"'';
|
||||
description = "Directory for Syncthing configuration.";
|
||||
};
|
||||
|
||||
guiAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1:8384";
|
||||
description = "Address for the Syncthing web GUI.";
|
||||
};
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Whether to open the firewall for Syncthing.";
|
||||
};
|
||||
|
||||
devices = mkOption {
|
||||
type = types.attrsOf (types.submodule {
|
||||
options = {
|
||||
id = mkOption {
|
||||
type = types.str;
|
||||
description = "Device ID (from syncthing CLI or GUI).";
|
||||
example = "XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Friendly name for the device.";
|
||||
};
|
||||
|
||||
addresses = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ "dynamic" ];
|
||||
description = "Addresses to connect to this device.";
|
||||
};
|
||||
|
||||
autoAcceptFolders = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Automatically accept shared folders from this device.";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = {};
|
||||
description = "Syncthing devices to connect to.";
|
||||
example = literalExpression ''
|
||||
{
|
||||
laptop = {
|
||||
id = "XXXXXXX-...";
|
||||
name = "My Laptop";
|
||||
};
|
||||
server = {
|
||||
id = "YYYYYYY-...";
|
||||
addresses = [ "tcp://server.local:22000" ];
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
folders = mkOption {
|
||||
type = types.attrsOf (types.submodule ({ name, ... }: {
|
||||
options = {
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
description = "Local path to sync.";
|
||||
example = "~/Documents";
|
||||
};
|
||||
|
||||
devices = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = attrNames cfg.devices;
|
||||
defaultText = literalExpression "attrNames cfg.devices";
|
||||
description = "Devices to share this folder with.";
|
||||
};
|
||||
|
||||
id = mkOption {
|
||||
type = types.str;
|
||||
default = name;
|
||||
description = "Unique folder ID.";
|
||||
};
|
||||
|
||||
type = mkOption {
|
||||
type = types.enum [ "sendreceive" "sendonly" "receiveonly" ];
|
||||
default = "sendreceive";
|
||||
description = ''
|
||||
Folder type:
|
||||
- sendreceive: Full two-way sync (default)
|
||||
- sendonly: Only send changes, ignore remote changes
|
||||
- receiveonly: Only receive changes, don't send local changes
|
||||
'';
|
||||
};
|
||||
|
||||
versioning = mkOption {
|
||||
type = types.nullOr (types.submodule {
|
||||
options = {
|
||||
type = mkOption {
|
||||
type = types.enum [ "simple" "staggered" "trashcan" "external" ];
|
||||
default = "simple";
|
||||
description = "Versioning type.";
|
||||
};
|
||||
|
||||
params = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = { keep = "5"; };
|
||||
description = "Versioning parameters.";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = null;
|
||||
description = "Versioning configuration for this folder.";
|
||||
example = literalExpression ''
|
||||
{
|
||||
type = "staggered";
|
||||
params = {
|
||||
cleanInterval = "3600";
|
||||
maxAge = "31536000";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
ignorePerms = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Ignore permission changes.";
|
||||
};
|
||||
|
||||
rescanInterval = mkOption {
|
||||
type = types.int;
|
||||
default = 3600;
|
||||
description = "How often to rescan the folder (seconds). 0 to disable.";
|
||||
};
|
||||
|
||||
fsWatcherEnabled = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Use filesystem watcher for real-time sync.";
|
||||
};
|
||||
};
|
||||
}));
|
||||
default = {};
|
||||
description = "Folders to synchronize.";
|
||||
example = literalExpression ''
|
||||
{
|
||||
documents = {
|
||||
path = "~/Documents";
|
||||
devices = [ "laptop" "desktop" ];
|
||||
versioning = { type = "simple"; params.keep = "5"; };
|
||||
};
|
||||
music = {
|
||||
path = "~/Music";
|
||||
type = "receiveonly"; # Don't upload changes
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
extraOptions = mkOption {
|
||||
type = types.attrs;
|
||||
default = {};
|
||||
description = "Extra options to pass to services.syncthing.settings.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.syncthing = {
|
||||
enable = true;
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
dataDir = cfg.dataDir;
|
||||
configDir = cfg.configDir;
|
||||
guiAddress = cfg.guiAddress;
|
||||
|
||||
overrideDevices = true; # Declarative device management
|
||||
overrideFolders = true; # Declarative folder management
|
||||
|
||||
settings = {
|
||||
devices = mapAttrs (name: device: {
|
||||
inherit (device) id addresses autoAcceptFolders;
|
||||
name = if device.name != null then device.name else name;
|
||||
}) cfg.devices;
|
||||
|
||||
folders = mapAttrs (name: folder: {
|
||||
inherit (folder) path id type ignorePerms rescanInterval fsWatcherEnabled;
|
||||
devices = folder.devices;
|
||||
versioning = if folder.versioning != null then folder.versioning else {};
|
||||
}) cfg.folders;
|
||||
|
||||
options = {
|
||||
urAccepted = -1; # Disable usage reporting
|
||||
relaysEnabled = true;
|
||||
globalAnnounceEnabled = true;
|
||||
localAnnounceEnabled = true;
|
||||
} // cfg.extraOptions;
|
||||
};
|
||||
};
|
||||
|
||||
# Open firewall if requested
|
||||
networking.firewall = mkIf cfg.openFirewall {
|
||||
allowedTCPPorts = [ 22000 ]; # Syncthing protocol
|
||||
allowedUDPPorts = [ 22000 21027 ]; # Syncthing + discovery
|
||||
};
|
||||
|
||||
# Convenience alias
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeShellScriptBin "st" ''
|
||||
# Syncthing shortcut
|
||||
case "$1" in
|
||||
status)
|
||||
${pkgs.syncthing}/bin/syncthing cli show system
|
||||
;;
|
||||
scan)
|
||||
${pkgs.syncthing}/bin/syncthing cli scan --all
|
||||
;;
|
||||
errors)
|
||||
${pkgs.syncthing}/bin/syncthing cli errors
|
||||
;;
|
||||
*)
|
||||
echo "Usage: st {status|scan|errors}"
|
||||
;;
|
||||
esac
|
||||
'')
|
||||
];
|
||||
};
|
||||
}
|
||||
227
scripts/ustatus
Executable file
227
scripts/ustatus
Executable file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# ustatus - Status dashboard for the Ultimate Notetaking System
|
||||
#
|
||||
# Shows status of all sync and backup systems.
|
||||
#
|
||||
# Usage:
|
||||
# ustatus - Show full status
|
||||
# ustatus brief - Show brief status
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
status_ok() {
|
||||
echo -e "${GREEN}●${NC} $1"
|
||||
}
|
||||
|
||||
status_warn() {
|
||||
echo -e "${YELLOW}●${NC} $1"
|
||||
}
|
||||
|
||||
status_error() {
|
||||
echo -e "${RED}●${NC} $1"
|
||||
}
|
||||
|
||||
header() {
|
||||
echo -e "\n${BOLD}═══ $1 ═══${NC}"
|
||||
}
|
||||
|
||||
# Check if a command exists
|
||||
has_cmd() {
|
||||
command -v "$1" &> /dev/null
|
||||
}
|
||||
|
||||
brief_status() {
|
||||
echo -e "${BOLD}Ultimate Notetaking System Status${NC}"
|
||||
echo ""
|
||||
|
||||
# nb
|
||||
if has_cmd nb; then
|
||||
notebook_count=$(nb notebooks --names 2>/dev/null | wc -l || echo 0)
|
||||
status_ok "nb: $notebook_count notebooks"
|
||||
else
|
||||
status_error "nb: not installed"
|
||||
fi
|
||||
|
||||
# Syncthing
|
||||
if has_cmd syncthing && syncthing cli show system &> /dev/null 2>&1; then
|
||||
status_ok "Syncthing: running"
|
||||
elif has_cmd syncthing; then
|
||||
status_warn "Syncthing: not running"
|
||||
else
|
||||
status_error "Syncthing: not installed"
|
||||
fi
|
||||
|
||||
# Backup
|
||||
if systemctl is-active restic-backup.timer &> /dev/null; then
|
||||
status_ok "Backup: timer active"
|
||||
elif has_cmd restic; then
|
||||
status_warn "Backup: timer not active"
|
||||
else
|
||||
status_error "Backup: restic not installed"
|
||||
fi
|
||||
}
|
||||
|
||||
full_status() {
|
||||
echo -e "${BOLD}╔════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}║ Ultimate Notetaking, Sync & Backup System ║${NC}"
|
||||
echo -e "${BOLD}╚════════════════════════════════════════════════════════════╝${NC}"
|
||||
|
||||
# ==================== TIER 1: Configuration ====================
|
||||
header "TIER 1: Configuration"
|
||||
|
||||
# Nix/NixOS
|
||||
if has_cmd nixos-rebuild; then
|
||||
generation=$(nixos-rebuild list-generations 2>/dev/null | head -1 | awk '{print $1}' || echo "?")
|
||||
status_ok "NixOS generation: $generation"
|
||||
fi
|
||||
|
||||
if has_cmd nix; then
|
||||
nix_version=$(nix --version | head -1)
|
||||
status_ok "Nix: $nix_version"
|
||||
fi
|
||||
|
||||
# Git
|
||||
if has_cmd git; then
|
||||
status_ok "Git: $(git --version | cut -d' ' -f3)"
|
||||
fi
|
||||
|
||||
# ==================== TIER 2: Notes ====================
|
||||
header "TIER 2: Notes (nb)"
|
||||
|
||||
if has_cmd nb; then
|
||||
nb_version=$(nb version 2>/dev/null || echo "unknown")
|
||||
status_ok "nb version: $nb_version"
|
||||
|
||||
echo ""
|
||||
echo "Notebooks:"
|
||||
nb_dir="${NB_DIR:-$HOME/.nb}"
|
||||
for notebook in $(nb notebooks --names 2>/dev/null || true); do
|
||||
if [ -d "$nb_dir/$notebook" ]; then
|
||||
note_count=$(find "$nb_dir/$notebook" -name "*.md" 2>/dev/null | wc -l)
|
||||
|
||||
# Check if has remote
|
||||
if [ -d "$nb_dir/$notebook/.git" ]; then
|
||||
remote=$(git -C "$nb_dir/$notebook" remote get-url origin 2>/dev/null || echo "")
|
||||
if [ -n "$remote" ]; then
|
||||
last_sync=$(git -C "$nb_dir/$notebook" log -1 --format="%ar" 2>/dev/null || echo "never")
|
||||
echo -e " ${GREEN}●${NC} $notebook: $note_count notes, last sync: $last_sync"
|
||||
else
|
||||
echo -e " ${YELLOW}●${NC} $notebook: $note_count notes (no remote)"
|
||||
fi
|
||||
else
|
||||
echo -e " ${YELLOW}●${NC} $notebook: $note_count notes (not git repo)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
status_error "nb not installed"
|
||||
fi
|
||||
|
||||
# ==================== TIER 2: Syncthing ====================
|
||||
header "TIER 2: File Sync (Syncthing)"
|
||||
|
||||
if has_cmd syncthing; then
|
||||
if syncthing cli show system &> /dev/null 2>&1; then
|
||||
uptime=$(syncthing cli show system 2>/dev/null | jq -r '.uptime // 0' | awk '{printf "%dd %dh %dm", $1/86400, ($1%86400)/3600, ($1%3600)/60}')
|
||||
status_ok "Syncthing running (uptime: $uptime)"
|
||||
|
||||
echo ""
|
||||
echo "Folders:"
|
||||
syncthing cli show config 2>/dev/null | jq -r '.folders[] | "\(.label // .id)|\(.path)"' 2>/dev/null | while IFS='|' read -r label path; do
|
||||
if [ -d "$path" ]; then
|
||||
file_count=$(find "$path" -type f 2>/dev/null | wc -l)
|
||||
echo -e " ${GREEN}●${NC} $label: $path ($file_count files)"
|
||||
else
|
||||
echo -e " ${YELLOW}●${NC} $label: $path (not found)"
|
||||
fi
|
||||
done || echo " Unable to list folders"
|
||||
|
||||
echo ""
|
||||
echo "Devices:"
|
||||
syncthing cli show config 2>/dev/null | jq -r '.devices[] | "\(.name // .deviceID[:8])"' 2>/dev/null | while read -r device; do
|
||||
echo " ● $device"
|
||||
done || echo " Unable to list devices"
|
||||
else
|
||||
status_warn "Syncthing installed but not running"
|
||||
fi
|
||||
else
|
||||
status_error "Syncthing not installed"
|
||||
fi
|
||||
|
||||
# ==================== TIER 3: Backup ====================
|
||||
header "TIER 3: Backup (restic)"
|
||||
|
||||
if has_cmd restic; then
|
||||
status_ok "restic: $(restic version | head -1)"
|
||||
|
||||
# Check systemd timer
|
||||
if systemctl is-active restic-backup.timer &> /dev/null; then
|
||||
next_run=$(systemctl show restic-backup.timer --property=NextElapseUSecRealtime 2>/dev/null | cut -d= -f2)
|
||||
status_ok "Backup timer active (next: $next_run)"
|
||||
else
|
||||
status_warn "Backup timer not active"
|
||||
fi
|
||||
|
||||
# Try to show recent snapshots (requires RESTIC_REPOSITORY to be set)
|
||||
if [ -n "${RESTIC_REPOSITORY:-}" ]; then
|
||||
echo ""
|
||||
echo "Recent snapshots:"
|
||||
restic snapshots --last 3 2>/dev/null | tail -n +3 | head -5 || echo " Unable to list snapshots"
|
||||
fi
|
||||
else
|
||||
status_error "restic not installed"
|
||||
fi
|
||||
|
||||
# ==================== TIER 3: Services ====================
|
||||
header "TIER 3: Services"
|
||||
|
||||
# Check common services
|
||||
for service in forgejo immich jellyfin; do
|
||||
if systemctl is-active "$service" &> /dev/null 2>&1; then
|
||||
status_ok "$service: running"
|
||||
elif systemctl is-enabled "$service" &> /dev/null 2>&1; then
|
||||
status_warn "$service: enabled but not running"
|
||||
else
|
||||
echo -e " ${BLUE}○${NC} $service: not configured"
|
||||
fi
|
||||
done
|
||||
|
||||
# ==================== Summary ====================
|
||||
header "Quick Actions"
|
||||
echo " usync - Sync all (nb + Syncthing)"
|
||||
echo " ubackup now - Run backup immediately"
|
||||
echo " nb add - Create new note"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main
|
||||
case "${1:-full}" in
|
||||
brief|short|b)
|
||||
brief_status
|
||||
;;
|
||||
full|"")
|
||||
full_status
|
||||
;;
|
||||
-h|--help|help)
|
||||
echo "ustatus - System status dashboard"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " ustatus Show full status"
|
||||
echo " ustatus brief Show brief status"
|
||||
echo " ustatus help Show this help"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Run 'ustatus help' for usage"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
156
scripts/usync
Executable file
156
scripts/usync
Executable file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# usync - Unified sync command for the Ultimate Notetaking System
|
||||
#
|
||||
# Syncs both nb notebooks and triggers Syncthing scans.
|
||||
#
|
||||
# Usage:
|
||||
# usync - Sync everything
|
||||
# usync nb - Sync only nb notebooks
|
||||
# usync st - Sync only Syncthing folders
|
||||
# usync status - Show sync status
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
sync_nb() {
|
||||
log_info "Syncing nb notebooks..."
|
||||
|
||||
if ! command -v nb &> /dev/null; then
|
||||
log_error "nb not found in PATH"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get list of notebooks
|
||||
notebooks=$(nb notebooks --names 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$notebooks" ]; then
|
||||
log_warn "No nb notebooks found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Sync each notebook
|
||||
for notebook in $notebooks; do
|
||||
log_info " Syncing notebook: $notebook"
|
||||
if nb "$notebook:sync" 2>/dev/null; then
|
||||
log_success " $notebook synced"
|
||||
else
|
||||
log_warn " $notebook sync failed (no remote?)"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
sync_syncthing() {
|
||||
log_info "Triggering Syncthing scan..."
|
||||
|
||||
if ! command -v syncthing &> /dev/null; then
|
||||
log_error "syncthing not found in PATH"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if Syncthing is running
|
||||
if ! syncthing cli show system &> /dev/null; then
|
||||
log_warn "Syncthing is not running"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Trigger scan on all folders
|
||||
if syncthing cli scan --all 2>/dev/null; then
|
||||
log_success "Syncthing scan triggered"
|
||||
else
|
||||
log_error "Failed to trigger Syncthing scan"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
show_status() {
|
||||
echo "=== Sync Status ==="
|
||||
echo ""
|
||||
|
||||
# nb status
|
||||
echo "--- nb Notebooks ---"
|
||||
if command -v nb &> /dev/null; then
|
||||
nb notebooks 2>/dev/null || echo " No notebooks"
|
||||
echo ""
|
||||
echo "Last sync times:"
|
||||
for notebook in $(nb notebooks --names 2>/dev/null); do
|
||||
nb_dir="${NB_DIR:-$HOME/.nb}"
|
||||
if [ -d "$nb_dir/$notebook/.git" ]; then
|
||||
last_commit=$(git -C "$nb_dir/$notebook" log -1 --format="%ar" 2>/dev/null || echo "never")
|
||||
echo " $notebook: $last_commit"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo " nb not installed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Syncthing status
|
||||
echo "--- Syncthing ---"
|
||||
if command -v syncthing &> /dev/null && syncthing cli show system &> /dev/null; then
|
||||
syncthing cli show system 2>/dev/null | grep -E "(myID|startTime)" || true
|
||||
echo ""
|
||||
echo "Folders:"
|
||||
syncthing cli show config 2>/dev/null | jq -r '.folders[] | " \(.label // .id): \(.path)"' 2>/dev/null || echo " Unable to get folder info"
|
||||
else
|
||||
echo " Syncthing not running"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
case "${1:-all}" in
|
||||
nb)
|
||||
sync_nb
|
||||
;;
|
||||
st|syncthing)
|
||||
sync_syncthing
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
all|"")
|
||||
sync_nb
|
||||
echo ""
|
||||
sync_syncthing
|
||||
echo ""
|
||||
log_success "All sync operations complete"
|
||||
;;
|
||||
-h|--help|help)
|
||||
echo "usync - Unified sync command"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " usync Sync everything (nb + Syncthing)"
|
||||
echo " usync nb Sync only nb notebooks"
|
||||
echo " usync st Trigger Syncthing scan"
|
||||
echo " usync status Show sync status"
|
||||
echo " usync help Show this help"
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown command: $1"
|
||||
echo "Run 'usync help' for usage"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
38
secrets/secrets.yaml.example
Normal file
38
secrets/secrets.yaml.example
Normal file
@@ -0,0 +1,38 @@
|
||||
# Example secrets file for sops-nix
|
||||
#
|
||||
# This file shows the structure of secrets.yaml.
|
||||
# DO NOT put actual secrets in this file!
|
||||
#
|
||||
# To use:
|
||||
# 1. Install sops and age
|
||||
# 2. Create age key: age-keygen -o ~/.config/sops/age/keys.txt
|
||||
# 3. Create .sops.yaml in repo root with your public key
|
||||
# 4. Copy this to secrets.yaml and encrypt: sops secrets/secrets.yaml
|
||||
#
|
||||
# Example .sops.yaml:
|
||||
# ---
|
||||
# keys:
|
||||
# - &admin age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# creation_rules:
|
||||
# - path_regex: secrets/secrets\.yaml$
|
||||
# key_groups:
|
||||
# - age:
|
||||
# - *admin
|
||||
|
||||
# Restic backup password
|
||||
restic-password: "your-secure-backup-password-here"
|
||||
|
||||
# Syncthing API key (optional, for automation)
|
||||
syncthing-api-key: "your-syncthing-api-key"
|
||||
|
||||
# Cloud storage credentials
|
||||
# For Backblaze B2:
|
||||
b2-account-id: "your-b2-account-id"
|
||||
b2-account-key: "your-b2-account-key"
|
||||
|
||||
# For AWS S3:
|
||||
# aws-access-key-id: "your-aws-key"
|
||||
# aws-secret-access-key: "your-aws-secret"
|
||||
|
||||
# Forgejo admin password (initial setup)
|
||||
forgejo-admin-password: "your-forgejo-admin-password"
|
||||
Reference in New Issue
Block a user