Remove unused assets, refactor migration handling, and add crash reporting
- Deleted obsolete icons, layout files, and SVG assets. - Added `useAutoUpdate` hook for streamlined update logic. - Introduced `auto_migrate_electron` and migration commands for Electron-to-Tauri data migration. - Implemented `init_panic_hook` for improved crash reporting. - Updated `.gitignore` for Tauri build output and IDE-specific files.
18
.gitignore
vendored
@@ -6,10 +6,6 @@ node_modules
|
|||||||
# Testing
|
# Testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# Next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# Production
|
# Production
|
||||||
/dist
|
/dist
|
||||||
/build
|
/build
|
||||||
@@ -32,7 +28,17 @@ yarn-error.log*
|
|||||||
|
|
||||||
# TypeScript
|
# TypeScript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
.next/
|
||||||
|
/out/
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
# Electron
|
# Electron build output
|
||||||
/release
|
/release/
|
||||||
|
|
||||||
|
# Tauri build output
|
||||||
|
src-tauri/target/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import type {Metadata} from "next";
|
|
||||||
import "./globals.css";
|
|
||||||
import {ReactNode} from "react";
|
|
||||||
import ScribeShell from "@/components/layout/ScribeShell";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "ERitors Scribe",
|
|
||||||
description: "Les meilleurs livres sont ceux qui ont le meilleur plan.",
|
|
||||||
icons: {
|
|
||||||
icon: "/eritors-favicon-white.png"
|
|
||||||
},
|
|
||||||
robots: {
|
|
||||||
index: false,
|
|
||||||
follow: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout(
|
|
||||||
{
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: ReactNode;
|
|
||||||
}>) {
|
|
||||||
return (
|
|
||||||
<html lang="fr">
|
|
||||||
<body>
|
|
||||||
<ScribeShell>{children}</ScribeShell>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import * as tauri from '@/lib/tauri';
|
|||||||
import PulseLoader from '@/components/ui/PulseLoader';
|
import PulseLoader from '@/components/ui/PulseLoader';
|
||||||
import {useTranslations} from '@/lib/i18n';
|
import {useTranslations} from '@/lib/i18n';
|
||||||
|
|
||||||
listen('auth-success', () => window.location.reload());
|
listen('auth-success', () => window.location.reload()).then();
|
||||||
|
|
||||||
type MigrationState = 'pending' | 'error' | 'done';
|
type MigrationState = 'pending' | 'error' | 'done';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import BookList from '@/components/book/BookList';
|
|
||||||
|
|
||||||
export default function HomePage() {
|
|
||||||
return <BookList/>;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.cs.debugger</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
BIN
build/icon.png
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 507 B |
|
Before Width: | Height: | Size: 838 B |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 353 KiB |
67
hooks/useAutoUpdate.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import {check, Update} from '@tauri-apps/plugin-updater';
|
||||||
|
import {relaunch} from '@tauri-apps/plugin-process';
|
||||||
|
|
||||||
|
export interface UpdateState {
|
||||||
|
available: boolean;
|
||||||
|
version: string;
|
||||||
|
notes: string;
|
||||||
|
downloading: boolean;
|
||||||
|
progress: number;
|
||||||
|
install: () => Promise<void>;
|
||||||
|
dismiss: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAutoUpdate(): UpdateState {
|
||||||
|
const [available, setAvailable] = useState(false);
|
||||||
|
const [version, setVersion] = useState('');
|
||||||
|
const [notes, setNotes] = useState('');
|
||||||
|
const [downloading, setDownloading] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [update, setUpdate] = useState<Update | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function checkForUpdate() {
|
||||||
|
try {
|
||||||
|
const result = await check();
|
||||||
|
if (cancelled || !result) return;
|
||||||
|
setUpdate(result);
|
||||||
|
setAvailable(true);
|
||||||
|
setVersion(result.version);
|
||||||
|
setNotes(result.body ?? '');
|
||||||
|
} catch (_) {
|
||||||
|
// silently fail — no update or offline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForUpdate();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function install() {
|
||||||
|
if (!update) return;
|
||||||
|
setDownloading(true);
|
||||||
|
try {
|
||||||
|
await update.downloadAndInstall((event) => {
|
||||||
|
if (event.event === 'Started' && event.data.contentLength) {
|
||||||
|
setProgress(0);
|
||||||
|
} else if (event.event === 'Progress') {
|
||||||
|
setProgress((prev) => prev + event.data.chunkLength);
|
||||||
|
} else if (event.event === 'Finished') {
|
||||||
|
setProgress(100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await relaunch();
|
||||||
|
} catch (_) {
|
||||||
|
setDownloading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
setAvailable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {available, version, notes, downloading, progress, install, dismiss};
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 391 B |
@@ -1 +0,0 @@
|
|||||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000">
|
|
||||||
<path d="m577.3 0 577.4 1000H0z" fill="#fff"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 138 B |
@@ -1 +0,0 @@
|
|||||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 385 B |
378
src-tauri/src/domains/migration/commands.rs
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::crypto::key_manager;
|
||||||
|
use crate::db::connection::DbManager;
|
||||||
|
use crate::db::schema;
|
||||||
|
use crate::error::{AppError, AppResult};
|
||||||
|
use crate::shared::session::SessionState;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ImportMigrationData {
|
||||||
|
pub migration_file_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MigrationResult {
|
||||||
|
pub success: bool,
|
||||||
|
pub user_id: Option<String>,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MigrationCheckResult {
|
||||||
|
pub found: bool,
|
||||||
|
pub user_id: Option<String>,
|
||||||
|
pub has_db: bool,
|
||||||
|
pub migration_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AutoMigrationResult {
|
||||||
|
pub migrated: bool,
|
||||||
|
pub user_id: Option<String>,
|
||||||
|
pub token_migrated: bool,
|
||||||
|
pub key_migrated: bool,
|
||||||
|
pub pin_migrated: bool,
|
||||||
|
pub db_migrated: bool,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ElectronMigrationFile {
|
||||||
|
version: u32,
|
||||||
|
user_id: String,
|
||||||
|
encryption_key: String,
|
||||||
|
pin_hash: Option<String>,
|
||||||
|
db_source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Electron safeStorage vault format ───────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
struct ElectronVault {
|
||||||
|
#[serde(default)]
|
||||||
|
token: Option<String>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
extra: HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Auto-migration from Electron ────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn electron_data_paths() -> Vec<PathBuf> {
|
||||||
|
let home = dirs_next::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let app_support = home.join("Library").join("Application Support");
|
||||||
|
paths.push(app_support.join("ERitors Scribe"));
|
||||||
|
paths.push(app_support.join("Electron"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(appdata) = dirs_next::data_dir() {
|
||||||
|
paths.push(appdata.join("ERitors Scribe"));
|
||||||
|
paths.push(appdata.join("Electron"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_electron_vault(data_path: &PathBuf) -> Option<PathBuf> {
|
||||||
|
let candidate = data_path.join("secure-config.json");
|
||||||
|
if candidate.exists() { return Some(candidate); }
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_electron_dbs(data_path: &PathBuf) -> Vec<(String, PathBuf)> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
if let Ok(entries) = std::fs::read_dir(data_path) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if name.starts_with("eritors-local-") && name.ends_with(".db") {
|
||||||
|
let user_id = name
|
||||||
|
.trim_start_matches("eritors-local-")
|
||||||
|
.trim_end_matches(".db")
|
||||||
|
.to_string();
|
||||||
|
result.push((user_id, entry.path()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn decrypt_electron_value(encrypted_b64: &str) -> Option<String> {
|
||||||
|
use aes::Aes128;
|
||||||
|
use cbc::cipher::{block_padding::Pkcs7, BlockDecryptMut as _, KeyIvInit};
|
||||||
|
use pbkdf2::pbkdf2_hmac;
|
||||||
|
use sha1::Sha1;
|
||||||
|
|
||||||
|
type Aes128CbcDec = cbc::Decryptor<Aes128>;
|
||||||
|
|
||||||
|
let b64 = if encrypted_b64.starts_with("encrypted:") {
|
||||||
|
&encrypted_b64[10..]
|
||||||
|
} else {
|
||||||
|
encrypted_b64
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = base64::Engine::decode(
|
||||||
|
&base64::engine::general_purpose::STANDARD,
|
||||||
|
b64,
|
||||||
|
).ok()?;
|
||||||
|
|
||||||
|
if !data.starts_with(b"v10") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ciphertext = &data[3..];
|
||||||
|
|
||||||
|
let app_names = ["ERitors Scribe", "Electron"];
|
||||||
|
for app_name in &app_names {
|
||||||
|
let service = format!("{} Safe Storage", app_name);
|
||||||
|
if let Ok(entry) = keyring::Entry::new(&service, app_name) {
|
||||||
|
if let Ok(password) = entry.get_password() {
|
||||||
|
let mut key = [0u8; 16];
|
||||||
|
pbkdf2_hmac::<Sha1>(
|
||||||
|
password.as_bytes(),
|
||||||
|
b"saltysalt",
|
||||||
|
1003,
|
||||||
|
&mut key,
|
||||||
|
);
|
||||||
|
let iv = [0x20u8; 16];
|
||||||
|
let mut buf = ciphertext.to_vec();
|
||||||
|
if let Ok(decryptor) = Aes128CbcDec::new_from_slices(&key, &iv) {
|
||||||
|
if let Ok(decrypted) = decryptor.decrypt_padded_mut::<Pkcs7>(&mut buf) {
|
||||||
|
if let Ok(s) = String::from_utf8(decrypted.to_vec()) {
|
||||||
|
return Some(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
fn decrypt_electron_value(_encrypted_b64: &str) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_electron_vault(vault_path: &PathBuf) -> HashMap<String, String> {
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
let Ok(content) = std::fs::read_to_string(vault_path) else { return result; };
|
||||||
|
let Ok(parsed) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&content) else { return result; };
|
||||||
|
|
||||||
|
for (key, value) in &parsed {
|
||||||
|
if let Some(s) = value.as_str() {
|
||||||
|
if let Some(decrypted) = decrypt_electron_value(s) {
|
||||||
|
result.insert(key.clone(), decrypted);
|
||||||
|
} else if !s.starts_with("encrypted:") {
|
||||||
|
result.insert(key.clone(), s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called automatically at startup in production — detects and migrates Electron data.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn auto_migrate_electron(
|
||||||
|
db: State<DbManager>,
|
||||||
|
session: State<SessionState>,
|
||||||
|
) -> AppResult<AutoMigrationResult> {
|
||||||
|
let already_migrated = key_manager::get_last_user_id().ok().flatten();
|
||||||
|
if already_migrated.is_some() {
|
||||||
|
return Ok(AutoMigrationResult {
|
||||||
|
migrated: false, user_id: None,
|
||||||
|
token_migrated: false, key_migrated: false,
|
||||||
|
pin_migrated: false, db_migrated: false,
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for data_path in electron_data_paths() {
|
||||||
|
if !data_path.exists() { continue; }
|
||||||
|
|
||||||
|
let dbs = find_electron_dbs(&data_path);
|
||||||
|
if dbs.is_empty() { continue; }
|
||||||
|
|
||||||
|
let vault_data = if let Some(vault_path) = find_electron_vault(&data_path) {
|
||||||
|
read_electron_vault(&vault_path)
|
||||||
|
} else {
|
||||||
|
HashMap::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let token = vault_data.get("token").cloned();
|
||||||
|
let last_user_id = vault_data.get("lastUserId").cloned()
|
||||||
|
.or_else(|| dbs.first().map(|(uid, _)| uid.clone()));
|
||||||
|
|
||||||
|
let Some(user_id) = last_user_id else { continue; };
|
||||||
|
|
||||||
|
let encryption_key = vault_data.get(&format!("encryptionKey-{}", user_id)).cloned();
|
||||||
|
let pin_hash = vault_data.get(&format!("pinHash-{}", user_id)).cloned();
|
||||||
|
|
||||||
|
// Copy DB files
|
||||||
|
let mut db_migrated = false;
|
||||||
|
for (uid, db_path) in &dbs {
|
||||||
|
if uid != &user_id { continue; }
|
||||||
|
{
|
||||||
|
let mut db_manager = db.lock()
|
||||||
|
.map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
|
||||||
|
db_manager.initialize(uid)?;
|
||||||
|
let dest = db_manager.get_db_path(uid);
|
||||||
|
if let Some(parent) = dest.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
if std::fs::copy(db_path, &dest).is_ok() {
|
||||||
|
for ext in &["db-wal", "db-shm"] {
|
||||||
|
let src_extra = db_path.with_extension(ext);
|
||||||
|
if src_extra.exists() {
|
||||||
|
let dst_extra = dest.with_extension(ext);
|
||||||
|
let _ = std::fs::copy(&src_extra, &dst_extra);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db_migrated = true;
|
||||||
|
}
|
||||||
|
let conn = db_manager.get_connection(uid)?;
|
||||||
|
schema::initialize_schema(conn)?;
|
||||||
|
if !cfg!(debug_assertions) { schema::run_migrations(conn)?; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store secrets
|
||||||
|
let token_migrated = if let Some(ref t) = token {
|
||||||
|
key_manager::set_token(t).is_ok()
|
||||||
|
} else { false };
|
||||||
|
|
||||||
|
let key_migrated = if let Some(ref k) = encryption_key {
|
||||||
|
key_manager::set_user_encryption_key(&user_id, k).is_ok()
|
||||||
|
} else { false };
|
||||||
|
|
||||||
|
let pin_migrated = if let Some(ref p) = pin_hash {
|
||||||
|
key_manager::set_pin_hash(&user_id, p).is_ok()
|
||||||
|
} else { false };
|
||||||
|
|
||||||
|
key_manager::set_last_user_id(&user_id)?;
|
||||||
|
|
||||||
|
let mut session_guard = session.lock()
|
||||||
|
.map_err(|e| AppError::Internal(format!("Session lock: {}", e)))?;
|
||||||
|
session_guard.user_id = Some(user_id.clone());
|
||||||
|
|
||||||
|
return Ok(AutoMigrationResult {
|
||||||
|
migrated: true,
|
||||||
|
user_id: Some(user_id),
|
||||||
|
token_migrated,
|
||||||
|
key_migrated,
|
||||||
|
pin_migrated,
|
||||||
|
db_migrated,
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AutoMigrationResult {
|
||||||
|
migrated: false, user_id: None,
|
||||||
|
token_migrated: false, key_migrated: false,
|
||||||
|
pin_migrated: false, db_migrated: false,
|
||||||
|
error: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an Electron migration file + DB exist at the given path (legacy).
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn check_electron_migration(migration_file_path: String) -> AppResult<MigrationCheckResult> {
|
||||||
|
let path = PathBuf::from(&migration_file_path);
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(MigrationCheckResult { found: false, user_id: None, has_db: false, migration_path: None });
|
||||||
|
}
|
||||||
|
let content = std::fs::read_to_string(&path).map_err(AppError::Io)?;
|
||||||
|
let migration: ElectronMigrationFile = serde_json::from_str(&content)?;
|
||||||
|
let db_name = format!("eritors-local-{}.db", migration.user_id);
|
||||||
|
let db_path = path.parent().unwrap_or(&path).join(&db_name);
|
||||||
|
Ok(MigrationCheckResult {
|
||||||
|
found: true,
|
||||||
|
user_id: Some(migration.user_id),
|
||||||
|
has_db: db_path.exists(),
|
||||||
|
migration_path: Some(migration_file_path),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import from Electron export file (legacy).
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn import_from_electron(
|
||||||
|
data: ImportMigrationData,
|
||||||
|
db: State<DbManager>,
|
||||||
|
session: State<SessionState>,
|
||||||
|
) -> AppResult<MigrationResult> {
|
||||||
|
let migration_path = PathBuf::from(&data.migration_file_path);
|
||||||
|
if !migration_path.exists() {
|
||||||
|
return Ok(MigrationResult { success: false, user_id: None, error: Some("Migration file not found".into()) });
|
||||||
|
}
|
||||||
|
let content = std::fs::read_to_string(&migration_path)?;
|
||||||
|
let migration: ElectronMigrationFile = serde_json::from_str(&content)?;
|
||||||
|
if migration.version != 1 {
|
||||||
|
return Ok(MigrationResult { success: false, user_id: None, error: Some(format!("Unsupported version: {}", migration.version)) });
|
||||||
|
}
|
||||||
|
let source_db = migration_path.parent()
|
||||||
|
.unwrap_or_else(|| std::path::Path::new("."))
|
||||||
|
.join(format!("eritors-local-{}.db", migration.user_id));
|
||||||
|
if !source_db.exists() {
|
||||||
|
let electron_db = PathBuf::from(&migration.db_source);
|
||||||
|
if !electron_db.exists() {
|
||||||
|
return Ok(MigrationResult { success: false, user_id: None, error: Some("Database file not found.".into()) });
|
||||||
|
}
|
||||||
|
copy_db_files(&electron_db, &migration.user_id, &db)?;
|
||||||
|
} else {
|
||||||
|
copy_db_files(&source_db, &migration.user_id, &db)?;
|
||||||
|
}
|
||||||
|
key_manager::set_user_encryption_key(&migration.user_id, &migration.encryption_key)?;
|
||||||
|
if let Some(ref pin_hash) = migration.pin_hash {
|
||||||
|
key_manager::set_pin_hash(&migration.user_id, pin_hash)?;
|
||||||
|
}
|
||||||
|
key_manager::set_last_user_id(&migration.user_id)?;
|
||||||
|
{
|
||||||
|
let mut db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
|
||||||
|
db_manager.initialize(&migration.user_id)?;
|
||||||
|
let conn = db_manager.get_connection(&migration.user_id)?;
|
||||||
|
schema::initialize_schema(conn)?;
|
||||||
|
if !cfg!(debug_assertions) { schema::run_migrations(conn)?; }
|
||||||
|
}
|
||||||
|
let mut session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock: {}", e)))?;
|
||||||
|
session_guard.user_id = Some(migration.user_id.clone());
|
||||||
|
cleanup_migration_files(&migration_path, &migration.user_id);
|
||||||
|
Ok(MigrationResult { success: true, user_id: Some(migration.user_id), error: None })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_migration_files(migration_path: &PathBuf, user_id: &str) {
|
||||||
|
let _ = std::fs::remove_file(migration_path);
|
||||||
|
if let Some(parent) = migration_path.parent() {
|
||||||
|
let db_copy = parent.join(format!("eritors-local-{}.db", user_id));
|
||||||
|
let _ = std::fs::remove_file(&db_copy);
|
||||||
|
let _ = std::fs::remove_file(db_copy.with_extension("db-wal"));
|
||||||
|
let _ = std::fs::remove_file(db_copy.with_extension("db-shm"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_db_files(source_db: &PathBuf, user_id: &str, db: &State<DbManager>) -> AppResult<()> {
|
||||||
|
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
|
||||||
|
let dest_db = db_manager.get_db_path(user_id);
|
||||||
|
if let Some(parent) = dest_db.parent() { std::fs::create_dir_all(parent)?; }
|
||||||
|
std::fs::copy(source_db, &dest_db)?;
|
||||||
|
for ext in &["db-wal", "db-shm"] {
|
||||||
|
let src = source_db.with_extension(ext);
|
||||||
|
if src.exists() {
|
||||||
|
let dst = dest_db.with_file_name(format!("eritors-local-{}.{}", user_id, ext));
|
||||||
|
let _ = std::fs::copy(&src, &dst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
1
src-tauri/src/domains/migration/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod commands;
|
||||||
56
src-tauri/src/shared/crash_reporter.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use serde_json::json;
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
|
const API_URL: &str = if cfg!(debug_assertions) { "http://localhost:3001/" } else { "https://api.eritors.com/" };
|
||||||
|
const APP_NAME: &str = "Eritors Scribe";
|
||||||
|
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
fn get_platform() -> &'static str {
|
||||||
|
if cfg!(target_os = "macos") { "desktop-macos" }
|
||||||
|
else if cfg!(target_os = "windows") { "desktop-windows" }
|
||||||
|
else if cfg!(target_os = "linux") { "desktop-linux" }
|
||||||
|
else { "desktop" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_os_version() -> String {
|
||||||
|
format!("{} {}", std::env::consts::OS, std::env::consts::ARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_crash_report(error_type: &str, error_message: &str, stack_trace: &str) {
|
||||||
|
let payload = json!({
|
||||||
|
"appName": APP_NAME,
|
||||||
|
"appVersion": APP_VERSION,
|
||||||
|
"platform": get_platform(),
|
||||||
|
"osVersion": get_os_version(),
|
||||||
|
"errorType": error_type,
|
||||||
|
"errorMessage": error_message,
|
||||||
|
"stackTrace": stack_trace,
|
||||||
|
});
|
||||||
|
|
||||||
|
let url = format!("{}crash-report", API_URL);
|
||||||
|
let _ = reqwest::blocking::Client::new()
|
||||||
|
.post(&url)
|
||||||
|
.json(&payload)
|
||||||
|
.timeout(std::time::Duration::from_secs(5))
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_panic_hook() {
|
||||||
|
let default_hook = panic::take_hook();
|
||||||
|
|
||||||
|
panic::set_hook(Box::new(move |info| {
|
||||||
|
let message = if let Some(s) = info.payload().downcast_ref::<&str>() {
|
||||||
|
s.to_string()
|
||||||
|
} else if let Some(s) = info.payload().downcast_ref::<String>() {
|
||||||
|
s.clone()
|
||||||
|
} else {
|
||||||
|
"Unknown panic".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let location = info.location().map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())).unwrap_or_default();
|
||||||
|
let stack_trace = format!("panic at {}\n{}", location, std::backtrace::Backtrace::force_capture());
|
||||||
|
|
||||||
|
send_crash_report("RustPanic", &message, &stack_trace);
|
||||||
|
default_hook(info);
|
||||||
|
}));
|
||||||
|
}
|
||||||