Compare commits
No files matched your search
@@ -0,0 +1,17 @@
|
|||||||
|
name: Report a bug
|
||||||
|
description: Something did not work as expected
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes: { label: "What happened?", description: "Steps to reproduce, expected behavior, and actual behavior." }
|
||||||
|
validations: { required: true }
|
||||||
|
- type: input
|
||||||
|
id: system
|
||||||
|
attributes: { label: "Operating system and Guará version", placeholder: "Ubuntu 24.04 / guara 1.3.2" }
|
||||||
|
- type: input
|
||||||
|
id: media
|
||||||
|
attributes: { label: "Media type and filesystem", placeholder: "32 GB USB drive, FAT32" }
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes: { label: "Terminal output (guara inspect / scan)", render: shell }
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## What changes and why
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] `cargo fmt --all` and `cargo clippy --workspace --all-targets` pass without warnings
|
||||||
|
- [ ] `cargo test --workspace` passes
|
||||||
|
- [ ] Nothing in this PR writes to the source device
|
||||||
|
- [ ] Commits follow Conventional Commits in English and code/comments are in English
|
||||||
|
- [ ] CHANGELOG updated (if there is a user-visible change)
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Documentation-only or local agent state changes do not trigger CI jobs.
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**', '.wolf/**', '.claude/**', '.opencode/**', '.codex/**', '.cursor/**', '.superpowers/**', '.superset/**', '.gitea/ISSUE_TEMPLATE/**', 'scripts/release-notes.sh', 'scripts/release-upload.sh', '.gitea/workflows/release.yml']
|
||||||
|
pull_request:
|
||||||
|
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**', '.wolf/**', '.claude/**', '.opencode/**', '.codex/**', '.cursor/**', '.superpowers/**', '.superset/**', '.gitea/ISSUE_TEMPLATE/**', 'scripts/release-notes.sh', 'scripts/release-upload.sh', '.gitea/workflows/release.yml']
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUSTFLAGS: -D warnings
|
||||||
|
# The runner persists /opt/hostedtoolcache across jobs: rustup toolchains,
|
||||||
|
# cargo registry, npm/apt caches, and target directories live there so nothing
|
||||||
|
# is re-downloaded or rebuilt from scratch on every run. Each job uses its own
|
||||||
|
# target directory so differing RUSTFLAGS do not invalidate another job's cache.
|
||||||
|
CARGO_HOME: /opt/hostedtoolcache/cargo
|
||||||
|
RUSTUP_HOME: /opt/hostedtoolcache/rustup
|
||||||
|
NPM_CONFIG_CACHE: /opt/hostedtoolcache/npm-cache
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Format and clippy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/guara-lint
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
- uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
- name: UI translation coverage
|
||||||
|
run: node scripts/check-ui-i18n.mjs
|
||||||
|
- name: Updater latest.json unit tests
|
||||||
|
run: python3 scripts/test_make_latest_json.py
|
||||||
|
- run: cargo fmt --all -- --check
|
||||||
|
- run: cargo clippy --workspace --all-targets
|
||||||
|
|
||||||
|
# Linux-only runner (ARM64).
|
||||||
|
test:
|
||||||
|
name: Tests (Linux)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/guara-test
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Test image tools (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
timeout-minutes: 25
|
||||||
|
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/guara-tools mtools dosfstools exfatprogs ntfs-3g e2fsprogs xfsprogs imagemagick zip
|
||||||
|
- name: Reference disk images (BitLocker, FileVault 2, dfvfs, AFF4)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p /opt/hostedtoolcache/guara-testdata target
|
||||||
|
ln -sfn /opt/hostedtoolcache/guara-testdata target/testdata
|
||||||
|
scripts/fetch-testdata.sh
|
||||||
|
# Mounts an image via loop device (mount -o loop), which requires a
|
||||||
|
# privileged container. Unprivileged runners skip this step cleanly.
|
||||||
|
- name: Real XFS deletion fixture (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if [ ! -f target/testdata/xfs-real-delete/disk.img ]; then
|
||||||
|
sudo scripts/make-xfs-real-deletion.sh
|
||||||
|
fi
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${RUNNER_OS}" = "Linux" ]; then export GUARA_REQUIRE_FIXTURES=1; fi
|
||||||
|
cargo test --workspace
|
||||||
|
|
||||||
|
audit:
|
||||||
|
name: Dependency audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
# rustsec/audit-check depends on GitHub's checks API. cargo-audit itself
|
||||||
|
# needs nothing but the advisory database, and installing it once into the
|
||||||
|
# persistent CARGO_HOME makes every later run skip compilation.
|
||||||
|
- name: cargo audit
|
||||||
|
run: |
|
||||||
|
command -v cargo-audit >/dev/null || cargo install cargo-audit --locked
|
||||||
|
cargo audit
|
||||||
|
|
||||||
|
gui:
|
||||||
|
name: Check desktop app (Linux)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/guara-gui
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
- name: Tauri system dependencies
|
||||||
|
timeout-minutes: 25
|
||||||
|
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
- name: Generate icons
|
||||||
|
run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons
|
||||||
|
- run: cargo check --manifest-path apps/gui/src-tauri/Cargo.toml
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: ""
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Triggered when a v* tag is pushed: creates the Gitea Release entry with notes
|
||||||
|
# from CHANGELOG.md, builds the CLI (Linux x86_64 and aarch64) and the desktop
|
||||||
|
# app (Linux aarch64, Linux x86_64, and Windows via cargo-xwin cross-compilation),
|
||||||
|
# and generates the latest.json manifest read by the desktop updater.
|
||||||
|
#
|
||||||
|
# No macOS: Tauri requires Apple's SDK and code-signing hardware/account, which
|
||||||
|
# this Linux ARM64 runner does not have.
|
||||||
|
#
|
||||||
|
# Required repository/user secrets: GUARA_TAURI_SIGNING_PRIVATE_KEY and
|
||||||
|
# GUARA_TAURI_SIGNING_PRIVATE_KEY_PASSWORD (sign updater bundles). Unprefixed
|
||||||
|
# secret names remain supported as fallback.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
CARGO_HOME: /opt/hostedtoolcache/cargo
|
||||||
|
RUSTUP_HOME: /opt/hostedtoolcache/rustup
|
||||||
|
NPM_CONFIG_CACHE: /opt/hostedtoolcache/npm-cache
|
||||||
|
XWIN_CACHE_DIR: /opt/hostedtoolcache/xwin-cache
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Create release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Release
|
||||||
|
env:
|
||||||
|
TAG: ${{ gitea.ref_name }}
|
||||||
|
TOKEN: ${{ gitea.token }}
|
||||||
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
|
run: |
|
||||||
|
# Re-running the workflow does not recreate an existing release.
|
||||||
|
if curl -fsS -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" >/dev/null 2>&1; then
|
||||||
|
echo "release $TAG already exists"; exit 0
|
||||||
|
fi
|
||||||
|
scripts/release-notes.sh "$TAG" > notes.md
|
||||||
|
pre=false; case "$TAG" in *-*) pre=true ;; esac
|
||||||
|
jq -n --arg tag "$TAG" --arg name "Guará ${TAG#v}" --rawfile body notes.md --argjson pre "$pre" \
|
||||||
|
'{tag_name:$tag, name:$name, body:$body, prerelease:$pre}' > release.json
|
||||||
|
curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \
|
||||||
|
-H 'Content-Type: application/json' -d @release.json >/dev/null
|
||||||
|
|
||||||
|
cli:
|
||||||
|
name: CLI (${{ matrix.platform }})
|
||||||
|
needs: release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/guara-release-cli-${{ matrix.platform }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { platform: linux-aarch64, target: aarch64-unknown-linux-gnu, apt: "" }
|
||||||
|
- { platform: linux-x86_64, target: x86_64-unknown-linux-gnu, apt: "gcc-x86-64-linux-gnu" }
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
- name: Platform cross-linker
|
||||||
|
if: ${{ matrix.apt != '' }}
|
||||||
|
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/cli-x86_64 ${{ matrix.apt }}
|
||||||
|
- name: Build and package
|
||||||
|
run: scripts/package-cli.sh "${{ gitea.ref_name }}" "${{ matrix.platform }}" "${{ matrix.target }}" | tee packages.txt
|
||||||
|
- name: Upload to release
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ gitea.token }}
|
||||||
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
|
run: scripts/release-upload.sh "${{ gitea.ref_name }}" $(cat packages.txt)
|
||||||
|
|
||||||
|
gui:
|
||||||
|
name: Desktop app (${{ matrix.platform }})
|
||||||
|
needs: release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/guara-release-gui-${{ matrix.platform }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.GUARA_TAURI_SIGNING_PRIVATE_KEY || secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.GUARA_TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [linux-aarch64, linux-x86_64, windows]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
- name: Tauri dependencies (Linux aarch64)
|
||||||
|
if: matrix.platform == 'linux-aarch64'
|
||||||
|
timeout-minutes: 25
|
||||||
|
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
- name: Tauri dependencies (Linux x86_64)
|
||||||
|
if: matrix.platform == 'linux-x86_64'
|
||||||
|
timeout-minutes: 25
|
||||||
|
run: |
|
||||||
|
if [ "$(uname -m)" = aarch64 ]; then
|
||||||
|
dpkg --add-architecture amd64
|
||||||
|
. /etc/os-release
|
||||||
|
if [ "${ID:-}" = ubuntu ]; then
|
||||||
|
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
|
||||||
|
grep -q '^Architectures:' /etc/apt/sources.list.d/ubuntu.sources || sed -i '/^Types: deb/a Architectures: arm64' /etc/apt/sources.list.d/ubuntu.sources
|
||||||
|
printf 'Types: deb\nURIs: http://archive.ubuntu.com/ubuntu/\nSuites: %s %s-updates %s-security\nComponents: main universe\nArchitectures: amd64\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n' "$VERSION_CODENAME" "$VERSION_CODENAME" "$VERSION_CODENAME" >/etc/apt/sources.list.d/amd64.sources
|
||||||
|
elif [ -f /etc/apt/sources.list ]; then
|
||||||
|
sed -i 's/^deb http/deb [arch=arm64] http/' /etc/apt/sources.list
|
||||||
|
printf 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s main universe\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s-updates main universe\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s-security main universe\n' "$VERSION_CODENAME" "$VERSION_CODENAME" "$VERSION_CODENAME" >/etc/apt/sources.list.d/amd64.list
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-amd64 gcc-x86-64-linux-gnu g++-x86-64-linux-gnu pkg-config patchelf file squashfs-tools qemu-user libglib2.0-bin libwebkit2gtk-4.1-dev:amd64 libayatana-appindicator3-dev:amd64 librsvg2-dev:amd64
|
||||||
|
else
|
||||||
|
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
fi
|
||||||
|
- name: Cross-compilation tools (Windows)
|
||||||
|
if: matrix.platform == 'windows'
|
||||||
|
run: |
|
||||||
|
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-win nsis lld llvm clang
|
||||||
|
command -v cargo-xwin >/dev/null || cargo install cargo-xwin --locked
|
||||||
|
- name: Build and package
|
||||||
|
run: scripts/build-gui.sh "${{ matrix.platform }}"
|
||||||
|
- name: Upload to release
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ gitea.token }}
|
||||||
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
|
run: scripts/release-upload.sh "${{ gitea.ref_name }}" dist-gui/*
|
||||||
|
|
||||||
|
updater:
|
||||||
|
name: Build latest.json
|
||||||
|
needs: gui
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: latest.json
|
||||||
|
env:
|
||||||
|
TAG: ${{ gitea.ref_name }}
|
||||||
|
TOKEN: ${{ gitea.token }}
|
||||||
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
|
run: |
|
||||||
|
scripts/release-notes.sh "$TAG" > notes.md
|
||||||
|
python3 scripts/make-latest-json.py "$TAG" notes.md latest.json
|
||||||
|
- name: Upload to release
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ gitea.token }}
|
||||||
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
|
run: scripts/release-upload.sh "${{ gitea.ref_name }}" latest.json
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Enforces Conventional Commits 1.0.0 and English-only commit messages.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
msg_file="${1:?usage: commit-msg <file>}"
|
||||||
|
python3 - "$msg_file" <<'PY'
|
||||||
|
import re, sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
raw = Path(sys.argv[1]).read_text(encoding="utf-8")
|
||||||
|
lines = [l for l in raw.splitlines() if not l.startswith("#")]
|
||||||
|
while lines and not lines[0].strip():
|
||||||
|
lines.pop(0)
|
||||||
|
if not lines:
|
||||||
|
sys.exit("commit-msg: empty commit message")
|
||||||
|
|
||||||
|
subject = lines[0].strip()
|
||||||
|
if subject.startswith(("Merge ", "Revert \"")):
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
conv_re = re.compile(
|
||||||
|
r"^(feat|fix|chore|ci|docs|refactor|test|perf|build|style|revert)(\([a-z0-9_./-]+\))?!?: .+$"
|
||||||
|
)
|
||||||
|
if not conv_re.match(subject):
|
||||||
|
sys.exit(
|
||||||
|
f"commit-msg: invalid Conventional Commit header: {subject!r}\n"
|
||||||
|
"Expected: <type>[optional scope]: <description in English>\n"
|
||||||
|
"Allowed types: feat, fix, chore, ci, docs, refactor, test, perf, build, style, revert"
|
||||||
|
)
|
||||||
|
|
||||||
|
body = "\n".join(lines)
|
||||||
|
cleaned = re.sub(r"\b(Guará|Sabiá|Acauã|Ciberfauna)\b", "", body)
|
||||||
|
pt_accents = re.search(r"[áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ]", cleaned)
|
||||||
|
pt_words = re.search(
|
||||||
|
r"\b(não|para|como|quando|então|pelos|pelas|pelo|pela|estão|está|também|versão|"
|
||||||
|
r"corrige|adiciona|atualiza|remove|padroniza|implementa|ajusta|arquivo|arquivos)\b",
|
||||||
|
cleaned,
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
if pt_accents or pt_words:
|
||||||
|
sys.exit("commit-msg: commit messages must be written 100% in English.")
|
||||||
|
PY
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
exec "$(git rev-parse --show-toplevel)/scripts/check.sh"
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
name: Relatar um problema
|
|
||||||
description: Algo não funcionou como esperado
|
|
||||||
labels: [bug]
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
id: o-que
|
|
||||||
attributes: { label: O que aconteceu?, description: "Passos, o que esperava e o que viu." }
|
|
||||||
validations: { required: true }
|
|
||||||
- type: input
|
|
||||||
id: sistema
|
|
||||||
attributes: { label: "Sistema operacional e versão do Guará", placeholder: "Windows 11 / guara 0.1.0" }
|
|
||||||
- type: input
|
|
||||||
id: midia
|
|
||||||
attributes: { label: "Tipo de mídia e sistema de arquivos", placeholder: "pendrive 32 GB, FAT32" }
|
|
||||||
- type: textarea
|
|
||||||
id: log
|
|
||||||
attributes: { label: "Saída do terminal (guara inspect / scan)", render: shell }
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
## O que muda e por quê
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
- [ ] `cargo fmt --all` e `cargo clippy --workspace --all-targets` sem avisos
|
|
||||||
- [ ] `cargo test --workspace` passa
|
|
||||||
- [ ] Nada neste PR escreve no dispositivo de origem
|
|
||||||
- [ ] CHANGELOG atualizado (se houver mudança visível)
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ci-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
RUSTFLAGS: -D warnings
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: Formatação e clippy
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
components: rustfmt, clippy
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
- name: Cobertura da tradução da interface
|
|
||||||
run: node scripts/check-ui-i18n.mjs
|
|
||||||
- run: cargo fmt --all -- --check
|
|
||||||
- run: cargo clippy --workspace --all-targets
|
|
||||||
|
|
||||||
test:
|
|
||||||
name: Testes (${{ matrix.os }})
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-24.04, windows-latest, macos-latest]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
- name: Ferramentas para imagens de teste (Linux)
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y mtools dosfstools exfatprogs ntfs-3g e2fsprogs imagemagick zip
|
|
||||||
- name: Testes (com imagens reais FAT32/NTFS/ext4 no Linux)
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if [ "$RUNNER_OS" = "Linux" ]; then export GUARA_REQUIRE_FIXTURES=1; fi
|
|
||||||
cargo test --workspace
|
|
||||||
|
|
||||||
audit:
|
|
||||||
name: Auditoria de dependências
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: rustsec/audit-check@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
gui:
|
|
||||||
name: Compila o app desktop (Linux)
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: apps/gui/src-tauri
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
- name: Dependências do Tauri
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
|
||||||
- name: Gera os ícones
|
|
||||||
run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons
|
|
||||||
- run: cargo check --manifest-path apps/gui/src-tauri/Cargo.toml
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: ""
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
# Publica uma release pública ao empurrar uma tag vX.Y.Z (ex.: git tag v0.1.0 && git push --tags)
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: ["v*.*.*"]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-release:
|
|
||||||
name: Cria a release
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
outputs:
|
|
||||||
release_id: ${{ steps.rel.outputs.result }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- name: Notas da versão (do CHANGELOG)
|
|
||||||
id: notes
|
|
||||||
run: |
|
|
||||||
v="${GITHUB_REF_NAME#v}"
|
|
||||||
v="${v%%-*}"
|
|
||||||
awk -v v="$v" '$0 ~ "^## \\[" v "\\]" {f=1; next} /^## \[/ {f=0} f' CHANGELOG.md > notes.md
|
|
||||||
[ -s notes.md ] || echo "Veja o CHANGELOG." > notes.md
|
|
||||||
- uses: actions/github-script@v9
|
|
||||||
id: rel
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
const tag = context.ref.replace('refs/tags/', '');
|
|
||||||
const r = await github.rest.repos.createRelease({
|
|
||||||
owner: context.repo.owner, repo: context.repo.repo, tag_name: tag,
|
|
||||||
name: `Guará ${tag}`, body: fs.readFileSync('notes.md', 'utf8'),
|
|
||||||
draft: true, prerelease: tag.includes('-'),
|
|
||||||
});
|
|
||||||
return r.data.id;
|
|
||||||
|
|
||||||
cli:
|
|
||||||
name: CLI (${{ matrix.name }})
|
|
||||||
needs: create-release
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- { name: linux-x86_64, os: ubuntu-24.04, target: x86_64-unknown-linux-gnu }
|
|
||||||
- { name: linux-aarch64, os: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu }
|
|
||||||
- { name: macos-aarch64, os: macos-latest, target: aarch64-apple-darwin }
|
|
||||||
- { name: macos-x86_64, os: macos-latest, target: x86_64-apple-darwin }
|
|
||||||
- { name: windows-x86_64, os: windows-latest, target: x86_64-pc-windows-msvc }
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: ${{ matrix.target }}
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
- run: cargo build --release --locked -p guara-cli --target ${{ matrix.target }}
|
|
||||||
- name: Empacota
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
name="guara-${GITHUB_REF_NAME}-${{ matrix.name }}"
|
|
||||||
mkdir "$name"
|
|
||||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
|
||||||
cp target/${{ matrix.target }}/release/guara.exe "$name/"
|
|
||||||
else
|
|
||||||
cp target/${{ matrix.target }}/release/guara "$name/"
|
|
||||||
fi
|
|
||||||
cp README.md LICENSE-MIT LICENSE-APACHE "$name/"
|
|
||||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
|
||||||
7z a "$name.zip" "$name" >/dev/null; asset="$name.zip"
|
|
||||||
else
|
|
||||||
tar czf "$name.tar.gz" "$name"; asset="$name.tar.gz"
|
|
||||||
fi
|
|
||||||
if command -v sha256sum >/dev/null; then sha256sum "$asset" > "$asset.sha256"; else shasum -a 256 "$asset" > "$asset.sha256"; fi
|
|
||||||
echo "ASSET=$asset" >> "$GITHUB_ENV"
|
|
||||||
- name: Envia para a release
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: gh release upload "$GITHUB_REF_NAME" "$ASSET" "$ASSET.sha256" --clobber
|
|
||||||
|
|
||||||
gui:
|
|
||||||
name: App desktop (${{ matrix.name }})
|
|
||||||
needs: create-release
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- { name: linux-x86_64, os: ubuntu-24.04, args: "" }
|
|
||||||
- { name: macos-aarch64, os: macos-latest, args: "--target aarch64-apple-darwin" }
|
|
||||||
- { name: macos-x86_64, os: macos-latest, args: "--target x86_64-apple-darwin" }
|
|
||||||
- { name: windows-x86_64, os: windows-latest, args: "" }
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: ${{ contains(matrix.args, 'apple') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: apps/gui/src-tauri
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
- name: Dependências do Tauri (Linux)
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
|
||||||
- name: Gera os ícones
|
|
||||||
run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons
|
|
||||||
- uses: tauri-apps/tauri-action@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
projectPath: apps/gui/src-tauri
|
|
||||||
releaseId: ${{ needs.create-release.outputs.release_id }}
|
|
||||||
args: ${{ matrix.args }}
|
|
||||||
|
|
||||||
publish:
|
|
||||||
name: Publica a release
|
|
||||||
needs: [create-release, cli, gui]
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- name: Notas da versão (do CHANGELOG)
|
|
||||||
run: |
|
|
||||||
v="${GITHUB_REF_NAME#v}"
|
|
||||||
v="${v%%-*}"
|
|
||||||
awk -v v="$v" '$0 ~ "^## \\[" v "\\]" {f=1; next} /^## \[/ {f=0} f' CHANGELOG.md > notes.md
|
|
||||||
[ -s notes.md ] || echo "Veja o CHANGELOG." > notes.md
|
|
||||||
- uses: actions/github-script@v9
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
await github.rest.repos.updateRelease({
|
|
||||||
owner: context.repo.owner, repo: context.repo.repo,
|
|
||||||
release_id: ${{ needs.create-release.outputs.release_id }},
|
|
||||||
name: `Guará ${context.ref.replace('refs/tags/', '')}`,
|
|
||||||
body: fs.readFileSync('notes.md', 'utf8'),
|
|
||||||
draft: false,
|
|
||||||
});
|
|
||||||
@@ -1,18 +1,42 @@
|
|||||||
# Material de trabalho local, não versionado
|
# Build artifacts
|
||||||
.superpowers/
|
|
||||||
|
|
||||||
# Estado de ferramentas locais
|
|
||||||
.wolf/
|
|
||||||
.claude/
|
|
||||||
CLAUDE.md
|
|
||||||
|
|
||||||
# Variáveis de ambiente locais (o setup do Superset copia da raiz)
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
|
|
||||||
# Build
|
|
||||||
target/
|
target/
|
||||||
|
apps/gui/src-tauri/target/
|
||||||
node_modules/
|
node_modules/
|
||||||
*.img
|
*.img
|
||||||
*.guara-session.json
|
*.guara-session.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# Sabiá local captures and audit log
|
||||||
|
*.pcap
|
||||||
|
*.cap
|
||||||
|
.sabia-audit.log
|
||||||
|
|
||||||
|
# Logo copied to the UI directory by build.rs
|
||||||
|
/apps/gui/ui/logo.png
|
||||||
|
|
||||||
|
# Local AI agent configuration and state — not part of the product
|
||||||
|
.claude/
|
||||||
|
.wolf/
|
||||||
|
.opencode/
|
||||||
|
.codex/
|
||||||
|
.cursor/
|
||||||
|
.superpowers/
|
||||||
|
.superset/
|
||||||
|
CLAUDE.md
|
||||||
|
AGENTS.md
|
||||||
|
GEMINI.md
|
||||||
|
|
||||||
|
# Local environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Local scratch files
|
||||||
|
*.eml
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Packages generated by scripts/package-cli.sh and scripts/build-gui.sh
|
||||||
|
/guara-v*
|
||||||
|
/sabia-v*
|
||||||
|
/dist-gui
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"setup": ["./.superset/setup.sh"],
|
|
||||||
"teardown": ["./.superset/teardown.sh"],
|
|
||||||
"run": ["cargo run -p guara-cli -- ui --no-open"]
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Runs once when a Superset workspace (git worktree) is created. Safe to run again.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Local files that are not tracked by git but that a new worktree needs.
|
|
||||||
# Nothing is overwritten if it already exists in the workspace.
|
|
||||||
if [ -n "${SUPERSET_ROOT_PATH:-}" ] && [ -d "$SUPERSET_ROOT_PATH" ]; then
|
|
||||||
for f in "$SUPERSET_ROOT_PATH"/.env "$SUPERSET_ROOT_PATH"/.env.* "$SUPERSET_ROOT_PATH"/CLAUDE.md; do
|
|
||||||
[ -f "$f" ] || continue
|
|
||||||
dest="./$(basename "$f")"
|
|
||||||
[ -e "$dest" ] || cp "$f" "$dest"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Download the crates now so the first build and the first test run do not wait on the network.
|
|
||||||
cargo fetch --locked
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Runs when a Superset workspace is deleted.
|
|
||||||
# Setup starts no background service. The only long-running process is the Run command
|
|
||||||
# (`guara ui`), which this workspace builds into its own target/ directory, so stop any
|
|
||||||
# copy that is still alive before the worktree is removed.
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
pkill -f -- "$PWD/target/.*/guara ui" 2>/dev/null || true
|
|
||||||
exit 0
|
|
||||||
@@ -4,6 +4,54 @@
|
|||||||
|
|
||||||
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
|
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
|
||||||
|
|
||||||
|
## [1.3.2] - 2026-10-30
|
||||||
|
|
||||||
|
The project moved from GitHub to Gitea, and the update check followed it.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Updates now come from Gitea.** The desktop app's update card reads
|
||||||
|
`latest.json` from `git.lucasrguerra.dev.br` instead of GitHub Releases.
|
||||||
|
Versions up to 1.3.1 still look at GitHub, which is no longer reachable, so
|
||||||
|
they cannot find this release: download 1.3.2 once, and later updates are
|
||||||
|
automatic.
|
||||||
|
- The CLI is published for Linux x86_64 and aarch64 only, built by the Gitea CI
|
||||||
|
when a `v*` tag is pushed, with a `.sha256` next to each package. **There is
|
||||||
|
no Windows CLI any more.** The desktop app still ships for Linux (AppImage,
|
||||||
|
`.deb`) and Windows (NSIS `.exe`, cross-compiled from Linux). There is no
|
||||||
|
macOS build: it needs Apple's SDK and code signing, which this project's CI
|
||||||
|
(one Linux runner) cannot provide.
|
||||||
|
|
||||||
|
## [1.3.1] - 2026-10-23
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **New official logo:** the maned wolf (guará) inside the orange ring, drawn by the project's author, with a transparent background so the same art works in the light and dark themes. It replaces the old track-disc logo in the app (sidebar, top bar and home screen), the favicon, the desktop app icons and the README.
|
||||||
|
- `guara ui` serves the new `guara.png` and `guara-s.png`; `scripts/gerar-logo.py` derives the small logo and the app icon from the official art.
|
||||||
|
- Development: `scripts/verificar.sh` runs locally what the CI runs, with an optional pre-push hook (`scripts/instalar-hooks.sh`), and the CI no longer runs on documentation-only changes.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- No change to scanning, recovery, signing or any other behavior. This is the first release after the in-app updater: an app on 1.3.0 should offer **Update now**.
|
||||||
|
- The screenshots in `docs/img/` still show the old logo and will be redone.
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-10-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Update from inside the desktop app.** On startup the app asks GitHub for the release's `latest.json`; if there is a newer version, a card offers **Update now**. It downloads the package, verifies its minisign signature against the public key built into the app, installs and reopens (the AppImage is replaced in place; Windows and macOS use the installer). Installs from `.deb` or `.rpm` only show the notice. **Settings** can turn the startup check off and has a **Check now** button. The check is one HTTP request and sends nothing of yours.
|
||||||
|
- **Digital signatures (Ed25519):** `guara sign keygen | file | verify | recovery`. With a key configured, `recover` and `report` sign the manifest and the report by themselves. `guara sign recovery` checks the manifest signature and the hash of every recovered file (and refuses manifest paths that leave the folder). The signature is plain Ed25519 over a documented message, so `openssl` verifies it too. Comparing the key fingerprint (`--trust`) says whose key it is; the signed time is the local clock, not a third-party timestamp.
|
||||||
|
- **Virtual disks:** QCOW2 with a backing file, VMDK snapshots (parent by name or, in VirtualBox, by UUID; the child is refused if the parent's CID changed) and DMG with LZFSE chunks.
|
||||||
|
- **CI with real reference images** on Linux, macOS and Windows (they were skipped before), and an XFS image deleted by the real kernel generated with `sudo` on the Linux runner.
|
||||||
|
- Release pipeline: update packages are signed with a key kept in the repository's `release` environment (tags `v*` only), and `latest.json` is built in one step by `scripts/make-latest-json.py`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **XFS is now 🟡.** On a real XFS (v5, kernel deletion) the inode and extent list are wiped, so the metadata path finds nothing; the content of files with a recognizable structure comes back by carving, without name or folder. The earlier path only worked on simulated deletions. The name-and-folder test is kept `ignore`d until the journal is read.
|
||||||
|
- CI `apt` steps use cached `.deb`s, a time limit per attempt and retries (they took up to 28 minutes on a slow mirror day).
|
||||||
|
- Dependencies: `ctutils` 0.4.5 (RUSTSEC-2026-0385); new `ed25519-compact`, `lzfse_rust`, `tauri-plugin-updater`.
|
||||||
|
- The README and ROADMAP now say the update check is the only network use by default.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- Versions before 1.3.0 do not have the updater: from 1.2.0, replace the AppImage by hand one last time. The first update inside the app is 1.3.0 to the next release.
|
||||||
|
- Still missing: password-protected DMG, an identity check for VHD, VHDX and QCOW2 parents, third-party timestamps (RFC 3161), XFS journal reading, and APFS validation with real deletion (needs an image from a real Mac).
|
||||||
|
- CLI output remains in Portuguese.
|
||||||
|
|
||||||
## [1.2.0] - 2026-10-22
|
## [1.2.0] - 2026-10-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -4,6 +4,54 @@
|
|||||||
|
|
||||||
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer.
|
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer.
|
||||||
|
|
||||||
|
## [1.3.2] - 2026-10-30
|
||||||
|
|
||||||
|
O projeto saiu do GitHub para o Gitea, e a verificação de atualização foi junto.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- **As atualizações agora vêm do Gitea.** O cartão de atualização do app
|
||||||
|
desktop lê o `latest.json` em `git.lucasrguerra.dev.br`, e não mais as
|
||||||
|
Releases do GitHub. As versões até a 1.3.1 ainda olham o GitHub, que não
|
||||||
|
responde mais, e por isso não enxergam esta release: baixe a 1.3.2 uma vez, e
|
||||||
|
as próximas atualizações são automáticas.
|
||||||
|
- O CLI é publicado só para Linux x86_64 e aarch64, compilado pelo CI do Gitea
|
||||||
|
quando uma tag `v*` é enviada, com um `.sha256` ao lado de cada pacote. **Não
|
||||||
|
há mais CLI para Windows.** O app desktop continua saindo para Linux
|
||||||
|
(AppImage, `.deb`) e Windows (`.exe` NSIS, compilado a partir do Linux). Não
|
||||||
|
há build para macOS: exige o SDK e a assinatura da Apple, que o CI deste
|
||||||
|
projeto (um único runner Linux) não tem.
|
||||||
|
|
||||||
|
## [1.3.1] - 2026-10-23
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- **Logo oficial nova:** o guará dentro do anel laranja, desenhado pelo autor do projeto, com fundo transparente para a mesma arte servir aos temas claro e escuro. Ela substitui a logo antiga das trilhas de disco no app (barra lateral, topo e tela inicial), no favicon, nos ícones do app desktop e no README.
|
||||||
|
- O `guara ui` serve os novos `guara.png` e `guara-s.png`; o `scripts/gerar-logo.py` deriva a logo pequena e o ícone do app da arte oficial.
|
||||||
|
- Desenvolvimento: o `scripts/verificar.sh` roda localmente o que o CI roda, com um hook opcional de pre-push (`scripts/instalar-hooks.sh`), e o CI deixa de rodar em mudanças só de documentação.
|
||||||
|
|
||||||
|
### Notas
|
||||||
|
- Nenhuma mudança na varredura, na recuperação, na assinatura ou em qualquer outro comportamento. É a primeira versão depois do atualizador do app: um app na 1.3.0 deve oferecer **Atualizar agora**.
|
||||||
|
- Os screenshots em `docs/img/` ainda mostram a logo antiga e serão refeitos.
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-10-23
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- **Atualização dentro do app desktop.** Ao abrir, o app pede ao GitHub o `latest.json` da release; havendo versão nova, um cartão oferece **Atualizar agora**. Ele baixa o pacote, confere a assinatura minisign com a chave pública embutida no app, instala e reabre (o AppImage é substituído no lugar; Windows e macOS usam o instalador). Instalações por `.deb` ou `.rpm` só mostram o aviso. **Configurações** desliga a consulta ao abrir e tem o botão **Verificar agora**. A consulta é uma requisição HTTP e não envia nada seu.
|
||||||
|
- **Assinatura digital (Ed25519):** `guara sign keygen | file | verify | recovery`. Com uma chave configurada, `recover` e `report` assinam sozinhos o manifesto e o relatório. `guara sign recovery` confere a assinatura do manifesto e o hash de cada arquivo recuperado (e recusa caminhos do manifesto que saem da pasta). É Ed25519 comum sobre uma mensagem documentada, então o `openssl` também confere. Comparar a impressão digital da chave (`--trust`) diz de quem ela é; a hora assinada é a do relógio local, não um carimbo de terceiros.
|
||||||
|
- **Discos virtuais:** QCOW2 com arquivo de apoio, snapshots de VMDK (pai por nome ou, no VirtualBox, por UUID; o filho é recusado se o CID do pai mudou) e DMG com chunks LZFSE.
|
||||||
|
- **CI com imagens de referência reais** em Linux, macOS e Windows (antes eram puladas) e uma imagem XFS apagada pelo kernel de verdade, gerada com `sudo` no runner Linux.
|
||||||
|
- Pipeline de release: os pacotes de atualização são assinados com uma chave guardada no ambiente `release` do repositório (só tags `v*`), e o `latest.json` é montado de uma vez por `scripts/make-latest-json.py`.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- **O XFS passa a 🟡.** Num XFS real (v5, exclusão do kernel) o inode e a lista de extensões são zerados, então o caminho por metadados não acha nada; o conteúdo de arquivos com estrutura reconhecível volta por carving, sem nome nem pasta. O caminho anterior só funcionava na exclusão simulada. O teste de nome e pasta fica `ignore`d até o diário ser lido.
|
||||||
|
- Os passos de `apt` do CI usam `.deb` em cache, tempo limite por tentativa e nova tentativa (levavam até 28 minutos num dia de espelho lento).
|
||||||
|
- Dependências: `ctutils` 0.4.5 (RUSTSEC-2026-0385); novas `ed25519-compact`, `lzfse_rust` e `tauri-plugin-updater`.
|
||||||
|
- O README e o ROADMAP passam a dizer que a verificação de atualização é o único uso da rede por padrão.
|
||||||
|
|
||||||
|
### Notas
|
||||||
|
- As versões anteriores à 1.3.0 não têm o atualizador: da 1.2.0, troque o AppImage à mão uma última vez. A primeira atualização dentro do app é da 1.3.0 para a próxima versão.
|
||||||
|
- Ainda faltam: DMG com senha, conferência da identidade do pai em VHD, VHDX e QCOW2, carimbo de tempo de terceiros (RFC 3161), leitura do diário do XFS e validação de APFS com exclusão real (precisa de imagem de um Mac de verdade).
|
||||||
|
- A saída da CLI continua em português.
|
||||||
|
|
||||||
## [1.2.0] - 2026-10-22
|
## [1.2.0] - 2026-10-22
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|||||||
@@ -7,19 +7,32 @@ Thanks for helping! Golden rules:
|
|||||||
1. **Never write to the source.** Any code that opens a device must use `source::open_path` (read-only). PRs that write to the source disk are rejected.
|
1. **Never write to the source.** Any code that opens a device must use `source::open_path` (read-only). PRs that write to the source disk are rejected.
|
||||||
2. **Test with real data.** New detectors/readers need a test that recovers a known file bit for bit (see `crates/guara-core/tests/` and `scripts/make-fixtures.sh`).
|
2. **Test with real data.** New detectors/readers need a test that recovers a known file bit for bit (see `crates/guara-core/tests/` and `scripts/make-fixtures.sh`).
|
||||||
3. **Be honest about confidence.** If the content cannot be validated, confidence is at most medium.
|
3. **Be honest about confidence.** If the content cannot be validated, confidence is at most medium.
|
||||||
|
4. **100% English codebase and comments.** All source code, comments, scripts, CI workflows, and commit messages must be written in English, while the GUI/CLI remains bilingual (PT-BR and EN via `i18n-en.js`).
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo fmt --all
|
scripts/install-hooks.sh # once: enables commit-msg and pre-push Git hooks
|
||||||
cargo clippy --workspace --all-targets
|
scripts/check.sh # fast (seconds): fmt, clippy as in CI, i18n, scripts, workflows, semver
|
||||||
cargo test --workspace
|
scripts/check.sh --full # includes cargo test --workspace and cargo audit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The CI takes 5 to 30 minutes on Linux, so run the check before pushing: almost everything that makes the CI fail shows up locally in seconds. Documentation-only changes (`*.md`, `docs/`) skip the CI.
|
||||||
|
|
||||||
On Linux the tests build images with `mtools`, `ntfs-3g`, `e2fsprogs`, `exfatprogs`, `imagemagick`, `zip` and `python3`. Without these tools the image tests are skipped (set `GUARA_REQUIRE_FIXTURES=1` to make them fail).
|
On Linux the tests build images with `mtools`, `ntfs-3g`, `e2fsprogs`, `exfatprogs`, `imagemagick`, `zip` and `python3`. Without these tools the image tests are skipped (set `GUARA_REQUIRE_FIXTURES=1` to make them fail).
|
||||||
|
|
||||||
For the external reference images (BitLocker, FileVault 2, dfvfs, AFF4), run `scripts/fetch-testdata.sh`; without them those tests are skipped. The BitLocker test takes about 75 s.
|
For the external reference images (BitLocker, FileVault 2, dfvfs, AFF4), run `scripts/fetch-testdata.sh`; without them those tests are skipped. The BitLocker test takes about 75 s.
|
||||||
|
|
||||||
|
The desktop app signs its update packages: `tauri.conf.json` has `createUpdaterArtifacts` on, so a local `tauri build` needs `TAURI_SIGNING_PRIVATE_KEY` (any key made with `tauri signer generate`; the release key lives only in the repository secrets). For a quick local bundle, pass `--config '{"bundle":{"createUpdaterArtifacts":false}}'`.
|
||||||
|
|
||||||
|
## Releases and Versioning
|
||||||
|
|
||||||
|
Versions strictly follow **Semantic Versioning 2.0.0** (`MAJOR.MINOR.PATCH`), synchronized across `Cargo.toml`, `apps/gui/src-tauri/Cargo.toml`, and `apps/gui/src-tauri/tauri.conf.json` (enforced by `scripts/check.sh`).
|
||||||
|
|
||||||
|
Pushing a `v*` tag makes the Gitea CI create the release (notes come from `CHANGELOG.md`, so update it first), attach the Linux CLI (x86_64 and aarch64), the desktop app for Linux (x86_64 and aarch64) and Windows, and build the updater's `latest.json`.
|
||||||
|
|
||||||
|
There is no macOS build: it needs Apple's SDK and code signing, which a Linux runner cannot provide.
|
||||||
|
|
||||||
## Where to make changes
|
## Where to make changes
|
||||||
|
|
||||||
A map of the modules is in [docs/architecture.md](docs/architecture.md). What is missing and what is welcome is in the [ROADMAP](ROADMAP.md).
|
A map of the modules is in [docs/architecture.md](docs/architecture.md). What is missing and what is welcome is in the [ROADMAP](ROADMAP.md).
|
||||||
@@ -38,4 +51,4 @@ The main version is in English and every file has a Portuguese translation (`*.p
|
|||||||
|
|
||||||
## Commits
|
## Commits
|
||||||
|
|
||||||
Short messages in the imperative (`fix reading of sparse runs in NTFS`). One logical change per PR.
|
Commits must follow **Conventional Commits 1.0.0** in English (`feat:`, `fix:`, `chore:`, `ci:`, `docs:`, `refactor:`, `test:`, `perf:`, `build:`), e.g. `fix(ntfs): handle sparse data runs correctly`. One logical change per PR. Enforced automatically by `.githooks/commit-msg`.
|
||||||
@@ -7,19 +7,32 @@ Obrigado por ajudar! Regras de ouro:
|
|||||||
1. **Nunca escreva na origem.** Qualquer código que abra um dispositivo deve usar `source::open_path` (somente leitura). PRs que escrevem no disco de origem são recusados.
|
1. **Nunca escreva na origem.** Qualquer código que abra um dispositivo deve usar `source::open_path` (somente leitura). PRs que escrevem no disco de origem são recusados.
|
||||||
2. **Teste com dados reais.** Novos detectores/leitores precisam de teste que recupera um arquivo conhecido bit a bit (veja `crates/guara-core/tests/` e `scripts/make-fixtures.sh`).
|
2. **Teste com dados reais.** Novos detectores/leitores precisam de teste que recupera um arquivo conhecido bit a bit (veja `crates/guara-core/tests/` e `scripts/make-fixtures.sh`).
|
||||||
3. **Seja honesto sobre confiança.** Se o conteúdo não puder ser validado, a confiança é no máximo média.
|
3. **Seja honesto sobre confiança.** Se o conteúdo não puder ser validado, a confiança é no máximo média.
|
||||||
|
4. **Código, comentários e commits 100% em inglês.** Todo código-fonte, comentários, scripts, workflows de CI e mensagens de commit devem ser escritos em inglês, enquanto a interface gráfica/CLI permanece bilíngue (PT-BR e EN via `i18n-en.js`).
|
||||||
|
|
||||||
## Fluxo
|
## Fluxo
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo fmt --all
|
scripts/install-hooks.sh # uma vez: ativa os hooks commit-msg e pre-push
|
||||||
cargo clippy --workspace --all-targets
|
scripts/check.sh # rápido (segundos): fmt, clippy como no CI, tradução, scripts, workflows, semver
|
||||||
cargo test --workspace
|
scripts/check.sh --full # inclui cargo test --workspace e cargo audit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
O CI leva de 5 a 30 minutos em Linux, então rode a verificação antes do push: quase tudo que faz o CI falhar aparece aqui em segundos. Mudanças só de documentação (`*.md`, `docs/`) pulam o CI.
|
||||||
|
|
||||||
No Linux os testes geram imagens com `mtools`, `ntfs-3g`, `e2fsprogs`, `exfatprogs`, `imagemagick`, `zip` e `python3`. Sem essas ferramentas os testes com imagem são ignorados (defina `GUARA_REQUIRE_FIXTURES=1` para falhar).
|
No Linux os testes geram imagens com `mtools`, `ntfs-3g`, `e2fsprogs`, `exfatprogs`, `imagemagick`, `zip` e `python3`. Sem essas ferramentas os testes com imagem são ignorados (defina `GUARA_REQUIRE_FIXTURES=1` para falhar).
|
||||||
|
|
||||||
Para as imagens de referência externas (BitLocker, FileVault 2, dfvfs, AFF4), rode `scripts/fetch-testdata.sh`; sem elas esses testes são pulados. O teste de BitLocker leva cerca de 75 s.
|
Para as imagens de referência externas (BitLocker, FileVault 2, dfvfs, AFF4), rode `scripts/fetch-testdata.sh`; sem elas esses testes são pulados. O teste de BitLocker leva cerca de 75 s.
|
||||||
|
|
||||||
|
O app desktop assina os pacotes de atualização: o `tauri.conf.json` tem `createUpdaterArtifacts` ligado, então um `tauri build` local precisa de `TAURI_SIGNING_PRIVATE_KEY` (qualquer chave feita com `tauri signer generate`; a chave da release fica só nos segredos do repositório). Para um pacote local rápido, passe `--config '{"bundle":{"createUpdaterArtifacts":false}}'`.
|
||||||
|
|
||||||
|
## Releases e Versionamento
|
||||||
|
|
||||||
|
As versões seguem estritamente o **Versionamento Semântico 2.0.0** (`MAJOR.MINOR.PATCH`), sincronizadas entre `Cargo.toml`, `apps/gui/src-tauri/Cargo.toml` e `apps/gui/src-tauri/tauri.conf.json` (validado por `scripts/check.sh`).
|
||||||
|
|
||||||
|
Enviar uma tag `v*` faz o CI do Gitea criar a release (as notas vêm do `CHANGELOG.md`, então atualize-o antes), anexar o CLI para Linux (x86_64 e aarch64), o app desktop para Linux (x86_64 e aarch64) e Windows, e montar o `latest.json` do atualizador.
|
||||||
|
|
||||||
|
Não há build para macOS: ele exige o SDK e a assinatura da Apple, que um runner Linux não tem.
|
||||||
|
|
||||||
## Onde mexer
|
## Onde mexer
|
||||||
|
|
||||||
Mapa dos módulos em [docs/pt-BR/arquitetura.md](docs/pt-BR/arquitetura.md). O que falta e o que é bem-vindo está no [ROADMAP](ROADMAP.pt-BR.md).
|
Mapa dos módulos em [docs/pt-BR/arquitetura.md](docs/pt-BR/arquitetura.md). O que falta e o que é bem-vindo está no [ROADMAP](ROADMAP.pt-BR.md).
|
||||||
@@ -38,4 +51,4 @@ A versão principal é em inglês e cada arquivo tem uma tradução em portuguê
|
|||||||
|
|
||||||
## Commits
|
## Commits
|
||||||
|
|
||||||
Mensagens curtas no imperativo (`corrige leitura de runs esparsos no NTFS`). Uma mudança lógica por PR.
|
Os commits devem seguir **Conventional Commits 1.0.0** em inglês (`feat:`, `fix:`, `chore:`, `ci:`, `docs:`, `refactor:`, `test:`, `perf:`, `build:`), ex.: `fix(ntfs): handle sparse data runs correctly`. Uma mudança lógica por PR. Validado automaticamente por `.githooks/commit-msg`.
|
||||||
@@ -309,9 +309,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctutils"
|
name = "ctutils"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574"
|
checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cmov",
|
"cmov",
|
||||||
]
|
]
|
||||||
@@ -340,6 +340,15 @@ dependencies = [
|
|||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519-compact"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1454db4f2edb7f0e8fe0c5b375b0c978fc63244cc9f010d160e417eb10139aa8"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -389,8 +398,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -405,7 +416,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "guara-cli"
|
name = "guara-cli"
|
||||||
version = "1.2.0"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
@@ -418,11 +429,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "guara-core"
|
name = "guara-core"
|
||||||
version = "1.2.0"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"argon2",
|
"argon2",
|
||||||
"bzip2",
|
"bzip2",
|
||||||
|
"ed25519-compact",
|
||||||
"flate2",
|
"flate2",
|
||||||
"hmac",
|
"hmac",
|
||||||
"image",
|
"image",
|
||||||
@@ -430,6 +442,7 @@ dependencies = [
|
|||||||
"jpeg-encoder",
|
"jpeg-encoder",
|
||||||
"libc",
|
"libc",
|
||||||
"lz4_flex",
|
"lz4_flex",
|
||||||
|
"lzfse_rust",
|
||||||
"md-5",
|
"md-5",
|
||||||
"mp4",
|
"mp4",
|
||||||
"pbkdf2",
|
"pbkdf2",
|
||||||
@@ -552,6 +565,16 @@ version = "0.7.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0370574b86f7eca156b9f298392b5e69a23f8c86f3f865add60bbc2e79467a6"
|
checksum = "a0370574b86f7eca156b9f298392b5e69a23f8c86f3f865add60bbc2e79467a6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libbz2-rs-sys"
|
name = "libbz2-rs-sys"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -582,6 +605,12 @@ version = "0.14.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ecbdfe44b1bd960b68170b417450a628c43f7cf56bb3c5317e61cb230ee7f226"
|
checksum = "ecbdfe44b1bd960b68170b417450a628c43f7cf56bb3c5317e61cb230ee7f226"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lzfse_rust"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5edeac3ec657925dececc672bdd58fa2212291886ddcb3e08aa46924c78340c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -844,6 +873,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruzstd"
|
name = "ruzstd"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
@@ -1053,6 +1088,51 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.9",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ resolver = "2"
|
|||||||
members = ["crates/guara-core", "crates/guara-cli"]
|
members = ["crates/guara-core", "crates/guara-cli"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.2.0"
|
version = "1.3.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/lucasrguerra/guara"
|
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/guara"
|
||||||
authors = ["Guará contributors"]
|
authors = ["Guará contributors"]
|
||||||
rust-version = "1.80"
|
rust-version = "1.80"
|
||||||
|
|
||||||
@@ -17,6 +17,6 @@ opt-level = 3
|
|||||||
strip = true
|
strip = true
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
# Dependências otimizadas também em debug: os testes de cifra fazem milhões de SHA-256.
|
# Optimize dependencies in debug builds as well: cipher tests run millions of SHA-256 rounds.
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
@@ -18,7 +18,7 @@ RUN apt-get update \
|
|||||||
COPY --from=build /src/target/release/guara /usr/local/bin/guara
|
COPY --from=build /src/target/release/guara /usr/local/bin/guara
|
||||||
LABEL org.opencontainers.image.title="Guará" \
|
LABEL org.opencontainers.image.title="Guará" \
|
||||||
org.opencontainers.image.description="File recovery for hard drives, SSDs and USB sticks (CLI)" \
|
org.opencontainers.image.description="File recovery for hard drives, SSDs and USB sticks (CLI)" \
|
||||||
org.opencontainers.image.source="https://github.com/lucasrguerra/guara" \
|
org.opencontainers.image.source="https://git.lucasrguerra.dev.br/lucasrguerra/guara" \
|
||||||
org.opencontainers.image.licenses="MIT OR Apache-2.0"
|
org.opencontainers.image.licenses="MIT OR Apache-2.0"
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
ENTRYPOINT ["guara"]
|
ENTRYPOINT ["guara"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<p align="center"><img src="apps/gui/ui/guara-logo.svg" width="360" alt="Guará"></p>
|
<p align="center"><img src="logo.png" width="300" alt="Guará"></p>
|
||||||
<h1 align="center">Guará</h1>
|
<h1 align="center">Guará</h1>
|
||||||
<p align="center">Free file recovery for hard drives, SSDs and USB sticks.<br>Terminal and graphical interface · Windows, Linux and macOS · read-only on the source.</p>
|
<p align="center">Free file recovery for hard drives, SSDs and USB sticks.<br>Terminal and graphical interface · Windows and Linux · read-only on the source.</p>
|
||||||
|
|
||||||
<p align="center"><a href="docs/support.md">What it supports</a> · <a href="docs/interface.md">Interface</a> · <a href="docs/cli.md">Terminal</a> · <a href="docs/architecture.md">Architecture</a> · <a href="ROADMAP.md">Roadmap</a> · <a href="CHANGELOG.md">Changelog</a></p>
|
<p align="center"><a href="docs/support.md">What it supports</a> · <a href="docs/interface.md">Interface</a> · <a href="docs/cli.md">Terminal</a> · <a href="docs/architecture.md">Architecture</a> · <a href="ROADMAP.md">Roadmap</a> · <a href="CHANGELOG.md">Changelog</a></p>
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
In a single scan, Guará does what tools like Recuva, PhotoRec, TestDisk and Disk Drill do separately:
|
In a single scan, Guará does what tools like Recuva, PhotoRec, TestDisk and Disk Drill do separately:
|
||||||
|
|
||||||
- **File system metadata:** finds deleted files **with name, folder and date** on NTFS, FAT12/16/32, exFAT, ext2/3/4, HFS+, Btrfs, XFS and APFS.
|
- **File system metadata:** finds deleted files **with name, folder and date** on NTFS, FAT12/16/32, exFAT, ext2/3/4, HFS+, Btrfs and APFS (on XFS only the content comes back, by carving, see below).
|
||||||
- **Structure-aware carving** in free space, damaged partitions and unpartitioned space. The detectors read the real structure of dozens of formats to learn the **exact size** and a **confidence** level.
|
- **Structure-aware carving** in free space, damaged partitions and unpartitioned space. The detectors read the real structure of dozens of formats to learn the **exact size** and a **confidence** level.
|
||||||
- **Encrypted volumes:** opens BitLocker, LUKS and FileVault 2 with your password or key and scans inside.
|
- **Encrypted volumes:** opens BitLocker, LUKS and FileVault 2 with your password or key and scans inside.
|
||||||
- **RAID and LVM:** reassembles arrays from loose disks (RAID 0, 1, 4, 5, 6, 10, LVM2), working out level, stripe size and order by itself when there is no metadata.
|
- **RAID and LVM:** reassembles arrays from loose disks (RAID 0, 1, 4, 5, 6, 10, LVM2), working out level, stripe size and order by itself when there is no metadata.
|
||||||
@@ -38,13 +38,16 @@ Download the package for your system from **[Releases](../../releases)**:
|
|||||||
|
|
||||||
| | CLI | Graphical app |
|
| | CLI | Graphical app |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Windows | `guara-*-windows-x86_64.zip` | `.msi` / `.exe` installer |
|
| Windows | — | `.msi` / `.exe` installer |
|
||||||
| macOS | `guara-*-macos-{aarch64,x86_64}.tar.gz` | `.dmg` |
|
|
||||||
| Linux | `guara-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` |
|
| Linux | `guara-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` |
|
||||||
|
|
||||||
Every file has a `.sha256` next to it. To build from source: `cargo build --release -p guara-cli` (Rust 1.80+).
|
The CLI is Linux-only; the desktop app ships for Linux and Windows. Every CLI
|
||||||
|
package has a `.sha256` next to it. To build from source: `cargo build
|
||||||
|
--release -p guara-cli` (Rust 1.80+, Linux).
|
||||||
|
|
||||||
> The binaries are **not signed yet**: Windows SmartScreen and macOS Gatekeeper will warn you. Check the SHA-256.
|
**Updates:** the desktop app (AppImage and Windows) checks Gitea for a newer version when it opens; if there is one, a card offers **Update now**: it downloads, verifies the package signature, installs and reopens, so you no longer swap the AppImage by hand. The check is a single request for the release's `latest.json` and nothing of yours is sent; turn it off in **Settings**. Installs from `.deb`/`.rpm` only show the notice, because the package manager owns those files.
|
||||||
|
|
||||||
|
> The binaries are **not signed yet**: Windows SmartScreen will warn you. Check the SHA-256.
|
||||||
|
|
||||||
Reading raw disks requires privileges: **administrator** (Windows), `sudo` (Linux and macOS). Even as root, Guará writes sessions, reports and recovered files with the owner and permissions of the user who launched it.
|
Reading raw disks requires privileges: **administrator** (Windows), `sudo` (Linux and macOS). Even as root, Guará writes sessions, reports and recovered files with the owner and permissions of the user who launched it.
|
||||||
|
|
||||||
@@ -91,18 +94,18 @@ A desktop app (Tauri) in the style of a file manager, designed for when Guará f
|
|||||||
- **Automatic sessions** and reconnection: Guará recognizes the same disk coming back under another name.
|
- **Automatic sessions** and reconnection: Guará recognizes the same disk coming back under another name.
|
||||||
- **Duplicate detection** by content signature.
|
- **Duplicate detection** by content signature.
|
||||||
|
|
||||||
> **Warning:** Guará has **not been externally validated** yet (for example, in the NIST CFTT tests) and has no audit log or signed report. It is well suited to recovery and triage; for use in court, check the results by other means. The path to get there is in the [ROADMAP](ROADMAP.md).
|
> **Warning:** Guará has **not been externally validated** yet (for example, in the NIST CFTT tests) and has no trusted timestamp (RFC 3161) on its signatures. It is well suited to recovery and triage; for use in court, check the results by other means. The path to get there is in the [ROADMAP](ROADMAP.md).
|
||||||
|
|
||||||
## Known limits
|
## Known limits
|
||||||
|
|
||||||
- **Overwritten data does not come back.** On an SSD with TRIM the disk may zero deleted blocks; Guará warns you, but it cannot work miracles.
|
- **Overwritten data does not come back.** On an SSD with TRIM the disk may zero deleted blocks; Guará warns you, but it cannot work miracles.
|
||||||
- **Fragments:** only JPEG (up to three pieces) and MP4 without an index are reassembled. Other formats, if fragmented, come out truncated with low confidence.
|
- **Fragments:** only JPEG (up to three pieces) and MP4 without an index are reassembled. Other formats, if fragmented, come out truncated with low confidence.
|
||||||
- **ReFS and Storage Spaces** are not supported (ReFS is only detected).
|
- **ReFS and Storage Spaces** are not supported (ReFS is only detected).
|
||||||
- **APFS:** no free-space map. **XFS and APFS:** deletion recovery tested only in simulation.
|
- **XFS:** on a real XFS (v5, kernel deletion) the inode and its extent list are wiped, so Guará recovers the **content** of files with a recognizable structure by carving, but not their name or folder; reading names needs the file system journal, which is not implemented. The old inode-based path only works on simulated deletions. **APFS:** deletion recovery tested only in simulation.
|
||||||
- **NTFS:** compressed files, EFS and heavily fragmented records are skipped or marked as incomplete.
|
- **NTFS:** compressed files, EFS and heavily fragmented records are skipped or marked as incomplete.
|
||||||
- **exFAT:** validated with a synthetic image; real media are still missing.
|
- **exFAT:** validated with a synthetic image; real media are still missing.
|
||||||
- **Phones:** Guará works with storage media and does **not** unlock devices or bypass passwords.
|
- **Phones:** Guará works with storage media and does **not** unlock devices or bypass passwords.
|
||||||
- **Windows and macOS:** the code builds and is tested in CI, but the tests with real images run only on Linux. Reports from real media are very welcome.
|
- **Windows:** the code builds and is tested in CI, but the tests with real images run only on Linux. There is no macOS build: Tauri needs the macOS SDK and code signing, both tied to Apple hardware and an Apple Developer account, which this project's CI (a single Linux runner) does not have. The source still compiles for macOS; it is just not built or tested.
|
||||||
- **GUI:** on Linux and macOS the app restarts entirely as administrator (`pkexec` or `osascript`) instead of using a helper process.
|
- **GUI:** on Linux and macOS the app restarts entirely as administrator (`pkexec` or `osascript`) instead of using a helper process.
|
||||||
|
|
||||||
## Where it is going
|
## Where it is going
|
||||||
@@ -111,8 +114,9 @@ Summary of the [ROADMAP](ROADMAP.md):
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Done** | Metadata from 8 file systems, BitLocker, LUKS, FileVault 2, RAID, LVM, E01, Ex01, AFF4, fragmented JPEG, MP4 without an index, SQLite reader, SMART, QCOW2/VMDK/VHD/VHDX/DMG, hash-chained audit trail, PDF report, APFS free map |
|
| **Done** | Metadata from 8 file systems, BitLocker, LUKS, FileVault 2, RAID, LVM, E01, Ex01, AFF4, fragmented JPEG, MP4 without an index, SQLite reader, SMART, QCOW2/VMDK/VHD/VHDX/DMG, hash-chained audit trail, PDF report, Ed25519 signatures, APFS free map |
|
||||||
| **Next** | Digitally signed report, tests on Windows and macOS |
|
| **Next** | Trusted timestamps (RFC 3161), more tests on Windows |
|
||||||
|
| **Not planned** | A macOS build — see [Known limits](#known-limits) |
|
||||||
| **Later** | NSRL and hash lists, content search, unified timeline, browser interpretation, Windows registry and messaging apps, dictionary and brute-force attacks on encrypted volumes |
|
| **Later** | NSRL and hash lists, content search, unified timeline, browser interpretation, Windows registry and messaging apps, dictionary and brute-force attacks on encrypted volumes |
|
||||||
| **No reference image** | ReFS and Storage Spaces: they only land with validation |
|
| **No reference image** | ReFS and Storage Spaces: they only land with validation |
|
||||||
| **Long term** | iOS and Android backups (iTunes, ADB), OCR, NIST CFTT validation, physical phone access through bootloader/chip exploits |
|
| **Long term** | iOS and Android backups (iTunes, ADB), OCR, NIST CFTT validation, physical phone access through bootloader/chip exploits |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<p align="center"><img src="apps/gui/ui/guara-logo.svg" width="360" alt="Guará"></p>
|
<p align="center"><img src="logo.png" width="300" alt="Guará"></p>
|
||||||
<h1 align="center">Guará</h1>
|
<h1 align="center">Guará</h1>
|
||||||
<p align="center">Recuperação de arquivos livre e gratuita para HD, SSD e pendrive.<br>Terminal e interface gráfica · Windows, Linux e macOS · somente leitura na origem.</p>
|
<p align="center">Recuperação de arquivos livre e gratuita para HD, SSD e pendrive.<br>Terminal e interface gráfica · Windows e Linux · somente leitura na origem.</p>
|
||||||
|
|
||||||
<p align="center"><a href="docs/pt-BR/suporte.md">O que suporta</a> · <a href="docs/pt-BR/interface.md">Interface</a> · <a href="docs/pt-BR/cli.md">Terminal</a> · <a href="docs/pt-BR/arquitetura.md">Arquitetura</a> · <a href="ROADMAP.pt-BR.md">Roadmap</a> · <a href="CHANGELOG.pt-BR.md">Changelog</a></p>
|
<p align="center"><a href="docs/pt-BR/suporte.md">O que suporta</a> · <a href="docs/pt-BR/interface.md">Interface</a> · <a href="docs/pt-BR/cli.md">Terminal</a> · <a href="docs/pt-BR/arquitetura.md">Arquitetura</a> · <a href="ROADMAP.pt-BR.md">Roadmap</a> · <a href="CHANGELOG.pt-BR.md">Changelog</a></p>
|
||||||
|
|
||||||
@@ -38,13 +38,16 @@ Baixe o pacote do seu sistema em **[Releases](../../releases)**:
|
|||||||
|
|
||||||
| | CLI | App gráfico |
|
| | CLI | App gráfico |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Windows | `guara-*-windows-x86_64.zip` | instalador `.msi` / `.exe` |
|
| Windows | — | instalador `.msi` / `.exe` |
|
||||||
| macOS | `guara-*-macos-{aarch64,x86_64}.tar.gz` | `.dmg` |
|
|
||||||
| Linux | `guara-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` |
|
| Linux | `guara-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` |
|
||||||
|
|
||||||
Cada arquivo tem um `.sha256` ao lado. Compilar do código: `cargo build --release -p guara-cli` (Rust 1.80+).
|
O CLI é só para Linux; o app desktop sai para Linux e Windows. Cada pacote do
|
||||||
|
CLI tem um `.sha256` ao lado. Compilar do código: `cargo build --release -p
|
||||||
|
guara-cli` (Rust 1.80+, Linux).
|
||||||
|
|
||||||
> Os binários ainda **não são assinados**: o Windows SmartScreen e o Gatekeeper do macOS vão avisar. Confira o SHA-256.
|
**Atualizações:** o app desktop (AppImage e Windows) consulta o Gitea ao abrir; havendo versão nova, um cartão oferece **Atualizar agora**: ele baixa, confere a assinatura do pacote, instala e reabre, e você deixa de trocar o AppImage na mão. A consulta é uma única requisição ao `latest.json` da release e nada seu é enviado; desligue em **Configurações**. Instalações por `.deb`/`.rpm` só mostram o aviso, porque quem manda nesses arquivos é o gerenciador de pacotes.
|
||||||
|
|
||||||
|
> Os binários ainda **não são assinados**: o Windows SmartScreen vai avisar. Confira o SHA-256.
|
||||||
|
|
||||||
Ler discos brutos exige privilégios: **administrador** (Windows), `sudo` (Linux e macOS). Mesmo como root, o Guará grava sessões, relatórios e arquivos recuperados com o dono e as permissões do usuário que o abriu.
|
Ler discos brutos exige privilégios: **administrador** (Windows), `sudo` (Linux e macOS). Mesmo como root, o Guará grava sessões, relatórios e arquivos recuperados com o dono e as permissões do usuário que o abriu.
|
||||||
|
|
||||||
@@ -91,7 +94,7 @@ App desktop (Tauri) no estilo de um gerenciador de arquivos, pensado para quando
|
|||||||
- **Sessões automáticas** e reconexão: o Guará reconhece o mesmo disco que volta com outro nome.
|
- **Sessões automáticas** e reconexão: o Guará reconhece o mesmo disco que volta com outro nome.
|
||||||
- **Detecção de duplicadas** por assinatura de conteúdo.
|
- **Detecção de duplicadas** por assinatura de conteúdo.
|
||||||
|
|
||||||
> **Atenção:** o Guará ainda **não foi validado externamente** (por exemplo, nos testes do NIST CFTT) e não tem registro de auditoria nem relatório assinado. Serve bem para recuperação e triagem; para uso em juízo, confira os resultados por outro meio. O caminho para isso está no [ROADMAP](ROADMAP.pt-BR.md).
|
> **Atenção:** o Guará ainda **não foi validado externamente** (por exemplo, nos testes do NIST CFTT) e suas assinaturas não têm carimbo de tempo de terceiros (RFC 3161). Serve bem para recuperação e triagem; para uso em juízo, confira os resultados por outro meio. O caminho para isso está no [ROADMAP](ROADMAP.pt-BR.md).
|
||||||
|
|
||||||
## Limites conhecidos
|
## Limites conhecidos
|
||||||
|
|
||||||
@@ -102,7 +105,7 @@ App desktop (Tauri) no estilo de um gerenciador de arquivos, pensado para quando
|
|||||||
- **NTFS:** arquivos comprimidos, EFS e registros muito fragmentados são ignorados ou marcados como incompletos.
|
- **NTFS:** arquivos comprimidos, EFS e registros muito fragmentados são ignorados ou marcados como incompletos.
|
||||||
- **exFAT:** validado com imagem sintética; faltam mídias reais.
|
- **exFAT:** validado com imagem sintética; faltam mídias reais.
|
||||||
- **Celulares:** o Guará trabalha com mídias de armazenamento e **não** desbloqueia aparelhos nem contorna senhas.
|
- **Celulares:** o Guará trabalha com mídias de armazenamento e **não** desbloqueia aparelhos nem contorna senhas.
|
||||||
- **Windows e macOS:** o código compila e é testado em CI, mas os testes com imagens reais rodam só no Linux. Relatos de mídias reais são muito bem-vindos.
|
- **Windows:** o código compila e é testado em CI, mas os testes com imagens reais rodam só no Linux. Não há build para macOS: o Tauri precisa do SDK do macOS e de assinatura, os dois presos a hardware Apple e a uma conta Apple Developer, que o CI deste projeto (um único runner Linux) não tem. O código-fonte continua compilando para macOS; só não é compilado nem testado.
|
||||||
- **GUI:** no Linux e macOS o app reinicia inteiro como administrador (`pkexec` ou `osascript`) em vez de usar um processo auxiliar.
|
- **GUI:** no Linux e macOS o app reinicia inteiro como administrador (`pkexec` ou `osascript`) em vez de usar um processo auxiliar.
|
||||||
|
|
||||||
## Para onde vai
|
## Para onde vai
|
||||||
@@ -111,8 +114,9 @@ Resumo do [ROADMAP](ROADMAP.pt-BR.md):
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Feito** | Metadados de 8 sistemas de arquivos, BitLocker, LUKS, FileVault 2, RAID, LVM, E01, Ex01, AFF4, JPEG fragmentado, MP4 sem índice, leitor de SQLite, SMART, QCOW2/VMDK/VHD/VHDX/DMG, trilha de auditoria encadeada por hash, relatório em PDF, mapa livre do APFS |
|
| **Feito** | Metadados de 8 sistemas de arquivos, BitLocker, LUKS, FileVault 2, RAID, LVM, E01, Ex01, AFF4, JPEG fragmentado, MP4 sem índice, leitor de SQLite, SMART, QCOW2/VMDK/VHD/VHDX/DMG, trilha de auditoria encadeada por hash, relatório em PDF, assinaturas Ed25519, mapa livre do APFS |
|
||||||
| **Próximo** | Relatório assinado digitalmente, testes em Windows e macOS |
|
| **Próximo** | Carimbo de tempo de terceiros (RFC 3161), mais testes em Windows |
|
||||||
|
| **Fora de plano** | Um build para macOS — veja [Limites conhecidos](#limites-conhecidos) |
|
||||||
| **Depois** | NSRL e listas de hashes, busca em conteúdo, linha do tempo unificada, interpretação de navegadores, registro do Windows e apps de mensagem, ataque de dicionário e força bruta a volumes cifrados |
|
| **Depois** | NSRL e listas de hashes, busca em conteúdo, linha do tempo unificada, interpretação de navegadores, registro do Windows e apps de mensagem, ataque de dicionário e força bruta a volumes cifrados |
|
||||||
| **Sem imagem de referência** | ReFS e Storage Spaces: só entram com validação |
|
| **Sem imagem de referência** | ReFS e Storage Spaces: só entram com validação |
|
||||||
| **Longo prazo** | Backups de iOS e Android (iTunes, ADB), OCR, validação NIST CFTT, acesso físico a celulares por exploit de bootloader/chip |
|
| **Longo prazo** | Backups de iOS e Android (iTunes, ADB), OCR, validação NIST CFTT, acesso físico a celulares por exploit de bootloader/chip |
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ Where Guará has been, where it is and where it is going. What is marked as done
|
|||||||
|
|
||||||
Legend: ✅ done · 🟡 partial · 🔜 planned · 🔭 long-term idea · ⛔ out of scope
|
Legend: ✅ done · 🟡 partial · 🔜 planned · 🔭 long-term idea · ⛔ out of scope
|
||||||
|
|
||||||
|
> Items marked done for macOS reflect what the GitHub CI used to build and test,
|
||||||
|
> before the move to Gitea. The Gitea CI has one Linux runner; it builds and
|
||||||
|
> tests Linux and Windows. macOS is not built or tested any more — see the
|
||||||
|
> README's [Known limits](README.md#known-limits).
|
||||||
|
|
||||||
## Principles that do not change
|
## Principles that do not change
|
||||||
|
|
||||||
1. **The source is read-only.** Nothing in the core writes to the device being recovered.
|
1. **The source is read-only.** Nothing in the core writes to the device being recovered.
|
||||||
2. **Do not ship without validating.** A feature only lands with a test against a real or reference image. That is why ReFS and Storage Spaces do not exist yet: there is no reference image to check against.
|
2. **Do not ship without validating.** A feature only lands with a test against a real or reference image. That is why ReFS and Storage Spaces do not exist yet: there is no reference image to check against.
|
||||||
3. **Honest confidence.** If the content could not be checked, confidence is low or medium, never high.
|
3. **Honest confidence.** If the content could not be checked, confidence is low or medium, never high.
|
||||||
4. **Free and auditable.** Open source (MIT or Apache-2.0), no telemetry, and every result can be reproduced.
|
4. **Free and auditable.** Open source (MIT or Apache-2.0), no telemetry (the only thing that touches the network by default is the update check, a single request you can turn off), and every result can be reproduced.
|
||||||
|
|
||||||
## History
|
## History
|
||||||
|
|
||||||
@@ -27,10 +32,12 @@ Legend: ✅ done · 🟡 partial · 🔜 planned · 🔭 long-term idea · ⛔ o
|
|||||||
| 1.0.1 | 2026-10-20 | Documentation in English with Portuguese translations, complete English interface, screenshots, Docker image of the CLI |
|
| 1.0.1 | 2026-10-20 | Documentation in English with Portuguese translations, complete English interface, screenshots, Docker image of the CLI |
|
||||||
| 1.1.0 | 2026-10-20 | Core messages and dates in English, automated translation check, RustCrypto on digest 0.11, verified Docker device access and Superset workspaces |
|
| 1.1.0 | 2026-10-20 | Core messages and dates in English, automated translation check, RustCrypto on digest 0.11, verified Docker device access and Superset workspaces |
|
||||||
| 1.2.0 | 2026-10-22 | QCOW2, VMDK, VHD, VHDX and DMG virtual disks, hash-chained audit trail, PDF report, APFS free-space map |
|
| 1.2.0 | 2026-10-22 | QCOW2, VMDK, VHD, VHDX and DMG virtual disks, hash-chained audit trail, PDF report, APFS free-space map |
|
||||||
|
| 1.3.0 | 2026-10-23 | Update from inside the app, Ed25519 signatures, differencing QCOW2/VMDK and LZFSE DMG, reference images tested on Linux, macOS and Windows, XFS real-deletion test |
|
||||||
|
| 1.3.1 | 2026-10-23 | New official logo |
|
||||||
|
|
||||||
Details of each version in the [CHANGELOG](CHANGELOG.md).
|
Details of each version in the [CHANGELOG](CHANGELOG.md).
|
||||||
|
|
||||||
## Current status (1.2.0) against what is expected from a forensic suite
|
## Current status (1.3.1) against what is expected from a forensic suite
|
||||||
|
|
||||||
### 1. File systems with metadata
|
### 1. File systems with metadata
|
||||||
|
|
||||||
@@ -40,12 +47,13 @@ Details of each version in the [CHANGELOG](CHANGELOG.md).
|
|||||||
| ext2/3/4 | ✅ | Deleted inodes and old copies in the journal |
|
| ext2/3/4 | ✅ | Deleted inodes and old copies in the journal |
|
||||||
| HFS+ | ✅ | Tree node leftovers and journal |
|
| HFS+ | ✅ | Tree node leftovers and journal |
|
||||||
| Btrfs | ✅ | Old versions of the trees; zlib, zstd and lzo extents |
|
| Btrfs | ✅ | Old versions of the trees; zlib, zstd and lzo extents |
|
||||||
| XFS | ✅ | Freed inodes and names of freed entries. Tested only with simulated deletion |
|
| XFS | 🟡 | Freed inodes and names of freed entries work on simulated deletion only. With real kernel deletion (tested in CI) the metadata is wiped: content comes back by carving, names do not. **Journal reading is missing** |
|
||||||
| APFS | 🟡 | Container, object map, free-space map (spaceman and bitmaps), snapshots and old nodes. Deletion tested only in simulation; containers with chunk-info address blocks (large ones) fall back to scanning the whole volume |
|
| APFS | 🟡 | Container, object map, free-space map (spaceman and bitmaps), snapshots and old nodes. Deletion tested only in simulation; containers with chunk-info address blocks (large ones) fall back to scanning the whole volume |
|
||||||
| ReFS | 🔜 | Only detected today; recovery depends on carving. Reference image missing |
|
| ReFS | 🔜 | Only detected today; recovery depends on carving. Reference image missing |
|
||||||
| Storage Spaces | 🔜 | Not done. Reference image missing |
|
| Storage Spaces | 🔜 | Not done. Reference image missing |
|
||||||
| NTFS: compressed attributes, EFS, attribute list | 🔜 | Marked as incomplete today |
|
| NTFS: compressed attributes, EFS, attribute list | 🔜 | Marked as incomplete today |
|
||||||
| XFS and APFS validation with real deletion | 🔜 | Replace the simulation with images from real devices |
|
| XFS: names and folders after real deletion (read the journal) | 🔜 | The CI test exists and is ignored until this lands |
|
||||||
|
| APFS validation with real deletion | 🔜 | Needs an image from a real Mac |
|
||||||
|
|
||||||
### 2. Encrypted volumes
|
### 2. Encrypted volumes
|
||||||
|
|
||||||
@@ -68,7 +76,8 @@ Today Guará opens volumes when you **already have** the password or key. Passwo
|
|||||||
| E01 and Ex01: read, write and `guara verify` | ✅ |
|
| E01 and Ex01: read, write and `guara verify` | ✅ |
|
||||||
| AFF4: read and write | ✅ |
|
| AFF4: read and write | ✅ |
|
||||||
| Virtual disk containers: QCOW2, VMDK, VHD, VHDX and DMG (read) | ✅ |
|
| Virtual disk containers: QCOW2, VMDK, VHD, VHDX and DMG (read) | ✅ |
|
||||||
| QCOW2 with a backing file, VMDK snapshots, DMG with LZFSE or a password | 🔜 |
|
| QCOW2 with a backing file, VMDK snapshots (by name or VirtualBox UUID), DMG with LZFSE | ✅ |
|
||||||
|
| Password-protected DMG, parent identity check for VHD, VHDX and QCOW2 | 🔜 |
|
||||||
| Mobile device image (see item 7) | 🔭 |
|
| Mobile device image (see item 7) | 🔭 |
|
||||||
|
|
||||||
### 4. Assembling loose disks
|
### 4. Assembling loose disks
|
||||||
@@ -129,11 +138,12 @@ Physical access is the differentiator of commercial tools like Cellebrite. It de
|
|||||||
| HTML report with case, examiner, source, partitions, summary and hashes | ✅ |
|
| HTML report with case, examiner, source, partitions, summary and hashes | ✅ |
|
||||||
| Verification of an E01/Ex01 image against the stored hash | ✅ |
|
| Verification of an E01/Ex01 image against the stored hash | ✅ |
|
||||||
| **Audit trail** of every examiner action (who, when, what), chained by hash, with `guara audit --verify` | ✅ |
|
| **Audit trail** of every examiner action (who, when, what), chained by hash, with `guara audit --verify` | ✅ |
|
||||||
| **Digitally signed reports** and a timestamped manifest | 🔜 |
|
| **Digital signatures** (Ed25519) of manifests, reports and any file; `guara sign recovery` checks a whole recovery folder | ✅ |
|
||||||
|
| Trusted timestamp (RFC 3161) on signatures | 🔜 |
|
||||||
| **Reproducibility:** replay the recorded procedure and get the same result | 🔜 |
|
| **Reproducibility:** replay the recorded procedure and get the same result | 🔜 |
|
||||||
| PDF report (A4, built in) | ✅ |
|
| PDF report (A4, built in) | ✅ |
|
||||||
| **External validation:** NIST CFTT (Computer Forensic Tool Testing) tests and publication of the results | 🔭 |
|
| **External validation:** NIST CFTT (Computer Forensic Tool Testing) tests and publication of the results | 🔭 |
|
||||||
| Signed binaries (Windows and macOS) | 🔜 |
|
| Signed binaries (Windows) | 🔜 |
|
||||||
|
|
||||||
External validation is what makes a report accepted. Without it, Guará is good for triage, recovery and support, and the examiner must check the results by other means.
|
External validation is what makes a report accepted. Without it, Guará is good for triage, recovery and support, and the examiner must check the results by other means.
|
||||||
|
|
||||||
@@ -153,9 +163,10 @@ External validation is what makes a report accepted. Without it, Guará is good
|
|||||||
|
|
||||||
| Item | Status |
|
| Item | Status |
|
||||||
|---|---|
|
|---|---|
|
||||||
| CLI and interface for Windows, Linux and macOS | ✅ |
|
| CLI (Linux) and interface for Windows and Linux | ✅ |
|
||||||
| CI on three systems, automatic release by tag | ✅ |
|
| CI on Linux and Windows, automatic release by tag | ✅ |
|
||||||
| Tests with real images on Windows and macOS (Linux only today) | 🔜 |
|
| Tests with real reference images on Linux and Windows | ✅ |
|
||||||
|
| Update from inside the desktop app (signed packages, one click) | ✅ |
|
||||||
| GUI: privileged helper process instead of restarting the whole app as administrator | 🔜 |
|
| GUI: privileged helper process instead of restarting the whole app as administrator | 🔜 |
|
||||||
| Signed and notarized binaries | 🔜 |
|
| Signed and notarized binaries | 🔜 |
|
||||||
| Packages in package managers (winget, Homebrew, Flatpak) | 🔭 |
|
| Packages in package managers (winget, Homebrew, Flatpak) | 🔭 |
|
||||||
@@ -164,17 +175,17 @@ External validation is what makes a report accepted. Without it, Guará is good
|
|||||||
|
|
||||||
No dates, in the order that makes the most sense.
|
No dates, in the order that makes the most sense.
|
||||||
|
|
||||||
**1.1: trust and custody**
|
**1.4: trust and custody**
|
||||||
Signed manifest, XFS and APFS validation with real deletions, tests with real images on Windows and macOS.
|
Trusted timestamps, XFS journal reading, APFS validation with real deletions.
|
||||||
|
|
||||||
**1.2: analysis**
|
**1.5: analysis**
|
||||||
NSRL and hash lists, search in file content, unified timeline.
|
NSRL and hash lists, search in file content, unified timeline.
|
||||||
|
|
||||||
**1.3: artifacts**
|
**1.6: artifacts**
|
||||||
Windows registry, browsers, EVTX and the most common messaging apps, built on the SQLite reader.
|
Windows registry, browsers, EVTX and the most common messaging apps, built on the SQLite reader.
|
||||||
|
|
||||||
**1.4: more structure**
|
**1.7: more structure**
|
||||||
ReFS and Storage Spaces (once there is a reference image), other image containers (VMDK, VHD, QCOW2), expanded fragmented carving.
|
ReFS and Storage Spaces (once there is a reference image), password-protected DMG, identity check of VHD, VHDX and QCOW2 parents, expanded fragmented carving.
|
||||||
|
|
||||||
**2.0: mobile devices and validation**
|
**2.0: mobile devices and validation**
|
||||||
iTunes backups, ADB and Android backups; submission to the NIST CFTT tests.
|
iTunes backups, ADB and Android backups; submission to the NIST CFTT tests.
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ Onde o Guará esteve, onde está e para onde vai. O que está marcado como feito
|
|||||||
|
|
||||||
Legenda: ✅ feito · 🟡 parcial · 🔜 planejado · 🔭 ideia a longo prazo · ⛔ fora do escopo
|
Legenda: ✅ feito · 🟡 parcial · 🔜 planejado · 🔭 ideia a longo prazo · ⛔ fora do escopo
|
||||||
|
|
||||||
|
> Itens marcados como feitos para macOS refletem o que o CI do GitHub compilava e
|
||||||
|
> testava, antes da mudança para o Gitea. O CI do Gitea tem um único runner Linux;
|
||||||
|
> ele compila e testa Linux e Windows. O macOS deixou de ser compilado e testado —
|
||||||
|
> veja os [Limites conhecidos](README.pt-BR.md#limites-conhecidos) do README.
|
||||||
|
|
||||||
## Princípios que não mudam
|
## Princípios que não mudam
|
||||||
|
|
||||||
1. **A origem é somente leitura.** Nada no núcleo escreve no dispositivo recuperado.
|
1. **A origem é somente leitura.** Nada no núcleo escreve no dispositivo recuperado.
|
||||||
2. **Não entregar sem validar.** Um recurso só entra com teste contra imagem real ou de referência. Por isso ReFS e Storage Spaces ainda não existem: não há imagem de referência para conferir.
|
2. **Não entregar sem validar.** Um recurso só entra com teste contra imagem real ou de referência. Por isso ReFS e Storage Spaces ainda não existem: não há imagem de referência para conferir.
|
||||||
3. **Confiança honesta.** Se o conteúdo não pôde ser conferido, a confiança é baixa ou média, nunca alta.
|
3. **Confiança honesta.** Se o conteúdo não pôde ser conferido, a confiança é baixa ou média, nunca alta.
|
||||||
4. **Livre e auditável.** Código aberto (MIT ou Apache-2.0), sem telemetria, e cada resultado pode ser reproduzido.
|
4. **Livre e auditável.** Código aberto (MIT ou Apache-2.0), sem telemetria (a única coisa que usa a rede por padrão é a verificação de atualização, uma requisição só que você pode desligar), e cada resultado pode ser reproduzido.
|
||||||
|
|
||||||
## Histórico
|
## Histórico
|
||||||
|
|
||||||
@@ -27,10 +32,12 @@ Legenda: ✅ feito · 🟡 parcial · 🔜 planejado · 🔭 ideia a longo prazo
|
|||||||
| 1.0.1 | 2026-10-20 | Documentação em inglês com tradução em português, interface completa em inglês, telas, imagem Docker do terminal |
|
| 1.0.1 | 2026-10-20 | Documentação em inglês com tradução em português, interface completa em inglês, telas, imagem Docker do terminal |
|
||||||
| 1.1.0 | 2026-10-20 | Mensagens do núcleo e datas em inglês, verificação automática da tradução, RustCrypto em digest 0.11, acesso a dispositivo no Docker e workspaces do Superset verificados |
|
| 1.1.0 | 2026-10-20 | Mensagens do núcleo e datas em inglês, verificação automática da tradução, RustCrypto em digest 0.11, acesso a dispositivo no Docker e workspaces do Superset verificados |
|
||||||
| 1.2.0 | 2026-10-22 | Discos virtuais QCOW2, VMDK, VHD, VHDX e DMG, trilha de auditoria encadeada por hash, relatório em PDF, mapa de espaço livre do APFS |
|
| 1.2.0 | 2026-10-22 | Discos virtuais QCOW2, VMDK, VHD, VHDX e DMG, trilha de auditoria encadeada por hash, relatório em PDF, mapa de espaço livre do APFS |
|
||||||
|
| 1.3.0 | 2026-10-23 | Atualização pelo próprio app, assinaturas Ed25519, QCOW2/VMDK diferenciais e DMG com LZFSE, imagens de referência testadas em Linux, macOS e Windows, teste de exclusão real do XFS |
|
||||||
|
| 1.3.1 | 2026-10-23 | Nova logo oficial |
|
||||||
|
|
||||||
Detalhes de cada versão no [CHANGELOG](CHANGELOG.pt-BR.md).
|
Detalhes de cada versão no [CHANGELOG](CHANGELOG.pt-BR.md).
|
||||||
|
|
||||||
## Situação atual (1.2.0) contra o que se espera de uma suíte forense
|
## Situação atual (1.3.1) contra o que se espera de uma suíte forense
|
||||||
|
|
||||||
### 1. Sistemas de arquivos com metadados
|
### 1. Sistemas de arquivos com metadados
|
||||||
|
|
||||||
@@ -40,12 +47,13 @@ Detalhes de cada versão no [CHANGELOG](CHANGELOG.pt-BR.md).
|
|||||||
| ext2/3/4 | ✅ | Inodes apagados e cópias antigas no diário |
|
| ext2/3/4 | ✅ | Inodes apagados e cópias antigas no diário |
|
||||||
| HFS+ | ✅ | Sobras de nós da árvore e diário |
|
| HFS+ | ✅ | Sobras de nós da árvore e diário |
|
||||||
| Btrfs | ✅ | Versões antigas das árvores; extensões zlib, zstd e lzo |
|
| Btrfs | ✅ | Versões antigas das árvores; extensões zlib, zstd e lzo |
|
||||||
| XFS | ✅ | Inodes liberados e nomes de entradas liberadas. Testado só com exclusão simulada |
|
| XFS | 🟡 | Inodes liberados e nomes de entradas liberadas só funcionam na exclusão simulada. Com exclusão real do kernel (testada no CI) os metadados são zerados: o conteúdo volta por carving, o nome não. **Falta ler o diário** |
|
||||||
| APFS | 🟡 | Contêiner, mapa de objetos, mapa de espaço livre (spaceman e bitmaps), instantâneos e nós antigos. Exclusão testada só em simulação; contêineres com blocos de endereço de chunk-info (grandes) voltam a varrer o volume inteiro |
|
| APFS | 🟡 | Contêiner, mapa de objetos, mapa de espaço livre (spaceman e bitmaps), instantâneos e nós antigos. Exclusão testada só em simulação; contêineres com blocos de endereço de chunk-info (grandes) voltam a varrer o volume inteiro |
|
||||||
| ReFS | 🔜 | Hoje só é detectado; a recuperação depende do carving. Falta imagem de referência |
|
| ReFS | 🔜 | Hoje só é detectado; a recuperação depende do carving. Falta imagem de referência |
|
||||||
| Storage Spaces | 🔜 | Não feito. Falta imagem de referência |
|
| Storage Spaces | 🔜 | Não feito. Falta imagem de referência |
|
||||||
| NTFS: atributos comprimidos, EFS, lista de atributos | 🔜 | Hoje marcados como incompletos |
|
| NTFS: atributos comprimidos, EFS, lista de atributos | 🔜 | Hoje marcados como incompletos |
|
||||||
| Validação de XFS e APFS com exclusão real | 🔜 | Trocar a simulação por imagens de dispositivos reais |
|
| XFS: nomes e pastas após exclusão real (ler o diário) | 🔜 | O teste no CI existe e fica ignorado até isso entrar |
|
||||||
|
| Validação de APFS com exclusão real | 🔜 | Precisa de imagem de um Mac de verdade |
|
||||||
|
|
||||||
### 2. Volumes cifrados
|
### 2. Volumes cifrados
|
||||||
|
|
||||||
@@ -68,7 +76,8 @@ Hoje o Guará abre volumes quando você **já tem** a senha ou a chave. Recupera
|
|||||||
| E01 e Ex01: leitura, gravação e `guara verify` | ✅ |
|
| E01 e Ex01: leitura, gravação e `guara verify` | ✅ |
|
||||||
| AFF4: leitura e gravação | ✅ |
|
| AFF4: leitura e gravação | ✅ |
|
||||||
| Contêineres de disco virtual: QCOW2, VMDK, VHD, VHDX e DMG (leitura) | ✅ |
|
| Contêineres de disco virtual: QCOW2, VMDK, VHD, VHDX e DMG (leitura) | ✅ |
|
||||||
| QCOW2 com arquivo de apoio, snapshots de VMDK, DMG com LZFSE ou com senha | 🔜 |
|
| QCOW2 com arquivo de apoio, snapshots de VMDK (por nome ou UUID do VirtualBox), DMG com LZFSE | ✅ |
|
||||||
|
| DMG com senha, conferência da identidade do pai em VHD, VHDX e QCOW2 | 🔜 |
|
||||||
| Imagem de dispositivo móvel (ver item 7) | 🔭 |
|
| Imagem de dispositivo móvel (ver item 7) | 🔭 |
|
||||||
|
|
||||||
### 4. Montagem de discos soltos
|
### 4. Montagem de discos soltos
|
||||||
@@ -129,11 +138,12 @@ O acesso físico é o diferencial de ferramentas comerciais como a Cellebrite. D
|
|||||||
| Relatório HTML com caso, examinador, origem, partições, resumo e hashes | ✅ |
|
| Relatório HTML com caso, examinador, origem, partições, resumo e hashes | ✅ |
|
||||||
| Verificação de imagem E01/Ex01 contra o hash gravado | ✅ |
|
| Verificação de imagem E01/Ex01 contra o hash gravado | ✅ |
|
||||||
| **Trilha de auditoria** de cada ação do perito (quem, quando, o quê), encadeada por hash, com `guara audit --verify` | ✅ |
|
| **Trilha de auditoria** de cada ação do perito (quem, quando, o quê), encadeada por hash, com `guara audit --verify` | ✅ |
|
||||||
| **Relatórios assinados** digitalmente e manifesto com carimbo de tempo | 🔜 |
|
| **Assinatura digital** (Ed25519) de manifestos, relatórios e qualquer arquivo; `guara sign recovery` confere uma pasta de recuperação inteira | ✅ |
|
||||||
|
| Carimbo de tempo de terceiros (RFC 3161) nas assinaturas | 🔜 |
|
||||||
| **Reprodutibilidade:** repetir o procedimento gravado e obter o mesmo resultado | 🔜 |
|
| **Reprodutibilidade:** repetir o procedimento gravado e obter o mesmo resultado | 🔜 |
|
||||||
| Relatório em PDF (A4, próprio) | ✅ |
|
| Relatório em PDF (A4, próprio) | ✅ |
|
||||||
| **Validação externa:** testes do NIST CFTT (Computer Forensic Tool Testing) e publicação dos resultados | 🔭 |
|
| **Validação externa:** testes do NIST CFTT (Computer Forensic Tool Testing) e publicação dos resultados | 🔭 |
|
||||||
| Binários assinados (Windows e macOS) | 🔜 |
|
| Binários assinados (Windows) | 🔜 |
|
||||||
|
|
||||||
A validação externa é o que faz um laudo ser aceito. Sem ela, o Guará serve para triagem, recuperação e apoio, e o perito deve conferir os resultados por outro meio.
|
A validação externa é o que faz um laudo ser aceito. Sem ela, o Guará serve para triagem, recuperação e apoio, e o perito deve conferir os resultados por outro meio.
|
||||||
|
|
||||||
@@ -153,9 +163,10 @@ A validação externa é o que faz um laudo ser aceito. Sem ela, o Guará serve
|
|||||||
|
|
||||||
| Item | Estado |
|
| Item | Estado |
|
||||||
|---|---|
|
|---|---|
|
||||||
| CLI e interface para Windows, Linux e macOS | ✅ |
|
| CLI (Linux) e interface para Windows e Linux | ✅ |
|
||||||
| CI em três sistemas, release automática por tag | ✅ |
|
| CI em Linux e Windows, release automática por tag | ✅ |
|
||||||
| Testes com imagens reais em Windows e macOS (hoje só Linux) | 🔜 |
|
| Testes com imagens de referência reais em Linux e Windows | ✅ |
|
||||||
|
| Atualização dentro do app desktop (pacotes assinados, um clique) | ✅ |
|
||||||
| GUI: processo auxiliar privilegiado em vez de reiniciar o app inteiro como administrador | 🔜 |
|
| GUI: processo auxiliar privilegiado em vez de reiniciar o app inteiro como administrador | 🔜 |
|
||||||
| Binários assinados e notarizados | 🔜 |
|
| Binários assinados e notarizados | 🔜 |
|
||||||
| Pacotes em gerenciadores (winget, Homebrew, Flatpak) | 🔭 |
|
| Pacotes em gerenciadores (winget, Homebrew, Flatpak) | 🔭 |
|
||||||
@@ -164,17 +175,17 @@ A validação externa é o que faz um laudo ser aceito. Sem ela, o Guará serve
|
|||||||
|
|
||||||
Sem datas, na ordem em que fazem mais sentido.
|
Sem datas, na ordem em que fazem mais sentido.
|
||||||
|
|
||||||
**1.1: confiança e custódia**
|
**1.4: confiança e custódia**
|
||||||
Manifesto assinado, validação de XFS e APFS com exclusões reais, testes com imagens reais no Windows e macOS.
|
Carimbo de tempo de terceiros, leitura do diário do XFS, validação de APFS com exclusões reais.
|
||||||
|
|
||||||
**1.2: análise**
|
**1.5: análise**
|
||||||
NSRL e listas de hashes, busca em conteúdo de arquivos, linha do tempo unificada.
|
NSRL e listas de hashes, busca em conteúdo de arquivos, linha do tempo unificada.
|
||||||
|
|
||||||
**1.3: artefatos**
|
**1.6: artefatos**
|
||||||
Registro do Windows, navegadores, EVTX e os apps de mensagem mais comuns, apoiados no leitor de SQLite.
|
Registro do Windows, navegadores, EVTX e os apps de mensagem mais comuns, apoiados no leitor de SQLite.
|
||||||
|
|
||||||
**1.4: mais estrutura**
|
**1.7: mais estrutura**
|
||||||
ReFS e Storage Spaces (quando houver imagem de referência), outros contêineres de imagem (VMDK, VHD, QCOW2), carving fragmentado ampliado.
|
ReFS e Storage Spaces (quando houver imagem de referência), DMG com senha, conferência da identidade do pai em VHD, VHDX e QCOW2, carving fragmentado ampliado.
|
||||||
|
|
||||||
**2.0: dispositivos móveis e validação**
|
**2.0: dispositivos móveis e validação**
|
||||||
Backups do iTunes, ADB e backups do Android; submissão aos testes do NIST CFTT.
|
Backups do iTunes, ADB e backups do Android; submissão aos testes do NIST CFTT.
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
[English](SECURITY.md) · [Português](SECURITY.pt-BR.md)
|
[English](SECURITY.md) · [Português](SECURITY.pt-BR.md)
|
||||||
|
|
||||||
Guará opens devices in read-only mode and processes potentially corrupted or malicious data (forged file systems and files). Vulnerabilities (memory corruption in parsers, improper writes to the source, path escape during recovery) must be reported privately through the **Security → Report a vulnerability** tab of the repository on GitHub. Do not open a public issue before the fix.
|
Guará opens devices in read-only mode and processes potentially corrupted or malicious data (forged file systems and files). Vulnerabilities (memory corruption in parsers, improper writes to the source, path escape during recovery) must be reported privately through the private vulnerability reporting feature of the repository on Gitea. Do not open a public issue before the fix.
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
[English](SECURITY.md) · **Português**
|
[English](SECURITY.md) · **Português**
|
||||||
|
|
||||||
Guará abre dispositivos em modo somente leitura e processa dados potencialmente corrompidos ou maliciosos (sistemas de arquivos e arquivos forjados). Vulnerabilidades (corrupção de memória em parsers, escrita indevida na origem, escape de caminho na recuperação) devem ser reportadas em privado pela aba **Security → Report a vulnerability** do repositório no GitHub. Não abra issue pública antes da correção.
|
Guará abre dispositivos em modo somente leitura e processa dados potencialmente corrompidos ou maliciosos (sistemas de arquivos e arquivos forjados). Vulnerabilidades (corrupção de memória em parsers, escrita indevida na origem, escape de caminho na recuperação) devem ser reportadas em privado pelo recurso de relato privado de vulnerabilidade do repositório no Gitea. Não abra issue pública antes da correção.
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
# Aplicativo desktop (Tauri 2). Fica fora do workspace principal porque exige
|
# Desktop application (Tauri 2). Kept outside the main workspace because it
|
||||||
# as bibliotecas de WebView do sistema (WebKitGTK no Linux).
|
# requires system WebView libraries (WebKitGTK on Linux).
|
||||||
[package]
|
[package]
|
||||||
name = "guara-gui"
|
name = "guara-gui"
|
||||||
description = "Guará: interface gráfica de recuperação de arquivos"
|
description = "Guará: graphical file recovery interface"
|
||||||
version = "1.2.0"
|
version = "1.3.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/lucasrguerra/guara"
|
authors = ["Guará contributors"]
|
||||||
|
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/guara"
|
||||||
rust-version = "1.80"
|
rust-version = "1.80"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
@@ -17,6 +18,8 @@ tauri-build = { version = "2", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-updater = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
guara-core = { path = "../../../crates/guara-core" }
|
guara-core = { path = "../../../crates/guara-core" }
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=../../../logo.png");
|
||||||
|
let _ = std::fs::copy("../../../logo.png", "../ui/logo.png");
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
// Ler discos brutos no Windows exige administrador: o manifesto pede elevação (UAC).
|
// Reading raw disks on Windows requires Administrator privileges: embed UAC elevation manifest.
|
||||||
let manifest = include_str!("windows-app-manifest.xml");
|
let manifest = include_str!("windows-app-manifest.xml");
|
||||||
let attrs = tauri_build::Attributes::new()
|
let attrs = tauri_build::Attributes::new()
|
||||||
.windows_attributes(tauri_build::WindowsAttributes::new().app_manifest(manifest));
|
.windows_attributes(tauri_build::WindowsAttributes::new().app_manifest(manifest));
|
||||||
tauri_build::try_build(attrs).expect("falha no tauri-build");
|
tauri_build::try_build(attrs).expect("tauri-build failed");
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
tauri_build::build();
|
tauri_build::build();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schema/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Permissões da janela principal",
|
"description": "Main window permissions",
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": ["core:default", "core:window:allow-destroy", "core:window:allow-close", "dialog:default"]
|
"permissions": ["core:default", "core:window:allow-destroy", "core:window:allow-close", "dialog:default"]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-exit":{"identifier":"allow-exit","description":"Enables the exit command without any pre-configured scope.","commands":{"allow":["exit"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-exit":{"identifier":"deny-exit","description":"Denies the exit command without any pre-configured scope.","commands":{"allow":[],"deny":["exit"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"DenLine truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"default":{"identifier":"default","description":"Main window permissions","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-destroy","core:window:allow-close","dialog:default"]}}
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#FFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 835 B |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 416 KiB |
@@ -1,4 +1,4 @@
|
|||||||
// Evita a janela de console extra no Windows em release.
|
// Prevents an extra console window on Windows in release builds.
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use guara_core::service::Engine;
|
use guara_core::service::Engine;
|
||||||
@@ -6,28 +6,141 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
struct State(Arc<Engine>);
|
struct State(Arc<Engine>);
|
||||||
|
|
||||||
/// Ponte única entre a interface e o núcleo (mesmos métodos do modo `guara ui`).
|
/// Single bridge between the UI and the core (same methods as `guara ui` mode).
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn rpc(state: tauri::State<'_, State>, method: String, params: serde_json::Value) -> Result<serde_json::Value, String> {
|
async fn rpc(
|
||||||
|
state: tauri::State<'_, State>,
|
||||||
|
method: String,
|
||||||
|
params: serde_json::Value,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
let eng = state.0.clone();
|
let eng = state.0.clone();
|
||||||
tauri::async_runtime::spawn_blocking(move || eng.rpc(&method, params).map_err(|e| e.to_string()))
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
eng.rpc(&method, params).map_err(|e| e.to_string())
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reabre o app com privilégios de administrador (Linux/macOS). No Windows o
|
/// New version found on Gitea releases.
|
||||||
/// manifesto já exige elevação. O app novo encerra este quando abre; se a
|
#[derive(serde::Serialize)]
|
||||||
/// autorização falhar, a janela continua aqui e a interface mostra o motivo.
|
struct UpdateInfo {
|
||||||
|
current: String,
|
||||||
|
version: String,
|
||||||
|
notes: String,
|
||||||
|
/// Whether the app can self-update (AppImage, Windows, and macOS). Package installs
|
||||||
|
/// (.deb/.rpm) only notify the user and open the release page.
|
||||||
|
auto: bool,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn self_update_possible() -> bool {
|
||||||
|
!cfg!(target_os = "linux") || std::env::var_os("APPIMAGE").is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks for a newer version. `None` = already up to date.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn update_check(app: tauri::AppHandle) -> Result<Option<UpdateInfo>, String> {
|
||||||
|
use tauri_plugin_updater::UpdaterExt;
|
||||||
|
let found = app
|
||||||
|
.updater()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.check()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(found.map(|u| UpdateInfo {
|
||||||
|
current: u.current_version.clone(),
|
||||||
|
notes: u.body.clone().unwrap_or_default(),
|
||||||
|
url: format!(
|
||||||
|
"https://git.lucasrguerra.dev.br/lucasrguerra/guara/releases/tag/v{}",
|
||||||
|
u.version
|
||||||
|
),
|
||||||
|
version: u.version,
|
||||||
|
auto: self_update_possible(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads, verifies the signature (embedded public key), installs, and restarts the app.
|
||||||
|
/// Progress is emitted via the `update-progress` event.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn update_install(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
use tauri::Emitter;
|
||||||
|
use tauri_plugin_updater::UpdaterExt;
|
||||||
|
if !self_update_possible() {
|
||||||
|
return Err("Esta instalação não se atualiza sozinha (pacote do sistema). Baixe a versão nova na página da release.".into());
|
||||||
|
}
|
||||||
|
let update = app
|
||||||
|
.updater()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.check()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.ok_or("Já está na versão mais recente.")?;
|
||||||
|
let (a, b) = (app.clone(), app.clone());
|
||||||
|
let mut got = 0u64;
|
||||||
|
update
|
||||||
|
.download_and_install(
|
||||||
|
move |chunk, total| {
|
||||||
|
got += chunk as u64;
|
||||||
|
let _ = a.emit(
|
||||||
|
"update-progress",
|
||||||
|
serde_json::json!({ "downloaded": got, "total": total }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let _ = b.emit("update-progress", serde_json::json!({ "installing": true }));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Não foi possível atualizar: {e}"))?;
|
||||||
|
// When running as administrator, the new file is owned by root: hand it back to the invoking user.
|
||||||
|
if let Some(p) = std::env::var_os("APPIMAGE") {
|
||||||
|
guara_core::owner::give_to_user(std::path::Path::new(&p));
|
||||||
|
}
|
||||||
|
app.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` when the process already has sufficient privileges to read raw disks.
|
||||||
|
/// On Windows the manifest already requires administrator privileges before launch,
|
||||||
|
/// so this is always `true` here.
|
||||||
|
#[tauri::command]
|
||||||
|
fn is_elevated() -> bool {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
unsafe { libc::geteuid() == 0 }
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Relaunches the app with administrator privileges (Linux/macOS). On Windows the
|
||||||
|
/// manifest already enforces elevation. The new instance terminates this one on startup; if
|
||||||
|
/// authorization fails, this window stays open and the UI displays the reason.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
{
|
{
|
||||||
|
do_relaunch_elevated(app)
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let _ = app;
|
||||||
|
Err("o Guará já roda como administrador no Windows".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
let exe = elevated_exe()?;
|
let exe = elevated_exe()?;
|
||||||
let cfg = guara_core::settings::config_dir();
|
let cfg = guara_core::settings::config_dir();
|
||||||
let log = std::env::temp_dir().join(format!("guara-elevate-{}.log", std::process::id()));
|
let log = std::env::temp_dir().join(format!("guara-elevate-{}.log", std::process::id()));
|
||||||
let _ = std::fs::write(&log, b"");
|
let _ = std::fs::write(&log, b"");
|
||||||
let out = std::fs::OpenOptions::new().append(true).open(&log).map_err(|e| e.to_string())?;
|
let out = std::fs::OpenOptions::new()
|
||||||
|
.append(true)
|
||||||
|
.open(&log)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
let err = out.try_clone().map_err(|e| e.to_string())?;
|
let err = out.try_clone().map_err(|e| e.to_string())?;
|
||||||
let pid = std::process::id().to_string();
|
let pid = std::process::id().to_string();
|
||||||
|
|
||||||
@@ -35,7 +148,14 @@ fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
|||||||
let mut cmd = {
|
let mut cmd = {
|
||||||
let mut c = std::process::Command::new("pkexec");
|
let mut c = std::process::Command::new("pkexec");
|
||||||
c.arg("/usr/bin/env");
|
c.arg("/usr/bin/env");
|
||||||
for k in ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS", "GDK_BACKEND"] {
|
for k in [
|
||||||
|
"DISPLAY",
|
||||||
|
"WAYLAND_DISPLAY",
|
||||||
|
"XAUTHORITY",
|
||||||
|
"XDG_RUNTIME_DIR",
|
||||||
|
"DBUS_SESSION_BUS_ADDRESS",
|
||||||
|
"GDK_BACKEND",
|
||||||
|
] {
|
||||||
if let Ok(v) = std::env::var(k) {
|
if let Ok(v) = std::env::var(k) {
|
||||||
c.arg(format!("{k}={v}"));
|
c.arg(format!("{k}={v}"));
|
||||||
}
|
}
|
||||||
@@ -53,16 +173,21 @@ fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
|||||||
pid,
|
pid,
|
||||||
exe.display()
|
exe.display()
|
||||||
);
|
);
|
||||||
let script = format!("do shell script \"{}\" with administrator privileges", shell.replace('\\', "\\\\").replace('"', "\\\""));
|
let script = format!(
|
||||||
|
"do shell script \"{}\" with administrator privileges",
|
||||||
|
shell.replace('\\', "\\\\").replace('"', "\\\"")
|
||||||
|
);
|
||||||
let mut c = std::process::Command::new("osascript");
|
let mut c = std::process::Command::new("osascript");
|
||||||
c.args(["-e", &script]);
|
c.args(["-e", &script]);
|
||||||
c
|
c
|
||||||
};
|
};
|
||||||
cmd.stdout(out).stderr(err);
|
cmd.stdout(out).stderr(err);
|
||||||
let mut child = cmd.spawn().map_err(|e| format!("não foi possível pedir privilégios: {e}"))?;
|
let mut child = cmd
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("não foi possível pedir privilégios: {e}"))?;
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
// No Linux o pkexec só termina com sucesso quando o app elevado é fechado (e o app
|
// On Linux, pkexec only exits with success when the elevated app closes (and the elevated
|
||||||
// elevado já terá encerrado este processo). Qualquer falha antes disso vira aviso.
|
// app will have already terminated this process). Any failure before that becomes a warning.
|
||||||
let status = child.wait();
|
let status = child.wait();
|
||||||
if matches!(&status, Ok(s) if s.success()) {
|
if matches!(&status, Ok(s) if s.success()) {
|
||||||
let _ = std::fs::remove_file(&log);
|
let _ = std::fs::remove_file(&log);
|
||||||
@@ -72,45 +197,54 @@ fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
|||||||
let msg = match status.ok().and_then(|s| s.code()) {
|
let msg = match status.ok().and_then(|s| s.code()) {
|
||||||
Some(126) => "Autorização cancelada.".to_string(),
|
Some(126) => "Autorização cancelada.".to_string(),
|
||||||
Some(127) => "Não foi possível autenticar como administrador.".to_string(),
|
Some(127) => "Não foi possível autenticar como administrador.".to_string(),
|
||||||
_ => format!("Não foi possível reiniciar como administrador. {}", detail.lines().last().unwrap_or("").trim()),
|
_ => format!(
|
||||||
|
"Não foi possível reiniciar como administrador. {}",
|
||||||
|
detail.lines().last().unwrap_or("").trim()
|
||||||
|
),
|
||||||
};
|
};
|
||||||
let _ = app.emit("elevate-failed", msg);
|
let _ = app.emit("elevate-failed", msg);
|
||||||
let _ = std::fs::remove_file(&log);
|
let _ = std::fs::remove_file(&log);
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
let _ = app;
|
|
||||||
Err("o Guará já roda como administrador no Windows".into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Caminho que o root consegue executar. No AppImage, `current_exe` aponta para
|
/// Executable path accessible to root. Inside an AppImage, `current_exe` points
|
||||||
/// dentro de um ponto de montagem FUSE do usuário, inacessível ao root: usa o
|
/// inside a user FUSE mount that root cannot access: use the
|
||||||
/// próprio arquivo `.AppImage` (variável `APPIMAGE`).
|
/// `.AppImage` file itself (`APPIMAGE` environment variable).
|
||||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
fn elevated_exe() -> Result<std::path::PathBuf, String> {
|
fn elevated_exe() -> Result<std::path::PathBuf, String> {
|
||||||
if let Some(p) = std::env::var_os("APPIMAGE").map(std::path::PathBuf::from).filter(|p| p.is_file()) {
|
if let Some(p) = std::env::var_os("APPIMAGE")
|
||||||
|
.map(std::path::PathBuf::from)
|
||||||
|
.filter(|p| p.is_file())
|
||||||
|
{
|
||||||
return Ok(p);
|
return Ok(p);
|
||||||
}
|
}
|
||||||
std::env::current_exe().map_err(|e| e.to_string())
|
std::env::current_exe().map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serve os arquivos recuperados para pré-visualização (`guara://localhost/file/<job>/<id>`),
|
/// Serves recovered files for preview (`guara://localhost/file/<job>/<id>`),
|
||||||
/// com suporte a Range para vídeo/áudio/PDF, lendo direto da origem.
|
/// with Range support for video/audio/PDF, reading directly from the source.
|
||||||
fn serve_file(eng: &Engine, req: &tauri::http::Request<Vec<u8>>) -> tauri::http::Response<Vec<u8>> {
|
fn serve_file(eng: &Engine, req: &tauri::http::Request<Vec<u8>>) -> tauri::http::Response<Vec<u8>> {
|
||||||
use tauri::http::Response;
|
use tauri::http::Response;
|
||||||
if let Some(rest) = req.uri().path().strip_prefix("/thumb/") {
|
if let Some(rest) = req.uri().path().strip_prefix("/thumb/") {
|
||||||
let mut it = rest.split('/');
|
let mut it = rest.split('/');
|
||||||
let (job, id) = (it.next().unwrap_or(""), it.next().and_then(|s| s.parse::<u64>().ok()));
|
let (job, id) = (
|
||||||
|
it.next().unwrap_or(""),
|
||||||
|
it.next().and_then(|s| s.parse::<u64>().ok()),
|
||||||
|
);
|
||||||
let size = req
|
let size = req
|
||||||
.uri()
|
.uri()
|
||||||
.query()
|
.query()
|
||||||
.and_then(|q| q.split('&').find_map(|kv| kv.strip_prefix("s=")))
|
.and_then(|q| q.split('&').find_map(|kv| kv.strip_prefix("s=")))
|
||||||
.and_then(|v| v.parse::<u32>().ok())
|
.and_then(|v| v.parse::<u32>().ok())
|
||||||
.unwrap_or(256);
|
.unwrap_or(256);
|
||||||
let err = |code: u16| Response::builder().status(code).header("Access-Control-Allow-Origin", "*").body(Vec::new()).unwrap();
|
let err = |code: u16| {
|
||||||
|
Response::builder()
|
||||||
|
.status(code)
|
||||||
|
.header("Access-Control-Allow-Origin", "*")
|
||||||
|
.body(Vec::new())
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
let Some(id) = id else { return err(400) };
|
let Some(id) = id else { return err(400) };
|
||||||
return match eng.thumb(job, id, size) {
|
return match eng.thumb(job, id, size) {
|
||||||
Ok(b) => Response::builder()
|
Ok(b) => Response::builder()
|
||||||
@@ -125,8 +259,17 @@ fn serve_file(eng: &Engine, req: &tauri::http::Request<Vec<u8>>) -> tauri::http:
|
|||||||
}
|
}
|
||||||
const CAP: u64 = 8 * 1024 * 1024;
|
const CAP: u64 = 8 * 1024 * 1024;
|
||||||
let mut it = req.uri().path().trim_start_matches("/file/").split('/');
|
let mut it = req.uri().path().trim_start_matches("/file/").split('/');
|
||||||
let (job, id) = (it.next().unwrap_or(""), it.next().and_then(|s| s.parse::<u64>().ok()));
|
let (job, id) = (
|
||||||
let err = |code: u16| Response::builder().status(code).header("Access-Control-Allow-Origin", "*").body(Vec::new()).unwrap();
|
it.next().unwrap_or(""),
|
||||||
|
it.next().and_then(|s| s.parse::<u64>().ok()),
|
||||||
|
);
|
||||||
|
let err = |code: u16| {
|
||||||
|
Response::builder()
|
||||||
|
.status(code)
|
||||||
|
.header("Access-Control-Allow-Origin", "*")
|
||||||
|
.body(Vec::new())
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
let Some(id) = id else { return err(400) };
|
let Some(id) = id else { return err(400) };
|
||||||
let range = req.headers().get("range").and_then(|v| v.to_str().ok());
|
let range = req.headers().get("range").and_then(|v| v.to_str().ok());
|
||||||
let slice = match eng.file_slice(job, id, range) {
|
let slice = match eng.file_slice(job, id, range) {
|
||||||
@@ -138,14 +281,24 @@ fn serve_file(eng: &Engine, req: &tauri::http::Request<Vec<u8>>) -> tauri::http:
|
|||||||
let want = slice.len();
|
let want = slice.len();
|
||||||
let mut headers = slice.headers();
|
let mut headers = slice.headers();
|
||||||
let partial = slice.partial || want > CAP;
|
let partial = slice.partial || want > CAP;
|
||||||
let Ok(data) = slice.read_all(CAP) else { return err(500) };
|
let Ok(data) = slice.read_all(CAP) else {
|
||||||
|
return err(500);
|
||||||
|
};
|
||||||
let mut b = Response::builder().status(if partial { 206 } else { 200 });
|
let mut b = Response::builder().status(if partial { 206 } else { 200 });
|
||||||
headers.retain(|(k, _)| *k != "Content-Range");
|
headers.retain(|(k, _)| *k != "Content-Range");
|
||||||
for (k, v) in &headers {
|
for (k, v) in &headers {
|
||||||
b = b.header(*k, v);
|
b = b.header(*k, v);
|
||||||
}
|
}
|
||||||
if partial && !data.is_empty() {
|
if partial && !data.is_empty() {
|
||||||
b = b.header("Content-Range", format!("bytes {}-{}/{}", start, start + data.len() as u64 - 1, total));
|
b = b.header(
|
||||||
|
"Content-Range",
|
||||||
|
format!(
|
||||||
|
"bytes {}-{}/{}",
|
||||||
|
start,
|
||||||
|
start + data.len() as u64 - 1,
|
||||||
|
total
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
b.body(data).unwrap_or_else(|_| err(500))
|
b.body(data).unwrap_or_else(|_| err(500))
|
||||||
}
|
}
|
||||||
@@ -155,18 +308,22 @@ fn main() {
|
|||||||
let for_protocol = engine.clone();
|
let for_protocol = engine.clone();
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.manage(State(engine))
|
.manage(State(engine))
|
||||||
.register_asynchronous_uri_scheme_protocol("guara", move |_ctx, request, responder| {
|
.register_asynchronous_uri_scheme_protocol("guara", move |_ctx, request, responder| {
|
||||||
let eng = for_protocol.clone();
|
let eng = for_protocol.clone();
|
||||||
std::thread::spawn(move || responder.respond(serve_file(&eng, &request)));
|
std::thread::spawn(move || responder.respond(serve_file(&eng, &request)));
|
||||||
})
|
})
|
||||||
.setup(|_app| {
|
.setup(|app| {
|
||||||
// Quando abrimos elevados a pedido do app normal, ele fecha o app antigo.
|
// When launched elevated by the unprivileged instance, terminate the old process.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some(pid) = std::env::var("GUARA_REPLACE_PID").ok().and_then(|p| p.parse::<i32>().ok()) {
|
if let Some(pid) = std::env::var("GUARA_REPLACE_PID")
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.parse::<i32>().ok())
|
||||||
|
{
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1200));
|
std::thread::sleep(std::time::Duration::from_millis(1200));
|
||||||
// SAFETY: só envia SIGTERM ao processo anterior do próprio usuário.
|
// SAFETY: only sends SIGTERM to the previous process owned by the invoking user.
|
||||||
unsafe {
|
unsafe {
|
||||||
if libc::geteuid() == 0 {
|
if libc::geteuid() == 0 {
|
||||||
libc::kill(pid, libc::SIGTERM);
|
libc::kill(pid, libc::SIGTERM);
|
||||||
@@ -174,9 +331,26 @@ fn main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reading raw disks requires root: request elevation as soon as the app launches,
|
||||||
|
// without waiting for the user to look for a button. If authorization fails,
|
||||||
|
// the window stays open and the UI displays a warning.
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
if unsafe { libc::geteuid() != 0 } && std::env::var("GUARA_REPLACE_PID").is_err() {
|
||||||
|
let handle = app.handle().clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = do_relaunch_elevated(handle);
|
||||||
|
});
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![rpc, relaunch_elevated])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
rpc,
|
||||||
|
is_elevated,
|
||||||
|
relaunch_elevated,
|
||||||
|
update_check,
|
||||||
|
update_install
|
||||||
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("erro ao iniciar o Guará");
|
.expect("erro ao iniciar o Guará");
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Guara",
|
"productName": "Guará",
|
||||||
"version": "1.2.0",
|
"version": "1.3.2",
|
||||||
"identifier": "dev.guara.app",
|
"identifier": "dev.guara.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../ui"
|
"frontendDist": "../ui"
|
||||||
@@ -34,6 +34,18 @@
|
|||||||
"icons/128x128@2x.png",
|
"icons/128x128@2x.png",
|
||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
]
|
],
|
||||||
|
"createUpdaterArtifacts": true
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDhENzRCRUU5NEI3QTAxQzgKUldUSUFYcEw2YjUwamFzMnpXM3ZKTEZPN1JIeXdLL3Q5SEF1M1FxQ284WFM0UWNsdlAyNUloWTEK",
|
||||||
|
"endpoints": [
|
||||||
|
"https://git.lucasrguerra.dev.br/lucasrguerra/guara/releases/latest/download/latest.json"
|
||||||
|
],
|
||||||
|
"windows": {
|
||||||
|
"installMode": "passive"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ h1, h2, h3 { margin: 0; letter-spacing: -0.02em; }
|
|||||||
.page { max-width: 1120px; margin: 0 auto; padding: 36px 40px 120px; animation: rise .35s cubic-bezier(.2, .8, .2, 1); }
|
.page { max-width: 1120px; margin: 0 auto; padding: 36px 40px 120px; animation: rise .35s cubic-bezier(.2, .8, .2, 1); }
|
||||||
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
||||||
|
|
||||||
/* ------------------------------------------------------------ componentes */
|
/* ------------------------------------------------------------ components */
|
||||||
.hero { padding: 56px 0 28px; max-width: 640px; }
|
.hero { padding: 56px 0 28px; max-width: 640px; }
|
||||||
.hero h1 { font-size: clamp(32px, 5vw, 48px); line-height: 1.05; font-weight: 700; }
|
.hero h1 { font-size: clamp(32px, 5vw, 48px); line-height: 1.05; font-weight: 700; }
|
||||||
.hero p { color: var(--text-2); font-size: 18px; margin: 14px 0 0; }
|
.hero p { color: var(--text-2); font-size: 18px; margin: 14px 0 0; }
|
||||||
@@ -194,7 +194,7 @@ details.adv[open] summary { margin-bottom: 10px; }
|
|||||||
.check { display: flex; gap: 10px; align-items: flex-start; padding: 6px 0; font-size: 14px; }
|
.check { display: flex; gap: 10px; align-items: flex-start; padding: 6px 0; font-size: 14px; }
|
||||||
.check input { margin-top: 3px; accent-color: var(--accent); width: 16px; height: 16px; }
|
.check input { margin-top: 3px; accent-color: var(--accent); width: 16px; height: 16px; }
|
||||||
|
|
||||||
/* progresso */
|
/* progress */
|
||||||
.progress-card { background: var(--surface); border-radius: 22px; box-shadow: var(--shadow); padding: 26px; display: grid; gap: 14px; }
|
.progress-card { background: var(--surface); border-radius: 22px; box-shadow: var(--shadow); padding: 26px; display: grid; gap: 14px; }
|
||||||
.bar { height: 10px; border-radius: 99px; background: color-mix(in srgb, var(--text) 9%, transparent); overflow: hidden; }
|
.bar { height: 10px; border-radius: 99px; background: color-mix(in srgb, var(--text) 9%, transparent); overflow: hidden; }
|
||||||
.bar > i { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--accent), var(--accent-2)); transition: width .3s ease; }
|
.bar > i { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--accent), var(--accent-2)); transition: width .3s ease; }
|
||||||
@@ -204,7 +204,7 @@ details.adv[open] summary { margin-bottom: 10px; }
|
|||||||
.stat b { display: block; font-size: 28px; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
|
.stat b { display: block; font-size: 28px; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
|
||||||
.stat span { color: var(--text-2); font-size: 13px; }
|
.stat span { color: var(--text-2); font-size: 13px; }
|
||||||
|
|
||||||
/* resultados */
|
/* results */
|
||||||
.toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin: 22px 0 12px; }
|
.toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin: 22px 0 12px; }
|
||||||
.chips { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
|
.chips { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
|
||||||
.chips::-webkit-scrollbar { display: none; }
|
.chips::-webkit-scrollbar { display: none; }
|
||||||
@@ -212,7 +212,7 @@ details.adv[open] summary { margin-bottom: 10px; }
|
|||||||
.chip[aria-pressed="true"] { background: var(--text); color: var(--bg); }
|
.chip[aria-pressed="true"] { background: var(--text); color: var(--bg); }
|
||||||
.chip small { opacity: .6; margin-left: 4px; font-variant-numeric: tabular-nums; }
|
.chip small { opacity: .6; margin-left: 4px; font-variant-numeric: tabular-nums; }
|
||||||
.input, .select { border: 0; border-radius: 12px; padding: 9px 14px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; min-width: 0; }
|
.input, .select { border: 0; border-radius: 12px; padding: 9px 14px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; min-width: 0; }
|
||||||
/* O fundo do select é translúcido; a lista aberta não herda isso e vinha branca com texto claro no tema escuro. */
|
/* The select background is translucent; the open dropdown does not inherit it and rendered white with light text in dark mode. */
|
||||||
.select option, .select optgroup { background-color: var(--surface); color: var(--text); }
|
.select option, .select optgroup { background-color: var(--surface); color: var(--text); }
|
||||||
.input { flex: 1 1 200px; }
|
.input { flex: 1 1 200px; }
|
||||||
.input::placeholder { color: var(--text-3); }
|
.input::placeholder { color: var(--text-3); }
|
||||||
@@ -255,7 +255,7 @@ code, .mono { overflow-wrap: anywhere; font-family: var(--mono); font-size: .92e
|
|||||||
.scrim { display: none; }
|
.scrim { display: none; }
|
||||||
.back { color: var(--accent); background: none; border: 0; cursor: pointer; font-weight: 500; padding: 0; margin-bottom: 8px; }
|
.back { color: var(--accent); background: none; border: 0; cursor: pointer; font-weight: 500; padding: 0; margin-bottom: 8px; }
|
||||||
|
|
||||||
/* ------------------------------------------------------------ responsivo */
|
/* ------------------------------------------------------------ responsive */
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.split { grid-template-columns: 1fr; }
|
.split { grid-template-columns: 1fr; }
|
||||||
.preview { position: static; order: -1; grid-template-columns: 120px 1fr; align-items: start; }
|
.preview { position: static; order: -1; grid-template-columns: 120px 1fr; align-items: start; }
|
||||||
@@ -285,12 +285,12 @@ code, .mono { overflow-wrap: anywhere; font-family: var(--mono); font-size: .92e
|
|||||||
* { animation: none !important; transition: none !important; }
|
* { animation: none !important; transition: none !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------ tema */
|
/* ------------------------------------------------------------ theme */
|
||||||
.theme-switch { display: inline-flex; padding: 2px; border-radius: 999px; background: color-mix(in srgb, var(--text) 8%, transparent); }
|
.theme-switch { display: inline-flex; padding: 2px; border-radius: 999px; background: color-mix(in srgb, var(--text) 8%, transparent); }
|
||||||
.theme-switch button { border: 0; background: transparent; cursor: pointer; padding: 4px 10px; border-radius: 999px; font-size: 12px; color: var(--text-2); }
|
.theme-switch button { border: 0; background: transparent; cursor: pointer; padding: 4px 10px; border-radius: 999px; font-size: 12px; color: var(--text-2); }
|
||||||
.theme-switch button[aria-pressed="true"] { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0, 0, 0, .18); font-weight: 600; }
|
.theme-switch button[aria-pressed="true"] { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0, 0, 0, .18); font-weight: 600; }
|
||||||
|
|
||||||
/* ------------------------------------------------------------ pré-visualização */
|
/* ------------------------------------------------------------ preview */
|
||||||
.viewer { position: relative; min-height: 160px; max-height: 52vh; overflow: auto; border-radius: 12px; background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--line); display: flex; flex-direction: column; align-items: center; }
|
.viewer { position: relative; min-height: 160px; max-height: 52vh; overflow: auto; border-radius: 12px; background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--line); display: flex; flex-direction: column; align-items: center; }
|
||||||
.viewer.large { max-height: none; height: 100%; min-height: 0; }
|
.viewer.large { max-height: none; height: 100%; min-height: 0; }
|
||||||
.viewer > * { max-width: 100%; }
|
.viewer > * { max-width: 100%; }
|
||||||
@@ -332,10 +332,10 @@ dialog.lightbox[open] { display: grid; }
|
|||||||
.lb-head .ttl { flex: 1; min-width: 0; } .lb-head .ttl b { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .lb-head .ttl small { color: var(--text-2); }
|
.lb-head .ttl { flex: 1; min-width: 0; } .lb-head .ttl b { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .lb-head .ttl small { color: var(--text-2); }
|
||||||
.lb-body { padding: 12px; min-height: 0; }
|
.lb-body { padding: 12px; min-height: 0; }
|
||||||
|
|
||||||
.hero-logo { width: min(440px, 80%); height: auto; display: block; margin: 0 0 22px -4px; }
|
.hero-logo { width: min(340px, 70%); height: auto; margin: 0 0 14px -6px; }
|
||||||
.pill.accent { background: var(--accent-soft); color: var(--accent); }
|
.pill.accent { background: var(--accent-soft); color: var(--accent); }
|
||||||
|
|
||||||
/* ------------------------------------------------------------ explorador de arquivos */
|
/* ------------------------------------------------------------ file explorer */
|
||||||
.main.fixed { overflow: hidden; }
|
.main.fixed { overflow: hidden; }
|
||||||
.page.wide { max-width: none; padding: 14px 18px 0; }
|
.page.wide { max-width: none; padding: 14px 18px 0; }
|
||||||
.page.fill { height: 100%; display: flex; flex-direction: column; }
|
.page.fill { height: 100%; display: flex; flex-direction: column; }
|
||||||
@@ -467,7 +467,7 @@ dialog.lightbox[open] { display: grid; }
|
|||||||
.cand .dev[aria-current="true"] { outline: 2px solid var(--accent); }
|
.cand .dev[aria-current="true"] { outline: 2px solid var(--accent); }
|
||||||
.cand .s { white-space: normal; }
|
.cand .s { white-space: normal; }
|
||||||
|
|
||||||
/* ------------------------------------------------------------ visualizador de bancos de dados */
|
/* ------------------------------------------------------------ database viewer */
|
||||||
.db-sum { width: 100%; padding: 16px; display: grid; gap: 8px; text-align: left; }
|
.db-sum { width: 100%; padding: 16px; display: grid; gap: 8px; text-align: left; }
|
||||||
.db-sum ul { list-style: none; margin: 4px 0; padding: 0; display: grid; gap: 2px; font-size: 13px; }
|
.db-sum ul { list-style: none; margin: 4px 0; padding: 0; display: grid; gap: 2px; font-size: 13px; }
|
||||||
.db-sum li { display: flex; justify-content: space-between; gap: 10px; } .db-sum li small { color: var(--text-3); }
|
.db-sum li { display: flex; justify-content: space-between; gap: 10px; } .db-sum li small { color: var(--text-3); }
|
||||||
@@ -497,7 +497,7 @@ dialog.lightbox[open] { display: grid; }
|
|||||||
.xp-tiles img { width: 100%; height: 100%; display: block; }
|
.xp-tiles img { width: 100%; height: 100%; display: block; }
|
||||||
.xp-pin { position: absolute; width: 14px; height: 14px; margin: -7px 0 0 -7px; border-radius: 50%; background: #0f8f8f; border: 3px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,.4); }
|
.xp-pin { position: absolute; width: 14px; height: 14px; margin: -7px 0 0 -7px; border-radius: 50%; background: #0f8f8f; border: 3px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,.4); }
|
||||||
|
|
||||||
/* saúde do disco (SMART) */
|
/* disk health (SMART) */
|
||||||
.h-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; }
|
.h-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; }
|
||||||
.h-tile { background: var(--surface); border-radius: 14px; box-shadow: var(--shadow); padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; }
|
.h-tile { background: var(--surface); border-radius: 14px; box-shadow: var(--shadow); padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; }
|
||||||
.h-tile small { color: var(--text-3); font-size: 12px; }
|
.h-tile small { color: var(--text-3); font-size: 12px; }
|
||||||
@@ -516,5 +516,14 @@ dialog.lightbox[open] { display: grid; }
|
|||||||
.h-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--ok); }
|
.h-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--ok); }
|
||||||
.h-dot.warning { background: var(--warn); } .h-dot.failing { background: var(--bad); }
|
.h-dot.warning { background: var(--warn); } .h-dot.failing { background: var(--bad); }
|
||||||
|
|
||||||
/* logo inline: segue o tema escolhido no app, não o do sistema */
|
/* logo: a single artwork (orange on transparent) works for both light and dark themes */
|
||||||
.logo .a { stroke: var(--accent); } .logo .b { stroke: var(--accent-2); } .logo .t { fill: var(--text); }
|
.logo-img { display: block; flex: none; }
|
||||||
|
|
||||||
|
/* app update */
|
||||||
|
.update-card { position: fixed; right: 18px; bottom: 18px; z-index: 60; width: min(400px, calc(100vw - 36px)); display: grid; gap: 10px; padding: 16px 18px; border-radius: 18px; background: var(--surface); box-shadow: var(--shadow); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); }
|
||||||
|
.update-card .upd-head { display: grid; gap: 2px; }
|
||||||
|
.update-card .upd-ver { font-size: 12.5px; color: var(--text-2); }
|
||||||
|
.update-card .upd-url { word-break: break-all; font-size: 12.5px; }
|
||||||
|
.update-card .upd-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.update-card .sub { margin: 0; }
|
||||||
|
pre.upd-notes { max-height: 50vh; overflow: auto; white-space: pre-wrap; font-size: 13px; }
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Guará: interface. Fala com o núcleo por `rpc(método, parâmetros)`:
|
// Guará: UI. Communicates with the core via `rpc(method, params)`:
|
||||||
// • app Tauri: invoke('rpc') • modo `guara ui`: POST /rpc/<método> com token.
|
// • Tauri app: invoke('rpc') • `guara ui` mode: POST /rpc/<method> with token.
|
||||||
|
|
||||||
import { createExplorer } from './explorer.js';
|
import { createExplorer } from './explorer.js';
|
||||||
import { setLang, LOC, currentLang } from './i18n.js';
|
import { setLang, LOC, currentLang } from './i18n.js';
|
||||||
@@ -10,7 +10,7 @@ let token = params.get('token');
|
|||||||
try {
|
try {
|
||||||
if (token) sessionStorage.setItem('guara-token', token);
|
if (token) sessionStorage.setItem('guara-token', token);
|
||||||
else token = sessionStorage.getItem('guara-token');
|
else token = sessionStorage.getItem('guara-token');
|
||||||
} catch { /* armazenamento indisponível */ }
|
} catch { /* storage unavailable */ }
|
||||||
|
|
||||||
async function rpc(method, p = {}) {
|
async function rpc(method, p = {}) {
|
||||||
if (tauri?.core?.invoke) return tauri.core.invoke('rpc', { method, params: p });
|
if (tauri?.core?.invoke) return tauri.core.invoke('rpc', { method, params: p });
|
||||||
@@ -29,7 +29,7 @@ const fileBase = tauri ? (isWin ? 'http://guara.localhost' : 'guara://localhost'
|
|||||||
const thumbUrl = (job, id, s = 256) => `${fileBase}/thumb/${job}/${id}?s=${s}` + (tauri ? '' : `&t=${encodeURIComponent(token || '')}`);
|
const thumbUrl = (job, id, s = 256) => `${fileBase}/thumb/${job}/${id}?s=${s}` + (tauri ? '' : `&t=${encodeURIComponent(token || '')}`);
|
||||||
const fileUrl = (job, id) => `${fileBase}/file/${job}/${id}` + (tauri ? '' : `?t=${encodeURIComponent(token || '')}`);
|
const fileUrl = (job, id) => `${fileBase}/file/${job}/${id}` + (tauri ? '' : `?t=${encodeURIComponent(token || '')}`);
|
||||||
|
|
||||||
// Atualiza o HTML só quando mudou (evita piscar e perder o foco/hover).
|
// Update HTML only when changed (prevents flickering and losing focus/hover).
|
||||||
function setHTML(el, html) {
|
function setHTML(el, html) {
|
||||||
if (!el || el._h === html) return false;
|
if (!el || el._h === html) return false;
|
||||||
el._h = html;
|
el._h = html;
|
||||||
@@ -37,7 +37,7 @@ function setHTML(el, html) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ tema
|
// ------------------------------------------------------------------ theme
|
||||||
function applyTheme(t) {
|
function applyTheme(t) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
if (t === 'light' || t === 'dark') root.dataset.theme = t; else delete root.dataset.theme;
|
if (t === 'light' || t === 'dark') root.dataset.theme = t; else delete root.dataset.theme;
|
||||||
@@ -47,19 +47,19 @@ try { applyTheme(localStorage.getItem('guara-theme') || 'auto'); } catch { apply
|
|||||||
document.querySelectorAll('[data-theme-pick]').forEach((b) => (b.onclick = async () => {
|
document.querySelectorAll('[data-theme-pick]').forEach((b) => (b.onclick = async () => {
|
||||||
const t = b.dataset.themePick;
|
const t = b.dataset.themePick;
|
||||||
applyTheme(t);
|
applyTheme(t);
|
||||||
try { localStorage.setItem('guara-theme', t); } catch { /* sem armazenamento */ }
|
try { localStorage.setItem('guara-theme', t); } catch { /* no storage */ }
|
||||||
saveSettings({ theme: t });
|
saveSettings({ theme: t });
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ------------------------------------------------------------------ configurações
|
// ------------------------------------------------------------------ settings
|
||||||
// (declarado antes do uso: `S` é definido mais abaixo, por isso o objeto fica aqui)
|
// (declared before use: `S` is defined further below, so the object lives here)
|
||||||
const SETTINGS = { theme: 'auto', lang: 'auto', ui_lang: null, notify: true, maps: false };
|
const SETTINGS = { theme: 'auto', lang: 'auto', ui_lang: null, notify: true, maps: false, updates: true };
|
||||||
async function saveSettings(patch) {
|
async function saveSettings(patch) {
|
||||||
Object.assign(SETTINGS, patch);
|
Object.assign(SETTINGS, patch);
|
||||||
try { await rpc('settings_set', SETTINGS); } catch { /* segue só com o armazenamento local */ }
|
try { await rpc('settings_set', SETTINGS); } catch { /* continue with local storage only */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ utilidades
|
// ------------------------------------------------------------------ utilities
|
||||||
const $ = (s, el = document) => el.querySelector(s);
|
const $ = (s, el = document) => el.querySelector(s);
|
||||||
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||||
const human = (n) => {
|
const human = (n) => {
|
||||||
@@ -147,19 +147,20 @@ function info({ title, html }) {
|
|||||||
|
|
||||||
// ------------------------------------------------------------------ estado
|
// ------------------------------------------------------------------ estado
|
||||||
const S = {
|
const S = {
|
||||||
|
elevated: true,
|
||||||
devices: [],
|
devices: [],
|
||||||
defaults: {},
|
defaults: {},
|
||||||
sessions: [], // {job, target, label}
|
sessions: [], // {job, target, label}
|
||||||
device: null, // {path,name,...}
|
device: null, // {path,name,...}
|
||||||
layout: null,
|
layout: null,
|
||||||
job: null, // id da varredura atual
|
job: null, // active scan job id
|
||||||
status: null,
|
status: null,
|
||||||
filters: { category: null, minConfidence: null, query: '', sort: 'confidence' },
|
filters: { category: null, minConfidence: null, query: '', sort: 'confidence' },
|
||||||
items: [],
|
items: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
selected: new Set(),
|
selected: new Set(),
|
||||||
allFiltered: false,
|
allFiltered: false,
|
||||||
current: null, // arquivo em foco (pré-visualização)
|
current: null, // focused file (preview)
|
||||||
poll: null,
|
poll: null,
|
||||||
recJob: null,
|
recJob: null,
|
||||||
};
|
};
|
||||||
@@ -258,25 +259,26 @@ $('#loadSessionBtn').onclick = async () => {
|
|||||||
} catch (e) { info({ title: 'Não foi possível abrir a sessão', html: `<p>${esc(e.message)}</p>` }); }
|
} catch (e) { info({ title: 'Não foi possível abrir a sessão', html: `<p>${esc(e.message)}</p>` }); }
|
||||||
};
|
};
|
||||||
|
|
||||||
// ------------------------------------------------------------------ início
|
// ------------------------------------------------------------------ home
|
||||||
function renderHome() {
|
function renderHome() {
|
||||||
main.classList.remove('fixed');
|
main.classList.remove('fixed');
|
||||||
stopPoll();
|
stopPoll();
|
||||||
S.device = null; S.job = null;
|
S.device = null; S.job = null;
|
||||||
renderSide();
|
renderSide();
|
||||||
main.innerHTML = `<div class="page">
|
main.innerHTML = `<div class="page">
|
||||||
<section class="hero"><img class="hero-logo" src="guara-logo.svg" alt="Guará"><h1>Encontre o que você achou que tinha perdido.</h1>
|
<section class="hero"><img class="logo-img hero-logo" src="logo.png" alt="Guará"><h1>Encontre o que você achou que tinha perdido.</h1>
|
||||||
<p>Escolha um disco, SSD ou pendrive ao lado. O Guará lê tudo: arquivos apagados, partições perdidas e espaço não particionado, sem escrever nada no disco de origem.</p></section>
|
<p>Escolha um disco, SSD ou pendrive ao lado. O Guará lê tudo: arquivos apagados, partições perdidas e espaço não particionado, sem escrever nada no disco de origem.</p></section>
|
||||||
<div class="cards">
|
<div class="cards">
|
||||||
<div class="card"><h3>Uma varredura, tudo junto</h3><p>Metadados do sistema de arquivos e busca por assinatura na mesma passada, sem duplicatas.</p></div>
|
<div class="card"><h3>Uma varredura, tudo junto</h3><p>Metadados do sistema de arquivos e busca por assinatura na mesma passada, sem duplicatas.</p></div>
|
||||||
<div class="card"><h3>Confiança por arquivo</h3><p>Cada achado mostra a chance de abrir corretamente: alta, média ou baixa.</p></div>
|
<div class="card"><h3>Confiança por arquivo</h3><p>Cada achado mostra a chance de abrir corretamente: alta, média ou baixa.</p></div>
|
||||||
<div class="card"><h3>Origem protegida</h3><p>Somente leitura. A recuperação sempre vai para outra pasta, com SHA-256 de tudo.</p></div>
|
<div class="card"><h3>Origem protegida</h3><p>Somente leitura. A recuperação sempre vai para outra pasta, com SHA-256 de tudo.</p></div>
|
||||||
</div>
|
</div>
|
||||||
${S.devices.length ? '' : `<div class="notice info" style="margin-top:22px">${warnSvg}<div><b>Nenhum disco apareceu.</b> Discos brutos exigem administrador. Rode <code>sudo guara ui</code> (Linux/macOS) ou abra o Guará como administrador (Windows). Você ainda pode abrir um arquivo de imagem pelo menu.</div></div>`}
|
${!S.elevated ? `<div class="notice bad" style="margin-top:22px">${warnSvg}<div>Este disco exige permissão de administrador. <button class="btn sm" id="elevateHome" style="margin-left:6px">Reiniciar como administrador</button></div></div>` : S.devices.length ? '' : `<div class="notice info" style="margin-top:22px">${warnSvg}<div><b>Nenhum disco apareceu.</b> Discos brutos exigem administrador. Rode <code>sudo guara ui</code> (Linux/macOS) ou abra o Guará como administrador (Windows). Você ainda pode abrir um arquivo de imagem pelo menu.</div></div>`}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
$('#elevateHome')?.addEventListener('click', () => tauri.core.invoke('relaunch_elevated').catch((e) => toast(String(e))));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ dispositivo
|
// ------------------------------------------------------------------ device
|
||||||
async function openDevice(d) {
|
async function openDevice(d) {
|
||||||
main.classList.remove('fixed');
|
main.classList.remove('fixed');
|
||||||
stopPoll();
|
stopPoll();
|
||||||
@@ -289,7 +291,7 @@ async function openDevice(d) {
|
|||||||
try { layout = await rpc('inspect', { path: d.path }); } catch (e) { err = e.message; }
|
try { layout = await rpc('inspect', { path: d.path }); } catch (e) { err = e.message; }
|
||||||
S.layout = layout;
|
S.layout = layout;
|
||||||
renderDevice(err);
|
renderDevice(err);
|
||||||
// saúde (SMART) em segundo plano: não atrasa a tela
|
// Query SMART health in the background without blocking the screen.
|
||||||
rpc('health', { path: d.path }).then((h) => { if (S.device === d) { S.health = h; paintHealth(); } }).catch(() => {});
|
rpc('health', { path: d.path }).then((h) => { if (S.device === d) { S.health = h; paintHealth(); } }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +325,7 @@ function paintHealth() {
|
|||||||
${attrs.map((a) => `<tr><td><span class="h-dot ${esc(a.status)}" title="${HEALTH_WORD[a.status] || ''}"></span></td><td class="mono">${esc(a.id)}</td><td class="mono">${esc(a.name)}</td><td>${dash(a.value)}</td><td>${dash(a.worst)}</td><td>${dash(a.thresh)}</td><td class="mono">${esc(a.raw)}</td></tr>`).join('')}
|
${attrs.map((a) => `<tr><td><span class="h-dot ${esc(a.status)}" title="${HEALTH_WORD[a.status] || ''}"></span></td><td class="mono">${esc(a.id)}</td><td class="mono">${esc(a.name)}</td><td>${dash(a.value)}</td><td>${dash(a.worst)}</td><td>${dash(a.thresh)}</td><td class="mono">${esc(a.raw)}</td></tr>`).join('')}
|
||||||
</tbody></table></div></details>` : '';
|
</tbody></table></div></details>` : '';
|
||||||
const scope = h.partition ? `<p class="sub">O SMART é do disco inteiro: os dados abaixo são de <span class="mono">${esc(h.disk)}</span>, que contém <span class="mono">${esc(h.partition)}</span>.</p>` : '';
|
const scope = h.partition ? `<p class="sub">O SMART é do disco inteiro: os dados abaixo são de <span class="mono">${esc(h.disk)}</span>, que contém <span class="mono">${esc(h.partition)}</span>.</p>` : '';
|
||||||
// o que motivou o alerta (os campos da ficha já aparecem abaixo)
|
// Fields that triggered the alert (standard summary fields appear below).
|
||||||
const known = new Set(['Modelo', 'Nº de série', 'Horas ligado', 'Temperatura', 'Autoavaliação SMART', 'Vida útil usada']);
|
const known = new Set(['Modelo', 'Nº de série', 'Horas ligado', 'Temperatura', 'Autoavaliação SMART', 'Vida útil usada']);
|
||||||
const why = (h.details || []).filter(([k]) => !known.has(k)).map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join('');
|
const why = (h.details || []).filter(([k]) => !known.has(k)).map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join('');
|
||||||
el.innerHTML = `${bad ? `<div class="notice ${h.status === 'failing' ? 'bad' : ''}">${warnSvg}<div><b>${esc(h.summary)}</b>
|
el.innerHTML = `${bad ? `<div class="notice ${h.status === 'failing' ? 'bad' : ''}">${warnSvg}<div><b>${esc(h.summary)}</b>
|
||||||
@@ -336,7 +338,7 @@ function paintHealth() {
|
|||||||
$('#healthImg')?.addEventListener('click', startImage);
|
$('#healthImg')?.addEventListener('click', startImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Disco com sinais de defeito: oferece criar a imagem antes de varrer. */
|
/** Disk showing signs of failure: offer to create a disk image before scanning. */
|
||||||
function confirmRisky() {
|
function confirmRisky() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const h = S.health;
|
const h = S.health;
|
||||||
@@ -530,8 +532,8 @@ function openSearch(job, terms) {
|
|||||||
(async () => { while (await tick2()) await new Promise((r) => setTimeout(r, 700)); })();
|
(async () => { while (await tick2()) await new Promise((r) => setTimeout(r, 700)); })();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ varredura
|
// ------------------------------------------------------------------ scan
|
||||||
let EX = null; // explorador de arquivos (grade, navegação, detalhes)
|
let EX = null; // file explorer (grid, navigation, details)
|
||||||
|
|
||||||
function openJob(s) {
|
function openJob(s) {
|
||||||
stopPoll();
|
stopPoll();
|
||||||
@@ -554,7 +556,7 @@ async function tick() {
|
|||||||
S.status = st;
|
S.status = st;
|
||||||
paintStatus();
|
paintStatus();
|
||||||
EX?.onStatus(st);
|
EX?.onStatus(st);
|
||||||
// continua consultando enquanto a varredura roda ou as datas/EXIF ainda estão sendo lidas
|
// Keep polling while the scan is running or dates/EXIF are still being enriched.
|
||||||
if (st.status !== 'running' && !st.enriching) {
|
if (st.status !== 'running' && !st.enriching) {
|
||||||
stopPoll();
|
stopPoll();
|
||||||
loadRecent();
|
loadRecent();
|
||||||
@@ -613,7 +615,7 @@ function paintStatus() {
|
|||||||
|
|
||||||
async function chooseSource() {
|
async function chooseSource() {
|
||||||
let cands = [];
|
let cands = [];
|
||||||
try { cands = await rpc('session_candidates', { job: S.job }); } catch { /* segue só com o campo manual */ }
|
try { cands = await rpc('session_candidates', { job: S.job }); } catch { /* fall back to manual path input */ }
|
||||||
const st = S.status || {};
|
const st = S.status || {};
|
||||||
const size = st.layout?.size;
|
const size = st.layout?.size;
|
||||||
const d = $('#dlg');
|
const d = $('#dlg');
|
||||||
@@ -709,7 +711,7 @@ async function recover(sel) {
|
|||||||
} catch (e) { info({ title: 'Não foi possível recuperar', html: `<p>${esc(e.message)}</p>` }); }
|
} catch (e) { info({ title: 'Não foi possível recuperar', html: `<p>${esc(e.message)}</p>` }); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal de progresso de tarefas auxiliares (recuperação / imagem).
|
// Progress modal for auxiliary jobs (recovery / disk imaging).
|
||||||
function watchAux(job, title, doneHtml) {
|
function watchAux(job, title, doneHtml) {
|
||||||
const d = $('#dlg');
|
const d = $('#dlg');
|
||||||
d.onclose = null;
|
d.onclose = null;
|
||||||
@@ -738,18 +740,21 @@ function watchAux(job, title, doneHtml) {
|
|||||||
|
|
||||||
async function openJobFromList(s) { openJob(s); }
|
async function openJobFromList(s) { openJob(s); }
|
||||||
|
|
||||||
// ------------------------------------------------------------------ configurações (janela)
|
// ------------------------------------------------------------------ settings modal
|
||||||
$('#settingsBtn')?.addEventListener('click', () => {
|
$('#settingsBtn')?.addEventListener('click', () => {
|
||||||
const d = $('#dlg');
|
const d = $('#dlg');
|
||||||
d.innerHTML = `<form method="dialog"><h2>Configurações</h2>
|
d.innerHTML = `<form method="dialog"><h2>Configurações</h2>
|
||||||
<label class="fld"><span>Idioma</span><select class="select" name="lang"><option value="auto">Auto</option><option value="pt">Português</option><option value="en">English</option></select></label>
|
<label class="fld"><span>Idioma</span><select class="select" name="lang"><option value="auto">Auto</option><option value="pt">Português</option><option value="en">English</option></select></label>
|
||||||
<label class="check"><input type="checkbox" name="notify" ${SETTINGS.notify ? 'checked' : ''}><span><b>Notificações</b><br><span class="sub">Avisar quando uma tarefa longa terminar</span></span></label>
|
<label class="check"><input type="checkbox" name="notify" ${SETTINGS.notify ? 'checked' : ''}><span><b>Notificações</b><br><span class="sub">Avisar quando uma tarefa longa terminar</span></span></label>
|
||||||
<label class="check"><input type="checkbox" name="maps" ${SETTINGS.maps ? 'checked' : ''}><span><b>Mapas online</b><br><span class="sub">Mostrar mapas das fotos com GPS (usa a internet: baixa blocos do OpenStreetMap)</span></span></label>
|
<label class="check"><input type="checkbox" name="maps" ${SETTINGS.maps ? 'checked' : ''}><span><b>Mapas online</b><br><span class="sub">Mostrar mapas das fotos com GPS (usa a internet: baixa blocos do OpenStreetMap)</span></span></label>
|
||||||
|
${tauri?.core?.invoke ? `<label class="check"><input type="checkbox" name="updates" ${SETTINGS.updates ? 'checked' : ''}><span><b>Atualizações automáticas</b><br><span class="sub">Ao abrir, consulta o Gitea para ver se há versão nova (uma consulta, sem enviar dados seus)</span><br><button type="button" class="btn sm" id="updNow">Verificar agora</button></span></label>` : ''}
|
||||||
<menu><button class="btn" value="cancel">Cancelar</button><button class="btn primary" value="ok">Salvar</button></menu></form>`;
|
<menu><button class="btn" value="cancel">Cancelar</button><button class="btn primary" value="ok">Salvar</button></menu></form>`;
|
||||||
d.querySelector('[name=lang]').value = SETTINGS.lang;
|
d.querySelector('[name=lang]').value = SETTINGS.lang;
|
||||||
|
d.querySelector('#updNow')?.addEventListener('click', (e) => { e.preventDefault(); checkUpdate(true); });
|
||||||
d.onclose = () => {
|
d.onclose = () => {
|
||||||
if (d.returnValue !== 'ok') return;
|
if (d.returnValue !== 'ok') return;
|
||||||
const patch = { lang: d.querySelector('[name=lang]').value, notify: d.querySelector('[name=notify]').checked, maps: d.querySelector('[name=maps]').checked };
|
const patch = { lang: d.querySelector('[name=lang]').value, notify: d.querySelector('[name=notify]').checked, maps: d.querySelector('[name=maps]').checked };
|
||||||
|
if (d.querySelector('[name=updates]')) patch.updates = d.querySelector('[name=updates]').checked;
|
||||||
setLang(patch.lang);
|
setLang(patch.lang);
|
||||||
patch.ui_lang = currentLang();
|
patch.ui_lang = currentLang();
|
||||||
saveSettings(patch);
|
saveSettings(patch);
|
||||||
@@ -759,7 +764,65 @@ $('#settingsBtn')?.addEventListener('click', () => {
|
|||||||
d.showModal();
|
d.showModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------------ fechar com varredura em andamento
|
// ------------------------------------------------------------------ app updater
|
||||||
|
// The desktop shell (Tauri) queries `latest.json` from the Gitea release, verifies the
|
||||||
|
// package signature against the embedded public key, installs, and relaunches. No-op in browser mode (`guara ui`).
|
||||||
|
let UPD = null;
|
||||||
|
|
||||||
|
async function checkUpdate(manual) {
|
||||||
|
if (!tauri?.core?.invoke) return;
|
||||||
|
try {
|
||||||
|
UPD = await tauri.core.invoke('update_check');
|
||||||
|
renderUpdateCard();
|
||||||
|
if (manual) toast(UPD ? `Nova versão ${UPD.version} disponível` : 'Você já está na versão mais recente');
|
||||||
|
} catch (e) {
|
||||||
|
if (manual) toast(`Não foi possível verificar atualizações: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUpdateCard(state) {
|
||||||
|
let el = $('#updateCard');
|
||||||
|
if (!UPD) { el?.remove(); return; }
|
||||||
|
if (!el) { el = document.createElement('div'); el.id = 'updateCard'; el.className = 'update-card'; el.setAttribute('role', 'status'); document.body.appendChild(el); }
|
||||||
|
if (state === 'busy') return;
|
||||||
|
el.innerHTML = `<div class="upd-head"><b>Nova versão do Guará disponível</b><span class="mono upd-ver">v${esc(UPD.current)} → v${esc(UPD.version)}</span></div>
|
||||||
|
${UPD.auto ? '' : '<p class="sub">Esta instalação vem de um pacote do sistema e não se atualiza sozinha. Baixe a versão nova na página da release:</p><p class="mono upd-url">' + esc(UPD.url) + '</p>'}
|
||||||
|
<div class="upd-actions">
|
||||||
|
${UPD.auto ? '<button class="btn primary sm" id="updGo">Atualizar agora</button>' : ''}
|
||||||
|
${UPD.notes ? '<button class="btn sm" id="updNotes">Novidades</button>' : ''}
|
||||||
|
<button class="btn sm" id="updLater">Depois</button>
|
||||||
|
</div>`;
|
||||||
|
$('#updGo')?.addEventListener('click', installUpdate);
|
||||||
|
$('#updLater')?.addEventListener('click', () => { UPD = null; el.remove(); });
|
||||||
|
$('#updNotes')?.addEventListener('click', () => info({ title: `Guará ${UPD.version}`, html: `<pre class="upd-notes">${esc(UPD.notes)}</pre>` }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installUpdate() {
|
||||||
|
const el = $('#updateCard');
|
||||||
|
el.innerHTML = `<div class="upd-head"><b>Atualizando o Guará…</b></div>
|
||||||
|
<div class="bar indet" id="updBar"><i style="width:0"></i></div><p class="sub" id="updMsg">Baixando a atualização…</p>`;
|
||||||
|
renderUpdateCard('busy');
|
||||||
|
try {
|
||||||
|
await tauri.core.invoke('update_install');
|
||||||
|
} catch (e) {
|
||||||
|
toast(String(e));
|
||||||
|
renderUpdateCard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress(p) {
|
||||||
|
const bar = $('#updBar'); const msg = $('#updMsg');
|
||||||
|
if (!bar || !msg) return;
|
||||||
|
if (p.installing) { bar.classList.add('indet'); msg.textContent = 'Instalando… o Guará vai reabrir sozinho.'; return; }
|
||||||
|
if (p.total) {
|
||||||
|
const pct = Math.min(100, Math.round((p.downloaded / p.total) * 100));
|
||||||
|
bar.classList.remove('indet');
|
||||||
|
bar.firstElementChild.style.width = `${pct}%`;
|
||||||
|
msg.textContent = `Baixando… ${pct}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ close while scan is running
|
||||||
function askClose() {
|
function askClose() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const d = $('#dlg');
|
const d = $('#dlg');
|
||||||
@@ -785,14 +848,14 @@ function watchClose() {
|
|||||||
try {
|
try {
|
||||||
await rpc('scan_pause', { job: S.job });
|
await rpc('scan_pause', { job: S.job });
|
||||||
for (let i = 0; i < 40; i++) { const st = await rpc('status', { job: S.job }); if (st.status !== 'running') break; await new Promise((r) => setTimeout(r, 250)); }
|
for (let i = 0; i < 40; i++) { const st = await rpc('status', { job: S.job }); if (st.status !== 'running') break; await new Promise((r) => setTimeout(r, 250)); }
|
||||||
await new Promise((r) => setTimeout(r, 600)); // dá tempo do salvamento automático
|
await new Promise((r) => setTimeout(r, 600)); // allow auto-save to flush
|
||||||
} catch { /* sai mesmo assim */ }
|
} catch { /* exit anyway */ }
|
||||||
}
|
}
|
||||||
win.destroy();
|
win.destroy();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ início do app
|
// ------------------------------------------------------------------ app startup
|
||||||
(async function init() {
|
(async function init() {
|
||||||
try {
|
try {
|
||||||
const v = await rpc('version');
|
const v = await rpc('version');
|
||||||
@@ -807,6 +870,9 @@ function watchClose() {
|
|||||||
setLang(SETTINGS.lang);
|
setLang(SETTINGS.lang);
|
||||||
if (SETTINGS.ui_lang !== currentLang()) saveSettings({ ui_lang: currentLang() });
|
if (SETTINGS.ui_lang !== currentLang()) saveSettings({ ui_lang: currentLang() });
|
||||||
watchClose();
|
watchClose();
|
||||||
|
tauri?.event?.listen?.('update-progress', (e) => updateProgress(e.payload));
|
||||||
|
if (SETTINGS.updates !== false) checkUpdate(false);
|
||||||
|
if (tauri) S.elevated = await tauri.core.invoke('is_elevated').catch(() => true);
|
||||||
tauri?.event?.listen?.('elevate-failed', (e) => { toast(String(e.payload || 'Não foi possível reiniciar como administrador')); });
|
tauri?.event?.listen?.('elevate-failed', (e) => { toast(String(e.payload || 'Não foi possível reiniciar como administrador')); });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
main.innerHTML = `<div class="page"><div class="notice bad">${warnSvg}<div>Não foi possível falar com o núcleo do Guará: ${esc(e.message)}. Se abriu esta página manualmente, use o endereço completo exibido pelo comando <code>guara ui</code> (ele inclui o token).</div></div></div>`;
|
main.innerHTML = `<div class="page"><div class="notice bad">${warnSvg}<div>Não foi possível falar com o núcleo do Guará: ${esc(e.message)}. Se abriu esta página manualmente, use o endereço completo exibido pelo comando <code>guara ui</code> (ele inclui o token).</div></div></div>`;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Explorador de arquivos recuperados: grade de miniaturas (virtualizada, aguenta
|
// Recovered file explorer: thumbnail grid (virtualized, handles
|
||||||
// dezenas de milhares de itens), navegação por tipo/data/pasta/câmera, seleção
|
// tens of thousands of items), navigation by type/date/folder/camera, file-manager
|
||||||
// estilo gerenciador de arquivos (Ctrl/Shift/teclado), painel de detalhes e
|
// style selection (Ctrl/Shift/keyboard), inspector panel, and
|
||||||
// visualização rápida em tela cheia.
|
// fullscreen quick look.
|
||||||
|
|
||||||
import { renderPreview, kindOf, KIND_LABEL } from './preview.js';
|
import { renderPreview, kindOf, KIND_LABEL } from './preview.js';
|
||||||
import { LOC } from './i18n.js';
|
import { LOC } from './i18n.js';
|
||||||
@@ -66,7 +66,7 @@ export function createExplorer(deps) {
|
|||||||
try {
|
try {
|
||||||
E.view = localStorage.getItem('guara-view') || 'grid';
|
E.view = localStorage.getItem('guara-view') || 'grid';
|
||||||
E.tile = +localStorage.getItem('guara-tile') || 150;
|
E.tile = +localStorage.getItem('guara-tile') || 150;
|
||||||
} catch { /* sem armazenamento */ }
|
} catch { /* no storage */ }
|
||||||
|
|
||||||
host.innerHTML = `<div class="xp" data-view="${E.view}">
|
host.innerHTML = `<div class="xp" data-view="${E.view}">
|
||||||
<div class="xp-bar">
|
<div class="xp-bar">
|
||||||
@@ -91,7 +91,7 @@ export function createExplorer(deps) {
|
|||||||
$('#xpSort').value = E.sort;
|
$('#xpSort').value = E.sort;
|
||||||
$('#xpSize').value = E.tile;
|
$('#xpSize').value = E.tile;
|
||||||
|
|
||||||
// ------------------------------------------------------------ dados
|
// ------------------------------------------------------------ data
|
||||||
const toQuery = () => {
|
const toQuery = () => {
|
||||||
const f = E.filters;
|
const f = E.filters;
|
||||||
const q = {};
|
const q = {};
|
||||||
@@ -139,8 +139,8 @@ export function createExplorer(deps) {
|
|||||||
|
|
||||||
const reloadAll = () => { reload(); loadFacets(); };
|
const reloadAll = () => { reload(); loadFacets(); };
|
||||||
|
|
||||||
// ------------------------------------------------------------ layout virtual
|
// ------------------------------------------------------------ virtual layout
|
||||||
// Agrupa por dia; se houver poucos itens por dia, agrupa por mês.
|
// Group by day; if there are too few items per day, group by month.
|
||||||
let groupBy = 'day';
|
let groupBy = 'day';
|
||||||
const dayKey = (t) => (t == null ? 'Sem data' : new Date(t * 1000).toISOString().slice(0, groupBy === 'day' ? 10 : 7));
|
const dayKey = (t) => (t == null ? 'Sem data' : new Date(t * 1000).toISOString().slice(0, groupBy === 'day' ? 10 : 7));
|
||||||
const dayLabel = (k) => {
|
const dayLabel = (k) => {
|
||||||
@@ -150,7 +150,7 @@ export function createExplorer(deps) {
|
|||||||
: new Date(k + '-01T00:00:00Z').toLocaleDateString(LOC(), { timeZone: 'UTC', month: 'long', year: 'numeric' });
|
: new Date(k + '-01T00:00:00Z').toLocaleDateString(LOC(), { timeZone: 'UTC', month: 'long', year: 'numeric' });
|
||||||
};
|
};
|
||||||
const fmtDate = (t) => (t == null ? 'sem data' : new Date(t * 1000).toLocaleString(LOC(), { timeZone: 'UTC', dateStyle: 'medium', timeStyle: 'short' }));
|
const fmtDate = (t) => (t == null ? 'sem data' : new Date(t * 1000).toLocaleString(LOC(), { timeZone: 'UTC', dateStyle: 'medium', timeStyle: 'short' }));
|
||||||
// Metadados do núcleo trazem datas em ISO 8601; o idioma decide a ordem.
|
// Core metadata provides ISO 8601 dates; the active locale determines the order.
|
||||||
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/;
|
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/;
|
||||||
const fmtMeta = (v) => ISO_DATE.test(v) ? new Date(v.endsWith('Z') ? v : v + 'Z').toLocaleString(LOC(), { timeZone: 'UTC', dateStyle: 'medium', timeStyle: 'medium' }) : v;
|
const fmtMeta = (v) => ISO_DATE.test(v) ? new Date(v.endsWith('Z') ? v : v + 'Z').toLocaleString(LOC(), { timeZone: 'UTC', dateStyle: 'medium', timeStyle: 'medium' }) : v;
|
||||||
const fmtDay = (t) => (t == null ? '' : new Date(t * 1000).toLocaleDateString(LOC(), { timeZone: 'UTC', day: '2-digit', month: 'short', year: 'numeric' }));
|
const fmtDay = (t) => (t == null ? '' : new Date(t * 1000).toLocaleDateString(LOC(), { timeZone: 'UTC', day: '2-digit', month: 'short', year: 'numeric' }));
|
||||||
@@ -195,7 +195,7 @@ export function createExplorer(deps) {
|
|||||||
});
|
});
|
||||||
flush();
|
flush();
|
||||||
if (grouped) {
|
if (grouped) {
|
||||||
// contagem por grupo
|
// count per group
|
||||||
for (let r = 0; r < rows.length; r++) {
|
for (let r = 0; r < rows.length; r++) {
|
||||||
if (rows[r].t !== 'head') continue;
|
if (rows[r].t !== 'head') continue;
|
||||||
let n = 0;
|
let n = 0;
|
||||||
@@ -245,7 +245,7 @@ export function createExplorer(deps) {
|
|||||||
sizer.appendChild(frag);
|
sizer.appendChild(frag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------ itens
|
// ------------------------------------------------------------ items
|
||||||
const isSel = (id) => E.allFiltered || E.sel.has(id);
|
const isSel = (id) => E.allFiltered || E.sel.has(id);
|
||||||
|
|
||||||
function headHtml(row) {
|
function headHtml(row) {
|
||||||
@@ -288,7 +288,7 @@ export function createExplorer(deps) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// imagens que falham viram um aviso (arquivo possivelmente danificado)
|
// failed images become a warning (file possibly damaged)
|
||||||
scroll.addEventListener('error', (e) => {
|
scroll.addEventListener('error', (e) => {
|
||||||
const img = e.target;
|
const img = e.target;
|
||||||
if (img.tagName !== 'IMG' || !img.dataset.th) return;
|
if (img.tagName !== 'IMG' || !img.dataset.th) return;
|
||||||
@@ -300,7 +300,7 @@ export function createExplorer(deps) {
|
|||||||
: `<div class="xp-ico c-bad" title="Não foi possível gerar a miniatura (arquivo possivelmente danificado)"><span>${I.warn}</span><b>?</b></div>`);
|
: `<div class="xp-ico c-bad" title="Não foi possível gerar a miniatura (arquivo possivelmente danificado)"><span>${I.warn}</span><b>?</b></div>`);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// ------------------------------------------------------------ seleção
|
// ------------------------------------------------------------ selection
|
||||||
const selCount = () => (E.allFiltered ? E.total : E.sel.size);
|
const selCount = () => (E.allFiltered ? E.total : E.sel.size);
|
||||||
const idxOf = (id) => E.items.findIndex((x) => x.id === id);
|
const idxOf = (id) => E.items.findIndex((x) => x.id === id);
|
||||||
|
|
||||||
@@ -387,7 +387,7 @@ export function createExplorer(deps) {
|
|||||||
else if (e.key === 'Escape') { E.sel = new Set(); E.allFiltered = false; paintSel(); }
|
else if (e.key === 'Escape') { E.sel = new Set(); E.allFiltered = false; paintSel(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------ barra de ações
|
// ------------------------------------------------------------ action bar
|
||||||
function paintActions() {
|
function paintActions() {
|
||||||
const n = selCount();
|
const n = selCount();
|
||||||
const html = n
|
const html = n
|
||||||
@@ -407,7 +407,7 @@ export function createExplorer(deps) {
|
|||||||
else if (a === 'recall') onRecover({ filter: toQuery() });
|
else if (a === 'recall') onRecover({ filter: toQuery() });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------ navegação lateral
|
// ------------------------------------------------------------ sidebar navigation
|
||||||
function paintNav() {
|
function paintNav() {
|
||||||
const f = E.facets;
|
const f = E.facets;
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
@@ -424,7 +424,7 @@ export function createExplorer(deps) {
|
|||||||
+ (open ? y.months.map((m) => node(MONTHS[m.month - 1], m.count, `data-f="month" data-y="${y.year}" data-v="${m.month}"`, F.year === y.year && F.month === m.month, 1)).join('') : '');
|
+ (open ? y.months.map((m) => node(MONTHS[m.month - 1], m.count, `data-f="month" data-y="${y.year}" data-v="${m.month}"`, F.year === y.year && F.month === m.month, 1)).join('') : '');
|
||||||
}).join('') + (f.undated ? node('Sem data', f.undated, 'data-f="undated" data-v="1"', F.undated) : '');
|
}).join('') + (f.undated ? node('Sem data', f.undated, 'data-f="undated" data-v="1"', F.undated) : '');
|
||||||
const folders = f.folders.filter((d) => {
|
const folders = f.folders.filter((d) => {
|
||||||
// só mostra se todos os ancestrais estiverem expandidos
|
// only show if all ancestors are expanded
|
||||||
const parts = d.path.split('/');
|
const parts = d.path.split('/');
|
||||||
for (let k = 1; k < parts.length; k++) if (!E.expanded.has('d' + parts.slice(0, k).join('/')) && !(F.folder || '').startsWith(parts.slice(0, k).join('/'))) return false;
|
for (let k = 1; k < parts.length; k++) if (!E.expanded.has('d' + parts.slice(0, k).join('/')) && !(F.folder || '').startsWith(parts.slice(0, k).join('/'))) return false;
|
||||||
return true;
|
return true;
|
||||||
@@ -528,7 +528,7 @@ export function createExplorer(deps) {
|
|||||||
changed();
|
changed();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------ barra superior
|
// ------------------------------------------------------------ top bar
|
||||||
let qt;
|
let qt;
|
||||||
$('#xpQ').addEventListener('input', (e) => { clearTimeout(qt); qt = setTimeout(() => { E.filters.query = e.target.value.trim(); changed(); }, 250); });
|
$('#xpQ').addEventListener('input', (e) => { clearTimeout(qt); qt = setTimeout(() => { E.filters.query = e.target.value.trim(); changed(); }, 250); });
|
||||||
$('#xpSort').addEventListener('change', (e) => { E.sort = e.target.value; reload(); });
|
$('#xpSort').addEventListener('change', (e) => { E.sort = e.target.value; reload(); });
|
||||||
@@ -550,7 +550,7 @@ export function createExplorer(deps) {
|
|||||||
$('#xpNavBtn').onclick = () => root.classList.toggle('show-nav');
|
$('#xpNavBtn').onclick = () => root.classList.toggle('show-nav');
|
||||||
$('#xpVeil').onclick = () => root.classList.remove('show-nav', 'show-insp');
|
$('#xpVeil').onclick = () => root.classList.remove('show-nav', 'show-insp');
|
||||||
|
|
||||||
// ------------------------------------------------------------ painel de detalhes
|
// ------------------------------------------------------------ inspector panel
|
||||||
let inspT, stopPv = null, metaToken = 0;
|
let inspT, stopPv = null, metaToken = 0;
|
||||||
const schedInspector = () => { clearTimeout(inspT); inspT = setTimeout(paintInspector, 90); };
|
const schedInspector = () => { clearTimeout(inspT); inspT = setTimeout(paintInspector, 90); };
|
||||||
|
|
||||||
@@ -584,7 +584,7 @@ export function createExplorer(deps) {
|
|||||||
<div class="xp-meta" id="xpMeta"></div>
|
<div class="xp-meta" id="xpMeta"></div>
|
||||||
<div class="row"><button class="btn sm" data-i="full">Tela cheia</button><button class="btn primary sm" data-i="rec1">Recuperar este arquivo</button></div>`;
|
<div class="row"><button class="btn sm" data-i="full">Tela cheia</button><button class="btn primary sm" data-i="rec1">Recuperar este arquivo</button></div>`;
|
||||||
stopPv = renderPreview($('#xpPv'), it, { url, thumb: kindOf(it) === 'image', inlineMedia, db: dbApi(it), onOpen: () => openLightbox(idxOf(it.id)), onExternal: () => openExternal(it) });
|
stopPv = renderPreview($('#xpPv'), it, { url, thumb: kindOf(it) === 'image', inlineMedia, db: dbApi(it), onOpen: () => openLightbox(idxOf(it.id)), onExternal: () => openExternal(it) });
|
||||||
// clicar na miniatura abre o modal grande
|
// clicking the thumbnail opens the large modal
|
||||||
$('#xpPv').addEventListener('click', (ev) => {
|
$('#xpPv').addEventListener('click', (ev) => {
|
||||||
if (ev.target.closest('button, a, .pv-pdfbar')) return;
|
if (ev.target.closest('button, a, .pv-pdfbar')) return;
|
||||||
openLightbox(idxOf(it.id));
|
openLightbox(idxOf(it.id));
|
||||||
@@ -599,7 +599,7 @@ export function createExplorer(deps) {
|
|||||||
$('#xpOsm')?.addEventListener('click', () => rpc('open_url', { url: `https://www.openstreetmap.org/?mlat=${m.gps[0]}&mlon=${m.gps[1]}#map=15/${m.gps[0]}/${m.gps[1]}` }).catch(() => {}));
|
$('#xpOsm')?.addEventListener('click', () => rpc('open_url', { url: `https://www.openstreetmap.org/?mlat=${m.gps[0]}&mlon=${m.gps[1]}#map=15/${m.gps[0]}/${m.gps[1]}` }).catch(() => {}));
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
/** Coordenadas e, se o usuário permitiu, um mapa (blocos do OpenStreetMap, 3x3 ao redor do ponto). */
|
/** Coordinates and, if enabled by the user, a map (3x3 OpenStreetMap tiles around the point). */
|
||||||
function gpsHtml([lat, lon]) {
|
function gpsHtml([lat, lon]) {
|
||||||
let map = '';
|
let map = '';
|
||||||
if (deps.getSettings?.().maps) {
|
if (deps.getSettings?.().maps) {
|
||||||
@@ -625,7 +625,7 @@ export function createExplorer(deps) {
|
|||||||
else if (a === 'close') root.classList.remove('show-insp');
|
else if (a === 'close') root.classList.remove('show-insp');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------ tela cheia
|
// ------------------------------------------------------------ fullscreen
|
||||||
let stopLb = null;
|
let stopLb = null;
|
||||||
function openLightbox(i) {
|
function openLightbox(i) {
|
||||||
const it = E.items[i];
|
const it = E.items[i];
|
||||||
@@ -649,14 +649,14 @@ export function createExplorer(deps) {
|
|||||||
if (!d.open) d.showModal();
|
if (!d.open) d.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------ paint geral
|
// ------------------------------------------------------------ main paint
|
||||||
function paintAll() { paintCrumb(); paintChips(); paintActions(); renderWindow(); }
|
function paintAll() { paintCrumb(); paintChips(); paintActions(); renderWindow(); }
|
||||||
|
|
||||||
paintView(); paintCrumb(); paintActions(); paintInspector();
|
paintView(); paintCrumb(); paintActions(); paintInspector();
|
||||||
reloadAll();
|
reloadAll();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/** Chamado a cada atualização de status da varredura. */
|
/** Called on every scan status update. */
|
||||||
onStatus(st) {
|
onStatus(st) {
|
||||||
E.status = st;
|
E.status = st;
|
||||||
const enriching = !!st.enriching;
|
const enriching = !!st.enriching;
|
||||||
@@ -672,7 +672,7 @@ export function createExplorer(deps) {
|
|||||||
},
|
},
|
||||||
query: () => toQuery(),
|
query: () => toQuery(),
|
||||||
destroy() { E.reloadToken++; stopPv?.(); stopLb?.(); host.innerHTML = ''; },
|
destroy() { E.reloadToken++; stopPv?.(); stopLb?.(); host.innerHTML = ''; },
|
||||||
/** Idioma trocado em Configurações: refaz o que foi montado em JS (datas, meses, rótulos). */
|
/** Language changed in Settings: rebuild JS-rendered elements (dates, months, labels). */
|
||||||
relang() { paintCrumb(); paintChips(); paintNav(); paintActions(); paintInspector(); relayout(); },
|
relang() { paintCrumb(); paintChips(); paintNav(); paintActions(); paintInspector(); relayout(); },
|
||||||
focus() { scroll.focus({ preventScroll: true }); },
|
focus() { scroll.focus({ preventScroll: true }); },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" role="img" aria-label="Guará"><style>.a{stroke:#0f766e}.b{stroke:#14b8a6}.t{fill:#1d1d1f}@media (prefers-color-scheme:dark){.a{stroke:#2dd4bf}.b{stroke:#5eead4}.t{fill:#f5f5f7}}</style><defs><clipPath id="g-head"><path d="M50 95 C46 88 43 80 40 74 C33 70 25 64 22 56 L14 6 L36 30 C40 28 45 27 50 27 C55 27 60 28 64 30 L86 6 L78 56 C75 64 67 70 60 74 C57 80 54 88 50 95 Z"/></clipPath></defs><g clip-path="url(#g-head)" fill="none" stroke-width="4.2"><path class="a" d="M50 93A7 7 0 0 1 50 107"/><path class="b" d="M50 93A7 7 0 0 0 50 107" stroke-dasharray="3 3" stroke-dashoffset="0"/><path class="a" d="M50 86A14 14 0 0 1 50 114"/><path class="b" d="M50 86A14 14 0 0 0 50 114" stroke-dasharray="6 4" stroke-dashoffset="3"/><path class="a" d="M50 79A21 21 0 0 1 50 121"/><path class="b" d="M50 79A21 21 0 0 0 50 121" stroke-dasharray="9 5" stroke-dashoffset="6"/><path class="a" d="M50 72A28 28 0 0 1 50 128"/><path class="b" d="M50 72A28 28 0 0 0 50 128" stroke-dasharray="3 6" stroke-dashoffset="9"/><path class="a" d="M50 65A35 35 0 0 1 50 135"/><path class="b" d="M50 65A35 35 0 0 0 50 135" stroke-dasharray="6 3" stroke-dashoffset="12"/><path class="a" d="M50 58A42 42 0 0 1 50 142"/><path class="b" d="M50 58A42 42 0 0 0 50 142" stroke-dasharray="9 4" stroke-dashoffset="15"/><path class="a" d="M50 51A49 49 0 0 1 50 149"/><path class="b" d="M50 51A49 49 0 0 0 50 149" stroke-dasharray="3 5" stroke-dashoffset="18"/><path class="a" d="M50 44A56 56 0 0 1 50 156"/><path class="b" d="M50 44A56 56 0 0 0 50 156" stroke-dasharray="6 6" stroke-dashoffset="21"/><path class="a" d="M50 37A63 63 0 0 1 50 163"/><path class="b" d="M50 37A63 63 0 0 0 50 163" stroke-dasharray="9 3" stroke-dashoffset="24"/><path class="a" d="M50 30A70 70 0 0 1 50 170"/><path class="b" d="M50 30A70 70 0 0 0 50 170" stroke-dasharray="3 4" stroke-dashoffset="27"/><path class="a" d="M50 23A77 77 0 0 1 50 177"/><path class="b" d="M50 23A77 77 0 0 0 50 177" stroke-dasharray="6 5" stroke-dashoffset="30"/><path class="a" d="M50 16A84 84 0 0 1 50 184"/><path class="b" d="M50 16A84 84 0 0 0 50 184" stroke-dasharray="9 6" stroke-dashoffset="33"/><path class="a" d="M50 9A91 91 0 0 1 50 191"/><path class="b" d="M50 9A91 91 0 0 0 50 191" stroke-dasharray="3 3" stroke-dashoffset="36"/><path class="a" d="M50 2A98 98 0 0 1 50 198"/><path class="b" d="M50 2A98 98 0 0 0 50 198" stroke-dasharray="6 4" stroke-dashoffset="39"/><rect class="t" stroke="none" x="0" y="74" width="100" height="30"/></g><path class="t" d="M37 50Q42 47 46 52Q41 53 37 50ZM63 50Q58 47 54 52Q59 53 63 50Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" role="img" aria-label="Guará"><style>.a{stroke:#0f766e}.b{stroke:#14b8a6}.t{fill:#1d1d1f}@media (prefers-color-scheme:dark){.a{stroke:#2dd4bf}.b{stroke:#5eead4}.t{fill:#f5f5f7}}</style><defs><clipPath id="g-head"><path d="M50 95 C46 88 43 80 40 74 C33 70 25 64 22 56 L14 6 L36 30 C40 28 45 27 50 27 C55 27 60 28 64 30 L86 6 L78 56 C75 64 67 70 60 74 C57 80 54 88 50 95 Z"/></clipPath></defs><g clip-path="url(#g-head)" fill="none" stroke-width="4.2"><path class="a" d="M50 93A7 7 0 0 1 50 107"/><path class="b" d="M50 93A7 7 0 0 0 50 107" stroke-dasharray="3 3" stroke-dashoffset="0"/><path class="a" d="M50 86A14 14 0 0 1 50 114"/><path class="b" d="M50 86A14 14 0 0 0 50 114" stroke-dasharray="6 4" stroke-dashoffset="3"/><path class="a" d="M50 79A21 21 0 0 1 50 121"/><path class="b" d="M50 79A21 21 0 0 0 50 121" stroke-dasharray="9 5" stroke-dashoffset="6"/><path class="a" d="M50 72A28 28 0 0 1 50 128"/><path class="b" d="M50 72A28 28 0 0 0 50 128" stroke-dasharray="3 6" stroke-dashoffset="9"/><path class="a" d="M50 65A35 35 0 0 1 50 135"/><path class="b" d="M50 65A35 35 0 0 0 50 135" stroke-dasharray="6 3" stroke-dashoffset="12"/><path class="a" d="M50 58A42 42 0 0 1 50 142"/><path class="b" d="M50 58A42 42 0 0 0 50 142" stroke-dasharray="9 4" stroke-dashoffset="15"/><path class="a" d="M50 51A49 49 0 0 1 50 149"/><path class="b" d="M50 51A49 49 0 0 0 50 149" stroke-dasharray="3 5" stroke-dashoffset="18"/><path class="a" d="M50 44A56 56 0 0 1 50 156"/><path class="b" d="M50 44A56 56 0 0 0 50 156" stroke-dasharray="6 6" stroke-dashoffset="21"/><path class="a" d="M50 37A63 63 0 0 1 50 163"/><path class="b" d="M50 37A63 63 0 0 0 50 163" stroke-dasharray="9 3" stroke-dashoffset="24"/><path class="a" d="M50 30A70 70 0 0 1 50 170"/><path class="b" d="M50 30A70 70 0 0 0 50 170" stroke-dasharray="3 4" stroke-dashoffset="27"/><path class="a" d="M50 23A77 77 0 0 1 50 177"/><path class="b" d="M50 23A77 77 0 0 0 50 177" stroke-dasharray="6 5" stroke-dashoffset="30"/><path class="a" d="M50 16A84 84 0 0 1 50 184"/><path class="b" d="M50 16A84 84 0 0 0 50 184" stroke-dasharray="9 6" stroke-dashoffset="33"/><path class="a" d="M50 9A91 91 0 0 1 50 191"/><path class="b" d="M50 9A91 91 0 0 0 50 191" stroke-dasharray="3 3" stroke-dashoffset="36"/><path class="a" d="M50 2A98 98 0 0 1 50 198"/><path class="b" d="M50 2A98 98 0 0 0 50 198" stroke-dasharray="6 4" stroke-dashoffset="39"/><rect class="t" stroke="none" x="0" y="74" width="100" height="30"/></g><path class="t" d="M37 50Q42 47 46 52Q41 53 37 50ZM63 50Q58 47 54 52Q59 53 63 50Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,6 +1,6 @@
|
|||||||
// Dicionário português -> inglês da interface. {n} marca um valor variável.
|
// Portuguese -> English UI dictionary. {n} marks a variable placeholder.
|
||||||
export const EN = {
|
export const EN = {
|
||||||
// saúde do disco (SMART)
|
// disk health (SMART)
|
||||||
'O SMART não pôde ser lido. Instale o pacote smartmontools (comando smartctl) e rode como administrador para ver a saúde do disco.': 'SMART could not be read. Install the smartmontools package (smartctl command) and run as administrator to see the disk health.',
|
'O SMART não pôde ser lido. Instale o pacote smartmontools (comando smartctl) e rode como administrador para ver a saúde do disco.': 'SMART could not be read. Install the smartmontools package (smartctl command) and run as administrator to see the disk health.',
|
||||||
'Setores realocados': 'Reallocated sectors',
|
'Setores realocados': 'Reallocated sectors',
|
||||||
'Setores pendentes de realocação': 'Pending sectors',
|
'Setores pendentes de realocação': 'Pending sectors',
|
||||||
@@ -38,7 +38,7 @@ export const EN = {
|
|||||||
'O SMART é do disco inteiro: os dados abaixo são de': 'SMART belongs to the whole disk: the data below is for',
|
'O SMART é do disco inteiro: os dados abaixo são de': 'SMART belongs to the whole disk: the data below is for',
|
||||||
', que contém': ', which contains',
|
', que contém': ', which contains',
|
||||||
'Imagem de disco: não há SMART para avaliar.': 'Disk image: there is no SMART to check.',
|
'Imagem de disco: não há SMART para avaliar.': 'Disk image: there is no SMART to check.',
|
||||||
// barra lateral e estrutura
|
// sidebar and layout
|
||||||
'Recuperação de arquivos': 'File recovery',
|
'Recuperação de arquivos': 'File recovery',
|
||||||
'Dispositivos': 'Devices',
|
'Dispositivos': 'Devices',
|
||||||
'Dispositivos e sessões': 'Devices and sessions',
|
'Dispositivos e sessões': 'Devices and sessions',
|
||||||
@@ -64,7 +64,7 @@ export const EN = {
|
|||||||
'· requer admin': '· needs admin',
|
'· requer admin': '· needs admin',
|
||||||
'requer admin': 'needs admin',
|
'requer admin': 'needs admin',
|
||||||
'{n} · requer admin': '{n} · needs admin',
|
'{n} · requer admin': '{n} · needs admin',
|
||||||
// início
|
// home
|
||||||
'Encontre o que você achou que tinha perdido.': 'Find what you thought was lost.',
|
'Encontre o que você achou que tinha perdido.': 'Find what you thought was lost.',
|
||||||
'Escolha um disco, SSD ou pendrive ao lado. O Guará lê tudo: arquivos apagados, partições perdidas e espaço não particionado, sem escrever nada no disco de origem.': 'Pick a disk, SSD or USB drive on the left. Guará reads everything: deleted files, lost partitions and unpartitioned space, without writing anything to the source disk.',
|
'Escolha um disco, SSD ou pendrive ao lado. O Guará lê tudo: arquivos apagados, partições perdidas e espaço não particionado, sem escrever nada no disco de origem.': 'Pick a disk, SSD or USB drive on the left. Guará reads everything: deleted files, lost partitions and unpartitioned space, without writing anything to the source disk.',
|
||||||
'Uma varredura, tudo junto': 'One scan, everything together',
|
'Uma varredura, tudo junto': 'One scan, everything together',
|
||||||
@@ -74,7 +74,7 @@ export const EN = {
|
|||||||
'Origem protegida': 'Protected source',
|
'Origem protegida': 'Protected source',
|
||||||
'Somente leitura. A recuperação sempre vai para outra pasta, com SHA-256 de tudo.': 'Read-only. Recovery always goes to another folder, with SHA-256 of everything.',
|
'Somente leitura. A recuperação sempre vai para outra pasta, com SHA-256 de tudo.': 'Read-only. Recovery always goes to another folder, with SHA-256 of everything.',
|
||||||
'Nenhum disco apareceu.': 'No disks showed up.',
|
'Nenhum disco apareceu.': 'No disks showed up.',
|
||||||
// dispositivo
|
// device
|
||||||
'Mapa do disco': 'Disk map',
|
'Mapa do disco': 'Disk map',
|
||||||
'Como procurar': 'How to search',
|
'Como procurar': 'How to search',
|
||||||
'Recomendado': 'Recommended',
|
'Recomendado': 'Recommended',
|
||||||
@@ -108,7 +108,7 @@ export const EN = {
|
|||||||
'encontrada por varredura': 'found by scanning',
|
'encontrada por varredura': 'found by scanning',
|
||||||
'Desconhecido': 'Unknown',
|
'Desconhecido': 'Unknown',
|
||||||
'Este SSD usa TRIM: o sistema avisa o disco quais blocos foram apagados e o próprio SSD pode zerá-los. Arquivos apagados há bastante tempo tendem a não ser recuperáveis.': 'This SSD uses TRIM: the system tells the disk which blocks were deleted and the SSD itself may zero them. Files deleted a long time ago tend to be unrecoverable.',
|
'Este SSD usa TRIM: o sistema avisa o disco quais blocos foram apagados e o próprio SSD pode zerá-los. Arquivos apagados há bastante tempo tendem a não ser recuperáveis.': 'This SSD uses TRIM: the system tells the disk which blocks were deleted and the SSD itself may zero them. Files deleted a long time ago tend to be unrecoverable.',
|
||||||
// saúde do disco
|
// disk health
|
||||||
'Saúde do disco': 'Disk health',
|
'Saúde do disco': 'Disk health',
|
||||||
'Este disco mostra sinais sérios de defeito.': 'This disk shows serious signs of failure.',
|
'Este disco mostra sinais sérios de defeito.': 'This disk shows serious signs of failure.',
|
||||||
'Este disco mostra sinais de desgaste ou erros.': 'This disk shows signs of wear or errors.',
|
'Este disco mostra sinais de desgaste ou erros.': 'This disk shows signs of wear or errors.',
|
||||||
@@ -119,7 +119,7 @@ export const EN = {
|
|||||||
'Varrer mesmo assim': 'Scan anyway',
|
'Varrer mesmo assim': 'Scan anyway',
|
||||||
'Cancelar': 'Cancel',
|
'Cancelar': 'Cancel',
|
||||||
'Este disco parece estar com defeito': 'This disk seems to be failing',
|
'Este disco parece estar com defeito': 'This disk seems to be failing',
|
||||||
// varredura
|
// scan
|
||||||
'‹ Dispositivos': '‹ Devices',
|
'‹ Dispositivos': '‹ Devices',
|
||||||
'Procurando…': 'Searching…',
|
'Procurando…': 'Searching…',
|
||||||
'Varredura concluída': 'Scan complete',
|
'Varredura concluída': 'Scan complete',
|
||||||
@@ -152,7 +152,7 @@ export const EN = {
|
|||||||
'Lendo tabela de partições': 'Reading partition table',
|
'Lendo tabela de partições': 'Reading partition table',
|
||||||
'Lendo metadados {n} (partição {n})': 'Reading {n} metadata (partition {n})',
|
'Lendo metadados {n} (partição {n})': 'Reading {n} metadata (partition {n})',
|
||||||
'Varredura profunda por assinatura': 'Deep signature scan',
|
'Varredura profunda por assinatura': 'Deep signature scan',
|
||||||
// explorador
|
// explorer
|
||||||
'Navegação': 'Navigation',
|
'Navegação': 'Navigation',
|
||||||
'Filtros': 'Filters',
|
'Filtros': 'Filters',
|
||||||
'Todos os arquivos': 'All files',
|
'Todos os arquivos': 'All files',
|
||||||
@@ -246,7 +246,7 @@ export const EN = {
|
|||||||
'Tem localização GPS': 'Has GPS location',
|
'Tem localização GPS': 'Has GPS location',
|
||||||
'Confiança {n}': 'Confidence {n}',
|
'Confiança {n}': 'Confidence {n}',
|
||||||
'Nada por aqui': 'Nothing here',
|
'Nada por aqui': 'Nothing here',
|
||||||
// pré-visualização
|
// preview
|
||||||
'Imagem': 'Image',
|
'Imagem': 'Image',
|
||||||
'Vídeo': 'Video',
|
'Vídeo': 'Video',
|
||||||
'PDF': 'PDF',
|
'PDF': 'PDF',
|
||||||
@@ -277,7 +277,7 @@ export const EN = {
|
|||||||
'Neste app o vídeo abre no seu player: o navegador embutido do Linux não traz os codecs e travaria a janela.': 'In this app, video opens in your player: the embedded Linux browser does not ship the codecs and would freeze the window.',
|
'Neste app o vídeo abre no seu player: o navegador embutido do Linux não traz os codecs e travaria a janela.': 'In this app, video opens in your player: the embedded Linux browser does not ship the codecs and would freeze the window.',
|
||||||
'Neste app o áudio abre no seu player: o navegador embutido do Linux não traz os codecs e travaria a janela.': 'In this app, audio opens in your player: the embedded Linux browser does not ship the codecs and would freeze the window.',
|
'Neste app o áudio abre no seu player: o navegador embutido do Linux não traz os codecs e travaria a janela.': 'In this app, audio opens in your player: the embedded Linux browser does not ship the codecs and would freeze the window.',
|
||||||
'pasta': 'folder',
|
'pasta': 'folder',
|
||||||
// banco de dados
|
// database
|
||||||
'Abrir o visualizador': 'Open the viewer',
|
'Abrir o visualizador': 'Open the viewer',
|
||||||
'{n} tabela(s) · {n} linhas · páginas de {n}': '{n} table(s) · {n} rows · {n} pages',
|
'{n} tabela(s) · {n} linhas · páginas de {n}': '{n} table(s) · {n} rows · {n} pages',
|
||||||
'{n} linhas · {n} colunas': '{n} rows · {n} columns',
|
'{n} linhas · {n} colunas': '{n} rows · {n} columns',
|
||||||
@@ -297,7 +297,7 @@ export const EN = {
|
|||||||
'{n} a {n} de {n}': '{n} to {n} of {n}',
|
'{n} a {n} de {n}': '{n} to {n} of {n}',
|
||||||
'‹ Anterior': '‹ Previous',
|
'‹ Anterior': '‹ Previous',
|
||||||
'Próximas ›': 'Next ›',
|
'Próximas ›': 'Next ›',
|
||||||
// recuperar
|
// recover
|
||||||
'Recuperar para…': 'Recover to…',
|
'Recuperar para…': 'Recover to…',
|
||||||
'Recuperar': 'Recover',
|
'Recuperar': 'Recover',
|
||||||
'Pasta de destino': 'Destination folder',
|
'Pasta de destino': 'Destination folder',
|
||||||
@@ -336,7 +336,7 @@ export const EN = {
|
|||||||
'{n} arquivo(s) não puderam ser gravados.': '{n} file(s) could not be written.',
|
'{n} arquivo(s) não puderam ser gravados.': '{n} file(s) could not be written.',
|
||||||
'Se a recuperação for interrompida, escolha a mesma pasta de novo: o Guará pula o que já foi conferido.': 'If the recovery is interrupted, pick the same folder again: Guará skips what was already verified.',
|
'Se a recuperação for interrompida, escolha a mesma pasta de novo: o Guará pula o que já foi conferido.': 'If the recovery is interrupted, pick the same folder again: Guará skips what was already verified.',
|
||||||
'{n} já estavam recuperados e conferidos (pulados).': '{n} were already recovered and verified (skipped).',
|
'{n} já estavam recuperados e conferidos (pulados).': '{n} were already recovered and verified (skipped).',
|
||||||
// imagem, hash, busca
|
// image, hash, search
|
||||||
'Criando imagem do disco': 'Creating disk image',
|
'Criando imagem do disco': 'Creating disk image',
|
||||||
'Nenhum setor ruim encontrado.': 'No bad sectors found.',
|
'Nenhum setor ruim encontrado.': 'No bad sectors found.',
|
||||||
'Calculando hashes da origem': 'Computing source hashes',
|
'Calculando hashes da origem': 'Computing source hashes',
|
||||||
@@ -354,7 +354,7 @@ export const EN = {
|
|||||||
'{n} ocorrência{n}': '{n} occurrence{n}',
|
'{n} ocorrência{n}': '{n} occurrence{n}',
|
||||||
'Termos: {n}': 'Terms: {n}',
|
'Termos: {n}': 'Terms: {n}',
|
||||||
'{n}% · {n} até agora': '{n}% · {n} so far',
|
'{n}% · {n} até agora': '{n}% · {n} so far',
|
||||||
// sessões e origem
|
// sessions and source
|
||||||
'Abrir arquivo de imagem': 'Open disk image file',
|
'Abrir arquivo de imagem': 'Open disk image file',
|
||||||
'Caminho completo de uma imagem de disco (.img, .dd, .iso…).': 'Full path of a disk image (.img, .dd, .iso…).',
|
'Caminho completo de uma imagem de disco (.img, .dd, .iso…).': 'Full path of a disk image (.img, .dd, .iso…).',
|
||||||
'Caminho do arquivo de sessão (.json) criado pelo Guará.': 'Path of the session file (.json) created by Guará.',
|
'Caminho do arquivo de sessão (.json) criado pelo Guará.': 'Path of the session file (.json) created by Guará.',
|
||||||
@@ -374,7 +374,7 @@ export const EN = {
|
|||||||
'mesmo disco': 'same disk',
|
'mesmo disco': 'same disk',
|
||||||
'tamanho igual, conteúdo diferente': 'same size, different content',
|
'tamanho igual, conteúdo diferente': 'same size, different content',
|
||||||
'mesmo tamanho (precisa de administrador para conferir)': 'same size (needs administrator to verify)',
|
'mesmo tamanho (precisa de administrador para conferir)': 'same size (needs administrator to verify)',
|
||||||
// configurações e fechamento
|
// settings and shutdown
|
||||||
'Idioma': 'Language',
|
'Idioma': 'Language',
|
||||||
'Notificações': 'Notifications',
|
'Notificações': 'Notifications',
|
||||||
'Avisar quando uma tarefa longa terminar': 'Notify when a long task finishes',
|
'Avisar quando uma tarefa longa terminar': 'Notify when a long task finishes',
|
||||||
@@ -389,14 +389,14 @@ export const EN = {
|
|||||||
'Sair sem pausar': 'Quit without pausing',
|
'Sair sem pausar': 'Quit without pausing',
|
||||||
'Continuar varrendo': 'Keep scanning',
|
'Continuar varrendo': 'Keep scanning',
|
||||||
'A sessão é salva automaticamente. Pausando agora, você retoma exatamente do ponto em que parou.': 'The session is saved automatically. If you pause now, you resume exactly where you stopped.',
|
'A sessão é salva automaticamente. Pausando agora, você retoma exatamente do ponto em que parou.': 'The session is saved automatically. If you pause now, you resume exactly where you stopped.',
|
||||||
// erros comuns vindos do núcleo
|
// common errors from the core
|
||||||
'Não foi possível falar com o núcleo do Guará: {n}. Se abriu esta página manualmente, use o endereço completo exibido pelo comando': 'Could not talk to the Guará core: {n}. If you opened this page manually, use the full address shown by the command',
|
'Não foi possível falar com o núcleo do Guará: {n}. Se abriu esta página manualmente, use o endereço completo exibido pelo comando': 'Could not talk to the Guará core: {n}. If you opened this page manually, use the full address shown by the command',
|
||||||
'Não foi possível reiniciar como administrador': 'Could not restart as administrator',
|
'Não foi possível reiniciar como administrador': 'Could not restart as administrator',
|
||||||
'Autorização cancelada.': 'Authorization canceled.',
|
'Autorização cancelada.': 'Authorization canceled.',
|
||||||
'Origem desconectada': 'Source disconnected',
|
'Origem desconectada': 'Source disconnected',
|
||||||
'Este disco está em uso (montado em': 'This disk is in use (mounted at',
|
'Este disco está em uso (montado em': 'This disk is in use (mounted at',
|
||||||
'). O Guará só lê, mas evite gravar nele enquanto recupera, porque qualquer escrita pode sobrescrever o que você quer salvar.': '). Guará only reads, but avoid writing to it while recovering, because any write can overwrite what you want to save.',
|
'). O Guará só lê, mas evite gravar nele enquanto recupera, porque qualquer escrita pode sobrescrever o que você quer salvar.': '). Guará only reads, but avoid writing to it while recovering, because any write can overwrite what you want to save.',
|
||||||
// formulário de opções de varredura
|
// scan options form
|
||||||
'Volumes cifrados, RAID e vídeos': 'Encrypted volumes, RAID and videos',
|
'Volumes cifrados, RAID e vídeos': 'Encrypted volumes, RAID and videos',
|
||||||
'Senha do volume (BitLocker, LUKS, FileVault 2)': 'Volume password (BitLocker, LUKS, FileVault 2)',
|
'Senha do volume (BitLocker, LUKS, FileVault 2)': 'Volume password (BitLocker, LUKS, FileVault 2)',
|
||||||
'Chave de recuperação do BitLocker': 'BitLocker recovery key',
|
'Chave de recuperação do BitLocker': 'BitLocker recovery key',
|
||||||
@@ -407,7 +407,7 @@ export const EN = {
|
|||||||
'Vídeo íntegro da mesma câmera/drone (refaz o índice de MP4 sem moov)': 'Intact video from the same camera/drone (rebuilds the index of an MP4 without moov)',
|
'Vídeo íntegro da mesma câmera/drone (refaz o índice de MP4 sem moov)': 'Intact video from the same camera/drone (rebuilds the index of an MP4 without moov)',
|
||||||
'Lembrar as chaves dos volumes abertos na sessão': 'Remember the keys of opened volumes in the session',
|
'Lembrar as chaves dos volumes abertos na sessão': 'Remember the keys of opened volumes in the session',
|
||||||
'A senha nunca é gravada; só a chave derivada.': 'The password is never saved; only the derived key.',
|
'A senha nunca é gravada; só a chave derivada.': 'The password is never saved; only the derived key.',
|
||||||
// criar imagem do disco
|
// disk image creation
|
||||||
'Arquivo de destino': 'Destination file',
|
'Arquivo de destino': 'Destination file',
|
||||||
'Número do caso (E01/AFF4)': 'Case number (E01/AFF4)',
|
'Número do caso (E01/AFF4)': 'Case number (E01/AFF4)',
|
||||||
'Formato da imagem': 'Image format',
|
'Formato da imagem': 'Image format',
|
||||||
@@ -416,18 +416,18 @@ export const EN = {
|
|||||||
'Examinador (E01/AFF4)': 'Examiner (E01/AFF4)',
|
'Examinador (E01/AFF4)': 'Examiner (E01/AFF4)',
|
||||||
'Imagem salva em': 'Image saved to',
|
'Imagem salva em': 'Image saved to',
|
||||||
'Hashes do conteúdo inteiro de': 'Hashes of the entire content of',
|
'Hashes do conteúdo inteiro de': 'Hashes of the entire content of',
|
||||||
// resultados e partições
|
// results and partitions
|
||||||
'arquivo(s) salvos em': 'file(s) saved to',
|
'arquivo(s) salvos em': 'file(s) saved to',
|
||||||
'arquivos em {n}': 'files in {n}',
|
'arquivos em {n}': 'files in {n}',
|
||||||
'Espaço não particionado: {n}': 'Unpartitioned space: {n}',
|
'Espaço não particionado: {n}': 'Unpartitioned space: {n}',
|
||||||
'Partição {n}': 'Partition {n}',
|
'Partição {n}': 'Partition {n}',
|
||||||
'erro {n}': 'error {n}',
|
'erro {n}': 'error {n}',
|
||||||
// saúde do disco (campos vindos do núcleo)
|
// disk health (fields from the core)
|
||||||
'Autoavaliação SMART': 'SMART self-assessment',
|
'Autoavaliação SMART': 'SMART self-assessment',
|
||||||
'Vida útil usada': 'Life used',
|
'Vida útil usada': 'Life used',
|
||||||
'aprovado': 'passed',
|
'aprovado': 'passed',
|
||||||
'REPROVADO: o próprio disco prevê falha': 'FAILED: the disk itself predicts failure',
|
'REPROVADO: o próprio disco prevê falha': 'FAILED: the disk itself predicts failure',
|
||||||
// pré-visualização
|
// preview
|
||||||
'Pré-visualização': 'Preview',
|
'Pré-visualização': 'Preview',
|
||||||
'Pré-visualização de {n}': 'Preview of {n}',
|
'Pré-visualização de {n}': 'Preview of {n}',
|
||||||
'vídeo': 'video',
|
'vídeo': 'video',
|
||||||
@@ -438,7 +438,7 @@ export const EN = {
|
|||||||
'diretório do ZIP não encontrado (arquivo possivelmente incompleto)': 'ZIP directory not found (file possibly incomplete)',
|
'diretório do ZIP não encontrado (arquivo possivelmente incompleto)': 'ZIP directory not found (file possibly incomplete)',
|
||||||
'visualizador indisponível': 'viewer unavailable',
|
'visualizador indisponível': 'viewer unavailable',
|
||||||
'© colaboradores do OpenStreetMap': '© OpenStreetMap contributors',
|
'© colaboradores do OpenStreetMap': '© OpenStreetMap contributors',
|
||||||
// explorador: textos que a interface divide em nós separados
|
// explorer: strings split across separate DOM nodes by the UI
|
||||||
'selecionado': 'selected',
|
'selecionado': 'selected',
|
||||||
'selecionados': 'selected',
|
'selecionados': 'selected',
|
||||||
'selecionado (todos desta vista)': 'selected (all in this view)',
|
'selecionado (todos desta vista)': 'selected (all in this view)',
|
||||||
@@ -459,7 +459,7 @@ export const EN = {
|
|||||||
'Conteúdo do arquivo compactado · {n}': 'Archive contents · {n}',
|
'Conteúdo do arquivo compactado · {n}': 'Archive contents · {n}',
|
||||||
'Banco de dados SQLite · {n}': 'SQLite database · {n}',
|
'Banco de dados SQLite · {n}': 'SQLite database · {n}',
|
||||||
'Dados brutos (hexadecimal) · {n}': 'Raw data (hexadecimal) · {n}',
|
'Dados brutos (hexadecimal) · {n}': 'Raw data (hexadecimal) · {n}',
|
||||||
// tipos e campos de metadados vindos do núcleo
|
// metadata types and fields from the core
|
||||||
'Imagem JPEG': 'JPEG image',
|
'Imagem JPEG': 'JPEG image',
|
||||||
'Imagem PNG': 'PNG image',
|
'Imagem PNG': 'PNG image',
|
||||||
'Imagem GIF': 'GIF image',
|
'Imagem GIF': 'GIF image',
|
||||||
@@ -482,7 +482,7 @@ export const EN = {
|
|||||||
'Última modificação por': 'Last modified by',
|
'Última modificação por': 'Last modified by',
|
||||||
'Páginas': 'Pages',
|
'Páginas': 'Pages',
|
||||||
'Duração': 'Duration',
|
'Duração': 'Duration',
|
||||||
// saúde do disco e confiança vindas do núcleo
|
// disk health and confidence levels from the core
|
||||||
'SSD (sem rotação)': 'SSD (no rotation)',
|
'SSD (sem rotação)': 'SSD (no rotation)',
|
||||||
'Cache de gravação': 'Write cache',
|
'Cache de gravação': 'Write cache',
|
||||||
'Segurança ATA': 'ATA security',
|
'Segurança ATA': 'ATA security',
|
||||||
@@ -490,13 +490,13 @@ export const EN = {
|
|||||||
'membro de RAID': 'RAID member',
|
'membro de RAID': 'RAID member',
|
||||||
'sem rodapé/estrutura completa: pode estar truncado': 'no footer/complete structure: may be truncated',
|
'sem rodapé/estrutura completa: pode estar truncado': 'no footer/complete structure: may be truncated',
|
||||||
'vídeo sem índice (moov)': 'video without index (moov)',
|
'vídeo sem índice (moov)': 'video without index (moov)',
|
||||||
// visualizador de SQLite
|
// SQLite viewer
|
||||||
'Nenhuma tabela encontrada no esquema. A primeira página pode estar danificada.': 'No table found in the schema. The first page may be damaged.',
|
'Nenhuma tabela encontrada no esquema. A primeira página pode estar danificada.': 'No table found in the schema. The first page may be damaged.',
|
||||||
'Tabela virtual: o conteúdo não fica armazenado como linhas comuns.': 'Virtual table: the content is not stored as ordinary rows.',
|
'Tabela virtual: o conteúdo não fica armazenado como linhas comuns.': 'Virtual table: the content is not stored as ordinary rows.',
|
||||||
'Tabela WITHOUT ROWID: as linhas ficam numa árvore de índice, ainda não exibida.': 'WITHOUT ROWID table: the rows live in an index tree, not shown yet.',
|
'Tabela WITHOUT ROWID: as linhas ficam numa árvore de índice, ainda não exibida.': 'WITHOUT ROWID table: the rows live in an index tree, not shown yet.',
|
||||||
'Arquivo truncado: o cabeçalho indica {n} páginas, mas só há {n}. Parte dos dados pode estar faltando.': 'Truncated file: the header says {n} pages, but there are only {n}. Part of the data may be missing.',
|
'Arquivo truncado: o cabeçalho indica {n} páginas, mas só há {n}. Parte dos dados pode estar faltando.': 'Truncated file: the header says {n} pages, but there are only {n}. Part of the data may be missing.',
|
||||||
|
|
||||||
// mensagens de erro e avisos do núcleo (valores variáveis usam {n})
|
// core error and warning messages (variable values use {n})
|
||||||
'job {n} não existe': 'job {n} does not exist',
|
'job {n} não existe': 'job {n} does not exist',
|
||||||
'a varredura não está pausada': 'the scan is not paused',
|
'a varredura não está pausada': 'the scan is not paused',
|
||||||
'A origem desta sessão não está disponível ({n}). Reconecte o disco ou escolha a origem para continuar.': 'The source of this session is unavailable ({n}). Reconnect the disk or choose the source to continue.',
|
'A origem desta sessão não está disponível ({n}). Reconecte o disco ou escolha a origem para continuar.': 'The source of this session is unavailable ({n}). Reconnect the disk or choose the source to continue.',
|
||||||
@@ -575,4 +575,19 @@ export const EN = {
|
|||||||
'{n} · {n}{n} · confiança {n} · {n} de {n}': '{n} · {n}{n} · confidence {n} · {n} of {n}',
|
'{n} · {n}{n} · confiança {n} · {n} de {n}': '{n} · {n}{n} · confidence {n} · {n} of {n}',
|
||||||
'e mais {n}…': 'and {n} more…',
|
'e mais {n}…': 'and {n} more…',
|
||||||
'abre no seu player: o navegador embutido do Linux não traz os codecs e travaria a janela.': 'opens in your player: the embedded Linux browser does not include the codecs and would freeze the window.',
|
'abre no seu player: o navegador embutido do Linux não traz os codecs e travaria a janela.': 'opens in your player: the embedded Linux browser does not include the codecs and would freeze the window.',
|
||||||
|
'Nova versão do Guará disponível': 'New Guará version available',
|
||||||
|
'Atualizar agora': 'Update now',
|
||||||
|
'Novidades': 'What\'s new',
|
||||||
|
'Depois': 'Later',
|
||||||
|
'Atualizando o Guará…': 'Updating Guará…',
|
||||||
|
'Baixando a atualização…': 'Downloading the update…',
|
||||||
|
'Baixando… {n}%': 'Downloading… {n}%',
|
||||||
|
'Instalando… o Guará vai reabrir sozinho.': 'Installing… Guará will reopen by itself.',
|
||||||
|
'Esta instalação vem de um pacote do sistema e não se atualiza sozinha. Baixe a versão nova na página da release:': 'This install comes from a system package and cannot update itself. Download the new version from the release page:',
|
||||||
|
'Atualizações automáticas': 'Automatic updates',
|
||||||
|
'Ao abrir, consulta o Gitea para ver se há versão nova (uma consulta, sem enviar dados seus)': 'On startup, checks Gitea for a new version (one request, nothing of yours is sent)',
|
||||||
|
'Verificar agora': 'Check now',
|
||||||
|
'Nova versão {n} disponível': 'New version {n} available',
|
||||||
|
'Você já está na versão mais recente': 'You are already on the latest version',
|
||||||
|
'Não foi possível verificar atualizações: {n}': 'Could not check for updates: {n}',
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Idioma da interface. O texto-fonte é em português; no modo inglês um tradutor
|
// UI language. Source strings are in Portuguese; in English mode a translator
|
||||||
// troca os textos exibidos (nós de texto e atributos) usando um dicionário, e
|
// replaces displayed strings (text nodes and attributes) using a dictionary, and
|
||||||
// continua observando a página para traduzir o que for desenhado depois.
|
// keeps observing the page to translate dynamically rendered elements.
|
||||||
// Textos com valores variáveis usam {n} no dicionário (ex.: "{n} arquivos" -> "{n} files").
|
// Variable placeholders use {n} in the dictionary (e.g. "{n} files").
|
||||||
|
|
||||||
import { EN } from './i18n-en.js';
|
import { EN } from './i18n-en.js';
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export function resolveLang(pref) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ATTRS = ['placeholder', 'title', 'aria-label', 'alt', 'data-empty'];
|
const ATTRS = ['placeholder', 'title', 'aria-label', 'alt', 'data-empty'];
|
||||||
// Dados do usuário (nomes, caminhos, conteúdo) nunca são traduzidos.
|
// User data (names, paths, content) is never translated.
|
||||||
const SKIP = '.xp-cap, .xp-lnm, .db-table, .pv-zip, .hit, pre, code, .mono, .xp-ititle b, dd, textarea, input, script, style';
|
const SKIP = '.xp-cap, .xp-lnm, .db-table, .pv-zip, .hit, pre, code, .mono, .xp-ititle b, dd, textarea, input, script, style';
|
||||||
|
|
||||||
const exact = new Map(Object.entries(EN).filter(([k]) => !k.includes('{n}')));
|
const exact = new Map(Object.entries(EN).filter(([k]) => !k.includes('{n}')));
|
||||||
@@ -26,7 +26,7 @@ const patterns = Object.entries(EN)
|
|||||||
const parts = k.split('{n}').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
const parts = k.split('{n}').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||||
return { re: new RegExp('^' + parts.join('(.+?)') + '$', 's'), to: v };
|
return { re: new RegExp('^' + parts.join('(.+?)') + '$', 's'), to: v };
|
||||||
})
|
})
|
||||||
// padrões mais específicos (mais texto fixo) primeiro
|
// more specific patterns (more fixed text) first
|
||||||
.sort((a, b) => b.re.source.length - a.re.source.length);
|
.sort((a, b) => b.re.source.length - a.re.source.length);
|
||||||
|
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
@@ -43,7 +43,7 @@ export function tr(text) {
|
|||||||
const m = core.match(p.re);
|
const m = core.match(p.re);
|
||||||
if (m) {
|
if (m) {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
// os valores capturados também podem ter partes traduzíveis (ex.: nomes de tipos)
|
// captured values may also contain translatable parts (e.g. type names)
|
||||||
out = p.to.replace(/\{n\}/g, () => tr(m[++i] ?? ''));
|
out = p.to.replace(/\{n\}/g, () => tr(m[++i] ?? ''));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export function tr(text) {
|
|||||||
return lead + out + trail;
|
return lead + out + trail;
|
||||||
}
|
}
|
||||||
|
|
||||||
const orig = new WeakMap(); // nó -> texto original em português
|
const orig = new WeakMap(); // node -> original source text
|
||||||
let observer = null;
|
let observer = null;
|
||||||
|
|
||||||
function translateNode(n) {
|
function translateNode(n) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<title>Guará</title>
|
<title>Guará</title>
|
||||||
<link rel="icon" href="guara.svg" type="image/svg+xml">
|
<link rel="icon" href="logo.png" type="image/png">
|
||||||
<link rel="stylesheet" href="app.css">
|
<link rel="stylesheet" href="app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
<button class="icon-btn" id="menuBtn" aria-label="Abrir menu" aria-expanded="false">
|
<button class="icon-btn" id="menuBtn" aria-label="Abrir menu" aria-expanded="false">
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/></svg>
|
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<svg class="logo" width="24" height="24" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><clipPath id="g-head-top"><path d="M50 95 C46 88 43 80 40 74 C33 70 25 64 22 56 L14 6 L36 30 C40 28 45 27 50 27 C55 27 60 28 64 30 L86 6 L78 56 C75 64 67 70 60 74 C57 80 54 88 50 95 Z"/></clipPath></defs><g clip-path="url(#g-head-top)" fill="none" stroke-width="4.2"><path class="a" d="M50 93A7 7 0 0 1 50 107"/><path class="b" d="M50 93A7 7 0 0 0 50 107" stroke-dasharray="3 3" stroke-dashoffset="0"/><path class="a" d="M50 86A14 14 0 0 1 50 114"/><path class="b" d="M50 86A14 14 0 0 0 50 114" stroke-dasharray="6 4" stroke-dashoffset="3"/><path class="a" d="M50 79A21 21 0 0 1 50 121"/><path class="b" d="M50 79A21 21 0 0 0 50 121" stroke-dasharray="9 5" stroke-dashoffset="6"/><path class="a" d="M50 72A28 28 0 0 1 50 128"/><path class="b" d="M50 72A28 28 0 0 0 50 128" stroke-dasharray="3 6" stroke-dashoffset="9"/><path class="a" d="M50 65A35 35 0 0 1 50 135"/><path class="b" d="M50 65A35 35 0 0 0 50 135" stroke-dasharray="6 3" stroke-dashoffset="12"/><path class="a" d="M50 58A42 42 0 0 1 50 142"/><path class="b" d="M50 58A42 42 0 0 0 50 142" stroke-dasharray="9 4" stroke-dashoffset="15"/><path class="a" d="M50 51A49 49 0 0 1 50 149"/><path class="b" d="M50 51A49 49 0 0 0 50 149" stroke-dasharray="3 5" stroke-dashoffset="18"/><path class="a" d="M50 44A56 56 0 0 1 50 156"/><path class="b" d="M50 44A56 56 0 0 0 50 156" stroke-dasharray="6 6" stroke-dashoffset="21"/><path class="a" d="M50 37A63 63 0 0 1 50 163"/><path class="b" d="M50 37A63 63 0 0 0 50 163" stroke-dasharray="9 3" stroke-dashoffset="24"/><path class="a" d="M50 30A70 70 0 0 1 50 170"/><path class="b" d="M50 30A70 70 0 0 0 50 170" stroke-dasharray="3 4" stroke-dashoffset="27"/><path class="a" d="M50 23A77 77 0 0 1 50 177"/><path class="b" d="M50 23A77 77 0 0 0 50 177" stroke-dasharray="6 5" stroke-dashoffset="30"/><path class="a" d="M50 16A84 84 0 0 1 50 184"/><path class="b" d="M50 16A84 84 0 0 0 50 184" stroke-dasharray="9 6" stroke-dashoffset="33"/><path class="a" d="M50 9A91 91 0 0 1 50 191"/><path class="b" d="M50 9A91 91 0 0 0 50 191" stroke-dasharray="3 3" stroke-dashoffset="36"/><path class="a" d="M50 2A98 98 0 0 1 50 198"/><path class="b" d="M50 2A98 98 0 0 0 50 198" stroke-dasharray="6 4" stroke-dashoffset="39"/><rect class="t" stroke="none" x="0" y="74" width="100" height="30"/></g><path class="t" d="M37 50Q42 47 46 52Q41 53 37 50ZM63 50Q58 47 54 52Q59 53 63 50Z"/></svg><strong>Guará</strong>
|
<img class="logo-img" src="logo.png" width="24" height="24" alt=""><strong>Guará</strong>
|
||||||
</header>
|
</header>
|
||||||
<aside class="sidebar" id="sidebar" aria-label="Dispositivos e sessões">
|
<aside class="sidebar" id="sidebar" aria-label="Dispositivos e sessões">
|
||||||
<div class="brand"><svg class="logo" width="30" height="30" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><clipPath id="g-head-side"><path d="M50 95 C46 88 43 80 40 74 C33 70 25 64 22 56 L14 6 L36 30 C40 28 45 27 50 27 C55 27 60 28 64 30 L86 6 L78 56 C75 64 67 70 60 74 C57 80 54 88 50 95 Z"/></clipPath></defs><g clip-path="url(#g-head-side)" fill="none" stroke-width="4.2"><path class="a" d="M50 93A7 7 0 0 1 50 107"/><path class="b" d="M50 93A7 7 0 0 0 50 107" stroke-dasharray="3 3" stroke-dashoffset="0"/><path class="a" d="M50 86A14 14 0 0 1 50 114"/><path class="b" d="M50 86A14 14 0 0 0 50 114" stroke-dasharray="6 4" stroke-dashoffset="3"/><path class="a" d="M50 79A21 21 0 0 1 50 121"/><path class="b" d="M50 79A21 21 0 0 0 50 121" stroke-dasharray="9 5" stroke-dashoffset="6"/><path class="a" d="M50 72A28 28 0 0 1 50 128"/><path class="b" d="M50 72A28 28 0 0 0 50 128" stroke-dasharray="3 6" stroke-dashoffset="9"/><path class="a" d="M50 65A35 35 0 0 1 50 135"/><path class="b" d="M50 65A35 35 0 0 0 50 135" stroke-dasharray="6 3" stroke-dashoffset="12"/><path class="a" d="M50 58A42 42 0 0 1 50 142"/><path class="b" d="M50 58A42 42 0 0 0 50 142" stroke-dasharray="9 4" stroke-dashoffset="15"/><path class="a" d="M50 51A49 49 0 0 1 50 149"/><path class="b" d="M50 51A49 49 0 0 0 50 149" stroke-dasharray="3 5" stroke-dashoffset="18"/><path class="a" d="M50 44A56 56 0 0 1 50 156"/><path class="b" d="M50 44A56 56 0 0 0 50 156" stroke-dasharray="6 6" stroke-dashoffset="21"/><path class="a" d="M50 37A63 63 0 0 1 50 163"/><path class="b" d="M50 37A63 63 0 0 0 50 163" stroke-dasharray="9 3" stroke-dashoffset="24"/><path class="a" d="M50 30A70 70 0 0 1 50 170"/><path class="b" d="M50 30A70 70 0 0 0 50 170" stroke-dasharray="3 4" stroke-dashoffset="27"/><path class="a" d="M50 23A77 77 0 0 1 50 177"/><path class="b" d="M50 23A77 77 0 0 0 50 177" stroke-dasharray="6 5" stroke-dashoffset="30"/><path class="a" d="M50 16A84 84 0 0 1 50 184"/><path class="b" d="M50 16A84 84 0 0 0 50 184" stroke-dasharray="9 6" stroke-dashoffset="33"/><path class="a" d="M50 9A91 91 0 0 1 50 191"/><path class="b" d="M50 9A91 91 0 0 0 50 191" stroke-dasharray="3 3" stroke-dashoffset="36"/><path class="a" d="M50 2A98 98 0 0 1 50 198"/><path class="b" d="M50 2A98 98 0 0 0 50 198" stroke-dasharray="6 4" stroke-dashoffset="39"/><rect class="t" stroke="none" x="0" y="74" width="100" height="30"/></g><path class="t" d="M37 50Q42 47 46 52Q41 53 37 50ZM63 50Q58 47 54 52Q59 53 63 50Z"/></svg><div><strong>Guará</strong><small>Recuperação de arquivos</small></div></div>
|
<div class="brand"><img class="logo-img" src="logo.png" width="30" height="30" alt=""><div><strong>Guará</strong><small>Recuperação de arquivos</small></div></div>
|
||||||
<nav>
|
<nav>
|
||||||
<div class="side-title">Dispositivos <button class="icon-btn small" id="refreshBtn" title="Atualizar lista" aria-label="Atualizar lista"><svg viewBox="0 0 24 24" width="16" height="16"><path d="M20 12a8 8 0 1 1-2.5-5.8M20 4v5h-5" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg></button></div>
|
<div class="side-title">Dispositivos <button class="icon-btn small" id="refreshBtn" title="Atualizar lista" aria-label="Atualizar lista"><svg viewBox="0 0 24 24" width="16" height="16"><path d="M20 12a8 8 0 1 1-2.5-5.8M20 4v5h-5" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg></button></div>
|
||||||
<ul id="deviceList" class="list"></ul>
|
<ul id="deviceList" class="list"></ul>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { LOC } from './i18n.js';
|
import { LOC } from './i18n.js';
|
||||||
// Pré-visualização de arquivos recuperados.
|
// Preview of recovered files.
|
||||||
// O conteúdo é lido em streaming do núcleo (com Range) pela URL `url`; nada é
|
// Content is streamed from the core (with Range support) via `url`; nothing is
|
||||||
// gravado em disco. Tipos: imagem, vídeo, áudio, PDF (pdf.js), texto, documentos
|
// written to disk. Supported types: image, video, audio, PDF (pdf.js), text,
|
||||||
// Office/OpenDocument (texto extraído), listagem de ZIP e visão hexadecimal.
|
// Office/OpenDocument documents (extracted text), ZIP listing, and hex view.
|
||||||
|
|
||||||
const IMG = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'avif', 'ico']);
|
const IMG = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'avif', 'ico']);
|
||||||
const VID = new Set(['mp4', 'm4v', 'mov', 'webm', 'mkv', 'ogv']);
|
const VID = new Set(['mp4', 'm4v', 'mov', 'webm', 'mkv', 'ogv']);
|
||||||
@@ -44,7 +44,7 @@ async function fetchRange(url, a, b) {
|
|||||||
return new Uint8Array(await r.arrayBuffer());
|
return new Uint8Array(await r.arrayBuffer());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ ZIP mínimo
|
// ------------------------------------------------------------------ minimal ZIP reader
|
||||||
async function zipDir(url, size) {
|
async function zipDir(url, size) {
|
||||||
const tailLen = Math.min(size, 66000);
|
const tailLen = Math.min(size, 66000);
|
||||||
const tail = await fetchRange(url, size - tailLen, size - 1);
|
const tail = await fetchRange(url, size - tailLen, size - 1);
|
||||||
@@ -86,7 +86,7 @@ async function zipRead(url, e, cap = 8 << 20) {
|
|||||||
|
|
||||||
const xml = (bytes) => new DOMParser().parseFromString(new TextDecoder().decode(bytes), 'application/xml');
|
const xml = (bytes) => new DOMParser().parseFromString(new TextDecoder().decode(bytes), 'application/xml');
|
||||||
|
|
||||||
// ------------------------------------------------------------------ renderizadores
|
// ------------------------------------------------------------------ renderers
|
||||||
function note(host, html, cls = '') {
|
function note(host, html, cls = '') {
|
||||||
host.insertAdjacentHTML('beforeend', `<div class="pv-note ${cls}">${html}</div>`);
|
host.insertAdjacentHTML('beforeend', `<div class="pv-note ${cls}">${html}</div>`);
|
||||||
}
|
}
|
||||||
@@ -248,7 +248,7 @@ async function office(host, item, url, state) {
|
|||||||
const rows = [...d.getElementsByTagName('table:table-row')].slice(0, 80);
|
const rows = [...d.getElementsByTagName('table:table-row')].slice(0, 80);
|
||||||
html = `<div class="pv-sheet"><table><tbody>${rows.map((r) => `<tr>${[...r.getElementsByTagName('table:table-cell')].slice(0, 16).map((c) => `<td>${esc(c.textContent)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`;
|
html = `<div class="pv-sheet"><table><tbody>${rows.map((r) => `<tr>${[...r.getElementsByTagName('table:table-cell')].slice(0, 16).map((c) => `<td>${esc(c.textContent)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`;
|
||||||
} else {
|
} else {
|
||||||
// pptx / odp: texto por slide
|
// pptx / odp: per-slide text
|
||||||
let slides;
|
let slides;
|
||||||
if (ext === 'pptx') {
|
if (ext === 'pptx') {
|
||||||
const files = ents.filter((e) => /^ppt\/slides\/slide\d+\.xml$/.test(e.name)).sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true })).slice(0, 60);
|
const files = ents.filter((e) => /^ppt\/slides\/slide\d+\.xml$/.test(e.name)).sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true })).slice(0, 60);
|
||||||
@@ -275,9 +275,9 @@ async function zip(host, item, url, state) {
|
|||||||
note(host, `${ents.length} item(ns) no arquivo compactado.`);
|
note(host, `${ents.length} item(ns) no arquivo compactado.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ bancos de dados (SQLite)
|
// ------------------------------------------------------------------ databases (SQLite)
|
||||||
|
|
||||||
/** Valores inteiros que parecem datas (Unix, Chrome/WebKit, macOS) viram uma dica legível. */
|
/** Integer values that look like timestamps (Unix, Chrome/WebKit, macOS) become a readable hint. */
|
||||||
function tsHint(v) {
|
function tsHint(v) {
|
||||||
if (!Number.isInteger(v)) return null;
|
if (!Number.isInteger(v)) return null;
|
||||||
let ms = null, kind = '';
|
let ms = null, kind = '';
|
||||||
@@ -361,7 +361,7 @@ async function database(host, item, state, large, db, onOpen) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ API
|
// ------------------------------------------------------------------ API
|
||||||
/** Desenha a pré-visualização de `item` dentro de `host`. Devolve uma função de limpeza. */
|
/** Renders the preview of `item` inside `host`. Returns a cleanup function. */
|
||||||
function mediaCard(host, item, kind, { large, inlineMedia, onOpen, onExternal }) {
|
function mediaCard(host, item, kind, { large, inlineMedia, onOpen, onExternal }) {
|
||||||
const ico = kind === 'video' ? '🎞' : '🎵';
|
const ico = kind === 'video' ? '🎞' : '🎵';
|
||||||
const canInline = inlineMedia && large;
|
const canInline = inlineMedia && large;
|
||||||
@@ -385,8 +385,8 @@ export function renderPreview(host, item, { url, large = false, thumb = false, i
|
|||||||
host.classList.remove('zoomable', 'actual');
|
host.classList.remove('zoomable', 'actual');
|
||||||
host.innerHTML = '<div class="pv-loading"><i></i></div>';
|
host.innerHTML = '<div class="pv-loading"><i></i></div>';
|
||||||
const kind = kindOf(item);
|
const kind = kindOf(item);
|
||||||
// Vídeo e áudio: nunca criamos <video>/<audio> no painel pequeno, e no Linux do app desktop
|
// Video and audio: never instantiate <video>/<audio> in the small panel, nor in the large view
|
||||||
// (WebKitGTK sem plugins de mídia) nem na tela grande, pois o processo da página morre.
|
// on Linux desktop (WebKitGTK without media plugins), as the webview process would crash.
|
||||||
if ((kind === 'video' || kind === 'audio') && (!large || !inlineMedia)) {
|
if ((kind === 'video' || kind === 'audio') && (!large || !inlineMedia)) {
|
||||||
mediaCard(host, item, kind, { large, inlineMedia, onOpen, onExternal });
|
mediaCard(host, item, kind, { large, inlineMedia, onOpen, onExternal });
|
||||||
return () => { state.dead = true; };
|
return () => { state.dead = true; };
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Bibliotecas de terceiros
|
# Third-party libraries
|
||||||
|
|
||||||
- **pdf.js** 4.10.38 (build "legacy"), © Mozilla, licença Apache-2.0 (`PDFJS-LICENSE`). Usada só para pré-visualizar PDFs, localmente e sem internet. Origem: https://github.com/mozilla/pdf.js
|
- **pdf.js** 4.10.38 ("legacy" build), © Mozilla, Apache-2.0 license (`PDFJS-LICENSE`). Used exclusively for local offline PDF preview. Source: https://github.com/mozilla/pdf.js
|
||||||