dbxignore 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. dbxignore-0.3.0/.github/workflows/commit-check.yml +23 -0
  2. dbxignore-0.3.0/.github/workflows/release.yml +98 -0
  3. dbxignore-0.3.0/.github/workflows/test.yml +37 -0
  4. dbxignore-0.3.0/.gitignore +16 -0
  5. dbxignore-0.3.0/.pre-commit-config.yaml +15 -0
  6. dbxignore-0.3.0/CHANGELOG.md +131 -0
  7. dbxignore-0.3.0/CLAUDE.md +85 -0
  8. dbxignore-0.3.0/LICENSE +21 -0
  9. dbxignore-0.3.0/PKG-INFO +173 -0
  10. dbxignore-0.3.0/README.md +155 -0
  11. dbxignore-0.3.0/cchk.toml +35 -0
  12. dbxignore-0.3.0/docs/release-notes/v0.2.0.md +37 -0
  13. dbxignore-0.3.0/docs/release-notes/v0.2.1.md +23 -0
  14. dbxignore-0.3.0/docs/release-notes/v0.3.0.md +38 -0
  15. dbxignore-0.3.0/docs/superpowers/plans/2026-04-20-dropboxignore-implementation.md +3565 -0
  16. dbxignore-0.3.0/docs/superpowers/plans/2026-04-21-dropboxignore-v0.2-linux-followups.md +114 -0
  17. dbxignore-0.3.0/docs/superpowers/plans/2026-04-21-dropboxignore-v0.2-linux.md +1747 -0
  18. dbxignore-0.3.0/docs/superpowers/plans/2026-04-22-dropboxignore-negation-polish-followups.md +204 -0
  19. dbxignore-0.3.0/docs/superpowers/plans/2026-04-22-dropboxignore-negation-semantics.md +1573 -0
  20. dbxignore-0.3.0/docs/superpowers/plans/2026-04-23-v0.3-dbxignore-rename.md +1747 -0
  21. dbxignore-0.3.0/docs/superpowers/specs/2026-04-20-dropboxignore-design.md +494 -0
  22. dbxignore-0.3.0/docs/superpowers/specs/2026-04-21-dropboxignore-negation-semantics.md +280 -0
  23. dbxignore-0.3.0/docs/superpowers/specs/2026-04-21-dropboxignore-v0.2-linux.md +463 -0
  24. dbxignore-0.3.0/docs/superpowers/specs/2026-04-23-v0.3-dbxignore-rename.md +268 -0
  25. dbxignore-0.3.0/pyinstaller/dbxignore.spec +80 -0
  26. dbxignore-0.3.0/pyproject.toml +53 -0
  27. dbxignore-0.3.0/src/dbxignore/__init__.py +6 -0
  28. dbxignore-0.3.0/src/dbxignore/__main__.py +15 -0
  29. dbxignore-0.3.0/src/dbxignore/_backends/__init__.py +0 -0
  30. dbxignore-0.3.0/src/dbxignore/_backends/linux_xattr.py +91 -0
  31. dbxignore-0.3.0/src/dbxignore/_backends/windows_ads.py +61 -0
  32. dbxignore-0.3.0/src/dbxignore/_version.py +24 -0
  33. dbxignore-0.3.0/src/dbxignore/cli.py +359 -0
  34. dbxignore-0.3.0/src/dbxignore/daemon.py +324 -0
  35. dbxignore-0.3.0/src/dbxignore/debounce.py +96 -0
  36. dbxignore-0.3.0/src/dbxignore/install/__init__.py +33 -0
  37. dbxignore-0.3.0/src/dbxignore/install/linux_systemd.py +168 -0
  38. dbxignore-0.3.0/src/dbxignore/install/windows_task.py +108 -0
  39. dbxignore-0.3.0/src/dbxignore/markers.py +32 -0
  40. dbxignore-0.3.0/src/dbxignore/reconcile.py +104 -0
  41. dbxignore-0.3.0/src/dbxignore/roots.py +81 -0
  42. dbxignore-0.3.0/src/dbxignore/rules.py +537 -0
  43. dbxignore-0.3.0/src/dbxignore/state.py +116 -0
  44. dbxignore-0.3.0/tests/__init__.py +0 -0
  45. dbxignore-0.3.0/tests/conftest.py +50 -0
  46. dbxignore-0.3.0/tests/fixtures/info_malformed.json +1 -0
  47. dbxignore-0.3.0/tests/fixtures/info_not_object.json +1 -0
  48. dbxignore-0.3.0/tests/fixtures/info_personal.json +8 -0
  49. dbxignore-0.3.0/tests/fixtures/info_personal_business.json +12 -0
  50. dbxignore-0.3.0/tests/test_ads_unit.py +25 -0
  51. dbxignore-0.3.0/tests/test_cli_apply.py +37 -0
  52. dbxignore-0.3.0/tests/test_cli_enotsup.py +39 -0
  53. dbxignore-0.3.0/tests/test_cli_status_list_explain.py +184 -0
  54. dbxignore-0.3.0/tests/test_daemon_dispatch.py +163 -0
  55. dbxignore-0.3.0/tests/test_daemon_logging.py +153 -0
  56. dbxignore-0.3.0/tests/test_daemon_singleton.py +57 -0
  57. dbxignore-0.3.0/tests/test_daemon_smoke.py +70 -0
  58. dbxignore-0.3.0/tests/test_daemon_smoke_linux.py +158 -0
  59. dbxignore-0.3.0/tests/test_daemon_sweep.py +129 -0
  60. dbxignore-0.3.0/tests/test_debounce.py +163 -0
  61. dbxignore-0.3.0/tests/test_install.py +287 -0
  62. dbxignore-0.3.0/tests/test_linux_reconcile_smoke.py +84 -0
  63. dbxignore-0.3.0/tests/test_linux_systemd.py +304 -0
  64. dbxignore-0.3.0/tests/test_linux_xattr_integration.py +102 -0
  65. dbxignore-0.3.0/tests/test_markers_facade.py +33 -0
  66. dbxignore-0.3.0/tests/test_reconcile_basic.py +65 -0
  67. dbxignore-0.3.0/tests/test_reconcile_edges.py +141 -0
  68. dbxignore-0.3.0/tests/test_reconcile_enotsup.py +59 -0
  69. dbxignore-0.3.0/tests/test_reconcile_return_state.py +173 -0
  70. dbxignore-0.3.0/tests/test_roots.py +140 -0
  71. dbxignore-0.3.0/tests/test_rules_basic.py +60 -0
  72. dbxignore-0.3.0/tests/test_rules_case_and_protection.py +24 -0
  73. dbxignore-0.3.0/tests/test_rules_concurrency.py +55 -0
  74. dbxignore-0.3.0/tests/test_rules_conflicts.py +185 -0
  75. dbxignore-0.3.0/tests/test_rules_hierarchical.py +85 -0
  76. dbxignore-0.3.0/tests/test_rules_load_caching.py +125 -0
  77. dbxignore-0.3.0/tests/test_rules_reload_explain.py +317 -0
  78. dbxignore-0.3.0/tests/test_smoke.py +4 -0
  79. dbxignore-0.3.0/tests/test_state.py +58 -0
  80. dbxignore-0.3.0/tests/test_windows_ads_integration.py +49 -0
  81. dbxignore-0.3.0/uv.lock +206 -0
@@ -0,0 +1,23 @@
1
+ name: commit-check
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened]
6
+
7
+ permissions:
8
+ contents: read
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ check:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+ with:
17
+ fetch-depth: 0
18
+ - uses: commit-check/commit-check-action@v2.6.0
19
+ with:
20
+ message: true
21
+ branch: true
22
+ job-summary: true
23
+ pr-comments: true
@@ -0,0 +1,98 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+ # Manual dispatch lets us dry-run the build without cutting a tag.
7
+ # The publish jobs are tag-gated, so dispatch runs exercise build
8
+ # + artifact upload but skip both publish destinations.
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: windows-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v5
24
+
25
+ - name: Sync deps
26
+ run: uv sync --all-extras
27
+
28
+ - name: Build wheel + sdist
29
+ run: uv build
30
+
31
+ - name: Build Windows binaries
32
+ # pyinstaller is a build-only tool, not a runtime or dev dep — install
33
+ # ephemerally for this run rather than declaring it in pyproject.toml.
34
+ run: uv run --with pyinstaller pyinstaller pyinstaller/dbxignore.spec
35
+
36
+ - name: Upload dist artifacts
37
+ uses: actions/upload-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+ retention-days: 7
42
+
43
+ publish-github:
44
+ # GitHub Release: wheel + sdist + Windows binaries. Runs on every tag push.
45
+ needs: build
46
+ if: startsWith(github.ref, 'refs/tags/')
47
+ runs-on: ubuntu-latest
48
+ permissions:
49
+ contents: write
50
+ steps:
51
+ - name: Download dist artifacts
52
+ uses: actions/download-artifact@v4
53
+ with:
54
+ name: dist
55
+ path: dist/
56
+
57
+ - name: Publish GitHub Release
58
+ uses: softprops/action-gh-release@v2
59
+ with:
60
+ files: |
61
+ dist/*.whl
62
+ dist/*.tar.gz
63
+ dist/dbxignore.exe
64
+ dist/dbxignored.exe
65
+ generate_release_notes: true
66
+ # When `GH_RELEASE_TOKEN` is configured as a repo secret (a PAT with
67
+ # `contents: write` scope belonging to the repo owner), releases are
68
+ # authored under the owner's GitHub identity. Falls back to the
69
+ # default `github.token` (author: github-actions[bot]) when the
70
+ # secret isn't set, so the workflow never fails because of a
71
+ # missing secret — it just reverts to the bot-author attribution.
72
+ token: ${{ secrets.GH_RELEASE_TOKEN || github.token }}
73
+
74
+ publish-pypi:
75
+ # PyPI via Trusted Publishing (OIDC). Gated on the `pypi` GitHub
76
+ # environment, which requires manual approval before the upload
77
+ # step runs. The id-token:write permission is scoped to this job
78
+ # only — least privilege.
79
+ needs: build
80
+ if: startsWith(github.ref, 'refs/tags/')
81
+ runs-on: ubuntu-latest
82
+ environment: pypi
83
+ permissions:
84
+ id-token: write
85
+ steps:
86
+ - name: Download dist artifacts
87
+ uses: actions/download-artifact@v4
88
+ with:
89
+ name: dist
90
+ path: dist/
91
+
92
+ - name: Strip PyInstaller outputs (PyPI gets wheel + sdist only)
93
+ run: rm -f dist/*.exe
94
+
95
+ - name: Publish to PyPI
96
+ uses: pypa/gh-action-pypi-publish@release/v1
97
+ with:
98
+ packages-dir: dist/
@@ -0,0 +1,37 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ strategy:
10
+ fail-fast: false
11
+ matrix:
12
+ os: [ubuntu-latest, windows-latest]
13
+ runs-on: ${{ matrix.os }}
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v5
21
+
22
+ - name: Sync deps
23
+ run: uv sync --all-extras
24
+
25
+ - name: Lint
26
+ run: uv run ruff check
27
+
28
+ - name: Unit tests (non-Windows-only)
29
+ run: uv run pytest -m "not windows_only and not linux_only" -v
30
+
31
+ - name: Windows-only integration tests
32
+ if: runner.os == 'Windows'
33
+ run: uv run pytest -m windows_only -v
34
+
35
+ - name: Linux-only integration tests
36
+ if: runner.os == 'Linux'
37
+ run: uv run pytest -m linux_only -v
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ venv/
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ src/dbxignore/_version.py
11
+ .uv/
12
+ *.dist-info/
13
+ __editable__*.pth
14
+
15
+ # Claude Code session state (ScheduleWakeup lock files, etc.)
16
+ .claude/
@@ -0,0 +1,15 @@
1
+ # Local enforcement of the rules in cchk.toml. Install once per clone:
2
+ # uv tool install pre-commit
3
+ # pre-commit install --hook-type commit-msg --hook-type pre-push
4
+ # After that, `git commit` and `git push` run these checks automatically.
5
+ # CI re-runs them via commit-check-action, so local install is a convenience,
6
+ # not a merge gate — see CLAUDE.md "Git workflow".
7
+
8
+ repos:
9
+ - repo: https://github.com/commit-check/commit-check
10
+ rev: v2.6.0
11
+ hooks:
12
+ - id: check-message
13
+ stages: [commit-msg]
14
+ - id: check-branch
15
+ stages: [pre-push]
@@ -0,0 +1,131 @@
1
+ # Changelog
2
+
3
+ All notable changes to dropboxignore are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0] — 2026-04-24
9
+
10
+ Renames the project's owned surfaces from `dropboxignore` to `dbxignore`. The `.dropboxignore` rule-file name and the `com.dropbox.ignored` marker key are Dropbox's contracts and are **not** changed.
11
+
12
+ **Upgrade path (clean break):** on an existing v0.2.x install, run `dropboxignore uninstall --purge` to clear all ignore markers and remove v0.2.x local state, then `pip install dbxignore` (or download the new binaries), then `dbxignore install`. Your `.dropboxignore` rule files carry over untouched — they are not renamed and require no edits.
13
+
14
+ **GitHub repo rename:** `kiloscheffer/dropboxignore` → `kiloscheffer/dbxignore`, performed out-of-tree. GitHub auto-redirects handle all existing URLs.
15
+
16
+ ### Added
17
+
18
+ - **Published to PyPI as `dbxignore`.** First PyPI release — install with `pip install dbxignore` or `uv pip install dbxignore`.
19
+ - **PyPI publishing via Trusted Publishing (OIDC), gated on the `pypi` GitHub environment.** Release workflow (`.github/workflows/release.yml`) split into `build`, `publish-github`, and `publish-pypi` jobs. The PyPI upload step runs only after a required maintainer approval — single human checkpoint at the one irreversible step in the pipeline. No long-lived PyPI API token is stored as a repo secret; OIDC issues a short-lived credential scoped to this workflow and job.
20
+
21
+ ### Changed
22
+
23
+ - **PyPI distribution name: `dropboxignore` → `dbxignore`.** **Breaking** — `pip install dropboxignore` will no longer receive updates; switch to `pip install dbxignore`.
24
+ - **Python package directory: `src/dropboxignore/` → `src/dbxignore/`.** **Breaking** — any code that imports `dropboxignore.*` must be updated to `dbxignore.*`.
25
+ - **CLI entry points: `dropboxignore` / `dropboxignored` → `dbxignore` / `dbxignored`.** **Breaking** — shell scripts, aliases, and Task Scheduler / systemd registrations using the old names must be recreated. Run `dropboxignore uninstall` before upgrading, then `dbxignore install` after.
26
+ - **Logger hierarchy root: `dropboxignore` → `dbxignore`.** Affects any external log filter or handler referencing the old name (e.g. `logging.getLogger("dropboxignore")`).
27
+ - **Environment variables: `DROPBOXIGNORE_*` → `DBXIGNORE_*`.** All public env vars (`DBXIGNORE_ROOT`, `DBXIGNORE_DEBOUNCE_RULES_MS`, `DBXIGNORE_DEBOUNCE_DIRS_MS`, `DBXIGNORE_DEBOUNCE_OTHER_MS`) are renamed. **Breaking** — old names are not read.
28
+ - **Per-user state and log directory:**
29
+ - Windows: `%LOCALAPPDATA%\dropboxignore\` → `%LOCALAPPDATA%\dbxignore\`
30
+ - Linux: `$XDG_STATE_HOME/dropboxignore/` → `$XDG_STATE_HOME/dbxignore/` (fallback `~/.local/state/dbxignore/`)
31
+ - **Breaking** — existing `state.json` and `daemon.log` are not migrated automatically. `dropboxignore uninstall --purge` removes v0.2.x state as part of the recommended upgrade path.
32
+ - **systemd user unit: `dropboxignore.service` → `dbxignore.service`.** **Breaking** — the old unit name is not recognized; `dropboxignore uninstall` must be run on v0.2.x before upgrading.
33
+ - **Windows Task Scheduler task name: `dropboxignore` → `dbxignore`.** Same clean-break requirement.
34
+ - **PyInstaller binaries: `dropboxignore.exe` / `dropboxignored.exe` → `dbxignore.exe` / `dbxignored.exe`.** GitHub Release assets are renamed accordingly; the PyInstaller spec is now `pyinstaller/dbxignore.spec`.
35
+ - **GitHub Release asset names** changed to `dbxignore.exe` / `dbxignored.exe` to match the renamed entry points.
36
+ - **README** updated throughout: install examples, CLI examples, env-var reference table, state/log paths, systemd unit name, and GitHub repo links all reflect the new `dbxignore` name. An "Upgrading from v0.2.x" section with step-by-step instructions was added.
37
+ - **v0.2.0-era Linux legacy state-path fallback removed.** `state._legacy_linux_path()` and its transparent read fallback from `~/AppData/Local/dropboxignore/` are gone. The v0.2.0 CHANGELOG had scheduled this for v0.4; it is brought forward to v0.3 because the clean-break upgrade path (`dropboxignore uninstall --purge` before `dbxignore install`) eliminates any remaining callers. **Breaking** — anyone who skipped `uninstall --purge` on v0.2.x and had a legacy path will not have their old state read; run `dbxignore install` and let the daemon rebuild state from scratch.
38
+
39
+ ## [0.2.1] — 2026-04-22
40
+
41
+ Maintenance release. Release-workflow hardening and project-documentation scaffolding. **No user-facing behavior changes.** Existing `.dropboxignore` rules, CLI commands, and daemon behavior are identical to v0.2.0; upgrade is a no-op for anyone running v0.2.0 today.
42
+
43
+ ### Added
44
+
45
+ - **`workflow_dispatch` trigger on `.github/workflows/release.yml`.** The release workflow is now manually runnable via `gh workflow run release.yml` (or the GitHub Actions UI) for dry-run validation without cutting a tag. The `Publish GitHub Release` step is gated on `startsWith(github.ref, 'refs/tags/')`, so dispatch runs build + surface artifacts in the workflow run summary but don't create a Release object. Prevents the "workflow's first real exercise is the actual release" failure mode.
46
+ - **`GH_RELEASE_TOKEN` PAT override on the Publish step.** When the repo secret `GH_RELEASE_TOKEN` is set (fine-grained PAT with `Contents: Read and write`), releases attribute to the repo owner instead of `github-actions[bot]`. Missing secret falls back to the default `GITHUB_TOKEN` via a `||` expression — zero risk of workflow breakage if the PAT isn't configured or expires.
47
+ - **`CHANGELOG.md`** — this file. Retrospective v0.1.0 and v0.2.0 entries plus this one, following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
48
+ - **`docs/release-notes/v<X.Y.Z>.md`** convention. Hand-crafted per-release bodies override the workflow's auto-generated PR list via `gh release edit <tag> --notes-file docs/release-notes/<tag>.md` after the workflow publishes. Each release's body is versioned alongside its tag.
49
+
50
+ ### Documentation
51
+
52
+ - **CLAUDE.md Git workflow:** new bullet documenting a pre-flight snippet that runs `commit-check` against every commit in `origin/main..HEAD`, matching CI's behavior. Prevents the "local HEAD-only check passes, intermediate commit trips CI, amend + force-push" round-trip hit on PR #12.
53
+ - **CLAUDE.md Release:** additional bullets for `hatch-vcs`-derived versioning (no manual `pyproject.toml` bumps), the Keep a Changelog + per-version release-notes conventions, and the pre-1.0 SemVer stance (breaking changes ride MINOR bumps with explicit `**Breaking**` callouts).
54
+ - **`docs/superpowers/plans/2026-04-22-dropboxignore-negation-polish-followups.md`:** expanded backlog — items 9–13 covering release-workflow gaps, the PyPI + rename dependency chain, and the Node.js 20 action deprecation timeline.
55
+
56
+ ## [0.2.0] — 2026-04-22
57
+
58
+ First cross-platform release. Adds Linux support alongside the existing Windows port, plus rule-conflict detection, cross-platform CI with Conventional Commits enforcement, and significant UX + docs hardening.
59
+
60
+ ### Added
61
+
62
+ #### Linux support
63
+
64
+ - **`user.com.dropbox.ignored` xattr backend** covering files and directories. Tested on Ubuntu 22.04 / 24.04.
65
+ - **systemd user-unit integration** — `dropboxignore install` writes `~/.config/systemd/user/dropboxignore.service`, runs `daemon-reload` + `enable --now`. `dropboxignore uninstall` is the symmetric operation.
66
+ - **XDG-compliant paths** — `state.json` and `daemon.log` land at `$XDG_STATE_HOME/dropboxignore/` (fallback `~/.local/state/dropboxignore/`).
67
+ - **Dual-sink logging** — records flow to both the rotating file and `sys.stderr` so systemd-journald captures them (`journalctl --user -u dropboxignore.service`).
68
+ - **Linux root discovery** from `~/.dropbox/info.json`.
69
+ - Graceful handling of filesystems that reject `user.*` xattrs (tmpfs without `user_xattr`, vfat, some FUSE mounts) — `OSError(errno.ENOTSUP|EOPNOTSUPP)` is treated as WARNING + continue, not a sweep abort.
70
+ - Linux xattr operations use `follow_symlinks=False`; symlinks cannot themselves carry `user.*` xattrs (kernel restriction), handled via existing `PermissionError` arm.
71
+
72
+ #### Rule-conflict detection
73
+
74
+ - `.dropboxignore` negation patterns whose target lives under a directory ignored by an earlier rule (canonical case: `build/` + `!build/keep/`) are detected at rule-load time and **dropped from the active rule set**. Dropbox's ignored-folder inheritance makes such negations inert regardless of xattr state; the tool now surfaces the mismatch rather than letting users discover the failure via sync surprise.
75
+ - Three diagnostic surfaces: daemon-log WARNING, `dropboxignore status` "rule conflicts" section, `dropboxignore explain` `[dropped]` annotation with a pointer to the masking rule.
76
+ - Design doc: `docs/superpowers/specs/2026-04-21-dropboxignore-negation-semantics.md`.
77
+
78
+ #### Configuration & escape hatches
79
+
80
+ - **`DROPBOXIGNORE_ROOT` environment variable** — pre-`info.json` override for non-stock Dropbox installs. Set to an existing absolute path → that path is the sole Dropbox root. Automatically forwarded into the generated systemd unit at `dropboxignore install` time so shell-exported values survive the service boundary.
81
+
82
+ #### CI & repo hygiene
83
+
84
+ - **Conventional Commits + Conventional Branch enforcement** via [`commit-check-action@v2.6.0`](https://github.com/commit-check/commit-check-action) on every PR. `cchk.toml` at repo root is the single source of truth shared by the local `pre-commit` hook (commit-msg + pre-push stages) and CI.
85
+ - Linux test leg — `pytest -m linux_only` runs on `ubuntu-latest` alongside the existing Windows leg.
86
+ - Linux daemon smoke test with a `"watching roots:"` log-line readiness probe (inotify's strict post-`observer.schedule()` event window).
87
+ - Real-xattr reconcile integration test and full-daemon-loop integration test.
88
+
89
+ ### Changed
90
+
91
+ - **`dropboxignore uninstall --purge` now matches its name.** Previously cleared only ignore markers. Now also deletes `state.json`, `daemon.log` + rotated backups, the state directory itself (if empty — user-authored content preserved via `rmdir` not `rmtree`), and on Linux the systemd drop-in directory `~/.config/systemd/user/dropboxignore.service.d/`. Dropbox's sync behavior is unaffected — only our own bookkeeping is removed. **Breaking** for any automation that relied on `state.json` surviving `--purge`.
92
+ - **`dropboxignore explain` output format** — compact relative paths (via a formatter shared with `status`) and two-space field separators. The previous `path:line: = pattern` arrow-style form is replaced; include/negation distinction is now conveyed by the leading `!` on the raw pattern text. **Breaking** for any script that parses `explain` output.
93
+ - **`state.default_path()` on Linux migrated to XDG.** Pre-v0.2 Linux installs wrote to `~/AppData/Local/dropboxignore/` — a Windows-shaped tree inside a Linux HOME. Existing installs are read transparently from the legacy path for one release with a WARNING; the next daemon write migrates forward. Legacy fallback to be removed in v0.4.
94
+ - **`state.user_state_dir()`** is the single source of truth for the per-user state/log directory, used by both `state.default_path()` and `daemon._log_dir()`.
95
+
96
+ ### Fixed
97
+
98
+ - `cli.install` catches `RuntimeError` from the install backend and exits with `2` + a clean stderr message, mirroring `cli.uninstall`'s existing behavior. Previously install-backend failures escaped as raw Python tracebacks.
99
+ - `install/linux_systemd.py` emits POSIX paths in `ExecStart` regardless of the build platform.
100
+
101
+ ### Documentation
102
+
103
+ - README sections: Install (Linux), Configuration (with env-var reference table), Logs (with platform breakdown), State (with legacy-fallback note), Negations and Dropbox's ignore inheritance.
104
+ - CLAUDE.md expanded: Linux-specific gotchas, rule-cache conflict invariant, Git workflow section pointing at `cchk.toml`.
105
+ - Design specs and implementation plans for each major v0.2 arc under `docs/superpowers/`.
106
+
107
+ ## [0.1.0] — 2026-04-21
108
+
109
+ Initial release. Windows-only.
110
+
111
+ ### Added
112
+
113
+ - **Hierarchical `.dropboxignore` files** — drop a `.dropboxignore` at any level of a Dropbox tree; rules apply recursively from there. Supports full gitignore syntax via `pathspec`, including negations and anchored paths.
114
+ - **NTFS Alternate Data Stream backend** — writes the `com.dropbox.ignored` ADS that Dropbox's Windows client reads to skip sync.
115
+ - **Dual-trigger daemon** — `watchdog` observer for real-time filesystem events + hourly safety-net sweep + initial full sweep on startup (catches offline drift).
116
+ - **Event debouncer** — coalesces bursts of related events; per-event-kind timeouts (`RULES` 100 ms, `DIR_CREATE` 0 ms, `OTHER` 500 ms), configurable via `DROPBOXIGNORE_DEBOUNCE_{RULES,DIRS,OTHER}_MS`.
117
+ - **Case-insensitive rule matching** — NTFS-appropriate; `node_modules/` matches a directory named `Node_Modules`.
118
+ - **Automatic Dropbox root discovery** from `%APPDATA%\Dropbox\info.json` (Personal + Business accounts).
119
+ - **Task Scheduler integration** — `dropboxignore install` registers a user-logon trigger via `schtasks` XML; `dropboxignore uninstall` removes it.
120
+ - **CLI commands** — `apply` (one-shot reconcile), `status` (daemon pid, last sweep, last error), `list` (print all marked paths), `explain PATH` (show matching rules), `daemon` (run in foreground), `install` / `uninstall`.
121
+ - **`uninstall --purge` flag** — clears every ignore marker under each discovered root. (v0.2 broadens this to also remove local state.)
122
+ - **Rotating log file** at `%LOCALAPPDATA%\dropboxignore\daemon.log` (5 MB × 4 backups).
123
+ - **Persisted state** at `%LOCALAPPDATA%\dropboxignore\state.json` (daemon pid, sweep stats, watched roots).
124
+ - **`.dropboxignore` protection** — the rule file itself is never marked ignored; any stray marker on one is cleared with a WARNING.
125
+ - **PyInstaller-built standalone binaries** — `dropboxignore.exe` + `dropboxignored.exe`, published via GitHub Releases.
126
+ - **Windows test leg** with `pytest -m windows_only` NTFS-ADS integration tests.
127
+
128
+ [0.3.0]: https://github.com/kiloscheffer/dbxignore/releases/tag/v0.3.0
129
+ [0.2.1]: https://github.com/kiloscheffer/dropboxignore/releases/tag/v0.2.1
130
+ [0.2.0]: https://github.com/kiloscheffer/dropboxignore/releases/tag/v0.2.0
131
+ [0.1.0]: https://github.com/kiloscheffer/dropboxignore/pull/1
@@ -0,0 +1,85 @@
1
+ # dbxignore
2
+
3
+ Cross-platform Python utility: keeps Dropbox ignore markers (NTFS alternate data streams on Windows; `user.com.dropbox.ignored` xattrs on Linux) in sync with hierarchical `.dropboxignore` files.
4
+
5
+ ## Commands
6
+
7
+ - `uv sync --all-extras` — install
8
+ - `uv run pytest` — full suite; Windows adds a few ADS-integration tests via `@pytest.mark.windows_only`
9
+ - `uv run pytest -m "not windows_only"` — portable subset (what Ubuntu CI runs)
10
+ - `uv run pytest -W error::DeprecationWarning` — local strict mode (not enforced in CI)
11
+ - `uv run ruff check` — lint; rules E, F, I, B, UP, SIM; line length 100
12
+ - `dbxignore <apply|status|list|explain|daemon|install|uninstall>` — CLI console script (`cli:main`). `install` / `uninstall` register / remove the daemon with the platform's user-scoped service manager (Task Scheduler on Windows, systemd user unit on Linux). `uninstall --purge` also clears every ignore marker.
13
+ - `dbxignored` — daemon shim (`cli:daemon_main`), launched by the installed Scheduled Task
14
+
15
+ ## Architecture
16
+
17
+ `reconcile.reconcile_subtree(root, subdir, cache)` is the single source of truth for rule-driven marker mutations. `cli.apply`, `daemon._dispatch`, and `daemon._sweep_once` all call it — never bypass. The lone exception is `cli.uninstall --purge`, which issues an unconditional marker clear (no rule evaluation) while still honoring the `.dropboxignore`-found-marked `WARNING` contract inline.
18
+
19
+ Marker I/O is platform-dispatched via `dbxignore.markers`, which at import time re-exports `is_ignored`/`set_ignored`/`clear_ignored` from `_backends/windows_ads.py` (Windows NTFS ADS) or `_backends/linux_xattr.py` (Linux `user.com.dropbox.ignored`). No other module branches on `sys.platform` for markers. `reconcile._reconcile_path` catches `OSError(errno.ENOTSUP|EOPNOTSUPP)` from the Linux backend and treats it the same way as `PermissionError` — log WARNING, append to `Report.errors`, continue the sweep.
20
+
21
+ `daemon._sweep_once` fans `reconcile_subtree` out across roots via `ThreadPoolExecutor` (one worker per root). Safe because reconcile reads the cache lock-free (single-op `.get()`s) and writes per-file ignore markers on disjoint paths. `RuleCache._rules` is guarded by a `threading.RLock` — any mutation (`load_root`, `reload_file`, `remove_file`, or the stale-purge iteration in `load_root`) must go through it, otherwise the debouncer thread can race with the main-thread sweep. If you add cross-root shared state to `RuleCache` or reconcile, revisit this.
22
+
23
+ `rules.RuleCache` stores one `_LoadedRules(lines, entries, mtime_ns, size)` per `.dropboxignore`. `entries` is a list of `(source_line_index, pathspec.Pattern)` pairs and is the single source of truth for both `match()` and `explain()`.
24
+
25
+ `rules._load_if_changed` skips reparse when a `.dropboxignore`'s `mtime_ns` and `size` both match the cached values — that's why `_LoadedRules` carries stat fields. The sweep path (`load_root`) uses it; watchdog-driven `reload_file` bypasses it because an explicit event is authoritative.
26
+
27
+ The daemon's watchdog events are classified (`_classify` → `EventKind.{RULES,DIR_CREATE,OTHER}`) and funneled through `Debouncer` before `_dispatch` runs `reconcile_subtree`. `DEFAULT_TIMEOUTS_MS` per kind is overridable via `DBXIGNORE_DEBOUNCE_{RULES,DIRS,OTHER}_MS`.
28
+
29
+ `install/` is a package with `__init__.py` exposing `install_service()` / `uninstall_service()` dispatchers, plus `windows_task.py` (schtasks XML generation + invocation) and `linux_systemd.py` (writes `~/.config/systemd/user/dbxignore.service`, runs `systemctl --user daemon-reload && enable --now`). `cli.install` / `cli.uninstall` delegate only to the package dispatcher; don't call backend modules directly from the CLI layer.
30
+
31
+ ## Gotchas
32
+
33
+ - pathspec 1.0.4: subclass `GitIgnoreSpecPattern`, not deprecated `GitWildMatchPattern`.
34
+ - pathspec 1.0.4: `spec.check_file(path)` returns `CheckResult(include, index, file)` — use when you need pattern-level verdicts beyond a bare bool.
35
+ - pathspec: `pattern.match_file()` is public; `pattern.regex.match` is private API.
36
+ - pathspec: directory-only rules (`node_modules/`) require trailing `/` on the tested path string.
37
+ - pathspec: a line with leading whitespace before `#` (e.g. `" # indented"`) is an *active pattern*, not a comment — `rules._build_entries` detects the count mismatch and falls back to per-line reparse.
38
+ - `_backends/windows_ads` uses `open(r"\\?\path:com.dropbox.ignored")` directly — `\\?\` prefix mandatory for >260-char paths.
39
+ - NTFS is case-insensitive; `_CaseInsensitiveGitIgnorePattern` prepends `(?i)` to compiled regexes.
40
+ - `.dropboxignore` files are never marked ignored — guarded in `match()` and `explain()`; `reconcile._reconcile_path` clears any ADS marker it finds on one and logs at `WARNING` (spec contract — don't silence it in a refactor).
41
+ - `rules.match/explain` and `markers.{is,set,clear}_ignored` all require **absolute** paths and raise `ValueError` on relative ones. Resolve at the CLI/daemon boundary, never inside the cache or markers layer — `Path.resolve()` on Windows is a per-call syscall that dominated sweep wall-clock before.
42
+ - `daemon._configured_logging()` is a context manager: it snapshots the `dbxignore` logger on enter and restores handlers/propagate/level on exit. On Linux it installs two handlers — `RotatingFileHandler(daemon.log)` plus `StreamHandler(sys.stderr)` so systemd-journald captures the same records — on Windows only the file handler. Tests that count handlers must branch on `sys.platform`. `run()` wraps its body in it, so tests that call `daemon.run()` don't need to hand-restore logger state — but if you mock it out in a test, use `contextlib.nullcontext` (see `test_daemon_singleton.py`).
43
+ - Use `datetime.UTC`, not `timezone.utc` (ruff UP017).
44
+ - Test helpers (`FakeMarkers`, `fake_markers` fixture, `write_file` fixture) live in `tests/conftest.py` and are auto-available to every test module.
45
+ - Log-contract tests use `caplog.at_level(logging.WARNING, logger="dbxignore.<module>")` — narrow to the submodule that emits the log (see `tests/test_reconcile_edges.py`).
46
+ - Windows-only tests: set `pytestmark = pytest.mark.windows_only` at module level and guard with `if sys.platform != "win32": pytest.skip(..., allow_module_level=True)` so non-Windows collection skips cleanly.
47
+ - Daemon/sweep tests that trigger state writes: `monkeypatch.setattr(state, "default_path", lambda: tmp_path / "state.json")` redirects the persisted state off the real per-user state dir and keeps the test hermetic. Per-user state dir is `%LOCALAPPDATA%\dbxignore\` on Windows and `$XDG_STATE_HOME/dbxignore/` (fallback `~/.local/state/dbxignore/`) on Linux; the single source of truth is `state.user_state_dir()`, used by both `state.default_path()` and `daemon._log_dir()`. The v0.2-era Linux legacy-path fallback (`~/AppData/Local/dropboxignore/state.json`) was removed in v0.3 — clean-break upgrade path via `dropboxignore uninstall --purge` means no v0.2.x state survives, so the fallback had no remaining callers.
48
+ - Root-discovery test seams, by layer: CLI tests monkeypatch `cli._discover_roots`; daemon tests monkeypatch `daemon.roots_module.discover`; `roots.discover()` unit tests use `_stage_info(monkeypatch, tmp_path, fixture_name)` which sets `APPDATA` (Windows) or `HOME` (Linux) to point at a staged `Dropbox/info.json` / `.dropbox/info.json`. Pick the layer that matches the code path under test.
49
+ - Linux xattrs vanish silently through common operations: `cp` without `-a`, cross-filesystem `mv`, most archivers, and `vim`'s default save-via-rename. The watchdog event stream + hourly sweep are the recovery mechanism — don't add a preservation wrapper, the design intentionally leans on reconcile.
50
+ - Linux backends use `follow_symlinks=False` on all xattr calls (mirrors `os.walk(followlinks=False)` in reconcile). A symlink marked ignored means the link itself is marked, not its target — **except** that the Linux kernel refuses `user.*` xattrs on symlinks entirely (EPERM). `set_ignored`/`clear_ignored` raise `PermissionError` on symlinks, which reconcile's existing `PermissionError` arm already handles (log + skip). `is_ignored` on a symlink returns False (ENODATA).
51
+ - `roots.discover()` branches on `sys.platform`: `%APPDATA%\Dropbox\info.json` on Windows, `~/.dropbox/info.json` on Linux. Same JSON schema. The `_info_json_path()` helper returns `None` on unsupported platforms (raises a WARNING log); `discover()` then returns `[]`.
52
+ - `roots.discover()` honors `DBXIGNORE_ROOT` as a pre-`info.json` escape hatch for non-stock Dropbox installs. Set to an existing absolute path → `[Path(env)]`, bypassing `info.json` (and the platform check — useful on platforms where `_info_json_path()` returns `None`). Set to a nonexistent path → WARNING + `[]`. Empty string → treated as unset. Single-root only; multi-account setups with an override have to pick one.
53
+ - `RuleCache` runs a static conflict detector at every mutation (`load_root`, `reload_file`, `remove_file`). Negations whose literal path prefix lives under a directory matched by an earlier include rule are recorded in `_conflicts` and their `(source, line_idx)` tuple is added to `_dropped`. `match()` and reconcile ignore anything in `_dropped`; `explain()` still returns dropped matches but sets `Match.is_dropped=True` so CLI/log formatters can annotate them. Semantic reason: Dropbox's ignored-folder inheritance makes negations inert under ignored ancestors, so we don't pretend they're in effect.
54
+
55
+ ## Git workflow
56
+
57
+ - Never commit directly to `main`. Work on a topic branch and open a PR — that's what triggers `.github/workflows/test.yml` (the platform-gated test tiers `pytest -m windows_only` and `pytest -m linux_only` **only run in CI**) and `.github/workflows/commit-check.yml` (commit-message + branch-name validation). A local `uv run pytest` can only exercise one platform, so a single green local run is not a merge gate. The PR matrix is.
58
+ - **`cchk.toml` at repo root is the single source of truth** for allowed commit types (Conventional Commits, see `allow_commit_types`), branch types (Conventional Branch, see `allow_branch_types`), and the subject-length cap. Don't restate those lists here or elsewhere — reference the file so it can't drift. Local enforcement is optional but encouraged: `uv tool install pre-commit && pre-commit install --hook-type commit-msg --hook-type pre-push` wires the same rules at commit/push time. CI re-runs them on every PR via `commit-check/commit-check-action@v2.6.0`.
59
+ - **Pre-flight against every commit, not just HEAD.** CI runs `commit-check` against the full `origin/main..HEAD` range; a local check that only validates the planned PR title can pass while an intermediate commit fails CI (happened on PR #12 — one commit's description starting with `--` tripped commit-check's regex). Before pushing, loop over every commit's subject:
60
+ ```bash
61
+ git log --pretty=format:'%s%n' origin/main..HEAD | while IFS= read -r msg; do
62
+ [ -z "$msg" ] && continue
63
+ printf '%s\n' "$msg" > /tmp/m.txt
64
+ commit-check --message --no-banner --compact /tmp/m.txt || echo "FAIL: $msg"
65
+ done
66
+ ```
67
+ Local green then matches CI green on the message check.
68
+ - Branch names follow `<type>/<slug>` where `<type>` is from `cchk.toml`'s `allow_branch_types` and `<slug>` is lowercase-alphanumeric + hyphens. Note the asymmetry with commit subjects: the branch prefix `feature/` is the long form while the Conventional Commits subject tag `feat:` is the short form. Same repo, two conventions. Examples: `feature/v0.2-linux`, `fix/v0.2-followups-2-5`, `fix/v0.2-followup-1-linux-xdg-paths`.
69
+ - Commit subjects follow Conventional Commits: `<type>(<scope>): <description>` where `<type>` is from `cchk.toml`'s `allow_commit_types`. Scope tags mirror package names or doc categories, not ticket numbers. `!` before the colon — or a `BREAKING CHANGE:` footer — signals a breaking change.
70
+ - Split commits along revertability lines: a code change and a doc-only backlog update belong in separate commits because they could plausibly be reverted at different times. PR #4 is the template — one `feat` commit for the behavior change, one `docs` commit for the new follow-up entries.
71
+ - If commits land on `main` locally by mistake, create the topic branch at current `HEAD` **first**, then `git reset --hard origin/main` on `main`. The branch ref preserves the commits so the reset is non-destructive. Never run the reset before the branch is created.
72
+ - After a PR merges, `git checkout main && git pull --ff-only && git branch -d <merged-branch>` keeps the local tree clean. The GitHub-side branch is already deleted by the merge-and-delete UI; don't push a deletion for it.
73
+
74
+ ## Release
75
+
76
+ - `.github/workflows/test.yml` runs ruff + the portable pytest subset on `ubuntu-latest` and `windows-latest` for every push/PR. The Windows leg additionally runs `pytest -m windows_only`; the Linux leg additionally runs `pytest -m linux_only`.
77
+ - Push tag `v*` → `.github/workflows/release.yml` builds wheel + `dbxignore.exe` / `dbxignored.exe` (via `pyinstaller/dbxignore.spec`) and publishes a GitHub Release. `hatch-vcs` derives the version from the tag — no manual `pyproject.toml` bump needed.
78
+ - `CHANGELOG.md` at repo root follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): new entries accrue under an `[Unreleased]` heading (add it at the top when the first post-release change lands) and roll into a version heading with its release date when the tag goes out. Hand-crafted per-version release bodies live under `docs/release-notes/v<X.Y.Z>.md` for use with `gh release edit v<X.Y.Z> --notes-file docs/release-notes/v<X.Y.Z>.md` after the workflow publishes.
79
+ - This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Pre-1.0, breaking changes ride MINOR bumps with explicit **Breaking** callouts in the CHANGELOG — v0.2.0 introduced two (broadened `--purge`, changed `explain` format). Post-1.0, breaking changes will bump MAJOR.
80
+
81
+ ## Docs
82
+
83
+ - Design: `docs/superpowers/specs/2026-04-20-dropboxignore-design.md`
84
+ - Plan: `docs/superpowers/plans/2026-04-20-dropboxignore-implementation.md`
85
+ - v0.2 product/risk follow-ups: design doc § Open questions.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kilo Scheffer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: dbxignore
3
+ Version: 0.3.0
4
+ Summary: Hierarchical .dropboxignore for Dropbox on Windows (NTFS ADS) and Linux (xattrs)
5
+ Author: Kilo Scheffer
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: click>=8.1
10
+ Requires-Dist: pathspec>=0.12
11
+ Requires-Dist: psutil>=5.9
12
+ Requires-Dist: watchdog>=4.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest-timeout>=2.3; extra == 'dev'
15
+ Requires-Dist: pytest>=8; extra == 'dev'
16
+ Requires-Dist: ruff>=0.6; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # dbxignore
20
+
21
+ Hierarchical `.dropboxignore` files for Dropbox. Drop a `.dropboxignore` into any folder under your Dropbox root and matching paths get the Dropbox ignore marker set automatically — no more `node_modules/` cluttering your sync. Windows (NTFS alternate data streams) and Linux (`user.*` xattrs) supported.
22
+
23
+ ## Upgrading from v0.2.x
24
+
25
+ The project was renamed from `dropboxignore` to `dbxignore` in v0.3.0
26
+ (the old name collides with an unrelated 2019 PyPI project). Upgrade is
27
+ a one-time manual step:
28
+
29
+ ```bash
30
+ dropboxignore uninstall --purge # on v0.2.x — removes state, logs, service
31
+ pip install dbxignore # or: uv pip install dbxignore
32
+ dbxignore install # registers the new service under new names
33
+ ```
34
+
35
+ Your `.dropboxignore` rule files carry over untouched — they're never
36
+ modified by install/uninstall.
37
+
38
+ ## Requirements
39
+
40
+ - **Windows 10/11** (NTFS), **or** a modern Linux distro with a systemd user session
41
+ - Dropbox desktop client installed
42
+ - Python ≥ 3.11 with [`uv`](https://docs.astral.sh/uv/). The pre-built `.exe` (Windows only) is an alternative on Windows.
43
+
44
+ ## Install (Windows, from source)
45
+
46
+ ```powershell
47
+ uv tool install git+https://github.com/kiloscheffer/dbxignore
48
+ dbxignore install
49
+ ```
50
+
51
+ `dbxignore install` registers a Task Scheduler entry that launches the daemon (`pythonw -m dbxignore daemon`) at every user logon.
52
+
53
+ ## Install (Linux)
54
+
55
+ Requires a systemd user session (standard on Ubuntu, Fedora, Debian, Arch, and most modern distros; WSL2 requires `systemd=true` in `/etc/wsl.conf`).
56
+
57
+ ```bash
58
+ uv tool install git+https://github.com/kiloscheffer/dbxignore
59
+ dbxignore install # writes systemd user unit, enables it
60
+ systemctl --user status dbxignore.service
61
+ ```
62
+
63
+ `dbxignore install` writes `~/.config/systemd/user/dbxignore.service` and runs `systemctl --user enable --now` so the daemon starts at login.
64
+
65
+ For non-stock Dropbox installs, export `DBXIGNORE_ROOT` before running `dbxignore install` — the install step will read the variable from your shell environment and write a corresponding `Environment="DBXIGNORE_ROOT=..."` line into the generated unit's `[Service]` block. Without this, a shell-exported value won't reach the daemon when systemd launches it. If your Dropbox location ever changes, re-run `dbxignore install` after updating the export.
66
+
67
+ To uninstall:
68
+
69
+ ```bash
70
+ dbxignore uninstall # disables unit, removes the file
71
+ dbxignore uninstall --purge # clears markers, state files, logs, systemd drop-in
72
+ ```
73
+
74
+ Notes:
75
+ - Dropbox on Linux marks ignored paths with the xattr `user.com.dropbox.ignored=1`. Files on filesystems that don't support `user.*` xattrs (tmpfs without `user_xattr`, vfat, some FUSE mounts) are skipped with a `WARNING` in the daemon log — not a fatal error.
76
+ - Several common operations strip xattrs silently: `cp` without `-a`, `mv` across filesystems, most archivers, `vim`'s default save-via-rename. The watchdog plus hourly sweep re-apply markers automatically; no action needed.
77
+ - Linux symlinks cannot carry `user.*` xattrs (kernel restriction). A symlink matched by a rule logs one `WARNING` per sweep and is skipped. Its target is not affected.
78
+
79
+ ## Install (.exe)
80
+
81
+ 1. Download `dbxignore.exe` and `dbxignored.exe` from the latest [Release](https://github.com/kiloscheffer/dbxignore/releases).
82
+ 2. Place both in a stable directory (e.g. `%LOCALAPPDATA%\dbxignore\bin\`) and add it to your `PATH`.
83
+ 3. Run `dbxignore install`.
84
+
85
+ ## Platform support
86
+
87
+ - **Windows 10 / 11** — first-class (v0.1). Uses NTFS Alternate Data Streams.
88
+ - **Linux** — first-class (v0.2). Uses `user.com.dropbox.ignored` xattrs. Tested on Ubuntu 22.04 / 24.04. Requires a systemd user session.
89
+ - **macOS** — planned for v0.3. Dropbox on macOS uses a different attribute mechanism (Apple File Provider) that requires runtime detection — not yet implemented.
90
+
91
+ ## `.dropboxignore` syntax
92
+
93
+ Full `.gitignore` syntax via [`pathspec`](https://github.com/cpburnz/python-pathspec). Matching is case-insensitive to accommodate NTFS. A file named `.dropboxignore` is never itself ignored — it needs to sync so your other machines see the same rules.
94
+
95
+ Example (put in a project root):
96
+
97
+ ```
98
+ # everything javascripty
99
+ node_modules/
100
+
101
+ # Python
102
+ __pycache__/
103
+ .venv/
104
+ *.egg-info/
105
+
106
+ # Rust
107
+ target/
108
+
109
+ # build output
110
+ /dist/
111
+ /build/
112
+
113
+ # except this one specific artifact we want to share
114
+ !dist/release-notes.pdf
115
+ ```
116
+
117
+ ## Commands
118
+
119
+ | Command | Purpose |
120
+ |---|---|
121
+ | `dbxignore install` / `uninstall` | Register / remove the daemon with the platform's user-scoped service manager (Task Scheduler on Windows, systemd user unit on Linux). `uninstall --purge` also clears every existing marker, removes local dbxignore state (`state.json`, `daemon.log*`, the state directory), and on Linux removes any systemd drop-in directory. Any stray marker on a `.dropboxignore` file itself is logged at `WARNING` before being cleared. |
122
+ | `dbxignore daemon` | Run the watcher + hourly sweep in the foreground. Usually invoked by Task Scheduler. |
123
+ | `dbxignore apply [PATH]` | One-shot reconcile of the whole Dropbox (or a subtree). |
124
+ | `dbxignore status` | Is the daemon running? Last sweep counts, last error. |
125
+ | `dbxignore list [PATH]` | Print every path currently bearing the ignore marker. |
126
+ | `dbxignore explain PATH` | Which `.dropboxignore` rule (if any) matches the path? |
127
+
128
+ ## Behaviour
129
+
130
+ - **Source of truth.** `.dropboxignore` files declare what is ignored. Removing a rule unignores the matching paths on the next reconcile. A path marked ignored via Dropbox's right-click menu but not matching any rule will be unignored.
131
+ - **Hybrid trigger.** The daemon reacts to filesystem events in real time *and* runs an hourly safety-net sweep. If the daemon is offline, an initial sweep at the next start catches any drift.
132
+ - **Multi-root.** Personal and Business Dropbox roots are discovered automatically from `%APPDATA%\Dropbox\info.json` (Windows) or `~/.dropbox/info.json` (Linux).
133
+
134
+ ### Negations and Dropbox's ignore inheritance
135
+
136
+ Dropbox marks files and folders as ignored using xattrs. When a folder carries the ignore marker, Dropbox does not sync that folder or anything inside it — children inherit the ignored state regardless of whether they individually carry the marker. This matters for gitignore-style negation rules in your `.dropboxignore`.
137
+
138
+ If you write a negation whose target lives under a directory ignored by an earlier rule — the canonical case is `build/` followed by `!build/keep/` — the negation cannot take effect. Dropbox will ignore `build/keep/` because `build/` is ignored, no matter what xattr we put on the child. dbxignore detects this at the moment you save the `.dropboxignore`, logs a WARNING naming both rules, and drops the conflicted negation from the active rule set.
139
+
140
+ Negations that don't conflict with an ignored ancestor work normally. For example:
141
+
142
+ ```
143
+ *.log
144
+ !important.log
145
+ ```
146
+
147
+ Here nothing marks a parent directory as ignored (`*.log` matches files, not dirs), so the negation works — `important.log` gets synced, the other `.log` files don't.
148
+
149
+ **Limitation.** Detection uses static analysis on the rule's literal path prefix. Negations that begin with a glob (`!**/keep/`, `!*/cache/`) have no literal anchor to analyze and are accepted without conflict-check — if they land under an ignored ancestor at runtime, they silently fail to take effect. If you need guaranteed semantics, prefer negations with a literal prefix.
150
+
151
+ ## Configuration
152
+
153
+ Environment variables read at daemon startup:
154
+
155
+ | Variable | Default | Purpose |
156
+ |---|---|---|
157
+ | `DBXIGNORE_DEBOUNCE_RULES_MS` | `100` | Debounce window for `.dropboxignore` file events. |
158
+ | `DBXIGNORE_DEBOUNCE_DIRS_MS` | `0` | Debounce for directory-creation events (`0` = react immediately, no coalescing). |
159
+ | `DBXIGNORE_DEBOUNCE_OTHER_MS` | `500` | Debounce for other file events. |
160
+ | `DBXIGNORE_LOG_LEVEL` | `INFO` | Daemon log level. |
161
+ | `DBXIGNORE_ROOT` | *(unset)* | Escape hatch for non-stock Dropbox installs: overrides `info.json` discovery and treats the given absolute path as the sole Dropbox root. If the path doesn't exist, a WARNING is logged and no roots are returned (so `dbxignore apply` exits with "No Dropbox roots found"). |
162
+
163
+ Logs (rotated, 25 MB total):
164
+ - Windows — `%LOCALAPPDATA%\dbxignore\daemon.log`.
165
+ - Linux — two sinks, same records. The rotating file at `$XDG_STATE_HOME/dbxignore/daemon.log` (fallback `~/.local/state/dbxignore/daemon.log`) is authoritative for offline debugging and bug-report bundling; `journalctl --user -u dbxignore.service` surfaces the same records via systemd-journald for live tailing and cross-service filtering.
166
+
167
+ State:
168
+ - Windows — `%LOCALAPPDATA%\dbxignore\state.json`.
169
+ - Linux — `$XDG_STATE_HOME/dbxignore/state.json` (fallback `~/.local/state/dbxignore/state.json`). Installs that pre-date the XDG move are read transparently from the legacy `~/AppData/Local/dbxignore/state.json` for one release, with a WARNING; the next daemon write persists to the XDG path.
170
+
171
+ ## License
172
+
173
+ MIT — see [LICENSE](LICENSE).