Actualiser api/index.php
CI / PHP Syntax Check (push) Has been cancelled
CI / JavaScript Lint (push) Has been cancelled
CI / Docker Build Test (push) Has been cancelled
CI / Validate Translation Files (push) Has been cancelled
Security Scan (Trivy) / Trivy — Docker image scan (push) Has been cancelled
Security Scan (Trivy) / Trivy — Filesystem scan (push) Has been cancelled
CI / Auto-merge develop → main (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / PHP Syntax Check (push) Has been cancelled
CI / JavaScript Lint (push) Has been cancelled
CI / Docker Build Test (push) Has been cancelled
CI / Validate Translation Files (push) Has been cancelled
Security Scan (Trivy) / Trivy — Docker image scan (push) Has been cancelled
Security Scan (Trivy) / Trivy — Filesystem scan (push) Has been cancelled
CI / Auto-merge develop → main (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
This commit is contained in:
+239
-1
@@ -1111,7 +1111,12 @@ try {
|
||||
case 'db_cleanup':
|
||||
dbCleanup(getDB());
|
||||
break;
|
||||
|
||||
case 'export_full':
|
||||
exportFullBackup($db);
|
||||
break;
|
||||
case 'import_merge':
|
||||
importMergeBackup($db);
|
||||
break;
|
||||
case 'backup_now':
|
||||
echo json_encode(createLocalBackup($db));
|
||||
break;
|
||||
@@ -8911,6 +8916,239 @@ function restoreLocalBackup(string $filename, PDO $db): array {
|
||||
return ['success' => true, 'message' => 'Restore complete — reload the page to see the restored data.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Export complet : zip contenant evershelf.db + .env (config).
|
||||
*/
|
||||
function exportFullBackup(PDO $db): void {
|
||||
EverLog::info('exportFullBackup');
|
||||
try { $db->exec('PRAGMA wal_checkpoint(FULL)'); } catch (Throwable $e) {}
|
||||
|
||||
$dbPath = __DIR__ . '/../data/evershelf.db';
|
||||
$envPath = __DIR__ . '/../.env';
|
||||
$tmpZip = sys_get_temp_dir() . '/evershelf_export_' . uniqid() . '.zip';
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($tmpZip, ZipArchive::CREATE) !== true) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Cannot create zip']);
|
||||
return;
|
||||
}
|
||||
$zip->addFile($dbPath, 'evershelf.db');
|
||||
if (file_exists($envPath)) {
|
||||
$zip->addFile($envPath, '.env');
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
$filename = 'evershelf_export_' . date('Y-m-d_Hi') . '.zip';
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Content-Length: ' . filesize($tmpZip));
|
||||
readfile($tmpZip);
|
||||
@unlink($tmpZip);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import avec fusion intelligente (sans doublons) depuis un export_full.
|
||||
* - Tables référentielles : skip si key existe déjà.
|
||||
* - products : skip si barcode ou nom existe déjà, sinon insert + mapping ancien_id -> nouvel_id.
|
||||
* - inventory / transactions / chat_messages : toujours insérés, product_id remappé.
|
||||
* - .env du zip est ignoré (jamais écrasé automatiquement).
|
||||
*/
|
||||
function importMergeBackup(PDO $db): void {
|
||||
EverLog::info('importMergeBackup');
|
||||
if (empty($_FILES['file']['tmp_name'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'No file uploaded']);
|
||||
return;
|
||||
}
|
||||
|
||||
$tmpDir = sys_get_temp_dir() . '/evershelf_import_' . uniqid();
|
||||
mkdir($tmpDir, 0755, true);
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($_FILES['file']['tmp_name']) !== true) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid zip file']);
|
||||
return;
|
||||
}
|
||||
$zip->extractTo($tmpDir);
|
||||
$zip->close();
|
||||
|
||||
$srcDbPath = $tmpDir . '/evershelf.db';
|
||||
if (!file_exists($srcDbPath)) {
|
||||
echo json_encode(['success' => false, 'error' => 'evershelf.db not found in zip']);
|
||||
return;
|
||||
}
|
||||
|
||||
$src = new PDO('sqlite:' . $srcDbPath);
|
||||
$src->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
|
||||
$stats = ['products' => 0, 'inventory' => 0, 'transactions' => 0, 'recipes' => 0,
|
||||
'recipe_library' => 0, 'chat_messages' => 0, 'shopping_list' => 0,
|
||||
'categories' => 0, 'subcategories' => 0, 'locations' => 0,
|
||||
'recipe_tags' => 0, 'custom_units' => 0];
|
||||
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
// --- 1. Tables référentielles simples : skip si key existe ---
|
||||
$refTables = [
|
||||
'categories' => ['key'],
|
||||
'locations' => ['key'],
|
||||
'recipe_tags' => ['key'],
|
||||
'custom_units' => ['key'],
|
||||
];
|
||||
foreach ($refTables as $table => $uniqueCols) {
|
||||
$rows = $src->query("SELECT * FROM {$table}")->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
unset($row['id']);
|
||||
$where = [];
|
||||
$params = [];
|
||||
foreach ($uniqueCols as $c) { $where[] = "$c = ?"; $params[] = $row[$c]; }
|
||||
$exists = $db->prepare("SELECT 1 FROM {$table} WHERE " . implode(' AND ', $where));
|
||||
$exists->execute($params);
|
||||
if ($exists->fetch()) continue;
|
||||
$cols = array_keys($row);
|
||||
$stmt = $db->prepare("INSERT INTO {$table} (" . implode(',', $cols) . ") VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")");
|
||||
$stmt->execute(array_values($row));
|
||||
$stats[$table]++;
|
||||
}
|
||||
}
|
||||
|
||||
// subcategories : unique (category, key)
|
||||
$rows = $src->query("SELECT * FROM subcategories")->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
unset($row['id']);
|
||||
$exists = $db->prepare("SELECT 1 FROM subcategories WHERE category = ? AND key = ?");
|
||||
$exists->execute([$row['category'], $row['key']]);
|
||||
if ($exists->fetch()) continue;
|
||||
$cols = array_keys($row);
|
||||
$stmt = $db->prepare("INSERT INTO subcategories (" . implode(',', $cols) . ") VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")");
|
||||
$stmt->execute(array_values($row));
|
||||
$stats['subcategories']++;
|
||||
}
|
||||
|
||||
// --- 2. products : skip si barcode (non vide) ou nom existe déjà, sinon insert + map id ---
|
||||
$productMap = []; // old_id => new_id
|
||||
$rows = $src->query("SELECT * FROM products")->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
$oldId = (int)$row['id'];
|
||||
$barcode = trim((string)($row['barcode'] ?? ''));
|
||||
$existingId = null;
|
||||
|
||||
if ($barcode !== '') {
|
||||
$stmt = $db->prepare("SELECT id FROM products WHERE barcode = ?");
|
||||
$stmt->execute([$barcode]);
|
||||
$existingId = $stmt->fetchColumn() ?: null;
|
||||
}
|
||||
if (!$existingId) {
|
||||
$stmt = $db->prepare("SELECT id FROM products WHERE lower(trim(name)) = lower(trim(?))");
|
||||
$stmt->execute([$row['name']]);
|
||||
$existingId = $stmt->fetchColumn() ?: null;
|
||||
}
|
||||
|
||||
if ($existingId) {
|
||||
$productMap[$oldId] = (int)$existingId;
|
||||
continue;
|
||||
}
|
||||
|
||||
unset($row['id']);
|
||||
$cols = array_keys($row);
|
||||
$stmt = $db->prepare("INSERT INTO products (" . implode(',', $cols) . ") VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")");
|
||||
$stmt->execute(array_values($row));
|
||||
$productMap[$oldId] = (int)$db->lastInsertId();
|
||||
$stats['products']++;
|
||||
}
|
||||
|
||||
// --- 3. inventory : toujours inséré, product_id remappé ---
|
||||
$rows = $src->query("SELECT * FROM inventory")->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
$newPid = $productMap[(int)$row['product_id']] ?? null;
|
||||
if (!$newPid) continue;
|
||||
unset($row['id']);
|
||||
$row['product_id'] = $newPid;
|
||||
$cols = array_keys($row);
|
||||
$stmt = $db->prepare("INSERT INTO inventory (" . implode(',', $cols) . ") VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")");
|
||||
$stmt->execute(array_values($row));
|
||||
$stats['inventory']++;
|
||||
}
|
||||
|
||||
// --- 4. transactions : toujours insérées, product_id remappé ---
|
||||
$rows = $src->query("SELECT * FROM transactions")->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
$newPid = $productMap[(int)$row['product_id']] ?? null;
|
||||
if (!$newPid) continue;
|
||||
unset($row['id']);
|
||||
$row['product_id'] = $newPid;
|
||||
$cols = array_keys($row);
|
||||
$stmt = $db->prepare("INSERT INTO transactions (" . implode(',', $cols) . ") VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")");
|
||||
$stmt->execute(array_values($row));
|
||||
$stats['transactions']++;
|
||||
}
|
||||
|
||||
// --- 5. recipes : skip si (date, meal) existe ---
|
||||
$rows = $src->query("SELECT * FROM recipes")->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
$exists = $db->prepare("SELECT 1 FROM recipes WHERE date = ? AND meal = ?");
|
||||
$exists->execute([$row['date'], $row['meal']]);
|
||||
if ($exists->fetch()) continue;
|
||||
unset($row['id']);
|
||||
$cols = array_keys($row);
|
||||
$stmt = $db->prepare("INSERT INTO recipes (" . implode(',', $cols) . ") VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")");
|
||||
$stmt->execute(array_values($row));
|
||||
$stats['recipes']++;
|
||||
}
|
||||
|
||||
// --- 6. recipe_library : skip si title existe ---
|
||||
$rows = $src->query("SELECT * FROM recipe_library")->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
$exists = $db->prepare("SELECT 1 FROM recipe_library WHERE title = ?");
|
||||
$exists->execute([$row['title']]);
|
||||
if ($exists->fetch()) continue;
|
||||
unset($row['id']);
|
||||
$cols = array_keys($row);
|
||||
$stmt = $db->prepare("INSERT INTO recipe_library (" . implode(',', $cols) . ") VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")");
|
||||
$stmt->execute(array_values($row));
|
||||
$stats['recipe_library']++;
|
||||
}
|
||||
|
||||
// --- 7. chat_messages : toujours insérés ---
|
||||
$rows = $src->query("SELECT * FROM chat_messages")->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
unset($row['id']);
|
||||
$cols = array_keys($row);
|
||||
$stmt = $db->prepare("INSERT INTO chat_messages (" . implode(',', $cols) . ") VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")");
|
||||
$stmt->execute(array_values($row));
|
||||
$stats['chat_messages']++;
|
||||
}
|
||||
|
||||
// --- 8. shopping_list : skip si lower(name) existe ---
|
||||
$rows = $src->query("SELECT * FROM shopping_list")->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
$exists = $db->prepare("SELECT 1 FROM shopping_list WHERE lower(name) = lower(?)");
|
||||
$exists->execute([$row['name']]);
|
||||
if ($exists->fetch()) continue;
|
||||
unset($row['id']);
|
||||
$cols = array_keys($row);
|
||||
$stmt = $db->prepare("INSERT INTO shopping_list (" . implode(',', $cols) . ") VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")");
|
||||
$stmt->execute(array_values($row));
|
||||
$stats['shopping_list']++;
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
} catch (Throwable $e) {
|
||||
$db->rollBack();
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
return;
|
||||
} finally {
|
||||
@unlink($srcDbPath);
|
||||
@unlink($tmpDir . '/.env');
|
||||
@rmdir($tmpDir);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'stats' => $stats], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
// ===== GOOGLE DRIVE BACKUP =====
|
||||
|
||||
/** Write / overwrite a single key in the .env file (used by OAuth callback). */
|
||||
|
||||
Reference in New Issue
Block a user