mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-27 15:02:50 -05:00
clipboard: fix file transfer & export functionality
- grants read to all installed flatpak apps
This commit is contained in:
@@ -23,7 +23,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
@@ -920,55 +919,99 @@ func downloadToTempFile(rawURL string) (string, error) {
|
||||
}
|
||||
|
||||
func copyFileToClipboard(filePath string) error {
|
||||
fileURI := "file://" + filePath
|
||||
exportedPath, err := exportFileForFlatpak(filePath)
|
||||
if err != nil {
|
||||
log.Warnf("document export unavailable: %v, using local path", err)
|
||||
exportedPath = filePath
|
||||
}
|
||||
fileURI := "file://" + exportedPath
|
||||
|
||||
transferKey, err := startPortalFileTransfer(filePath)
|
||||
if err != nil {
|
||||
log.Warnf("portal file transfer unavailable: %v", err)
|
||||
}
|
||||
|
||||
offers := []clipboard.Offer{
|
||||
{MimeType: "text/uri-list", Data: []byte(fileURI + "\r\n")},
|
||||
}
|
||||
portalOnly := os.Getenv("DMS_PORTAL_ONLY") == "1"
|
||||
|
||||
var offers []clipboard.Offer
|
||||
if transferKey != "" {
|
||||
offers = append(offers, clipboard.Offer{
|
||||
MimeType: "application/vnd.portal.filetransfer",
|
||||
Data: []byte(transferKey),
|
||||
})
|
||||
}
|
||||
if !portalOnly {
|
||||
offers = append(offers, clipboard.Offer{
|
||||
MimeType: "text/uri-list",
|
||||
Data: []byte(fileURI + "\r\n"),
|
||||
})
|
||||
}
|
||||
|
||||
if len(offers) == 0 {
|
||||
return fmt.Errorf("no clipboard offers available")
|
||||
}
|
||||
|
||||
return clipboard.CopyMulti(offers, clipCopyForeground, clipCopyPasteOnce)
|
||||
}
|
||||
|
||||
func exportFileForFlatpak(filePath string) (string, error) {
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "clipboard.exportFile",
|
||||
Params: map[string]any{
|
||||
"filePath": filePath,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := sendServerRequest(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("server request: %w", err)
|
||||
}
|
||||
|
||||
if resp.Error != "" {
|
||||
return "", fmt.Errorf("server error: %s", resp.Error)
|
||||
}
|
||||
|
||||
result, ok := (*resp.Result).(map[string]any)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid response format")
|
||||
}
|
||||
|
||||
path, ok := result["path"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing path in response")
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func startPortalFileTransfer(filePath string) (string, error) {
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "clipboard.startFileTransfer",
|
||||
Params: map[string]any{
|
||||
"filePath": filePath,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := sendServerRequest(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("connect session bus: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
portal := conn.Object("org.freedesktop.portal.Documents", "/org/freedesktop/portal/documents")
|
||||
|
||||
var key string
|
||||
options := map[string]dbus.Variant{
|
||||
"writable": dbus.MakeVariant(false),
|
||||
"autostop": dbus.MakeVariant(true),
|
||||
}
|
||||
if err := portal.Call("org.freedesktop.portal.FileTransfer.StartTransfer", 0, options).Store(&key); err != nil {
|
||||
return "", fmt.Errorf("start transfer: %w", err)
|
||||
return "", fmt.Errorf("server request: %w", err)
|
||||
}
|
||||
|
||||
fd, err := syscall.Open(filePath, syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open file: %w", err)
|
||||
if resp.Error != "" {
|
||||
return "", fmt.Errorf("server error: %s", resp.Error)
|
||||
}
|
||||
|
||||
addOptions := map[string]dbus.Variant{}
|
||||
if err := portal.Call("org.freedesktop.portal.FileTransfer.AddFiles", 0, key, []dbus.UnixFD{dbus.UnixFD(fd)}, addOptions).Err; err != nil {
|
||||
syscall.Close(fd)
|
||||
return "", fmt.Errorf("add files: %w", err)
|
||||
result, ok := (*resp.Result).(map[string]any)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid response format")
|
||||
}
|
||||
|
||||
key, ok := result["key"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing key in response")
|
||||
}
|
||||
syscall.Close(fd)
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
@@ -45,6 +45,10 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
||||
handleGetPinnedEntries(conn, req, m)
|
||||
case "clipboard.getPinnedCount":
|
||||
handleGetPinnedCount(conn, req, m)
|
||||
case "clipboard.startFileTransfer":
|
||||
handleStartFileTransfer(conn, req, m)
|
||||
case "clipboard.exportFile":
|
||||
handleExportFile(conn, req, m)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
||||
}
|
||||
@@ -281,3 +285,35 @@ func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
|
||||
count := m.GetPinnedCount()
|
||||
models.Respond(conn, req.ID, map[string]int{"count": count})
|
||||
}
|
||||
|
||||
func handleStartFileTransfer(conn net.Conn, req models.Request, m *Manager) {
|
||||
filePath, err := params.String(req.Params, "filePath")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
key, err := m.StartFileTransfer(filePath)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, map[string]string{"key": key})
|
||||
}
|
||||
|
||||
func handleExportFile(conn net.Conn, req models.Request, m *Manager) {
|
||||
filePath, err := params.String(req.Params, "filePath")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
exportedPath, err := m.ExportFileForFlatpak(filePath)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, map[string]string{"path": exportedPath})
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
_ "image/png"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/godbus/dbus/v5"
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
_ "golang.org/x/image/webp"
|
||||
@@ -1529,3 +1531,133 @@ func (m *Manager) GetPinnedCount() int {
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func (m *Manager) StartFileTransfer(filePath string) (string, error) {
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
return "", fmt.Errorf("file not found: %w", err)
|
||||
}
|
||||
|
||||
if m.dbusConn == nil {
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("connect session bus: %w", err)
|
||||
}
|
||||
if !conn.SupportsUnixFDs() {
|
||||
conn.Close()
|
||||
return "", fmt.Errorf("D-Bus connection does not support Unix FD passing")
|
||||
}
|
||||
m.dbusConn = conn
|
||||
}
|
||||
|
||||
portal := m.dbusConn.Object("org.freedesktop.portal.Documents", "/org/freedesktop/portal/documents")
|
||||
|
||||
var key string
|
||||
options := map[string]dbus.Variant{
|
||||
"writable": dbus.MakeVariant(false),
|
||||
"autostop": dbus.MakeVariant(false),
|
||||
}
|
||||
if err := portal.Call("org.freedesktop.portal.FileTransfer.StartTransfer", 0, options).Store(&key); err != nil {
|
||||
return "", fmt.Errorf("start transfer: %w", err)
|
||||
}
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
if _, err := file.Seek(0, 0); err != nil {
|
||||
file.Close()
|
||||
return "", fmt.Errorf("seek file: %w", err)
|
||||
}
|
||||
fd := int(file.Fd())
|
||||
|
||||
addOptions := map[string]dbus.Variant{}
|
||||
if err := portal.Call("org.freedesktop.portal.FileTransfer.AddFiles", 0, key, []dbus.UnixFD{dbus.UnixFD(fd)}, addOptions).Err; err != nil {
|
||||
file.Close()
|
||||
return "", fmt.Errorf("add files: %w", err)
|
||||
}
|
||||
m.transferFiles = append(m.transferFiles, file)
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ExportFileForFlatpak(filePath string) (string, error) {
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
return "", fmt.Errorf("file not found: %w", err)
|
||||
}
|
||||
|
||||
if m.dbusConn == nil {
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("connect session bus: %w", err)
|
||||
}
|
||||
if !conn.SupportsUnixFDs() {
|
||||
conn.Close()
|
||||
return "", fmt.Errorf("D-Bus connection does not support Unix FD passing")
|
||||
}
|
||||
m.dbusConn = conn
|
||||
}
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
fd := int(file.Fd())
|
||||
|
||||
portal := m.dbusConn.Object("org.freedesktop.portal.Documents", "/org/freedesktop/portal/documents")
|
||||
|
||||
var docIds []string
|
||||
var extra map[string]dbus.Variant
|
||||
flags := uint32(0)
|
||||
|
||||
err = portal.Call(
|
||||
"org.freedesktop.portal.Documents.AddFull",
|
||||
0,
|
||||
[]dbus.UnixFD{dbus.UnixFD(fd)},
|
||||
flags,
|
||||
"",
|
||||
[]string{},
|
||||
).Store(&docIds, &extra)
|
||||
|
||||
file.Close()
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("AddFull: %w", err)
|
||||
}
|
||||
|
||||
if len(docIds) == 0 {
|
||||
return "", fmt.Errorf("no doc IDs returned")
|
||||
}
|
||||
|
||||
docId := docIds[0]
|
||||
|
||||
for _, app := range getInstalledFlatpaks() {
|
||||
_ = portal.Call(
|
||||
"org.freedesktop.portal.Documents.GrantPermissions",
|
||||
0,
|
||||
docId,
|
||||
app,
|
||||
[]string{"read"},
|
||||
).Err
|
||||
}
|
||||
|
||||
uid := os.Getuid()
|
||||
basename := filepath.Base(filePath)
|
||||
exportedPath := fmt.Sprintf("/run/user/%d/doc/%s/%s", uid, docId, basename)
|
||||
|
||||
return exportedPath, nil
|
||||
}
|
||||
|
||||
func getInstalledFlatpaks() []string {
|
||||
out, err := exec.Command("flatpak", "list", "--app", "--columns=application").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var apps []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
if app := strings.TrimSpace(line); app != "" {
|
||||
apps = append(apps, app)
|
||||
}
|
||||
}
|
||||
return apps
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
||||
@@ -157,6 +158,9 @@ type Manager struct {
|
||||
dirty chan struct{}
|
||||
notifierWg sync.WaitGroup
|
||||
lastState *State
|
||||
|
||||
dbusConn *dbus.Conn
|
||||
transferFiles []*os.File // Keep files open for portal transfers
|
||||
}
|
||||
|
||||
func (m *Manager) GetState() State {
|
||||
|
||||
Reference in New Issue
Block a user