150 lines
3.7 KiB
Go
150 lines
3.7 KiB
Go
package deploy
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
const desktopPathPrefix = "@desktop/"
|
|
|
|
// UserDesktopDir returns the interactive user's Desktop folder for the current OS.
|
|
func UserDesktopDir() (string, error) {
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
return windowsDesktopDir()
|
|
case "darwin":
|
|
return unixDesktopFromHome("Desktop")
|
|
default:
|
|
return linuxDesktopDir()
|
|
}
|
|
}
|
|
|
|
func windowsDesktopDir() (string, error) {
|
|
profile := strings.TrimSpace(os.Getenv("USERPROFILE"))
|
|
if profile == "" {
|
|
return "", fmt.Errorf("USERPROFILE not set")
|
|
}
|
|
candidates := []string{
|
|
filepath.Join(profile, "Desktop"),
|
|
filepath.Join(profile, "OneDrive", "Desktop"),
|
|
filepath.Join(profile, "OneDrive - Personal", "Desktop"),
|
|
}
|
|
for _, c := range candidates {
|
|
if st, err := os.Stat(c); err == nil && st.IsDir() {
|
|
return filepath.Clean(c), nil
|
|
}
|
|
}
|
|
// Create default Desktop if missing (unusual profiles)
|
|
fallback := candidates[0]
|
|
if err := os.MkdirAll(fallback, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
return fallback, nil
|
|
}
|
|
|
|
func unixDesktopFromHome(sub string) (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil || home == "" {
|
|
return "", fmt.Errorf("home directory unavailable")
|
|
}
|
|
desktop := filepath.Join(home, sub)
|
|
if st, err := os.Stat(desktop); err == nil && st.IsDir() {
|
|
return filepath.Clean(desktop), nil
|
|
}
|
|
if err := os.MkdirAll(desktop, 0o755); err != nil {
|
|
return "", fmt.Errorf("desktop: %w", err)
|
|
}
|
|
return desktop, nil
|
|
}
|
|
|
|
func linuxDesktopDir() (string, error) {
|
|
if out, err := exec.Command("xdg-user-dir", "DESKTOP").Output(); err == nil {
|
|
p := strings.TrimSpace(string(out))
|
|
if p != "" {
|
|
if st, err := os.Stat(p); err == nil && st.IsDir() {
|
|
return filepath.Clean(p), nil
|
|
}
|
|
}
|
|
}
|
|
return unixDesktopFromHome("Desktop")
|
|
}
|
|
|
|
// ResolveDesktopFile joins a sanitized filename onto the user Desktop.
|
|
func ResolveDesktopFile(filename string) (string, error) {
|
|
desktop, err := UserDesktopDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
name := sanitizeDesktopFilename(filename)
|
|
if name == "" {
|
|
name = "upload.bin"
|
|
}
|
|
return filepath.Join(desktop, name), nil
|
|
}
|
|
|
|
func sanitizeDesktopFilename(name string) string {
|
|
name = strings.TrimSpace(name)
|
|
name = strings.ReplaceAll(name, "\\", "/")
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
// Allow subfolders under Desktop but block traversal.
|
|
parts := strings.Split(name, "/")
|
|
var clean []string
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" || p == "." || p == ".." {
|
|
continue
|
|
}
|
|
clean = append(clean, p)
|
|
}
|
|
return filepath.Join(clean...)
|
|
}
|
|
|
|
func remotePathHasTraversal(remote string) bool {
|
|
remote = strings.TrimSpace(remote)
|
|
if remote == "" {
|
|
return false
|
|
}
|
|
if strings.HasPrefix(remote, "~/") {
|
|
remote = remote[2:]
|
|
}
|
|
remote = strings.ReplaceAll(remote, "\\", "/")
|
|
for _, part := range strings.Split(remote, "/") {
|
|
if part == ".." {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ResolveRemotePath expands @desktop/…, desktop:…, and ~/… for upload/download commands.
|
|
func ResolveRemotePath(remote string) (string, error) {
|
|
remote = strings.TrimSpace(remote)
|
|
if remote == "" {
|
|
return "", fmt.Errorf("remote path is empty")
|
|
}
|
|
if remotePathHasTraversal(remote) {
|
|
return "", fmt.Errorf("path traversal (..) is not allowed")
|
|
}
|
|
lower := strings.ToLower(remote)
|
|
if strings.HasPrefix(lower, "desktop:") {
|
|
return ResolveDesktopFile(remote[len("desktop:"):])
|
|
}
|
|
if strings.HasPrefix(remote, desktopPathPrefix) {
|
|
return ResolveDesktopFile(remote[len(desktopPathPrefix):])
|
|
}
|
|
if strings.HasPrefix(remote, "~/") {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Clean(filepath.Join(home, remote[2:])), nil
|
|
}
|
|
return filepath.Clean(remote), nil
|
|
}
|