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
@@ -0,0 +1,138 @@
|
||||
package shortcut
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"lazyrun/internal/db"
|
||||
"lazyrun/internal/store"
|
||||
)
|
||||
|
||||
// TestBackupRestoreEndToEnd meniru alur backup: pasang beberapa shortcut,
|
||||
// ekspor ke berkas .lazy, lalu pulihkan di "komputer lain" (HOME berbeda)
|
||||
// dan pastikan hasilnya benar-benar jalan.
|
||||
func TestBackupRestoreEndToEnd(t *testing.T) {
|
||||
codeDir := t.TempDir()
|
||||
homeA := t.TempDir()
|
||||
|
||||
// ---- komputer A: pasang shortcut ----
|
||||
t.Setenv("HOME", homeA)
|
||||
t.Setenv("XDG_DATA_HOME", "")
|
||||
t.Setenv("PATH", "")
|
||||
|
||||
site := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
_, _ = w.Write([]byte(`<html><head><link rel="icon" type="image/png" sizes="128x128" href="/web.png"></head></html>`))
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(append(pngMagic, []byte("ikon web")...))
|
||||
}))
|
||||
defer site.Close()
|
||||
|
||||
// script yang butuh folder kerjanya + tool di ~/.local/bin
|
||||
scriptSrc := filepath.Join(codeDir, "deploy.sh")
|
||||
if err := os.WriteFile(scriptSrc, []byte(`#!/bin/sh
|
||||
[ -f ./data.txt ] && echo "data ok" || echo "data hilang"
|
||||
case "$PATH" in *".local/bin"*) echo "path ok";; *) echo "path hilang";; esac
|
||||
`), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(codeDir, "data.txt"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
script, err := InstallExecutable(ExecOptions{BinaryPath: scriptSrc, Name: "Deploy", Category: "Utility", Menu: true, Desktop: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
binarySrc := fakeBinary(t, codeDir, "tool")
|
||||
binaryShortcut, err := InstallExecutable(ExecOptions{BinaryPath: binarySrc, Name: "Tool", Menu: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
web, err := InstallWebApp(WebOptions{
|
||||
URL: site.URL, Name: "Situs Web",
|
||||
Browser: BrowserArg{Name: "Chromium", Exec: []string{"/usr/bin/chromium"}},
|
||||
Menu: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// ---- ekspor ke .lazy ----
|
||||
backupPath := filepath.Join(t.TempDir(), "db.lazy")
|
||||
if err := db.Export(backupPath, "1.2.0", []store.Shortcut{script, binaryShortcut, web}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// ---- komputer B: HOME baru, pulihkan ----
|
||||
homeB := t.TempDir()
|
||||
t.Setenv("HOME", homeB)
|
||||
|
||||
backups, err := db.Import(backupPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(backups) != 3 {
|
||||
t.Fatalf("cadangan berisi %d entri, mau 3", len(backups))
|
||||
}
|
||||
|
||||
restored := map[string]store.Shortcut{}
|
||||
for _, backup := range backups {
|
||||
item, err := Restore(backup.Item, backup.Binary, backup.Icon)
|
||||
if err != nil {
|
||||
t.Fatalf("restore %s: %v", backup.Item.Name, err)
|
||||
}
|
||||
restored[backup.Item.Name] = item
|
||||
}
|
||||
|
||||
// ---- semua berkas harus di home B, bukan home A ----
|
||||
for name, item := range restored {
|
||||
if item.BinaryPath != "" && !strings.HasPrefix(item.BinaryPath, homeB) {
|
||||
t.Errorf("%s: binary masih menunjuk home lama: %s", name, item.BinaryPath)
|
||||
}
|
||||
if !strings.HasPrefix(item.DesktopPath, homeB) {
|
||||
t.Errorf("%s: .desktop masih menunjuk home lama: %s", name, item.DesktopPath)
|
||||
}
|
||||
if _, err := os.Stat(item.DesktopPath); err != nil {
|
||||
t.Errorf("%s: .desktop tidak ada: %v", name, err)
|
||||
}
|
||||
if _, err := os.Stat(item.IconPath); err != nil {
|
||||
t.Errorf("%s: ikon tidak ada: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// script hasil restore harus jalan seperti dijalankan desktop environment
|
||||
deploy := restored["Deploy"]
|
||||
if !strings.HasPrefix(deploy.DesktopPath, homeB) {
|
||||
t.Fatal("script tidak terpulihkan")
|
||||
}
|
||||
cmd := exec.Command(deploy.ExecArgs[0], deploy.ExecArgs[1:]...)
|
||||
cmd.Dir = homeB
|
||||
cmd.Env = []string{"HOME=" + homeB, "PATH=/usr/bin:/bin"}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("script hasil restore gagal jalan: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(string(out), "path ok") {
|
||||
t.Errorf("PATH tidak disiapkan wrapper: %s", out)
|
||||
}
|
||||
|
||||
// binary biasa hasil restore juga harus executable
|
||||
tool := restored["Tool"]
|
||||
if st, err := os.Stat(tool.BinaryPath); err != nil || st.Mode().Perm()&0o111 == 0 {
|
||||
t.Errorf("binary hasil restore tidak executable: %v %v", st, err)
|
||||
}
|
||||
if !strings.HasPrefix(tool.DesktopCopyPath, homeB) && tool.Targets.Desktop {
|
||||
t.Errorf("salinan desktop salah: %s", tool.DesktopCopyPath)
|
||||
}
|
||||
|
||||
// web app: exec args tetap menunjuk browser + URL
|
||||
if !strings.Contains(strings.Join(restored["Situs Web"].ExecArgs, " "), "--app="+site.URL) {
|
||||
t.Errorf("exec web app salah: %v", restored["Situs Web"].ExecArgs)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ type Entry struct {
|
||||
ExecArgs []string // argv lengkap; tiap arg di-quote sesuai spec Exec
|
||||
Icon string // path absolut atau nama ikon
|
||||
Categories string // sudah diakhiri ";" atau kosong
|
||||
Terminal bool // true = jalankan di terminal (untuk script interaktif)
|
||||
WorkingDir string // folder kerja saat dijalankan (Path=), opsional
|
||||
// NoDisplay menyembunyikan entry dari menu aplikasi. Dipakai kalau user
|
||||
// hanya memilih Desktop: file tetap ada di applications/ sebagai sumber kebenaran.
|
||||
NoDisplay bool
|
||||
@@ -29,7 +31,10 @@ func (e Entry) Render() string {
|
||||
if e.Icon != "" {
|
||||
fmt.Fprintf(&b, "Icon=%s\n", escapeValue(e.Icon))
|
||||
}
|
||||
b.WriteString("Terminal=false\n")
|
||||
fmt.Fprintf(&b, "Terminal=%t\n", e.Terminal)
|
||||
if e.WorkingDir != "" {
|
||||
fmt.Fprintf(&b, "Path=%s\n", escapeValue(e.WorkingDir))
|
||||
}
|
||||
if e.Categories != "" {
|
||||
fmt.Fprintf(&b, "Categories=%s\n", escapeValue(e.Categories))
|
||||
}
|
||||
@@ -71,6 +76,9 @@ func escapeValue(s string) string {
|
||||
return r.Replace(s)
|
||||
}
|
||||
|
||||
// Slug mengembalikan nama berkas yang aman dari nama aplikasi (dipakai UI).
|
||||
func Slug(name string) string { return slugify(name) }
|
||||
|
||||
// slugify membuat nama file yang aman dari nama aplikasi.
|
||||
func slugify(name string) string {
|
||||
var b strings.Builder
|
||||
|
||||
+19
-13
@@ -36,14 +36,11 @@ func EditExecutable(old store.Shortcut, o ExecOptions) (store.Shortcut, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// salin kalau sumbernya bukan berkas yang sudah terpasang
|
||||
if !samePath(src, binDst) {
|
||||
if err := copyFile(src, binDst, 0o755); err != nil {
|
||||
return old, err
|
||||
}
|
||||
kind := DetectKind(src)
|
||||
if err := installBinary(kind, src, binDst); err != nil {
|
||||
return old, err
|
||||
}
|
||||
|
||||
kind := DetectKind(binDst)
|
||||
execArgs := []string{binDst}
|
||||
if kind == KindWindows {
|
||||
wine, err := exec.LookPath("wine")
|
||||
@@ -73,24 +70,33 @@ func EditExecutable(old store.Shortcut, o ExecOptions) (store.Shortcut, error) {
|
||||
}
|
||||
}
|
||||
|
||||
entry := Entry{
|
||||
Name: name,
|
||||
ExecArgs: execArgs,
|
||||
Icon: iconPath,
|
||||
Categories: category,
|
||||
Terminal: o.Terminal && kind != KindWindows,
|
||||
NoDisplay: !o.Menu,
|
||||
}
|
||||
if kind == KindScript {
|
||||
if _, err := os.Stat(filepath.Dir(src)); err == nil {
|
||||
entry.WorkingDir = filepath.Dir(src)
|
||||
}
|
||||
}
|
||||
|
||||
updated := old
|
||||
updated.Slug = sl
|
||||
updated.Name = name
|
||||
updated.Kind = kind
|
||||
updated.ExecArgs = execArgs
|
||||
updated.Categories = category
|
||||
updated.Terminal = entry.Terminal
|
||||
updated.BinaryPath = binDst
|
||||
updated.IconPath = iconPath
|
||||
updated.IconSource = iconSource
|
||||
updated.Targets = store.Targets{Menu: o.Menu, Desktop: o.Desktop}
|
||||
|
||||
if err := rewriteShortcutFiles(old, &updated, Entry{
|
||||
Name: name,
|
||||
ExecArgs: execArgs,
|
||||
Icon: iconPath,
|
||||
Categories: category,
|
||||
NoDisplay: !o.Menu,
|
||||
}); err != nil {
|
||||
if err := rewriteShortcutFiles(old, &updated, entry); err != nil {
|
||||
return old, err
|
||||
}
|
||||
return updated, nil
|
||||
|
||||
@@ -21,6 +21,7 @@ type ExecOptions struct {
|
||||
IconURL string // opsional: URL ikon
|
||||
IconData []byte // opsional: ikon yang sudah diunduh UI
|
||||
Name string
|
||||
Terminal bool // jalankan di terminal (script interaktif)
|
||||
Category string
|
||||
Menu bool
|
||||
Desktop bool
|
||||
@@ -48,10 +49,9 @@ func InstallExecutable(o ExecOptions) (store.Shortcut, error) {
|
||||
return sc, fmt.Errorf("%s already exists, rename the app or remove that file first", binDst)
|
||||
}
|
||||
|
||||
if !samePath(o.BinaryPath, binDst) {
|
||||
if err := copyFile(o.BinaryPath, binDst, 0o755); err != nil {
|
||||
return sc, err
|
||||
}
|
||||
kind := DetectKind(o.BinaryPath)
|
||||
if err := installBinary(kind, o.BinaryPath, binDst); err != nil {
|
||||
return sc, err
|
||||
}
|
||||
iconPath, iconSource, err := installIconFor(iconOptions{
|
||||
IconPath: o.IconPath,
|
||||
@@ -66,8 +66,7 @@ func InstallExecutable(o ExecOptions) (store.Shortcut, error) {
|
||||
return sc, err
|
||||
}
|
||||
|
||||
// jenis aplikasi dideteksi otomatis; .exe Windows dijalankan lewat wine
|
||||
kind := DetectKind(o.BinaryPath)
|
||||
// .exe Windows dijalankan lewat wine; script lewat wrapper (PATH & cwd)
|
||||
execArgs := []string{binDst}
|
||||
if kind == KindWindows {
|
||||
wine, err := exec.LookPath("wine")
|
||||
@@ -82,8 +81,16 @@ func InstallExecutable(o ExecOptions) (store.Shortcut, error) {
|
||||
ExecArgs: execArgs,
|
||||
Icon: iconPath,
|
||||
Categories: category(o.Category, "Utility;"),
|
||||
Terminal: o.Terminal && kind != KindWindows,
|
||||
NoDisplay: !o.Menu,
|
||||
}
|
||||
if kind == KindScript {
|
||||
// desktop environment menjalankan dengan cwd = home, jadi folder asli
|
||||
// script dipakai sebagai Path= kalau masih ada
|
||||
if _, err := os.Stat(filepath.Dir(o.BinaryPath)); err == nil {
|
||||
entry.WorkingDir = filepath.Dir(o.BinaryPath)
|
||||
}
|
||||
}
|
||||
if err := WriteDesktop(deskDst, entry); err != nil {
|
||||
return sc, err
|
||||
}
|
||||
@@ -95,6 +102,7 @@ func InstallExecutable(o ExecOptions) (store.Shortcut, error) {
|
||||
Kind: kind,
|
||||
ExecArgs: entry.ExecArgs,
|
||||
Categories: entry.Categories,
|
||||
Terminal: entry.Terminal,
|
||||
BinaryPath: binDst,
|
||||
IconPath: iconPath,
|
||||
IconSource: iconSource,
|
||||
|
||||
@@ -105,3 +105,53 @@ func desktopCopy(desktopPath, slug string) (string, error) {
|
||||
}
|
||||
return dst, copyFile(desktopPath, dst, 0o755)
|
||||
}
|
||||
|
||||
// installBinary menyalin berkas target ke binDst.
|
||||
//
|
||||
// Script dapat perlakuan khusus: yang disalin adalah script aslinya (dengan
|
||||
// akhiran .real) plus wrapper kecil. Desktop environment menjalankan shortcut
|
||||
// dengan cwd = home dan PATH sesi, sehingga script yang memakai path relatif
|
||||
// atau tool di ~/.local/bin gagal; wrapper yang membereskan keduanya.
|
||||
func installBinary(kind, src, binDst string) error {
|
||||
if kind == KindScript {
|
||||
if err := copyFile(src, binDst+scriptSuffix, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeScriptWrapper(binDst, binDst+scriptSuffix, filepath.Dir(src))
|
||||
}
|
||||
if samePath(src, binDst) {
|
||||
return nil
|
||||
}
|
||||
return copyFile(src, binDst, 0o755)
|
||||
}
|
||||
|
||||
// writeFileMode menulis berkas dengan mode tertentu (dipakai saat restore).
|
||||
func writeFileMode(path string, data []byte, mode os.FileMode) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, mode)
|
||||
}
|
||||
|
||||
// scriptSuffix adalah akhiran berkas script asli di samping wrapper.
|
||||
const scriptSuffix = ".real"
|
||||
|
||||
// writeScriptWrapper menulis wrapper yang menyiapkan PATH dan folder kerja
|
||||
// sebelum menjalankan script aslinya.
|
||||
func writeScriptWrapper(path, real, originDir string) error {
|
||||
body := "#!/bin/sh\n" +
|
||||
"# dibuat oleh LazyRun: siapkan PATH & folder kerja lalu jalankan script aslinya\n" +
|
||||
`PATH="$HOME/.local/bin:$PATH"` + "\n" +
|
||||
"export PATH\n"
|
||||
if originDir != "" && originDir != "." {
|
||||
quoted := shellQuote(originDir)
|
||||
body += "if [ -d " + quoted + " ]; then cd " + quoted + " || exit 1; fi\n"
|
||||
}
|
||||
body += "exec " + shellQuote(real) + ` "$@"` + "\n"
|
||||
return os.WriteFile(path, []byte(body), 0o755)
|
||||
}
|
||||
|
||||
// shellQuote mengutip nilai untuk dipakai di dalam script shell.
|
||||
func shellQuote(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package shortcut
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"lazyrun/internal/icon"
|
||||
"lazyrun/internal/store"
|
||||
"lazyrun/internal/xdg"
|
||||
)
|
||||
|
||||
// Restore memasang ulang shortcut dari data cadangan ke mesin ini:
|
||||
// binary dan ikon ditulis dari isi cadangan (kalau ada), lalu .desktop dibuat
|
||||
// dengan path lokal. Metadata yang menunjuk ke komputer lain tidak dipakai
|
||||
// mentah-mentah, jadi hasil restore langsung bisa dipakai.
|
||||
func Restore(item store.Shortcut, binary, iconData []byte) (store.Shortcut, error) {
|
||||
name := strings.TrimSpace(item.Name)
|
||||
if name == "" {
|
||||
return item, errors.New("nama shortcut kosong")
|
||||
}
|
||||
sl := slugify(name)
|
||||
if sl == "" {
|
||||
return item, errors.New("nama shortcut tidak valid")
|
||||
}
|
||||
|
||||
restored := item
|
||||
restored.Slug = sl
|
||||
restored.Name = name
|
||||
restored.DesktopCopyPath = ""
|
||||
restored.IconPath = ""
|
||||
|
||||
if restored.Type == store.TypeExecutable {
|
||||
if err := restoreBinary(&restored, sl, binary); err != nil {
|
||||
return restored, err
|
||||
}
|
||||
}
|
||||
if restored.Type == store.TypeWebApp && len(restored.ExecArgs) == 0 && restored.Browser != "" {
|
||||
restored.ExecArgs = webAppArgs(restored.Browser, restored.URL)
|
||||
}
|
||||
|
||||
iconPath, err := restoreIcon(restored, sl, iconData)
|
||||
if err != nil {
|
||||
return restored, err
|
||||
}
|
||||
restored.IconPath = iconPath
|
||||
|
||||
entry := Entry{
|
||||
Name: name,
|
||||
ExecArgs: restored.ExecArgs,
|
||||
Icon: iconPath,
|
||||
Categories: restored.Categories,
|
||||
Terminal: restored.Terminal && restored.Kind != KindWindows,
|
||||
NoDisplay: !restored.Targets.Menu,
|
||||
}
|
||||
if entry.Categories == "" {
|
||||
entry.Categories = "Utility;"
|
||||
}
|
||||
if restored.Kind == KindScript {
|
||||
if dir := scriptOriginDir(restored); dir != "" {
|
||||
entry.WorkingDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
deskDst := filepath.Join(xdg.ApplicationsDir(), sl+".desktop")
|
||||
if err := WriteDesktop(deskDst, entry); err != nil {
|
||||
return restored, err
|
||||
}
|
||||
restored.DesktopPath = deskDst
|
||||
|
||||
if restored.Targets.Desktop {
|
||||
copyPath := filepath.Join(xdg.DesktopDir(), sl+".desktop")
|
||||
if err := copyFile(deskDst, copyPath, 0o755); err != nil {
|
||||
return restored, err
|
||||
}
|
||||
restored.DesktopCopyPath = copyPath
|
||||
}
|
||||
|
||||
go RefreshCache()
|
||||
return restored, nil
|
||||
}
|
||||
|
||||
// restoreBinary menulis binary/script dari cadangan ke ~/.local/bin.
|
||||
// Kalau cadangan tidak memuat isinya, berkas lokal yang masih ada dipakai.
|
||||
func restoreBinary(restored *store.Shortcut, slug string, binary []byte) error {
|
||||
binDst := filepath.Join(xdg.BinDir(), slug)
|
||||
originDir := scriptOriginDir(*restored)
|
||||
|
||||
if len(binary) == 0 {
|
||||
// tidak ada isi cadangan: pakai berkas lama kalau masih ada di mesin ini
|
||||
if _, err := os.Stat(restored.BinaryPath); err != nil {
|
||||
restored.BinaryPath = ""
|
||||
restored.ExecArgs = nil
|
||||
return nil
|
||||
}
|
||||
binary, _ = os.ReadFile(restored.BinaryPath)
|
||||
}
|
||||
|
||||
if err := writeFileMode(binDst, binary, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kind := DetectKind(binDst)
|
||||
if kind == KindScript {
|
||||
// script dijalankan lewat wrapper: script aslinya disimpan sebagai .real
|
||||
if err := os.Rename(binDst, binDst+scriptSuffix); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeScriptWrapper(binDst, binDst+scriptSuffix, originDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
restored.Kind = kind
|
||||
restored.BinaryPath = binDst
|
||||
restored.ExecArgs = []string{binDst}
|
||||
if kind == KindWindows {
|
||||
if wine, err := exec.LookPath("wine"); err == nil {
|
||||
restored.ExecArgs = []string{wine, binDst}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreIcon memakai ikon dari cadangan; kalau tidak ada, coba unduh ulang
|
||||
// dari sumber yang tercatat, lalu jatuh ke ikon default.
|
||||
func restoreIcon(item store.Shortcut, slug string, iconData []byte) (string, error) {
|
||||
if len(iconData) > 0 {
|
||||
if path, err := icon.Save(iconData, slug); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
source := item.IconSource
|
||||
if source != "" {
|
||||
options := iconOptions{Slug: slug}
|
||||
switch {
|
||||
case strings.HasPrefix(source, iconSourceFavicon):
|
||||
options.PageURL = strings.TrimPrefix(source, iconSourceFavicon)
|
||||
case strings.HasPrefix(source, iconSourceSearch):
|
||||
options.Query = strings.TrimPrefix(source, iconSourceSearch)
|
||||
case strings.HasPrefix(source, "http"):
|
||||
options.IconURL = source
|
||||
}
|
||||
if path, _, err := installIconFor(options); err == nil && path != "" {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
return icon.Default(slug)
|
||||
}
|
||||
|
||||
// scriptOriginDir menebak folder asal script dari metadata (dipakai sebagai
|
||||
// Path= dan folder kerja wrapper).
|
||||
func scriptOriginDir(item store.Shortcut) string {
|
||||
dir := filepath.Dir(item.BinaryPath)
|
||||
if dir == "" || dir == "." || dir == "/" {
|
||||
return ""
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// webAppArgs menyusun argv PWA dari perintah browser yang tersimpan.
|
||||
func webAppArgs(browser, pageURL string) []string {
|
||||
args := strings.Fields(browser)
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(strings.ToLower(args[0]), "firefox") {
|
||||
return append(args, "--new-window", pageURL)
|
||||
}
|
||||
return append(args, "--app="+pageURL)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package shortcut
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"lazyrun/internal/store"
|
||||
)
|
||||
|
||||
func TestRestoreExecutableFromBackup(t *testing.T) {
|
||||
home := testEnv(t)
|
||||
|
||||
backup := store.Shortcut{
|
||||
Slug: "winbox",
|
||||
Name: "Winbox",
|
||||
Type: store.TypeExecutable,
|
||||
Kind: "binary",
|
||||
BinaryPath: "/komputer-lama/.local/bin/winbox", // path dari mesin lain
|
||||
IconPath: "/komputer-lama/.local/share/icons/winbox.png",
|
||||
DesktopPath: "/komputer-lama/.local/share/applications/winbox.desktop",
|
||||
Categories: "Network;",
|
||||
Targets: store.Targets{Menu: true, Desktop: true},
|
||||
}
|
||||
restored, err := Restore(backup, append([]byte{0x7f, 'E', 'L', 'F', 0x02, 0x01, 0x01, 0x00}, []byte("isi")...), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantBin := filepath.Join(home, ".local", "bin", "winbox")
|
||||
if restored.BinaryPath != wantBin {
|
||||
t.Errorf("binary = %s, mau %s", restored.BinaryPath, wantBin)
|
||||
}
|
||||
if st, err := os.Stat(wantBin); err != nil || st.Mode().Perm()&0o111 == 0 {
|
||||
t.Fatalf("binary hasil restore tidak executable: %v %v", st, err)
|
||||
}
|
||||
// path dari mesin lama tidak boleh dipakai
|
||||
if strings.Contains(restored.DesktopPath, "komputer-lama") {
|
||||
t.Errorf("desktop path masih menunjuk mesin lama: %s", restored.DesktopPath)
|
||||
}
|
||||
if restored.DesktopCopyPath != filepath.Join(home, "Desktop", "winbox.desktop") {
|
||||
t.Errorf("salinan desktop = %s", restored.DesktopCopyPath)
|
||||
}
|
||||
content, err := os.ReadFile(restored.DesktopPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(content), "Exec="+wantBin+"\n") || !strings.Contains(string(content), "Categories=Network;\n") {
|
||||
t.Errorf("isi .desktop hasil restore salah:\n%s", content)
|
||||
}
|
||||
if _, err := os.Stat(restored.IconPath); err != nil {
|
||||
t.Errorf("ikon hasil restore tidak ada: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreScriptGetsWrapper(t *testing.T) {
|
||||
home := testEnv(t)
|
||||
|
||||
backup := store.Shortcut{
|
||||
Name: "Deploy",
|
||||
Type: store.TypeExecutable,
|
||||
Kind: "script",
|
||||
BinaryPath: "/komputer-lama/proyek/deploy.sh",
|
||||
Terminal: true,
|
||||
Targets: store.Targets{Menu: true},
|
||||
}
|
||||
restored, err := Restore(backup, []byte("#!/bin/sh\necho halo\n"), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if restored.Kind != KindScript {
|
||||
t.Errorf("kind = %q", restored.Kind)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(home, ".local", "bin", "deploy")); err != nil {
|
||||
t.Errorf("wrapper tidak dibuat: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(home, ".local", "bin", "deploy"+scriptSuffix)); err != nil {
|
||||
t.Errorf("script asli tidak dibuat: %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(restored.DesktopPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(content), "Terminal=true") {
|
||||
t.Errorf("flag terminal hilang:\n%s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreWithoutBinaryBlobMarksMissing(t *testing.T) {
|
||||
testEnv(t)
|
||||
|
||||
backup := store.Shortcut{
|
||||
Name: "Hilang",
|
||||
Type: store.TypeExecutable,
|
||||
BinaryPath: "/komputer-lama/.local/bin/hilang",
|
||||
Targets: store.Targets{Menu: true},
|
||||
}
|
||||
restored, err := Restore(backup, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if restored.BinaryPath != "" {
|
||||
t.Errorf("binary mestinya kosong (tidak bisa dipulihkan), dapat %s", restored.BinaryPath)
|
||||
}
|
||||
if restored.ExecArgs != nil {
|
||||
t.Errorf("exec args mestinya kosong: %v", restored.ExecArgs)
|
||||
}
|
||||
// .desktop tetap dibuat supaya entri bisa dibetulkan lewat edit
|
||||
if _, err := os.Stat(restored.DesktopPath); err != nil {
|
||||
t.Errorf(".desktop tidak dibuat: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreWebAppRebuildsExec(t *testing.T) {
|
||||
testEnv(t)
|
||||
|
||||
backup := store.Shortcut{
|
||||
Name: "WhatsApp Web",
|
||||
Type: store.TypeWebApp,
|
||||
Kind: KindPWA,
|
||||
URL: "https://web.whatsapp.com",
|
||||
Browser: "flatpak run org.chromium.Chromium",
|
||||
Targets: store.Targets{Menu: true},
|
||||
}
|
||||
restored, err := Restore(backup, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(restored.ExecArgs) != 4 || restored.ExecArgs[3] != "--app=https://web.whatsapp.com" {
|
||||
t.Errorf("exec args PWA salah: %v", restored.ExecArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreIconFromSource(t *testing.T) {
|
||||
testEnv(t)
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(append(pngMagic, []byte("ikon dari internet")...))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
backup := store.Shortcut{
|
||||
Name: "Icon",
|
||||
Type: store.TypeExecutable,
|
||||
BinaryPath: "",
|
||||
IconPath: "/komputer-lama/ikon.png",
|
||||
IconSource: srv.URL + "/ikon.png",
|
||||
Targets: store.Targets{Menu: true},
|
||||
}
|
||||
restored, err := Restore(backup, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := os.ReadFile(restored.IconPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ikon tidak dipasang: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "ikon dari internet") {
|
||||
t.Errorf("ikon tidak diunduh ulang dari sumber: %q", data)
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,11 @@ func Uninstall(sc store.Shortcut, o RemoveOptions) error {
|
||||
}
|
||||
}
|
||||
if o.Binary && sc.Type == store.TypeExecutable && sc.BinaryPath != "" {
|
||||
if err := os.Remove(sc.BinaryPath); err != nil && !os.IsNotExist(err) {
|
||||
errs = append(errs, err)
|
||||
// script punya dua berkas: wrapper + script aslinya
|
||||
for _, path := range []string{sc.BinaryPath, sc.BinaryPath + scriptSuffix} {
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user