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
158 lines
4.2 KiB
Go
158 lines
4.2 KiB
Go
package shortcut
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"lazyrun/internal/xdg"
|
|
)
|
|
|
|
func validate(name string, menu, desktop bool) error {
|
|
if name == "" {
|
|
return errors.New("name is required")
|
|
}
|
|
if strings.ContainsAny(name, "\n\r") {
|
|
return errors.New("name must not contain line breaks")
|
|
}
|
|
if !menu && !desktop {
|
|
return errors.New("pick at least one location (menu or desktop)")
|
|
}
|
|
if slugify(name) == "" {
|
|
return errors.New("name must contain at least one letter or digit")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkRunnable memastikan file adalah ELF (binary/AppImage) atau script dengan shebang.
|
|
func checkRunnable(path string) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
st, err := f.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if st.IsDir() {
|
|
return errors.New("this is a directory, not a file")
|
|
}
|
|
|
|
head := make([]byte, 4)
|
|
n, _ := io.ReadFull(f, head)
|
|
head = head[:n]
|
|
if bytes.HasPrefix(head, []byte{0x7f, 'E', 'L', 'F'}) ||
|
|
bytes.HasPrefix(head, []byte("#!")) ||
|
|
bytes.HasPrefix(head, []byte("MZ")) { // .exe Windows (dijalankan lewat wine)
|
|
return nil
|
|
}
|
|
return errors.New("not an executable (ELF, AppImage, script, or Windows .exe)")
|
|
}
|
|
|
|
// copyFile menyalin src ke dst lewat file sementara supaya dst tidak pernah setengah tertulis.
|
|
func copyFile(src, dst string, mode os.FileMode) error {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp, err := os.CreateTemp(filepath.Dir(dst), ".lazyrun-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
|
|
if _, err := io.Copy(tmp, in); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Chmod(mode); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp.Name(), dst)
|
|
}
|
|
|
|
func samePath(a, b string) bool {
|
|
sa, err := os.Stat(a)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
sb, err := os.Stat(b)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return os.SameFile(sa, sb)
|
|
}
|
|
|
|
// desktopCopy menyalin .desktop ke ~/Desktop agar bisa diklik ganda.
|
|
func desktopCopy(desktopPath, slug string) (string, error) {
|
|
dst := filepath.Join(xdg.DesktopDir(), slug+".desktop")
|
|
if _, err := os.Stat(dst); err == nil && !samePath(dst, desktopPath) {
|
|
return "", errors.New(dst + " already exists")
|
|
}
|
|
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, "'", `'\''`) + "'"
|
|
}
|