Files
CodeNomad/packages/ui/src/lib/native/desktop-file-drop.ts
Shantur Rathore 3b411e2e73 fix(ui): gate desktop privileges by host and window context (#347)
Don't let remote server windows use local features like local file browser etc
2026-04-20 20:28:11 +01:00

147 lines
4.0 KiB
TypeScript

import { listen } from "@tauri-apps/api/event"
import { getLogger } from "../logger"
import { canUseDesktopFolderDrop, isElectronHost, isTauriHost, runtimeEnv } from "../runtime-env"
const log = getLogger("actions")
type NativeFolderDropState = "enter" | "leave"
interface TauriFolderDropPayload {
paths?: unknown
}
function normalizePathList(input: unknown): string[] {
if (!Array.isArray(input)) {
return []
}
return input.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
}
function getFilePath(file: File): string | null {
if (typeof file.path === "string" && file.path.trim().length > 0) {
return file.path
}
if (isElectronHost()) {
const electronPath = (window as Window & { electronAPI?: ElectronAPI }).electronAPI?.getPathForFile?.(file)
if (typeof electronPath === "string" && electronPath.trim().length > 0) {
return electronPath
}
}
return null
}
async function resolveElectronDirectoryPaths(paths: string[]): Promise<string[]> {
const api = (window as Window & { electronAPI?: ElectronAPI }).electronAPI
if (!api?.getDirectoryPaths || paths.length === 0) {
return []
}
try {
return await api.getDirectoryPaths(paths)
} catch (error) {
log.error("[native] failed to validate dropped directory paths", error)
return []
}
}
export function supportsDesktopFolderDrop(): boolean {
return runtimeEnv.platform === "desktop" && canUseDesktopFolderDrop()
}
export function containsFileDrop(event: DragEvent): boolean {
const types = event.dataTransfer?.types
if (!types) {
return false
}
return Array.from(types).includes("Files")
}
export function extractDroppedDirectoryPaths(event: DragEvent): string[] {
const dataTransfer = event.dataTransfer
if (!dataTransfer) {
return []
}
const directoryHints = new Set<string>()
for (const item of Array.from(dataTransfer.items ?? [])) {
if (item.kind !== "file") {
continue
}
const entry = item.webkitGetAsEntry?.()
if (!entry?.isDirectory) {
continue
}
const file = item.getAsFile()
const filePath = file ? getFilePath(file) : null
if (filePath) {
directoryHints.add(filePath)
}
}
const paths = new Set<string>()
for (const file of Array.from(dataTransfer.files ?? [])) {
const filePath = getFilePath(file)
if (!filePath) {
continue
}
if (directoryHints.size > 0 && !directoryHints.has(filePath)) {
continue
}
paths.add(filePath)
}
return Array.from(paths)
}
export async function normalizeDroppedDirectoryPaths(paths: string[]): Promise<string[]> {
const uniquePaths = Array.from(new Set(paths.filter((path) => typeof path === "string" && path.trim().length > 0)))
if (uniquePaths.length === 0) {
return []
}
if (isElectronHost()) {
return resolveElectronDirectoryPaths(uniquePaths)
}
return uniquePaths
}
export async function listenForNativeFolderDrops(onDrop: (paths: string[]) => void): Promise<() => void> {
if (!isTauriHost()) {
return () => {}
}
try {
const unlisten = await listen("desktop:folder-drop", (event) => {
const payload = (event.payload ?? {}) as TauriFolderDropPayload
const paths = normalizePathList(payload.paths)
if (paths.length > 0) {
onDrop(paths)
}
})
return () => {
unlisten()
}
} catch (error) {
log.error("[native] failed to listen for folder-drop event", error)
return () => {}
}
}
export async function listenForNativeFolderDropState(onState: (state: NativeFolderDropState) => void): Promise<() => void> {
if (!isTauriHost()) {
return () => {}
}
try {
const [unlistenEnter, unlistenLeave] = await Promise.all([
listen("desktop:folder-drag-enter", () => onState("enter")),
listen("desktop:folder-drag-leave", () => onState("leave")),
])
return () => {
unlistenEnter()
unlistenLeave()
}
} catch (error) {
log.error("[native] failed to listen for folder-drop state", error)
return () => {}
}
}