Files
oxpoe 581cd07dba Perbaiki backup/restore dan script yang tidak jalan dari shortcut
Backup/restore:
- berkas .lazy (format versi 2) kini menyimpan ISI berkas: binary/script dan ikon
  (maks 512 MB per berkas); format versi 1 tetap bisa diimpor
- impor memakai shortcut.Restore: binary ditulis ke ~/.local/bin lokal, ikon dari
  cadangan (atau diunduh ulang dari icon_source), .desktop ditulis dengan path
  lokal, salinan Desktop dibuat sesuai target
- path dari komputer lama tidak dipakai mentah -> tidak ada lagi 'banyak file
  missing'; entri yang tidak bisa dipulihkan ditandai dan bisa dibetulkan via edit
- tes end-to-end: pasang 3 shortcut -> ekspor -> pulihkan di HOME lain -> semua
  berkas lokal, script hasil restore benar-benar jalan

Script:
- script dipasang sebagai wrapper + <slug>.real: wrapper menambahkan
  ~/.local/bin ke PATH dan cd ke folder asli script (desktop environment
  menjalankan dengan cwd = home, PATH sesi)
- .desktop dapat Path= sesuai folder asli script
- opsi 'Run in a terminal window (scripts)' di dialog install & edit
- uninstall membuang wrapper + script aslinya
2026-10-13 09:25:49 +07:00

202 lines
5.4 KiB
Go

// Package db menyimpan daftar shortcut LazyRun dalam satu berkas SQLite (.lazy)
// supaya bisa dipindah antar komputer.
package db
import (
"database/sql"
"errors"
"fmt"
"os"
"strings"
"time"
_ "modernc.org/sqlite"
"lazyrun/internal/store"
)
// FormatVersion dipakai untuk mengenali berkas .lazy buatan LazyRun.
// Versi 1 hanya metadata; versi 2 menyertakan isi binary dan ikon.
const FormatVersion = "2"
// DefaultName adalah nama berkas cadangan yang disarankan.
const DefaultName = "lazyrun-shortcuts.lazy"
const schema = `
CREATE TABLE meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE shortcuts (
slug TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
kind TEXT,
url TEXT,
browser TEXT,
exec_args TEXT,
categories TEXT,
binary_path TEXT,
icon_path TEXT,
icon_source TEXT,
desktop_path TEXT,
desktop_copy_path TEXT,
terminal INTEGER NOT NULL DEFAULT 0,
menu INTEGER NOT NULL,
desktop INTEGER NOT NULL,
installed_at TEXT,
binary_data BLOB,
icon_data BLOB
);`
// Export menulis seluruh daftar shortcut ke berkas SQLite baru.
func Export(path string, appVersion string, items []store.Shortcut) error {
if path == "" {
return errors.New("lokasi berkas ekspor kosong")
}
_ = os.Remove(path) // mulai dari berkas baru
conn, err := sql.Open("sqlite", path)
if err != nil {
return err
}
defer conn.Close()
if _, err := conn.Exec(schema); err != nil {
return err
}
meta := map[string]string{
"format": FormatVersion,
"app_version": appVersion,
"exported_at": time.Now().UTC().Format(time.RFC3339),
}
for key, value := range meta {
if _, err := conn.Exec("INSERT INTO meta(key, value) VALUES(?, ?)", key, value); err != nil {
return err
}
}
tx, err := conn.Begin()
if err != nil {
return err
}
insert, err := tx.Prepare(`INSERT INTO shortcuts(
slug, name, type, kind, url, browser, exec_args, categories,
binary_path, icon_path, icon_source, desktop_path, desktop_copy_path,
terminal, menu, desktop, installed_at, binary_data, icon_data)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
if err != nil {
tx.Rollback()
return err
}
defer insert.Close()
for _, item := range items {
_, err := insert.Exec(
item.Slug, item.Name, item.Type, item.Kind, item.URL, item.Browser,
strings.Join(item.ExecArgs, "\x00"), item.Categories,
item.BinaryPath, item.IconPath, item.IconSource, item.DesktopPath,
item.DesktopCopyPath, boolToInt(item.Terminal), boolToInt(item.Targets.Menu), boolToInt(item.Targets.Desktop),
item.InstalledAt.Format(time.RFC3339), readBlob(item.BinaryPath), readBlob(item.IconPath),
)
if err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
// Backup adalah satu shortcut beserta isi berkasnya dari .lazy.
type Backup struct {
Item store.Shortcut
Binary []byte // isi binary/script (kalau ada)
Icon []byte // isi berkas ikon (kalau ada)
}
// maxBlobFile adalah batas ukuran berkas yang ikut disimpan ke cadangan.
const maxBlobFile = 512 << 20
// readBlob membaca isi berkas untuk disimpan ke cadangan; gagal/kebesaran = kosong.
func readBlob(path string) []byte {
if path == "" {
return nil
}
info, err := os.Stat(path)
if err != nil || info.IsDir() || info.Size() > maxBlobFile {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return nil
}
return data
}
// Import membaca daftar shortcut dari berkas .lazy.
func Import(path string) ([]Backup, error) {
if _, err := os.Stat(path); err != nil {
return nil, err
}
conn, err := sql.Open("sqlite", path)
if err != nil {
return nil, err
}
defer conn.Close()
var format string
if err := conn.QueryRow("SELECT value FROM meta WHERE key = 'format'").Scan(&format); err != nil {
return nil, fmt.Errorf("bukan basis data LazyRun: %w", err)
}
switch format {
case FormatVersion:
// versi sekarang: ada blobs
case "1":
// cadangan lama: hanya metadata, berkas dipulihkan dari sumber lain
default:
return nil, fmt.Errorf("versi basis data %s tidak didukung (mendukung %s)", format, FormatVersion)
}
rows, err := conn.Query(`SELECT slug, name, type, kind, url, browser, exec_args,
categories, binary_path, icon_path, icon_source, desktop_path,
desktop_copy_path, terminal, menu, desktop, installed_at,
binary_data, icon_data FROM shortcuts ORDER BY name`)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Backup
for rows.Next() {
var item store.Shortcut
var args, installed string
var terminal, menu, desktop int
var binary, iconBlob []byte
if err := rows.Scan(&item.Slug, &item.Name, &item.Type, &item.Kind, &item.URL,
&item.Browser, &args, &item.Categories, &item.BinaryPath, &item.IconPath,
&item.IconSource, &item.DesktopPath, &item.DesktopCopyPath,
&terminal, &menu, &desktop, &installed, &binary, &iconBlob); err != nil {
return nil, err
}
if args != "" {
item.ExecArgs = strings.Split(args, "\x00")
}
item.Terminal = terminal != 0
item.Targets = store.Targets{Menu: menu != 0, Desktop: desktop != 0}
if t, err := time.Parse(time.RFC3339, installed); err == nil {
item.InstalledAt = t
}
items = append(items, Backup{Item: item, Binary: binary, Icon: iconBlob})
}
return items, rows.Err()
}
func boolToInt(v bool) int {
if v {
return 1
}
return 0
}