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
This commit is contained in:
1 parent
fdb503e152
commit
581cd07dba
16 files changed
+779
-59
No files matched your search
+51
-12
@@ -16,7 +16,8 @@ import (
|
||||
)
|
||||
|
||||
// FormatVersion dipakai untuk mengenali berkas .lazy buatan LazyRun.
|
||||
const FormatVersion = "1"
|
||||
// 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"
|
||||
@@ -40,9 +41,12 @@ CREATE TABLE shortcuts (
|
||||
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
|
||||
installed_at TEXT,
|
||||
binary_data BLOB,
|
||||
icon_data BLOB
|
||||
);`
|
||||
|
||||
// Export menulis seluruh daftar shortcut ke berkas SQLite baru.
|
||||
@@ -80,7 +84,8 @@ func Export(path string, appVersion string, items []store.Shortcut) error {
|
||||
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,
|
||||
menu, desktop, installed_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
|
||||
terminal, menu, desktop, installed_at, binary_data, icon_data)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
@@ -92,8 +97,8 @@ func Export(path string, appVersion string, items []store.Shortcut) error {
|
||||
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.Targets.Menu), boolToInt(item.Targets.Desktop),
|
||||
item.InstalledAt.Format(time.RFC3339),
|
||||
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()
|
||||
@@ -103,8 +108,34 @@ func Export(path string, appVersion string, items []store.Shortcut) error {
|
||||
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) ([]store.Shortcut, error) {
|
||||
func Import(path string) ([]Backup, error) {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,37 +150,45 @@ func Import(path string) ([]store.Shortcut, error) {
|
||||
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)
|
||||
}
|
||||
if format != FormatVersion {
|
||||
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, menu, desktop, installed_at FROM shortcuts ORDER BY name`)
|
||||
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 []store.Shortcut
|
||||
var items []Backup
|
||||
for rows.Next() {
|
||||
var item store.Shortcut
|
||||
var args, installed string
|
||||
var menu, desktop int
|
||||
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,
|
||||
&menu, &desktop, &installed); err != nil {
|
||||
&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, item)
|
||||
items = append(items, Backup{Item: item, Binary: binary, Icon: iconBlob})
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
+60
-1
@@ -62,7 +62,7 @@ func TestExportImportRoundtrip(t *testing.T) {
|
||||
}
|
||||
|
||||
// urutan hasil impor berdasarkan nama: WhatsApp Web lalu Winbox
|
||||
web, exe := got[0], got[1]
|
||||
web, exe := got[0].Item, got[1].Item
|
||||
if web.Slug != "whatsapp-web" || web.URL != "https://web.whatsapp.com" || !web.Targets.Menu || web.Targets.Desktop {
|
||||
t.Errorf("data webapp tidak utuh: %+v", web)
|
||||
}
|
||||
@@ -77,6 +77,65 @@ func TestExportImportRoundtrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportStoresFileContents(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
binary := filepath.Join(home, "app")
|
||||
if err := os.WriteFile(binary, []byte("isi-binary-penting"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
iconFile := filepath.Join(home, "app.png")
|
||||
iconBytes := append([]byte("\x89PNG\r\n\x1a\n"), []byte("ikon")...)
|
||||
if err := os.WriteFile(iconFile, iconBytes, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
items := []store.Shortcut{{
|
||||
Slug: "app", Name: "App", Type: store.TypeExecutable, Kind: "binary",
|
||||
BinaryPath: binary, IconPath: iconFile, DesktopPath: filepath.Join(home, "app.desktop"),
|
||||
Terminal: true, Targets: store.Targets{Menu: true},
|
||||
}}
|
||||
|
||||
path := filepath.Join(home, "db.lazy")
|
||||
if err := Export(path, "1.2.0", items); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := Import(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got[0].Binary) != "isi-binary-penting" {
|
||||
t.Errorf("isi binary tidak ikut tercadang: %q", got[0].Binary)
|
||||
}
|
||||
if string(got[0].Icon) != string(iconBytes) {
|
||||
t.Errorf("isi ikon tidak ikut tercadang: %q", got[0].Icon)
|
||||
}
|
||||
if !got[0].Item.Terminal {
|
||||
t.Error("flag terminal hilang")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportSkipsMissingFiles(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
items := []store.Shortcut{{
|
||||
Slug: "hilang", Name: "Hilang", Type: store.TypeExecutable,
|
||||
BinaryPath: filepath.Join(home, "tidak-ada"), DesktopPath: filepath.Join(home, "x.desktop"),
|
||||
Targets: store.Targets{Menu: true},
|
||||
}}
|
||||
|
||||
path := filepath.Join(home, "db.lazy")
|
||||
if err := Export(path, "1.2.0", items); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := Import(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 1 || len(got[0].Binary) != 0 {
|
||||
t.Errorf("berkas hilang tidak boleh bikin ekspor gagal: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportOverwritesOldFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "db.lazy")
|
||||
if err := os.WriteFile(path, []byte("bukan sqlite"), 0o644); err != nil {
|
||||
|
||||
Reference in new issue
Block a user