diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..fc89eb0 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +# Auto-updates the "Last updated" footer in staged Markdown docs. +# Enable once per clone with: git config core.hooksPath .githooks + +python "$(git rev-parse --show-toplevel)/scripts/update_docs_dates.py" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6b35a5..8cfbe8b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,10 +9,15 @@ on: - 'CHANGELOG.md' - 'LICENSE' - '*.md' + workflow_dispatch: {} env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: # ───────────────────────────────────────────────────────────────────────────── # TEST BUILD (develop only) — Verify code compiles and tests pass @@ -63,6 +68,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + env: + ELECTRON_CACHE: ${{ github.workspace }}/.electron-cache steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 @@ -97,35 +104,53 @@ jobs: cd frontend npm ci npm run build + - name: Cache Electron download + uses: actions/cache@v4 + with: + path: ${{ env.ELECTRON_CACHE }} + key: electron-${{ runner.os }}-${{ hashFiles('electron/package-lock.json') }} - name: Test electron setup run: | cd electron npm ci --include=dev # ───────────────────────────────────────────────────────────────────────────── - # RELEASE BUILD (main only) — Create and publish stable release + # BETA RELEASE (develop only) — Package and publish a prerelease build # ───────────────────────────────────────────────────────────────────────────── - get-version: - if: github.ref == 'refs/heads/main' + get-version-beta: + if: github.ref == 'refs/heads/develop' + needs: [test-python, test-frontend, test-electron] runs-on: ubuntu-latest permissions: contents: read outputs: - version: ${{ steps.ver.outputs.version }} + base_version: ${{ steps.ver.outputs.base_version }} + beta_version: ${{ steps.ver.outputs.beta_version }} steps: - uses: actions/checkout@v6 - id: ver + env: + GH_TOKEN: ${{ github.token }} run: | - VERSION=$(grep -oP 'VERSION\s*=\s*"\K[^"]+' python/config.py) - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "Building MetaLens v$VERSION" + BASE=$(grep -oP 'VERSION\s*=\s*"\K[^"]+' python/config.py) + # Count existing beta releases for this exact base version and increment — + # keeps beta.N scoped to the version instead of the workflow's global run number. + LAST=$(gh release list --repo "${{ github.repository }}" --limit 200 \ + | grep -oP "v${BASE}-beta\.\K[0-9]+" | sort -n | tail -1) + NEXT=$(( ${LAST:-0} + 1 )) + BETA="${BASE}-beta.${NEXT}" + echo "base_version=$BASE" >> "$GITHUB_OUTPUT" + echo "beta_version=$BETA" >> "$GITHUB_OUTPUT" + echo "Building MetaLens beta v$BETA" - build-windows: - if: github.ref == 'refs/heads/main' - needs: get-version + build-windows-beta: + if: github.ref == 'refs/heads/develop' + needs: get-version-beta runs-on: windows-latest permissions: contents: read + env: + ELECTRON_CACHE: ${{ github.workspace }}/.electron-cache steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 @@ -160,28 +185,35 @@ jobs: npm run build - name: Copy frontend dist into electron run: cp -r frontend/dist electron/frontend - - name: Sync electron version + - name: Sync electron version to beta run: | cd electron - npm version ${{ needs.get-version.outputs.version }} --no-git-tag-version --allow-same-version + npm version ${{ needs.get-version-beta.outputs.beta_version }} --no-git-tag-version --allow-same-version + - name: Cache Electron download + uses: actions/cache@v4 + with: + path: ${{ env.ELECTRON_CACHE }} + key: electron-${{ runner.os }}-${{ hashFiles('electron/package-lock.json') }} - name: Package with Electron Forge run: | cd electron npm ci --include=dev npx electron-forge make - - name: Upload Windows artifact + - name: Upload Windows beta artifact uses: actions/upload-artifact@v7 with: - name: windows-installer + name: windows-installer-beta path: electron/out/make/**/*.exe if-no-files-found: error - build-linux: - if: github.ref == 'refs/heads/main' - needs: get-version + build-linux-beta: + if: github.ref == 'refs/heads/develop' + needs: get-version-beta runs-on: ubuntu-latest permissions: contents: read + env: + ELECTRON_CACHE: ${{ github.workspace }}/.electron-cache steps: - uses: actions/checkout@v6 - name: Install system dependencies @@ -217,12 +249,17 @@ jobs: npm run build - name: Copy frontend dist into electron run: cp -r frontend/dist electron/frontend - - name: Sync electron version + - name: Sync electron version to beta run: | cd electron - npm version ${{ needs.get-version.outputs.version }} --no-git-tag-version --allow-same-version + npm version ${{ needs.get-version-beta.outputs.beta_version }} --no-git-tag-version --allow-same-version - name: Configure RPM macros run: echo '%_build_id_links none' >> ~/.rpmmacros + - name: Cache Electron download + uses: actions/cache@v4 + with: + path: ${{ env.ELECTRON_CACHE }} + key: electron-${{ runner.os }}-${{ hashFiles('electron/package-lock.json') }} - name: Package with Electron Forge run: | cd electron @@ -230,7 +267,7 @@ jobs: npx electron-forge make - name: Create Linux tar.gz run: | - VERSION=${{ needs.get-version.outputs.version }} + VERSION=${{ needs.get-version-beta.outputs.beta_version }} APP_DIR=$(find electron/out -maxdepth 1 -type d -name "MetaLens-linux-*" | head -1) if [ -n "$APP_DIR" ]; then tar -czf "electron/out/make/MetaLens-${VERSION}-Linux.tar.gz" \ @@ -239,19 +276,19 @@ jobs: else echo "Warning: no MetaLens-linux-* directory found, skipping tar.gz" fi - - name: Upload Linux artifacts + - name: Upload Linux beta artifacts uses: actions/upload-artifact@v7 with: - name: linux-packages + name: linux-packages-beta path: | electron/out/make/**/*.deb electron/out/make/**/*.rpm electron/out/make/**/*.tar.gz if-no-files-found: error - release: - if: github.ref == 'refs/heads/main' - needs: [get-version, build-windows, build-linux] + release-beta: + if: github.ref == 'refs/heads/develop' + needs: [get-version-beta, build-windows-beta, build-linux-beta] runs-on: ubuntu-latest permissions: contents: write @@ -261,7 +298,89 @@ jobs: path: artifacts - name: List artifacts run: find artifacts -type f - - name: Create GitHub Release + - name: Create beta GitHub Release + uses: softprops/action-gh-release@v3 + with: + tag_name: v${{ needs.get-version-beta.outputs.beta_version }} + name: MetaLens v${{ needs.get-version-beta.outputs.beta_version }} (beta) + draft: false + prerelease: true + generate_release_notes: true + make_latest: false + files: | + artifacts/windows-installer-beta/** + artifacts/linux-packages-beta/** + + # ───────────────────────────────────────────────────────────────────────────── + # STABLE RELEASE (main only) — Promote the matching beta build, no rebuild + # ───────────────────────────────────────────────────────────────────────────── + get-version: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + version: ${{ steps.ver.outputs.version }} + steps: + - uses: actions/checkout@v6 + - id: ver + run: | + VERSION=$(grep -oP 'VERSION\s*=\s*"\K[^"]+' python/config.py) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Building MetaLens v$VERSION" + + find-latest-beta: + if: github.ref == 'refs/heads/main' + needs: get-version + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + beta_tag: ${{ steps.find.outputs.beta_tag }} + steps: + - id: find + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.get-version.outputs.version }} + run: | + TAG=$(gh release list --repo "$GITHUB_REPOSITORY" --limit 200 --json tagName \ + --jq '.[].tagName' \ + | grep -E "^v${VERSION}-beta\.[0-9]+$" \ + | sort -t. -k4 -n \ + | tail -1) + if [ -z "$TAG" ]; then + echo "::error::Nessuna release beta trovata per v${VERSION}. Genera prima una beta da develop con la stessa VERSION in python/config.py." + exit 1 + fi + echo "beta_tag=$TAG" >> "$GITHUB_OUTPUT" + echo "Promuovendo $TAG a stable v${VERSION}" + + promote-stable: + if: github.ref == 'refs/heads/main' + needs: [get-version, find-latest-beta] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download beta release assets + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p promoted + gh release download "${{ needs.find-latest-beta.outputs.beta_tag }}" \ + --repo "$GITHUB_REPOSITORY" --dir promoted + - name: Strip beta suffix from filenames + run: | + cd promoted + # Each maker encodes the beta suffix differently: "-beta.N" (exe/tar.gz), + # "beta.N" with no separator (rpm, dash stripped for Version field rules), + # ".beta.N" (deb, dot-separated per Debian version conventions). + for f in *; do + new=$(echo "$f" | sed -E 's/[-.]?beta\.?[0-9]+//') + if [ "$f" != "$new" ]; then mv -- "$f" "$new"; fi + done + ls -la + - name: Create stable GitHub Release uses: softprops/action-gh-release@v3 with: tag_name: v${{ needs.get-version.outputs.version }} @@ -270,6 +389,4 @@ jobs: prerelease: false generate_release_notes: true make_latest: true - files: | - artifacts/windows-installer/** - artifacts/linux-packages/** + files: promoted/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ce368..d1cd357 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,32 @@ # Changelog All notable changes to MetaLens are documented in this file. +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) — `MAJOR.MINOR.PATCH`. -Format: [Semantic Versioning](https://semver.org/) — `MAJOR.MINOR.PATCH` +--- + +## [Unreleased] — Next Release + +### Roadmap + +- [ ] **CSV/JSON Export** — Export metadata for the current file or folder +- [ ] **Batch Edit** — Apply a field change to multiple selected files at once +- [ ] **Search/Filter Bar** — Filter the file list by name or extension --- -## [Unreleased] +## [0.2.1] — 2026-07-14 + +### Dependencies +- electron: 43.0.0 → 43.1.0 (fixed crash on replacing an open application menu, Chromium 150.0.7871.47, Node.js 24.18.0) +- vite: 8.1.3 → 8.1.4 (oxc minifier preference for legacy builds, StackBlitz build workaround, SSR stacktrace alignment) +- uvicorn: 0.49.0 → 0.51.0 (near-zero-downtime worker reload on SIGHUP, removed colorama from standard extra) +- lucide-react: 1.23.0 → 1.24.0 (new icons, several upstream icon fixes) -### Planned -- Export metadata as CSV/JSON -- Batch edit on multiple selected files -- Search/filter bar in file list +### Quality +- All tests passing: 47 passed, 1 skipped +- Frontend build and Electron startup verified with updated dependencies +- No regressions --- @@ -194,9 +209,9 @@ Format: [Semantic Versioning](https://semver.org/) — `MAJOR.MINOR.PATCH` ### Documentation - Added comprehensive security documentation (2000+ lines) - - `SECURITY_ANALYSIS_Q_AND_A.md`: Deep threat model analysis - `python/SECURITY.md`: Security design and implementation guide - `python/core/PATH_SECURITY_USAGE.md`: API reference for developers + - *(internal threat-model notes were later removed from the repo as temporary development files — see [0.1.2])* --- @@ -222,3 +237,7 @@ Format: [Semantic Versioning](https://semver.org/) — `MAJOR.MINOR.PATCH` - About dialog with version from `/health` - GitHub Actions CI: automated Windows (.exe) + Linux (.deb/.rpm) builds on push to `main` - MIT License — © 2026 Graziano Mariella + +--- + +*← [Back to README](./README.md)* diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..81f90cc --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# 🤝 Code of Conduct + +## Our Commitment + +We are committed to creating a welcoming, inclusive, and respectful community around MetaLens. This Code of Conduct outlines our expectations for all members of our community, regardless of background, experience level, or identity. + +--- + +## ✅ Expected Behavior + +- **Be respectful** — treat all community members with dignity and kindness +- **Be inclusive** — welcome and support people of all backgrounds +- **Be collaborative** — work together constructively toward shared goals +- **Be patient** — understand that people have different levels of expertise and may need help +- **Give credit** — acknowledge the contributions of others +- **Listen actively** — take time to understand different perspectives +- **Provide constructive feedback** — help others improve without being dismissive + +--- + +## ❌ Unacceptable Behavior + +The following behaviors are not tolerated in our community: + +- Harassment, discrimination, or bullying of any kind (based on race, gender, sexual orientation, disability, age, religion, etc.) +- Offensive, derogatory, or hateful comments or jokes +- Unwanted sexual advances or inappropriate comments +- Threats, intimidation, or aggressive behavior +- Trolling, spam, or deliberate disruption +- Sharing others' private information without consent (doxxing) +- Any form of abuse or violence + +--- + +## 🌍 Creating a Welcoming Environment + +To foster a positive community: + +- Use inclusive language — avoid gendered terms where possible +- Respect pronouns and names — use what people ask you to use +- Acknowledge mistakes — if you say something harmful, apologize sincerely and learn +- Challenge harmful behavior — speak up if you witness violations +- Support newcomers — help people feel welcome, especially those new to open source + +--- + +## 📢 Reporting & Enforcement + +If you witness or experience unacceptable behavior: + +1. **GitHub Issues** — [Report via GitHub Issues](https://github.com/overwrite00/MetaLens/issues) for documentation or policy concerns +2. **GitHub Discussions** — [Report via GitHub Discussions](https://github.com/overwrite00/MetaLens/discussions) for community concerns +3. **Direct Contact** — reach out to maintainers privately if needed + +We take all reports seriously and will respond promptly. Violators may be warned, temporarily banned, or permanently removed from the community at the maintainers' discretion. + +--- + +## 🔍 Scope + +This Code of Conduct applies to: +- GitHub Issues and Pull Requests +- GitHub Discussions +- Project documentation and comments +- Any community spaces associated with MetaLens + +--- + +## 📝 Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, and is used under the Creative Commons Attribution 4.0 International License. + +--- + +*Last updated: 2026-07-14* +*← [Contributing](./CONTRIBUTING.md) | [Security →](./SECURITY.md)* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..185589e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,225 @@ +# 🎯 Contributing to MetaLens + +Thank you for your interest in contributing to MetaLens! We welcome bug reports, feature requests, documentation improvements, and code contributions from the community. + +--- + +## 🎯 Welcome + +MetaLens is an open-source universal file metadata manager. Whether you're fixing a bug, improving documentation, adding a new metadata handler, or enhancing the UI, your contributions make MetaLens better for everyone. + +Before contributing, please review this guide and our [Code of Conduct](./CODE_OF_CONDUCT.md). + +--- + +## 🔀 Branch Strategy + +- **Main branch**: `main` (protected, release-only — tagged `vX.Y.Z` with GitHub Release binaries) +- **Development branch**: `develop` (main integration branch, PRs target here) +- **Feature branches**: Create from `develop`, e.g., `feature/csv-export` + +### Submitting Code + +1. **Fork** the repository on GitHub +2. **Clone** your fork locally +3. **Create a feature branch** from `develop`: + ```bash + git checkout develop + git pull origin develop + git checkout -b feature/your-feature-name + ``` +4. **Make your changes** and test thoroughly +5. **Commit** with clear messages (see [Code Standards](#-code-standards)) +6. **Push** to your fork +7. **Create a Pull Request** targeting `develop` (NOT `main`) + +> [!WARNING] +> ⚠️ PRs to `main` will be rejected. Always target `develop`. + +--- + +## 🛠️ Development Setup + +### Prerequisites + +- **Python**: 3.11, 3.12, or 3.13 (see [REQUIREMENTS.md](./docs/REQUIREMENTS.md)) +- **Node.js**: 20+ +- **npm**: 10+ +- **Git** + +### Setup + +```bash +git clone https://github.com/YOUR-USERNAME/MetaLens.git +cd MetaLens + +# Python sidecar +cd python +python -m venv .venv +source .venv/bin/activate # Linux +# .venv\Scripts\activate # Windows +pip install -r requirements.txt + +# Electron + frontend +cd ../electron && npm install +cd ../frontend && npm install + +# Run in development mode +cd ../electron && npm start +``` + +Then enable the repo's git hooks (one-time per clone) so the `Last updated` footer in docs stays in sync automatically: + +```bash +git config core.hooksPath .githooks +``` + +--- + +## 📝 Code Standards + +### Python + +- Follow the handler pattern in `python/core/handlers/` — one file per category, implementing `BaseMetadataHandler` +- Register new handlers in `python/core/registry.py` +- Add type hints on public functions + +### Frontend + +- Components live in `frontend/src/components/`, shared logic in `frontend/src/hooks/` +- Follow the existing Tailwind v4 cyber color palette (`frontend/src/styles/globals.css`) — never hardcode colors outside the defined CSS variables + +### Commit Message Format + +Follow the conventional commits format: + +``` +type(scope): description + +[optional body] +``` + +**Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` +**Scope**: `handlers`, `api`, `ui`, `electron`, `deps`, etc. + +**Examples**: +- `feat(handlers): add support for HEIC images` +- `fix(diff): correct field alignment when types differ` +- `docs(readme): update supported formats table` + +--- + +## ✅ Testing + +### Running Tests + +All Python changes must pass the test suite: + +```bash +pytest python/tests/ -v +``` + +### 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 + ``` + +--- + +## 📤 Submitting a Pull Request + +### PR Description Template + +```markdown +## Summary +Brief description of changes + +## Related Issue +Fixes #(issue number) or Relates to #(issue number) + +## Type of Change +- [ ] Bug fix (non-breaking) +- [ ] New feature +- [ ] Breaking change +- [ ] Documentation update + +## Testing +- [ ] Added tests for new functionality +- [ ] `pytest python/tests/ -v` passes locally +- [ ] Tested on Windows/Linux (if applicable) + +## Checklist +- [ ] Code follows style guidelines +- [ ] CLAUDE.md / CHANGELOG.md updated (if user-facing change) +- [ ] Version bumped in `python/config.py` (if applicable — see versioning rules below) +``` + +### PR Guidelines + +- **Keep PRs focused** — one feature or bug fix per PR +- **Reference related issues** — use `Fixes #123` to link issues +- **Be responsive** — address feedback promptly +- **Don't force-push** — makes reviewing history difficult + +--- + +## 🔢 Versioning + +MetaLens uses `MAJOR.MINOR.PATCH`, with `python/config.py` → `VERSION` as the single source of truth. + +| Component | When it bumps | +|---|---| +| **PATCH** | Bug fix, dependency update, behavior correction | +| **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`. + +--- + +## 🔧 Dependency Updates + +Dependabot proposes routine version bumps automatically, targeting `develop`. Security-alert PRs may target `main` due to a GitHub platform limitation — this is expected, not a misconfiguration. + +To update manually: + +```bash +# Python +pip list --outdated +pip install --upgrade package_name +pip freeze > python/requirements.txt + +# Node (electron/ or frontend/) +npm outdated +npm update package_name +``` + +Then test thoroughly before committing: + +```bash +pytest python/tests/ -v +cd frontend && npm run build +``` + +--- + +## ❓ Questions? + +- **Documentation**: Check [docs/](./docs/) folder +- **Bugs**: [GitHub Issues](https://github.com/overwrite00/MetaLens/issues) +- **Security**: See [SECURITY.md](./SECURITY.md) + +--- + +## 🙏 Thank You + +Thank you for contributing to MetaLens! Your efforts help make file metadata management more accessible and transparent for everyone. + +--- + +*Last updated: 2026-07-14* +*← [Code of Conduct](./CODE_OF_CONDUCT.md) | [Security →](./SECURITY.md)* diff --git a/README.md b/README.md index 1138e0a..9675dd5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -# MetaLens +# 🔍 MetaLens -![Version](https://img.shields.io/badge/version-0.1.5-cyan) +[![Stable](https://img.shields.io/github/v/release/overwrite00/MetaLens?label=stable&color=00d4ff&logo=github)](../../releases/latest) +[![Beta](https://img.shields.io/github/v/release/overwrite00/MetaLens?include_prereleases&label=beta&color=ffaa00&logo=github)](../../releases) ![License](https://img.shields.io/badge/license-MIT-blue) ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux-lightgrey) @@ -8,73 +9,110 @@ MetaLens — Universal File Metadata Manager

-**Universal File Metadata Manager** — read, edit, delete and compare metadata across all major file formats. +**Universal File Metadata Manager** — read, edit, delete and compare metadata across all major file formats, entirely offline. + +> [!TIP] +> 💡 **No account, no cloud, no setup.** Download a binary, double-click, and start browsing — MetaLens never touches the network. --- -## Features +## 📚 Documentation Index -- **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), any file (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 -- **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 +| 📄 Document | 📝 Purpose | +| ------------------------------------------- | -------------------------------------------- | +| [📋 REQUIREMENTS.md](docs/REQUIREMENTS.md) | System requirements and prerequisites | +| [🚀 INSTALLATION.md](docs/INSTALLATION.md) | Step-by-step installation & build-from-source | +| [📖 USAGE.md](docs/USAGE.md) | How to use the application | +| [📡 API.md](docs/API.md) | Sidecar REST API reference for developers | --- -## Download +## ⚡ Quick Start -See [Releases](../../releases) for pre-built binaries: +### 🪟 Windows / 🐧 Linux -| Platform | Download | -|---|---| -| Windows 11 | `MetaLens-vX.Y.Z-Setup-Windows.exe` | -| Linux (deb) | `MetaLens-vX.Y.Z-Linux.deb` | -| Linux (rpm) | `MetaLens-vX.Y.Z-Linux.rpm` | -| Linux (tar) | `MetaLens-vX.Y.Z-Linux.tar.gz` | +1. Grab the latest installer for your platform from [Releases](../../releases) +2. Run it — Windows SmartScreen may warn since the build is unsigned; click **"More info" → "Run anyway"** +3. MetaLens opens with a folder tree on the left — click a folder, then a file, to see its metadata ---- +> ⏱️ **No install wait, no dependency download.** Python and Node.js are bundled into the binary — nothing to set up. -## Build from Source +> [!IMPORTANT] +> ✅ For full details, see [INSTALLATION.md](docs/INSTALLATION.md) -### Prerequisites -- Python 3.11–3.13 -- Node.js 20+ -- npm 10+ +--- -### Steps +## 🎯 What It Does -```bash -# 1. Clone the repository -git clone https://github.com/YOUR_USERNAME/MetaLens.git -cd MetaLens - -# 2. Install Python dependencies -cd python -python -m venv .venv -source .venv/bin/activate # Linux -# .venv\Scripts\activate # Windows -pip install -r requirements.txt - -# 3. Install Node dependencies -cd ../electron && npm install -cd ../frontend && npm install - -# 4. Run in development mode -cd ../electron && npm start - -# 5. Build for production -cd python && pyinstaller main.py --onefile --name metalens-sidecar -cd ../frontend && npm run build -cd ../electron && npm run make ``` +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 │ +└─────────────────────────────────────────────┘ + │ + ▼ + 👁️ View · ✏️ Edit · 🗑️ Delete · ⚖️ Diff · 🔑 Hash +``` + +--- + +## ✨ 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 +- 🔑 **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 +- 🆓 **Free & open-source** — MIT license --- -## Supported Formats +## 🔧 Version + +**v0.2.1** — Routine dependency updates (Electron 43.1, Vite 8.1.4, uvicorn 0.51, lucide-react 1.24). No changes to application behavior or the API. + +📖 **See full version history** → [CHANGELOG.md](./CHANGELOG.md) + +--- + +## 📋 System Requirements + +- **Windows** 10/11 (64-bit) or a modern **Linux** distro (`.deb`/`.rpm`/`.tar.gz`) +- **RAM** 512 MB free +- **Disk** ~300 MB installed +- No Python or Node.js needed to *run* the app — only to build it from source + +> [!IMPORTANT] +> ✅ For complete requirements, see [REQUIREMENTS.md](docs/REQUIREMENTS.md) + +--- + +## 🚀 Getting Started + +### 1️⃣ Install +Follow [INSTALLATION.md](docs/INSTALLATION.md) for Windows/Linux binaries or building from source. + +### 2️⃣ Start Browsing +Learn the interface in [USAGE.md](docs/USAGE.md). + +### 💻 For Developers +Explore the sidecar API in [API.md](docs/API.md). + +--- + +## 🗂️ Supported Formats | Category | Formats | Read | Write | |---|---|---|---| @@ -89,9 +127,9 @@ cd ../electron && npm run make | Any file | Filesystem timestamps, permissions, xattr | ✅ | ✅ | | Other | Hundreds of formats via hachoir | ✅ | — | -### File Integrity +### 🔑 File Integrity -Compute cryptographic hashes on demand from the **Metadata** tab — no impact on browsing performance: +Compute cryptographic hashes on demand from the **Metadata** panel — no impact on browsing performance: | Algorithm | Notes | |---|---| @@ -103,7 +141,13 @@ Compute cryptographic hashes on demand from the **Metadata** tab — no impact o --- -## Architecture +## 🏗️ Architecture + +| Layer | Technology | Notes | +|---|---|---| +| **Desktop shell** | Electron (Node.js main process) | Spawns the Python sidecar, native menus & dialogs | +| **Renderer UI** | React 19, Vite, TailwindCSS v4 | Folder tree, file list, metadata detail panel | +| **Sidecar** | Python 3.11–3.13, FastAPI, uvicorn | Metadata extraction/write engine, `127.0.0.1` only | ``` Electron Main (Node.js) @@ -116,7 +160,8 @@ Python Sidecar (FastAPI) ├── /read — full metadata extraction ├── /write — atomic metadata write-back ├── /delete — field removal - └── /diff — metadata comparison + ├── /diff — metadata comparison + └── /hash — on-demand file hashing React Frontend (Vite + TailwindCSS) ├── FolderPanel — folder tree navigation @@ -124,16 +169,70 @@ React Frontend (Vite + TailwindCSS) └── DetailPanel — View / Edit / Diff tabs ``` +📖 Full endpoint reference → [API.md](docs/API.md) + +--- + +## 📊 Test Suite + +✅ **48 automated tests** covering handlers, the handler registry, and path-security validation. + +```bash +pytest python/tests/ -v +``` + --- -## License +## 🔐 Privacy & Security -MIT License — © 2026 Graziano Mariella +- 🛡️ **No cloud dependencies** — everything runs locally +- 🔒 **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 -See [LICENSE](LICENSE) for full text. +📖 See [SECURITY.md](./SECURITY.md) for the full security model. --- -## Credits +## 🤝 Contributing + +Contributions are welcome! Please: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/your-feature`) +3. Commit changes with clear messages +4. Push to your fork +5. Open a Pull Request to `develop` branch + +📖 See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide. + +> [!NOTE] +> 📖 All PRs should target the **`develop`** branch, not `main`. + +--- + +## 📜 License + +Distributed under the **MIT License**. See [LICENSE](LICENSE) for details. + +--- + +## 🙋 Support + +- 📖 **Documentation:** See [docs/](docs/) folder +- 🐛 **Report issues:** [GitHub Issues](https://github.com/overwrite00/MetaLens/issues) +- 💬 **Questions:** Open a [GitHub Discussion](https://github.com/overwrite00/MetaLens/discussions) + +--- + +## 👨‍💻 Credits + +Developed by **Graziano Mariella** + +Distributed with MIT License · [View License](LICENSE) + +--- -Developed by **Graziano Mariella**. +*Last updated: 2026-07-14* +*← [Requirements](docs/REQUIREMENTS.md) | [Docs →](docs/)* diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..9fc06ef --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,108 @@ +# 🔒 Security Policy + +## Security Model + +MetaLens is designed with security-first principles: + +- **Fully offline** — all metadata reading and writing happens on your machine; there is no network call anywhere in the app +- **Localhost-only sidecar** — the Python (FastAPI) backend binds to `127.0.0.1` only, on a dynamically chosen port, never `0.0.0.0` +- **Electron hardening** — `nodeIntegration: false`, `contextIsolation: true`; the renderer only reaches the OS through a narrow `contextBridge` API (folder/file dialogs, drive listing), never raw Node/`fs` access +- **Atomic writes** — every metadata write copies the file to a `.ml_tmp` temp file, applies the change there, then swaps it in with `os.replace()` (atomic on POSIX and Windows). A crash mid-write leaves the original file untouched +- **Path validation** — a dedicated `core.path_security` module validates and normalizes every path (resolves symlinks, blocks traversal) before any filesystem access, across all API routes +- **Transparent code** — all handler and analysis logic is open-source and auditable + +--- + +## 🛡️ Privacy Principles + +### What We Don't Do + +- ❌ No user accounts or authentication +- ❌ No telemetry or usage tracking +- ❌ No data collection or analytics +- ❌ No cloud processing or storage +- ❌ No auto-update phone-home (updates are manual, via GitHub Releases) + +### Data Control + +- ✅ Your files and their metadata never leave your machine +- ✅ Nothing is written anywhere except the file you explicitly edit +- ✅ The undo stack lives in memory only — it is cleared when you close the app, never written to disk +- ✅ Full source code transparency + +--- + +## 🔐 Technical Security + +### Metadata Handlers + +- Uses established, audited libraries per format: Pillow/piexif (images), mutagen (audio/video), pypdf (PDF), python-docx/openpyxl/python-pptx (Office), olefile (legacy Office), hachoir (fallback) +- Read-only handlers (RAW images, legacy Office, MKV/AVI) are enforced at the API level — `/write` and `/delete` return `422` if the handler doesn't support the operation +- Filesystem access always goes through `validate_file_path`/`validate_directory_path` before touching disk + +### IPC Boundary + +- The Electron renderer talks to the OS only through the `preload.js` `contextBridge` surface (dialogs, path helpers, drive listing) — no direct filesystem or process access +- The renderer talks to the Python sidecar only over HTTP to `127.0.0.1:{dynamic_port}`, never a hardcoded port + +--- + +## ⚠️ Known Limitations + +### What MetaLens Does NOT Do + +- **Code-signed installers** — releases are currently unsigned (no purchased certificate). Windows SmartScreen will warn on first run; this is expected, not a compromise indicator. See [Installation Guide](./docs/INSTALLATION.md) for how to proceed safely +- **Malware/content scanning** — MetaLens edits metadata; it does not scan file contents for malicious payloads +- **Encryption** — files and their metadata are stored and edited in plain form on disk, as with any local file manager +- **Guarantee against all data loss** — while writes are atomic, always keep backups of files you can't afford to lose + +### Defense-in-Depth Recommended + +For files handled outside MetaLens, also rely on your OS's built-in protections (Windows Defender / your Linux distro's package signing) and keep regular backups. + +--- + +## 📢 Responsible Disclosure + +If you discover a security vulnerability in MetaLens: + +### Reporting Process + +1. **Do not open a public GitHub issue** — this exposes the vulnerability before a fix ships +2. **Use GitHub Security Advisory**: + - Navigate to: https://github.com/overwrite00/MetaLens/security/advisories + - Click "Report a vulnerability" + - Describe the vulnerability in detail, including reproduction steps + +### Timeline + +- **48 hours**: acknowledgment of your report +- **2 weeks** (critical): target fix timeline for critical vulnerabilities +- **30 days** (high): target fix timeline for high-severity vulnerabilities +- **Coordinated disclosure**: we'll work with you on a disclosure timeline + +### Recognition + +We appreciate responsible disclosure and will credit you in the security advisory (unless you prefer anonymity). + +--- + +## 🔄 Security Updates + +- **Subscribe to releases**: Watch the [Releases](https://github.com/overwrite00/MetaLens/releases) page +- **Check security advisories**: [GitHub Security Advisory](https://github.com/overwrite00/MetaLens/security/advisories) +- **Update regularly**: dependency security fixes are tracked in [CHANGELOG.md](./CHANGELOG.md) + +--- + +## 📚 Additional Resources + +- [Code of Conduct](./CODE_OF_CONDUCT.md) — community guidelines +- [Contributing Guide](./CONTRIBUTING.md) — how to contribute securely +- [REQUIREMENTS.md](./docs/REQUIREMENTS.md) — dependency information +- [CHANGELOG.md](./CHANGELOG.md) — version history and security fixes + +--- + +*Last updated: 2026-07-14* +*← [Contributing](./CONTRIBUTING.md) | [Back to README →](./README.md)* diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..63dabd8 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,190 @@ +# 📡 API Reference — MetaLens + +REST API exposed by the Python (FastAPI) sidecar that the Electron main process talks to over `127.0.0.1`. + +> [!IMPORTANT] +> 🔒 The sidecar binds to `127.0.0.1` **only**, on a port chosen dynamically at launch (never `0.0.0.0`, never a fixed port). It is not reachable from other machines and is not intended to be called directly by end users — this reference is for developers extending MetaLens. + +--- + +## 📚 Table of Contents + +1. [GET /health](#get-health) +2. [GET /list](#get-list) +3. [GET /read](#get-read) +4. [POST /write](#post-write) +5. [POST /delete](#post-delete) +6. [GET /diff](#get-diff) +7. [GET /hash](#get-hash) +8. [📦 Data Models](#-data-models) + +--- + +## GET `/health` + +Health check + version, used by the About dialog and by Electron to know the sidecar is ready. + +**Response** +```json +{ "status": "ok", "version": "0.2.1", "app": "MetaLens" } +``` + +--- + +## GET `/list` + +Lists the contents of a directory with detected handler per file. + +| Param | Type | Description | +|---|---|---| +| `path` | string (query) | Absolute directory path | + +**Response** +```json +{ + "path": "/home/user/Pictures", + "items": [ + { "name": "photo.jpg", "path": "/home/user/Pictures/photo.jpg", "is_dir": false, "size": 2048301, "mtime": 1752480000, "ext": ".jpg", "handler": "image" } + ] +} +``` + +`handler` is `"—"` when no dedicated handler matches (filesystem-only metadata still applies). + +--- + +## GET `/read` + +Reads full metadata for a single file. Resolves the matching handler, then merges filesystem metadata as a fallback layer for any key not already populated by the handler. + +| Param | Type | Description | +|---|---|---| +| `path` | string (query) | Absolute file path | + +**Response** — a [`MetadataRecord`](#-data-models) + +--- + +## POST `/write` + +Atomically writes one or more metadata fields back to the file. + +**Body** +```json +{ + "path": "/home/user/Pictures/photo.jpg", + "fields": [ + { "key": "Artist", "value": "Jane Doe" } + ] +} +``` + +**Response** +```json +{ "ok": true, "errors": [] } +``` + +Returns **422** if the file's handler reports `supports_write: false` (e.g. RAW images, legacy Office formats, MKV/AVI). + +--- + +## POST `/delete` + +Removes the specified metadata keys from the file. + +**Body** +```json +{ "path": "/home/user/Pictures/photo.jpg", "keys": ["GPSLatitude", "GPSLongitude"] } +``` + +**Response** +```json +{ "ok": true, "errors": [] } +``` + +Returns **422** if the handler reports `supports_delete: false`. + +--- + +## GET `/diff` + +Compares metadata between two files. + +| Param | Type | Description | +|---|---|---| +| `a` | string (query) | Absolute path to the first file | +| `b` | string (query) | Absolute path to the second file | + +**Response** — a [`DiffResult`](#-data-models) + +--- + +## GET `/hash` + +Computes cryptographic hashes for a file on demand, streaming it in 1 MB chunks (no full read into memory). + +| Param | Type | Description | +|---|---|---| +| `path` | string (query) | Absolute file path | +| `algorithms` | string (query, optional) | Comma-separated list. Default: `md5,sha1,sha256,sha512,blake2b` | + +**Response** +```json +{ + "path": "/home/user/Pictures/photo.jpg", + "size": 2048301, + "hashes": { "md5": "...", "sha1": "...", "sha256": "...", "sha512": "...", "blake2b": "..." } +} +``` + +Returns **400** if an unsupported algorithm name is requested. + +--- + +## ⚠️ Error Handling + +All path-based endpoints validate input through `validate_file_path` / `validate_directory_path` before touching the filesystem (existence checks, traversal protection) and return **400** on violation. + +--- + +## 📦 Data Models + +Defined in `python/core/models.py`. + +### `MetadataField` +| Field | Type | Notes | +|---|---|---| +| `key` | string | Raw field identifier | +| `label` | string | Human-readable label | +| `value` | any | Current value | +| `raw_value` | any | Unformatted underlying value | +| `editable` | bool | Whether `/write` accepts this field | +| `deletable` | bool | Whether `/delete` accepts this key | +| `field_type` | enum | `str \| int \| float \| datetime \| bytes \| enum` | +| `source` | enum | `exif \| iptc \| xmp \| id3 \| vorbis \| pdf \| filesystem \| ...` | +| `choices` | array | Present only for `enum` fields | + +### `MetadataRecord` +| Field | Type | +|---|---| +| `file_path` | string | +| `file_size` | int | +| `mtime` | float | +| `handler_name` | string | +| `fields` | `MetadataField[]` | +| `read_errors` | string[] | +| `supports_write` | bool | +| `supports_delete` | bool | + +### `DiffResult` +| Field | Type | +|---|---| +| `file_a` / `file_b` | string | +| `only_in_a` / `only_in_b` | `MetadataField[]` | +| `changed` | `[MetadataField, MetadataField][]` | +| `identical` | `MetadataField[]` | + +--- + +*Last updated: 2026-07-14* +*← [Usage](./USAGE.md) | [Back to README →](../README.md)* diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md new file mode 100644 index 0000000..8ec844a --- /dev/null +++ b/docs/INSTALLATION.md @@ -0,0 +1,153 @@ +# 🚀 Installation Guide — MetaLens + +Step-by-step instructions for installing MetaLens on Windows and Linux, plus building it from source. + +> [!IMPORTANT] +> 💻 **Prerequisites:** Review [REQUIREMENTS.md](./REQUIREMENTS.md) before starting. + +--- + +## 📋 Table of Contents + +1. [🪟 Windows Installation](#-windows-installation) +2. [🐧 Linux Installation](#-linux-installation) +3. [✅ Verify Installation](#-verify-installation) +4. [🛠️ Build from Source](#-build-from-source) +5. [🔧 Troubleshooting](#-troubleshooting) + +--- + +## 🪟 Windows Installation + +1. Go to the [Releases](../../../releases) page +2. Download **`MetaLens-vX.Y.Z-Setup-Windows.exe`** (latest version) +3. Double-click the downloaded file + +> [!WARNING] +> ⚠️ **Installers are not code-signed.** Windows SmartScreen will show "Windows protected your PC". Click **"More info"** → **"Run anyway"** to proceed. This is expected for an unsigned open-source build — see [SECURITY.md](../SECURITY.md) for details. + +4. The installer (Squirrel.Windows) runs per-user — no admin rights required +5. MetaLens launches automatically after installation and adds a Start Menu shortcut + +--- + +## 🐧 Linux Installation + +### 📦 Debian / Ubuntu (`.deb`) + +```bash +wget https://github.com/overwrite00/MetaLens/releases/download/vX.Y.Z/MetaLens-vX.Y.Z-Linux.deb +sudo apt install ./MetaLens-vX.Y.Z-Linux.deb +``` + +### 📦 Fedora / RHEL (`.rpm`) + +```bash +wget https://github.com/overwrite00/MetaLens/releases/download/vX.Y.Z/MetaLens-vX.Y.Z-Linux.rpm +sudo rpm -i MetaLens-vX.Y.Z-Linux.rpm +``` + +### 📦 Any distro (`.tar.gz`) + +```bash +wget https://github.com/overwrite00/MetaLens/releases/download/vX.Y.Z/MetaLens-vX.Y.Z-Linux.tar.gz +tar -xzf MetaLens-vX.Y.Z-Linux.tar.gz +cd MetaLens-vX.Y.Z-Linux +./metalens +``` + +> [!NOTE] +> 📖 Replace `vX.Y.Z` with the version shown on the [Releases](../../../releases) page. + +--- + +## ✅ Verify Installation + +Launch MetaLens, then open **Help → About** (or the About dialog) to confirm the version number matches the one you installed — it's read live from the sidecar's `/health` endpoint. + +You should see: +- ✅ A folder tree on the left (drives and home directory) +- ✅ An empty file list until you select a folder +- ✅ No errors in the window + +--- + +## 🛠️ Build from Source + +Only needed for development or if no pre-built binary exists for your platform. + +```bash +# 1. Clone the repository +git clone https://github.com/overwrite00/MetaLens.git +cd MetaLens + +# 2. Install Python dependencies +cd python +python -m venv .venv +source .venv/bin/activate # Linux +# .venv\Scripts\activate # Windows +pip install -r requirements.txt + +# 3. Install Node dependencies +cd ../electron && npm install +cd ../frontend && npm install + +# 4. Run in development mode +cd ../electron && npm start + +# 5. Build for production +cd ../python && pyinstaller main.py --onefile --name metalens-sidecar +cd ../frontend && npm run build +cd ../electron && npm run make +``` + +`npm run make` (Electron Forge) produces the Windows `.exe`, Linux `.deb`/`.rpm` under `electron/out/make/`. + +--- + +## 🔧 Troubleshooting + +### ❌ "Windows protected your PC" (SmartScreen) + +**Cause:** The installer is not code-signed (no certificate purchased for this open-source project). + +**Solution:** Click **"More info"** → **"Run anyway"**. Verify you downloaded from the official [Releases](../../../releases) page if in doubt. + +--- + +### ❌ `.deb`/`.rpm` install fails with dependency errors + +**Cause:** Missing system libraries used by Electron (e.g. `libgtk-3-0`, `libnotify4`). + +**Solution:** +```bash +sudo apt --fix-broken install # Debian/Ubuntu, after a failed .deb install +``` + +--- + +### ❌ App window opens but the file list never loads + +**Cause:** The Python sidecar failed to start (rare — usually a port conflict or antivirus blocking the bundled executable). + +**Solution:** Restart the app. If it persists, check that no other process is holding the port MetaLens tries to bind on `127.0.0.1`, and confirm your antivirus isn't quarantining `metalens-sidecar.exe`. + +--- + +### ❌ `pyinstaller` or `npm run make` fails while building from source + +**Cause:** Wrong Python/Node version, or dependencies not installed in the right subfolder. + +**Solution:** Confirm Python 3.11–3.13 and Node.js 20+ per [REQUIREMENTS.md](./REQUIREMENTS.md), and re-run `pip install -r requirements.txt` / `npm install` in the exact folders shown above. + +--- + +## ✅ What's Next? + +- **First time?** → Learn the interface in [USAGE.md](./USAGE.md) +- **Developer?** → [API.md](./API.md) + +--- + +*Last updated: 2026-07-14* +*← [Requirements](./REQUIREMENTS.md) | [Usage →](./USAGE.md)* diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md new file mode 100644 index 0000000..aa0242c --- /dev/null +++ b/docs/REQUIREMENTS.md @@ -0,0 +1,93 @@ +# 📋 System Requirements — MetaLens + +Requirements for running the pre-built MetaLens application and, separately, for building it from source. + +> [!TIP] +> 🚀 **Just want to use the app?** You don't need Python or Node.js — download a pre-built binary and skip straight to the [Installation Guide](./INSTALLATION.md). + +--- + +## 📊 Quick Reference + +| 📦 Use case | ✅ Requirement | +|---|---| +| **Run the compiled app** | Windows 10/11 (64-bit) or a modern Linux distro — nothing else | +| **Build from source** | Python 3.11–3.13, Node.js 20+, npm 10+ | + +--- + +## 🖥️ Operating System Support + +### 🪟 Windows +- **Minimum:** Windows 10 (64-bit) +- **Tested on:** Windows 10, Windows 11 +- **Installer:** `MetaLens-vX.Y.Z-Setup-Windows.exe` (Squirrel.Windows) +- **Status:** ✅ Fully supported + +### 🐧 Linux +- **Packages:** `.deb` (Debian/Ubuntu), `.rpm` (Fedora/RHEL), `.tar.gz` (any distro) +- **Status:** ✅ Fully supported + +### 🍎 macOS +- **Status:** ❌ Not currently packaged — no macOS build is produced by CI. Building from source with `npm run make` is possible but unverified. + +--- + +## 💻 Hardware Requirements + +| 🔧 Resource | 📊 Requirement | +|---|---| +| **Processor** | Any modern 64-bit CPU | +| **RAM** | 512 MB free (Electron + Python sidecar) | +| **Storage** | ~300 MB for the installed application | + +--- + +## ❌ What You DON'T Need + +| ❌ | ℹ️ Why you don't need it | +|---|---| +| **Python** | The sidecar ships as a compiled binary (`metalens-sidecar[.exe]`) inside the installer | +| **Node.js** | The frontend is pre-built and bundled into the Electron app | +| **Internet connection** | MetaLens works fully offline — no cloud dependency, no telemetry | +| **Admin/root rights** | Windows installer runs per-user (Squirrel); Linux packages install standard desktop entries | + +--- + +## 🛠️ Building From Source + +Only needed if you want to modify MetaLens or package it yourself. + +| 📦 Tool | ✅ Version | 📝 Notes | +|---|---|---| +| **Python** | 3.11 – 3.13 | Used to run/package the FastAPI sidecar | +| **Node.js** | 20+ | Runs Electron and the Vite build | +| **npm** | 10+ | Installs both `electron/` and `frontend/` dependencies | + +See [Installation Guide → Build from Source](./INSTALLATION.md#-build-from-source) for exact steps. + +--- + +## 🔒 Data Privacy & Security + +> [!IMPORTANT] +> 🛡️ **Your files never leave your machine.** MetaLens has no network calls of any kind. + +- ✅ **No cloud transmission** — all metadata reads/writes happen locally +- ✅ **No telemetry** — zero data collection or external tracking +- ✅ **Localhost-only sidecar** — the Python API binds to `127.0.0.1` only, never `0.0.0.0` +- ✅ **Open source** — full source code available for audit (MIT license) +- ✅ **Offline capable** — no internet connection required at any point + +--- + +## 🚀 Next Steps + +1. **🆕 New to MetaLens?** → [Installation Guide](./INSTALLATION.md) +2. **📖 Ready to browse files?** → [Usage Guide](./USAGE.md) +3. **💻 Developer?** → [API Reference](./API.md) + +--- + +*Last updated: 2026-07-14* +*← [README](../README.md) | [Installation →](./INSTALLATION.md)* diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..ec374fa --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,105 @@ +# 📖 Usage Guide — MetaLens + +Learn how to browse files, inspect and edit metadata, compute hashes, and compare two files. + +> [!TIP] +> 💡 **First time?** Install the app first — see the [Installation Guide](./INSTALLATION.md). + +--- + +## 📚 Table of Contents + +1. [🌐 Open the App](#-open-the-app) +2. [📁 Browse Folders](#-browse-folders) +3. [📄 Select a File](#-select-a-file) +4. [👁️ View Metadata](#-view-metadata) +5. [✏️ Edit Metadata](#-edit-metadata) +6. [🗑️ Delete Metadata Fields](#-delete-metadata-fields) +7. [🔑 Compute File Hashes](#-compute-file-hashes) +8. [⚖️ Compare Two Files (Diff)](#-compare-two-files-diff) +9. [↩️ Undo](#-undo) + +--- + +## 🌐 Open the App + +Launch MetaLens from the Start Menu (Windows) or your application launcher (Linux). The window opens at 1280×800 with three panels: a folder tree on the left, a file list in the center, and an empty detail panel on the right. + +--- + +## 📁 Browse Folders + +The **left panel** (`FolderPanel`) lists your available drives and home directory. Click any entry to expand it and navigate into subfolders — MetaLens reads directly from the local filesystem, nothing is uploaded or synced anywhere. + +--- + +## 📄 Select a File + +The **center panel** (`FilePanel`) shows every file in the currently selected folder, with an icon colored by extension. Click a file to load its metadata into the detail panel on the right. + +> [!NOTE] +> 📖 Every file type is supported in some form — recognized formats get a dedicated handler (EXIF, ID3, PDF info, Office properties, …); anything else falls back to filesystem metadata (name, size, timestamps, permissions). + +--- + +## 👁️ View Metadata + +The **detail panel** (`DetailPanel`) opens on the **View** tab by default, showing a read-only, grouped table (`MetadataTable`) of every field MetaLens extracted — grouped and color-coded by source (EXIF, IPTC, XMP, ID3, Vorbis comments, PDF info, filesystem, …). + +--- + +## ✏️ Edit Metadata + +Switch to the **Edit** tab to open `MetadataEditor`: + +- Editable fields show an input matching their type (text, number, date, or a dropdown for enumerated values) +- Change a value and save — MetaLens writes it back **atomically**: it copies the file to a `.ml_tmp` temp file, applies the change there, then atomically replaces the original (`os.replace`) so a crash or write error never corrupts your file +- If the file's handler doesn't support writing (e.g. RAW images, legacy Office `.doc`/`.xls`/`.ppt`, MKV/AVI video), the Edit tab shows a **read-only** notice instead of an editable form + +--- + +## 🗑️ Delete Metadata Fields + +Individual fields marked as **deletable** can be removed from the Edit tab. Deletion uses the same atomic write-back as editing — the original file is only touched once the operation succeeds. + +--- + +## 🔑 Compute File Hashes + +From the **Metadata** area of the detail panel, compute cryptographic hashes **on demand** (not automatically, to avoid slowing down browsing): + +| Algorithm | Notes | +|---|---| +| MD5 | Legacy, widely used for checksums | +| SHA-1 | Legacy, used by Git and many tools | +| SHA-256 | Modern standard, recommended | +| SHA-512 | High-security workloads | +| BLAKE2b | Fast modern algorithm, built into Python | + +Each computed hash has a one-click **copy to clipboard** button. + +--- + +## ⚖️ Compare Two Files (Diff) + +Switch to the **Diff** tab (`DiffView`) to compare two files side by side: + +- Fields only present in file A, only in file B, changed between the two, and identical fields are each shown in their own section +- The diff view also includes its own hash comparison section, so you can confirm at a glance whether two files are byte-identical + +--- + +## ↩️ Undo + +MetaLens keeps an in-memory undo stack of your last **50** write/delete operations for the current session (`useUndoStack`). It is **not** persisted to disk — closing the app clears it. Trigger undo from the **Edit** menu or its keyboard shortcut. + +--- + +## ℹ️ Checking Your Version + +Open **Help → About** to see the running app version, read live from the Python sidecar's `/health` endpoint — useful when reporting a bug. + +--- + +*Last updated: 2026-07-14* +*← [Installation](./INSTALLATION.md) | [API Reference →](./API.md)* diff --git a/electron/forge.config.js b/electron/forge.config.js index 274a7ce..5c41b2e 100644 --- a/electron/forge.config.js +++ b/electron/forge.config.js @@ -8,6 +8,10 @@ const iconExists = ['.ico', '.icns', '.png'].some(ext => fs.existsSync(iconPath // Read version from package.json — synced by the CI workflow from python/config.py const { version } = require('./package.json') +// RPM's Version field forbids "-" (used for beta tags like 0.2.1-beta.5), +// so the rpm maker gets a sanitized copy while every other maker keeps the real semver. +const rpmSafeVersion = version.replace(/-/g, '') + module.exports = { packagerConfig: { name: 'MetaLens', @@ -28,6 +32,10 @@ module.exports = { config: { name: 'MetaLens', setupIcon: path.join(__dirname, '..', 'frontend', 'public', 'icon.ico'), + // Squirrel names the generated file " Setup.exe" by default, which + // reads as a traditional installer — it's actually a self-contained + // Squirrel bootstrapper, so give it a neutral name instead. + setupExe: `MetaLens-${version}-win-x64.exe`, }, }, { name: '@electron-forge/maker-zip', platforms: ['darwin'] }, @@ -57,6 +65,7 @@ module.exports = { license: 'MIT', icon: path.join(__dirname, '..', 'frontend', 'public', 'icon.png'), categories: ['Utility', 'FileManager'], + version: rpmSafeVersion, }, }, }, diff --git a/electron/package-lock.json b/electron/package-lock.json index 70d61c1..a3d7a79 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "metalens", - "version": "0.1.5", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metalens", - "version": "0.1.5", + "version": "0.2.0", "license": "MIT", "devDependencies": { "@electron-forge/cli": "^7.6.0", @@ -14,7 +14,7 @@ "@electron-forge/maker-rpm": "^7.6.0", "@electron-forge/maker-squirrel": "^7.6.0", "@electron-forge/maker-zip": "^7.11.2", - "electron": "^43.0.0" + "electron": "^43.1.0" } }, "node_modules/@electron-forge/cli": { @@ -2399,9 +2399,9 @@ "license": "MIT" }, "node_modules/electron": { - "version": "43.0.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-43.0.0.tgz", - "integrity": "sha512-PV60GsWU6qufhuOhw3n+Yix3WPDcqDtBqE8orbEQGQGHEkgp9o/JCPgb7L4vIL0r1HnfPdqSRtboOTqbDkcFDQ==", + "version": "43.1.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.1.0.tgz", + "integrity": "sha512-DPfxpQLd4NL3BJ8DBxYAfmLUKKesF5Rx9dQx5FyczAP8bhOPScjHE48GArVeXu68LlAainuwkmQTQvdZwpIIAQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/electron/package.json b/electron/package.json index 7ce6af4..8b0f572 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "metalens", - "version": "0.2.0", + "version": "0.2.1", "description": "Universal File Metadata Manager", "author": "Graziano Mariella", "license": "MIT", @@ -15,6 +15,6 @@ "@electron-forge/maker-rpm": "^7.6.0", "@electron-forge/maker-squirrel": "^7.6.0", "@electron-forge/maker-zip": "^7.11.2", - "electron": "^43.0.0" + "electron": "^43.1.0" } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a49694c..ffbcabc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,14 +1,14 @@ { "name": "metalens-frontend", - "version": "0.1.5", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metalens-frontend", - "version": "0.1.5", + "version": "0.2.0", "dependencies": { - "lucide-react": "^1.23.0", + "lucide-react": "^1.24.0", "react": "^19.1.0", "react-dom": "^19.2.7" }, @@ -16,7 +16,7 @@ "@tailwindcss/vite": "^4.3.2", "@vitejs/plugin-react": "^6.0.3", "tailwindcss": "^4.3.2", - "vite": "^8.1.3" + "vite": "^8.1.4" } }, "node_modules/@emnapi/core": { @@ -1083,9 +1083,9 @@ } }, "node_modules/lucide-react": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", - "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz", + "integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1287,16 +1287,16 @@ "optional": true }, "node_modules/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", + "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", + "picomatch": "^4.0.5", "postcss": "^8.5.16", - "rolldown": "~1.1.3", + "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "bin": { diff --git a/frontend/package.json b/frontend/package.json index cdfe91f..5b81c51 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "metalens-frontend", - "version": "0.2.0", + "version": "0.2.1", "private": true, "type": "module", "scripts": { @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "lucide-react": "^1.23.0", + "lucide-react": "^1.24.0", "react": "^19.1.0", "react-dom": "^19.2.7" }, @@ -17,6 +17,6 @@ "@tailwindcss/vite": "^4.3.2", "@vitejs/plugin-react": "^6.0.3", "tailwindcss": "^4.3.2", - "vite": "^8.1.3" + "vite": "^8.1.4" } } diff --git a/python/config.py b/python/config.py index b7f1629..b38ac19 100644 --- a/python/config.py +++ b/python/config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -VERSION = "0.2.0" +VERSION = "0.2.1" APP_NAME = "MetaLens" SIDECAR_HOST = "127.0.0.1" diff --git a/python/requirements.txt b/python/requirements.txt index 2c384fc..7d78d96 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -3,7 +3,7 @@ # Web server fastapi==0.139.0 -uvicorn==0.49.0 +uvicorn==0.51.0 # Images Pillow==12.3.0 diff --git a/scripts/update_docs_dates.py b/scripts/update_docs_dates.py new file mode 100644 index 0000000..3ed90f5 --- /dev/null +++ b/scripts/update_docs_dates.py @@ -0,0 +1,58 @@ +"""Update the '*Last updated: YYYY-MM-DD*' footer of staged Markdown docs to today. + +Invoked by the .githooks/pre-commit hook. Only touches files that are staged +for commit and already contain a footer line matching the expected pattern. +""" + +import datetime +import re +import subprocess +import sys + +FOOTER_PATTERN = re.compile(r"^(\*Last updated: )\d{4}-\d{2}-\d{2}(\*[ \t]*)$", re.MULTILINE) + + +def staged_markdown_files() -> list[str]: + output = subprocess.run( + ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"], + capture_output=True, + text=True, + check=True, + ).stdout + return [line for line in output.splitlines() if line.endswith(".md")] + + +def update_file(path: str, today: str) -> bool: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + new_content, count = FOOTER_PATTERN.subn(rf"\g<1>{today}\g<2>", content) + if count == 0 or new_content == content: + return False + + with open(path, "w", encoding="utf-8") as f: + f.write(new_content) + return True + + +def main() -> int: + today = datetime.date.today().isoformat() + changed = [] + + for path in staged_markdown_files(): + try: + if update_file(path, today): + changed.append(path) + except FileNotFoundError: + continue + + if changed: + subprocess.run(["git", "add", *changed], check=True) + for path in changed: + print(f"Updated 'Last updated' footer in {path} -> {today}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main())