Add public external file open API
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package externalopen
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type Runner func(name string, args ...string) error
|
||||
|
||||
type Service struct {
|
||||
goos string
|
||||
runner Runner
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return NewServiceFor(runtime.GOOS, func(name string, args ...string) error {
|
||||
return exec.Command(name, args...).Start()
|
||||
})
|
||||
}
|
||||
|
||||
func NewServiceFor(goos string, runner Runner) *Service {
|
||||
return &Service{goos: goos, runner: runner}
|
||||
}
|
||||
|
||||
func (s *Service) OpenPath(path string) error {
|
||||
name, args := s.openCommand(path)
|
||||
return s.runner(name, args...)
|
||||
}
|
||||
|
||||
func (s *Service) ShowInFolder(path string, isDir bool) error {
|
||||
name, args := s.showCommand(path, isDir)
|
||||
return s.runner(name, args...)
|
||||
}
|
||||
|
||||
func (s *Service) openCommand(path string) (string, []string) {
|
||||
switch s.goos {
|
||||
case "darwin":
|
||||
return "open", []string{path}
|
||||
case "windows":
|
||||
return "rundll32", []string{"url.dll,FileProtocolHandler", path}
|
||||
default:
|
||||
return "xdg-open", []string{path}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) showCommand(path string, isDir bool) (string, []string) {
|
||||
switch s.goos {
|
||||
case "darwin":
|
||||
return "open", []string{"-R", path}
|
||||
case "windows":
|
||||
if isDir {
|
||||
return "explorer", []string{path}
|
||||
}
|
||||
return "explorer", []string{"/select," + path}
|
||||
default:
|
||||
if isDir {
|
||||
return "xdg-open", []string{path}
|
||||
}
|
||||
return "xdg-open", []string{filepath.Dir(path)}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package externalopen
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpenPathCommands(t *testing.T) {
|
||||
cases := []struct {
|
||||
goos string
|
||||
path string
|
||||
want commandCall
|
||||
}{
|
||||
{goos: "linux", path: "/vault/file.txt", want: commandCall{name: "xdg-open", args: []string{"/vault/file.txt"}}},
|
||||
{goos: "darwin", path: "/vault/file.txt", want: commandCall{name: "open", args: []string{"/vault/file.txt"}}},
|
||||
{goos: "windows", path: `C:\Vault\file.txt`, want: commandCall{name: "rundll32", args: []string{"url.dll,FileProtocolHandler", `C:\Vault\file.txt`}}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.goos, func(t *testing.T) {
|
||||
var got commandCall
|
||||
svc := NewServiceFor(tc.goos, func(name string, args ...string) error {
|
||||
got = commandCall{name: name, args: args}
|
||||
return nil
|
||||
})
|
||||
if err := svc.OpenPath(tc.path); err != nil {
|
||||
t.Fatalf("OpenPath: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Fatalf("command = %#v, want %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowInFolderCommands(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
goos string
|
||||
path string
|
||||
isDir bool
|
||||
want commandCall
|
||||
}{
|
||||
{name: "linux file opens parent", goos: "linux", path: "/vault/Docs/file.txt", want: commandCall{name: "xdg-open", args: []string{"/vault/Docs"}}},
|
||||
{name: "linux dir opens dir", goos: "linux", path: "/vault/Docs", isDir: true, want: commandCall{name: "xdg-open", args: []string{"/vault/Docs"}}},
|
||||
{name: "darwin reveal", goos: "darwin", path: "/vault/file.txt", want: commandCall{name: "open", args: []string{"-R", "/vault/file.txt"}}},
|
||||
{name: "windows select file", goos: "windows", path: `C:\Vault\file.txt`, want: commandCall{name: "explorer", args: []string{`/select,C:\Vault\file.txt`}}},
|
||||
{name: "windows open dir", goos: "windows", path: `C:\Vault\Docs`, isDir: true, want: commandCall{name: "explorer", args: []string{`C:\Vault\Docs`}}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var got commandCall
|
||||
svc := NewServiceFor(tc.goos, func(name string, args ...string) error {
|
||||
got = commandCall{name: name, args: args}
|
||||
return nil
|
||||
})
|
||||
if err := svc.ShowInFolder(tc.path, tc.isDir); err != nil {
|
||||
t.Fatalf("ShowInFolder: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Fatalf("command = %#v, want %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type commandCall struct {
|
||||
name string
|
||||
args []string
|
||||
}
|
||||
@@ -88,6 +88,31 @@ func (s *Service) GetVaultFileMetadata(relativePath string) (FileMetadata, error
|
||||
return makeMetadata(rel, info), nil
|
||||
}
|
||||
|
||||
func (s *Service) ResolveExternalOpenTarget(relativePath string) (ExternalOpenTarget, error) {
|
||||
root, rel, full, err := s.resolveFile(relativePath)
|
||||
if err != nil {
|
||||
return ExternalOpenTarget{}, err
|
||||
}
|
||||
if err := rejectSymlinkPath(root, rel, true); err != nil {
|
||||
return ExternalOpenTarget{}, err
|
||||
}
|
||||
info, err := os.Lstat(full)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return ExternalOpenTarget{}, fmt.Errorf("not-found: %s", rel)
|
||||
}
|
||||
return ExternalOpenTarget{}, err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return ExternalOpenTarget{}, fmt.Errorf("symlink-not-allowed: %s", rel)
|
||||
}
|
||||
return ExternalOpenTarget{
|
||||
RelativePath: rel,
|
||||
AbsolutePath: full,
|
||||
Metadata: makeMetadata(rel, info),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) ReadVaultTextFile(relativePath string) (string, error) {
|
||||
root, rel, full, err := s.resolveFile(relativePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -39,6 +39,12 @@ type FileMetadata struct {
|
||||
CanWrite bool `json:"canWrite"`
|
||||
}
|
||||
|
||||
type ExternalOpenTarget struct {
|
||||
RelativePath string `json:"relativePath"`
|
||||
AbsolutePath string `json:"absolutePath"`
|
||||
Metadata FileMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type WriteOptions struct {
|
||||
CreateIfMissing bool `json:"createIfMissing"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
|
||||
@@ -36,6 +36,7 @@ func (r *Registry) registerDefaults() {
|
||||
{Name: "files.read", Description: "List files and read text files through the vault Files API", Dangerous: false},
|
||||
{Name: "files.write", Description: "Create folders, write text files, and move paths through the vault Files API", Dangerous: true},
|
||||
{Name: "files.delete", Description: "Trash vault files and folders through the vault Files API", Dangerous: true},
|
||||
{Name: "files.openExternal", Description: "Open vault files and folders in external OS applications", Dangerous: true},
|
||||
{Name: "storage.namespace", Description: "Read/write plugin's own storage namespace", Dangerous: false},
|
||||
{Name: "storage.migrations", Description: "Run database migrations in plugin namespace", Dangerous: false},
|
||||
{Name: "events.publish", Description: "Publish events to the event bus", Dangerous: false},
|
||||
|
||||
Reference in New Issue
Block a user