Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0087e29
ci: build Ubuntu 24.04/26.04 matrix and RPM in RHEL 9 container
overwrite00 Oct 29, 2026
76829b9
chore(release): prepare 0.3.6 beta and document per-distro Linux pack…
overwrite00 Oct 29, 2026
3106fcc
docs: fix beta rpm file name example
overwrite00 Oct 29, 2026
6c54887
ci: build one Linux sidecar in a RHEL 9 container for deb, rpm and ta…
overwrite00 Oct 29, 2026
7f27334
docs: record verified Linux distributions
overwrite00 Oct 29, 2026
b38c77c
fix(ui): make Refresh keep the folder tree and reload the current fil…
overwrite00 Oct 29, 2026
a9ad48a
test(ui): update Refresh test to the new behavior (keep tree, notify …
overwrite00 Oct 29, 2026
24a87f5
fix(ui): Refresh falls back to nearest existing folder and keeps unre…
overwrite00 Oct 29, 2026
a54ea1a
feat: update check, repository link in About, version 0.4.0
overwrite00 Oct 31, 2026
c07e541
feat: typed metadata model, exact server-side Undo, IPTC/XMP read-wri…
overwrite00 Nov 2, 2026
683cb8e
test: skip pre-1901 timestamp tests on filesystems that clamp dates t…
overwrite00 Nov 2, 2026
bf1dfd4
feat(ui): Home reveals its path in the folder tree; context menu to e…
overwrite00 Nov 2, 2026
4475c79
test: keep the sidecar token across tests with pytest 9.2 (State attr…
overwrite00 Nov 2, 2026
2dba4a5
ci(deps)(deps): bump github/codeql-action from 4.38.2 to 4.39.1 (#187)
dependabot[bot] Nov 2, 2026
7e8377c
ci(deps): bump pypdf from 6.21.2 to 6.21.4 in /python (#194)
dependabot[bot] Nov 2, 2026
4c25d25
ci(deps-dev): bump pytest-asyncio from 1.4.0 to 1.4.1 in /python (#189)
dependabot[bot] Nov 2, 2026
9f48382
ci(deps-dev): bump pytest from 9.1.1 to 9.2.0 in /python (#192)
dependabot[bot] Nov 2, 2026
00a6bd5
ci(deps): bump react-dom from 19.3.0 to 19.3.1 in /frontend (#193)
dependabot[bot] Nov 2, 2026
f8331df
ci(deps): bump lucide-react from 1.48.0 to 1.65.0 in /frontend (#188)
dependabot[bot] Nov 2, 2026
cfd57f6
ci(deps-dev): bump electron from 44.9.3 to 45.1.0 in /electron (#191)
dependabot[bot] Nov 2, 2026
994c7bb
ci(deps-dev): bump @vitejs/plugin-react from 6.1.1 to 6.1.2 in /front…
dependabot[bot] Nov 2, 2026
a8a31eb
ci(deps): bump fastapi from 0.141.1 to 0.162.0 in /python (#196)
dependabot[bot] Nov 2, 2026
761fe8d
docs(changelog): dependency updates merged into 0.4.0
overwrite00 Nov 2, 2026
c0ef7df
docs(changelog): release notes for 0.4.0
overwrite00 Nov 2, 2026
4bb5c65
fix(codeql): explicit XML char ranges, math.isfinite for NaN checks, …
overwrite00 Nov 2, 2026
6e91bcd
fix(codeql): XML char check without a character-range regex; type gua…
overwrite00 Nov 2, 2026
caefdbd
Merge main into develop (sync before 0.4.0 promotion)
overwrite00 Nov 2, 2026
fc530a4
refactor: break import cycles, unify ctypes imports, drop unused code…
overwrite00 Nov 2, 2026
657b2a9
fix(security): API responses use a fixed error description instead of…
overwrite00 Nov 2, 2026
518b9b3
test: clean up CodeQL notes in the test suite (mixed imports, unused …
overwrite00 Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 265 additions & 63 deletions .github/workflows/build.yml

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ jobs:
uses: actions/checkout@v7

- name: Initialize CodeQL
uses: github/codeql-action/init@v4.38.2
uses: github/codeql-action/init@v4.39.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql/codeql-config.yml

- name: Autobuild
uses: github/codeql-action/autobuild@v4.38.2
uses: github/codeql-action/autobuild@v4.39.1

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.38.2
uses: github/codeql-action/analyze@v4.39.1
with:
category: /language:${{ matrix.language }}
254 changes: 254 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

35 changes: 22 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ cd python
python -m venv .venv
source .venv/bin/activate # Linux
# .venv\Scripts\activate # Windows
pip install -r requirements.txt
pip install -r requirements-dev.txt # runtime + test dependencies (requirements.txt = runtime only)

# Electron + frontend
cd ../electron && npm install
Expand All @@ -81,7 +81,9 @@ git config core.hooksPath .githooks
### Python

- Follow the handler pattern in `python/core/handlers/` — one file per category, implementing `BaseMetadataHandler`
- Register new handlers in `python/core/registry.py`
- Register new handlers in `python/core/handlers/__init__.py` (`HandlerRegistry.register(...)`); `PRIORITY` decides which handler wins (hachoir, `PRIORITY = 100`, is the catch-all)
- Follow the value model in `python/core/values.py`: canonical JSON values, lists for multi-valued fields (never joined strings), binaries as read-only descriptors, stable keys `<namespace>:<native id>` that never depend on the value or label
- A handler that marks fields `editable`/`deletable` must implement `apply_write`/`apply_delete` and the `snapshot`/`restore` pair used by the exact Undo, and must not open its own transaction inside them
- Add type hints on public functions

### Frontend
Expand Down Expand Up @@ -113,20 +115,19 @@ type(scope): description

### Running Tests

All Python changes must pass the test suite:
Run the suites affected by your change (CI runs all of them; the Python suite on Linux and Windows):

```bash
pytest python/tests/ -v
pytest python/tests/ -v # sidecar
cd frontend && npm test && npm run build && npm run verify-icons # renderer
cd electron && npm test # Electron main process
```

### Test Requirements

- **New handlers or routes must include tests** in `python/tests/`
- **All tests must pass**: no regressions allowed
- After frontend changes, verify the production build still compiles:
```bash
cd frontend && npm run build
```
- **New formats need synthetic samples**: register a builder in the matching module of `python/tests/factories/` (files are generated at runtime, no binaries in the repository). `tests/test_contract.py` then runs the generic contract on it: unique JSON-serializable keys, coherent types, idempotent writes, round-trip of new values, exact Undo of edits and deletions, read-only fields refused. Inspect samples with `python -m tests.factories --out <dir>` (from `python/`)
- **All tests must pass**: no regressions allowed. If a test encodes old, wrong behavior, update it with an assertion of equal strength and explain why

---

Expand All @@ -150,6 +151,7 @@ Fixes #(issue number) or Relates to #(issue number)
## Testing
- [ ] Added tests for new functionality
- [ ] `pytest python/tests/ -v` passes locally
- [ ] `npm test` passes in `frontend/` and `electron/` (if touched)
- [ ] Tested on Windows/Linux (if applicable)

## Checklist
Expand Down Expand Up @@ -177,7 +179,7 @@ MetaLens uses `MAJOR.MINOR.PATCH`, with `python/config.py` → `VERSION` as the
| **MINOR** | New feature, new handler, new panel |
| **MAJOR** | Breaking change, milestone 1.0 |

If your change bumps the version, update all four locations: `python/config.py`, `CHANGELOG.md`, `electron/package.json`, `frontend/package.json`.
If your change bumps the version, update: `python/config.py`, `CHANGELOG.md`, `electron/package.json` + `electron/package-lock.json`, `frontend/package.json` + `frontend/package-lock.json` (the `"version"` fields; `npm install --package-lock-only` refreshes the lockfiles).

### Release notes come from the changelog

Expand All @@ -188,6 +190,10 @@ The body of every GitHub Release is the `CHANGELOG.md` section of that exact ver

Preview what a release will say with `python scripts/changelog_notes.py X.Y.Z`.

### Linux packages

The PyInstaller sidecar embeds the glibc of the machine that builds it, so the Linux jobs in `.github/workflows/build.yml` never use `ubuntu-latest` for it. `build-sidecar-linux-*` builds the sidecar once in a `rockylinux:9` container (glibc 2.34, Python 3.13 from `uv`); `build-linux-*` (`.deb` and `.tar.gz`, on `ubuntu-24.04`) and `build-rpm-*` (`.rpm`, in the same container) reuse it. Do not build the `.rpm` on `ubuntu-26.04`: RPM 6 breaks `electron-installer-redhat` 4.0.0. A workflow change can be tried without publishing a release by dispatching `build.yml` from a temporary branch whose job conditions are relaxed.

---

## 🔧 Dependency Updates
Expand All @@ -200,7 +206,9 @@ To update manually:
# Python
pip list --outdated
pip install --upgrade package_name
pip freeze > python/requirements.txt
# then edit the pinned version by hand: runtime packages in python/requirements.txt,
# test-only packages (pytest, python-docx, ...) in python/requirements-dev.txt
# (do not use `pip freeze`: it would add transitive and platform-specific packages)

# Node (electron/ or frontend/)
npm outdated
Expand All @@ -211,7 +219,8 @@ Then test thoroughly before committing:

```bash
pytest python/tests/ -v
cd frontend && npm run build
cd frontend && npm test && npm run build
cd ../electron && npm test
```

---
Expand All @@ -230,5 +239,5 @@ Thank you for contributing to MetaLens! Your efforts help make file metadata man

---

*Last updated: 2026-10-05*
*Last updated: 2026-11-02*
*← [Code of Conduct](./CODE_OF_CONDUCT.md) | [Security →](./SECURITY.md)*
74 changes: 41 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
<img src="docs/cover.png" alt="MetaLens — Universal File Metadata Manager" width="860" />
</p>

**Universal File Metadata Manager** — read, edit, delete and compare metadata across all major file formats, entirely offline.
**Universal File Metadata Manager** — read, edit, delete and compare metadata across all major file formats, offline by default.

> [!TIP]
> 💡 **No account, no cloud, no setup.** Download a binary, double-click, and start browsing — MetaLens never touches the network.
> 💡 **No account, no cloud, no setup.** Download a binary, double-click, and start browsing. MetaLens never touches the network unless you ask it to check for a new version (see [Privacy & Security](#-privacy--security)).

---

Expand Down Expand Up @@ -49,13 +49,13 @@ File (image / audio / video / document / anything)
│
▼
┌─────────────────────────────────────────────┐
│ 🖼️ Images → EXIF, IPTC, XMP (+ RAW read) │
│ 🎵 Audio → ID3, Vorbis comments, tags │
│ 🎬 Video → container metadata │
│ 📄 PDF → document info dictionary │
│ 📊 Office → DOCX/XLSX/PPTX properties │
│ 📁 Any file → filesystem timestamps, │
│ permissions, xattr │
│ 🖼️ Images → EXIF, XMP, IPTC (JPEG edit) │
│ 🎵 Audio → ID3, Vorbis, ASF, APEv2, MP4 │
│ 🎬 Video → MP4/MOV tags, other: read │
│ 📄 PDF → document info + XMP │
│ 📊 Office → OOXML core/app/custom, OLE │
│ 📁 Any file → size, timestamps, attributes,│
│ xattr; hachoir fallback │
└─────────────────────────────────────────────┘
│
▼
Expand All @@ -67,29 +67,32 @@ File (image / audio / video / document / anything)
## ✨ Key Features

- 🗂️ **File-manager UI** — folder tree + file list + metadata detail panel
- 🖼️ **All major formats** — images (JPEG/PNG/TIFF/WebP/RAW), audio (MP3/FLAC/OGG/M4A/WAV), video (MP4/MKV/MOV), documents (PDF/DOCX/XLSX/PPTX/DOC/XLS), and any other file via filesystem metadata
- ✏️ **Full operations** — read, edit, delete individual fields, compare/diff two files
- 🔒 **Atomic writes** — temp-file + rename, no file corruption on failure
- ↩️ **Undo stack** — 50 operations per session
- 🖼️ **All major formats** — images (JPEG/PNG/TIFF/WebP/GIF/RAW), audio (MP3/FLAC/OGG/Opus/M4A/WAV/AIFF/WMA/APE), video (MP4/MOV/MKV/AVI), documents (PDF/DOCX/XLSX/PPTX/DOC/XLS/PPT); any other file through the hachoir fallback and the filesystem layer
- ✏️ **Typed editing** — read, edit, add and delete individual fields with inputs matching their type (text, numbers, booleans, dates with UTC offset, exact rationals, lists); the sidecar validates every value and decides what is editable
- 🔒 **Verified atomic writes** — changes go to a private copy beside the file, are read back and checked, then replace the original (`ReplaceFileW` on Windows, `os.replace` elsewhere); a failure leaves the file untouched
- ↩️ **Exact Undo** — up to 50 saves per session, restored byte for byte in the usual case (pictures and other binaries included; very large changes fall back to restoring the values); refused if the file changed meanwhile
- 🔑 **File integrity hashes** — on-demand MD5 / SHA-1 / SHA-256 / SHA-512 / BLAKE2b with one-click copy
- 🎨 **Cyber dark theme** — professional dark UI with cyan/blue accents
- 📦 **Compiled binaries** — no Python or Node.js required to run
- 💾 **Offline-first** — zero network calls, zero telemetry
- 💾 **Offline-first** — zero telemetry; the only network call is the optional update check
- 🔔 **Update check** — manual from *About* / *Help*, or opt-in at startup; downloads are SHA-256 verified
- 🆓 **Free & open-source** — MIT license

---

## 🔧 Version

**v0.3.4** — Security and maintenance release: the build toolchain moves to Electron Forge 8 (resolving the open `extract-zip` advisories), closing the app no longer leaves the background sidecar process running, and dependencies are updated (uvicorn, pypdf, lucide-react, Vite, Electron). No API changes.
**v0.3.5** (latest stable) — Security and reliability release: every sidecar API call is authenticated with a per-session token, the Electron renderer is sandboxed with a Content Security Policy, PDF/Office edits preserve unrelated content, audio/video edits are atomic, and Undo restores original values. Dependencies updated (Pillow, pypdf, Vite, Electron). No API changes.

**Next: 0.4.0** (in development, not yet released) — update check and repository link in *About*; XMP and IPTC reading and writing for JPEG; typed values (lists, numbers, dates, exact rationals) instead of joined strings; adding fields; exact server-side Undo; uniform API errors; Linux packages built on a RHEL 9 baseline (Ubuntu 22.04+, Debian 12+, Fedora, RHEL 9/10).

📖 **See full version history** → [CHANGELOG.md](./CHANGELOG.md)

---

## 📋 System Requirements

- **Windows** 10/11 (64-bit) or a modern **Linux** distro (`.deb`/`.rpm`/`.tar.gz`)
- **Windows** 10/11 (64-bit) or **Linux** with glibc 2.34+ (Ubuntu 22.04+, Debian 12+, Fedora, RHEL 9/10): `.deb`/`.rpm`/`.tar.gz` — see [REQUIREMENTS.md](docs/REQUIREMENTS.md)
- **RAM** 512 MB free
- **Disk** ~300 MB installed
- No Python or Node.js needed to *run* the app — only to build it from source
Expand All @@ -116,17 +119,18 @@ Explore the sidecar API in [API.md](docs/API.md).

| Category | Formats | Read | Write |
|---|---|---|---|
| Images | JPEG (supported EXIF fields) | ✅ | ✅ |
| Images | PNG, TIFF, BMP, GIF, WebP | ✅ | — |
| Images (RAW) | CR2, NEF, ARW, DNG | ✅ | — |
| Audio | MP3, FLAC, OGG, M4A, WAV, AIFF, WMA, APE, Opus | ✅ | ✅ |
| Video | MP4, MOV | ✅ | ✅ |
| Video | MKV, AVI | ✅ | — |
| Documents | PDF (unencrypted) | ✅ | ✅ |
| Office | DOCX, XLSX, PPTX, DOCM, XLSM, PPTM (unsigned) | ✅ | ✅ |
| Office (legacy) | DOC, XLS, PPT | ✅ | — |
| Any file | Filesystem timestamps, permissions, xattr | ✅ | ✅ |
| Other | Hundreds of formats via hachoir | ✅ | — |
| Images | JPEG — EXIF/GPS, XMP, IPTC-IIM | ✅ | ✅ |
| Images | TIFF, PNG (text chunks, `eXIf`), WebP (EXIF, XMP), GIF (comment), BMP, ICO | ✅ | — |
| Images (RAW) | CR2, NEF, ARW, DNG, ORF, RW2, PEF, SRW (EXIF read for TIFF-based RAW) | ✅ | — |
| Audio | MP3, WAV, AIFF, TTA (ID3) · FLAC, OGG, OGA, Opus, SPX (Vorbis comments) · WMA (ASF) · APE, WV, MPC (APEv2) · M4A (MP4 atoms) | ✅ | ✅ |
| Audio | AAC (ADTS/ADIF) | ✅ | — |
| Video | MP4, M4V, MOV, M4B, M4R (MP4 atoms) | ✅ | ✅ |
| Video | MKV, WebM, AVI, WMV, FLV, TS, MTS, M2TS (via hachoir) | ✅ | — |
| Documents | PDF — document info + XMP (not encrypted, not digitally signed) | ✅ | ✅ |
| Office | DOCX, XLSX, PPTX, DOCM, XLSM, PPTM — core properties (not digitally signed); `app.xml`/`custom.xml` read-only | ✅ | ✅ |
| Office (legacy) | DOC, XLS, PPT, DOT, XLT, POT (OLE property sets) | ✅ | — |
| Any file | Filesystem: `fs:mtime` writable; size, other timestamps, permissions/owner (POSIX), Windows attributes read-only; xattr (Linux: `user.` writable; macOS: read-only) | ✅ | ✅ (mtime, xattr) |
| Other | Many more formats recognized from their content via hachoir | ✅ | — |

### 🔑 File Integrity

Expand Down Expand Up @@ -154,20 +158,24 @@ Compute cryptographic hashes on demand from the **Metadata** panel — no impact
Electron Main (Node.js)
├── Spawns Python sidecar (FastAPI, localhost only)
├── Manages window, native menus, file dialogs
└── IPC bridge (contextBridge) ↔ React renderer
└── IPC bridge (contextBridge): dialogs, paths, menu events, and the
sidecar port + session token for the renderer

React renderer ── HTTP + Bearer token ──▶ Python Sidecar (127.0.0.1)

Python Sidecar (FastAPI)
├── /list — directory listing with handler detection
├── /read — full metadata extraction
├── /write — atomic metadata write-back
├── /read — full metadata extraction (format + filesystem layer)
├── /write — verified atomic write (and optional deletes) in one save
├── /delete — field removal
├── /undo — exact undo of a save by its token
├── /diff — metadata comparison
└── /hash — on-demand file hashing

React Frontend (Vite + TailwindCSS)
├── FolderPanel — folder tree navigation
├── FilePanel — file list with metadata summary
└── DetailPanel — View / Edit / Diff tabs
└── DetailPanel — Metadata / Edit / Diff tabs
```

📖 Full endpoint reference → [API.md](docs/API.md)
Expand All @@ -193,7 +201,7 @@ cd ../frontend && npm test
- 🔒 **No telemetry** — zero data collection
- 📁 **Localhost-only sidecar** — the Python API never binds beyond `127.0.0.1`
- 🔓 **Open source** — fully auditable code
- ⚡ **Fully offline** — no internet connection required, ever
- ⚡ **Offline by default** — no internet connection required. The only network access is the update check against the public GitHub Releases API, started by you (**About → Check for updates** or **Help → Check for Updates…**) or, if you enable it, once at startup. It sends no file data, paths or identifiers (only a `User-Agent: MetaLens/<version>` header). Settings are stored locally in `settings.json` in the app data folder

📖 See [SECURITY.md](./SECURITY.md) for the full security model.

Expand Down Expand Up @@ -238,5 +246,5 @@ Distributed with MIT License · [View License](LICENSE)

---

*Last updated: 2026-10-27*
*Last updated: 2026-11-02*
*← [Requirements](docs/REQUIREMENTS.md) | [Docs →](docs/)*
Loading
Loading