Add public external file open API

This commit is contained in:
2026-06-27 13:30:31 +08:00
parent 6cc37972d1
commit 173cc93258
13 changed files with 367 additions and 10 deletions
+52
View File
@@ -17,6 +17,7 @@ import (
"github.com/verstak/verstak-desktop/internal/core/capability"
"github.com/verstak/verstak-desktop/internal/core/contribution"
"github.com/verstak/verstak-desktop/internal/core/events"
"github.com/verstak/verstak-desktop/internal/core/externalopen"
corefiles "github.com/verstak/verstak-desktop/internal/core/files"
"github.com/verstak/verstak-desktop/internal/core/permissions"
"github.com/verstak/verstak-desktop/internal/core/plugin"
@@ -40,6 +41,7 @@ type App struct {
vault *vault.Vault
storage *storage.Storage
files *corefiles.Service
externalOpen externalOpenService
appSettings *appsettings.Manager
pluginState *pluginstate.Manager
workbench *coreworkbench.Router
@@ -48,6 +50,11 @@ type App struct {
debug bool
}
type externalOpenService interface {
OpenPath(path string) error
ShowInFolder(path string, isDir bool) error
}
// NewApp creates a new App instance.
func NewApp(
capReg *capability.Registry,
@@ -73,6 +80,7 @@ func NewApp(
vault: vaultService,
storage: storageService,
files: filesService,
externalOpen: externalopen.NewService(),
appSettings: appSettingsMgr,
pluginState: pluginStateMgr,
workbench: coreworkbench.NewRouter(workbenchPrefsFromSettings(appSettingsMgr)),
@@ -749,6 +757,50 @@ func (a *App) TrashVaultPath(pluginID, relativePath string) (corefiles.TrashResu
return result, ""
}
// OpenVaultPathExternal opens a vault-relative file or folder in the OS default app.
func (a *App) OpenVaultPathExternal(pluginID, relativePath string) string {
if _, err := a.requirePluginAccess(pluginID, "files.openExternal"); err != nil {
return err.Error()
}
if a.files == nil {
return "files service not initialized"
}
target, err := a.files.ResolveExternalOpenTarget(relativePath)
if err != nil {
return err.Error()
}
if err := a.externalOpenService().OpenPath(target.AbsolutePath); err != nil {
return err.Error()
}
return ""
}
// ShowVaultPathInFolder reveals a vault-relative file or folder in the OS file manager.
func (a *App) ShowVaultPathInFolder(pluginID, relativePath string) string {
if _, err := a.requirePluginAccess(pluginID, "files.openExternal"); err != nil {
return err.Error()
}
if a.files == nil {
return "files service not initialized"
}
target, err := a.files.ResolveExternalOpenTarget(relativePath)
if err != nil {
return err.Error()
}
isDir := target.Metadata.Type == corefiles.FileTypeFolder
if err := a.externalOpenService().ShowInFolder(target.AbsolutePath, isDir); err != nil {
return err.Error()
}
return ""
}
func (a *App) externalOpenService() externalOpenService {
if a.externalOpen != nil {
return a.externalOpen
}
return externalopen.NewService()
}
func (a *App) recordFileSyncOp(entityType, entityID, opType string, payload interface{}) error {
if a.syncSvc == nil {
return nil
+83
View File
@@ -82,6 +82,22 @@ func newFilesTestApp(t *testing.T, perms []string) (*App, string) {
}, v.GetVaultPath()
}
type testExternalOpenService struct {
open func(path string) error
}
func newTestExternalOpenService(open func(path string) error) *testExternalOpenService {
return &testExternalOpenService{open: open}
}
func (s *testExternalOpenService) OpenPath(path string) error {
return s.open(path)
}
func (s *testExternalOpenService) ShowInFolder(path string, _ bool) error {
return s.open(path)
}
func newSyncFilesTestApp(t *testing.T, perms []string, deviceID string) (*App, string) {
t.Helper()
app, root := newFilesTestApp(t, perms)
@@ -358,6 +374,61 @@ func TestFilesBridgeReadWriteListMoveTrash(t *testing.T) {
}
}
func TestFilesBridgeOpenExternalUsesVaultPathPolicyAndPermission(t *testing.T) {
app, root := newFilesTestApp(t, []string{"files.openExternal"})
filePath := filepath.Join(root, "Docs", "one.txt")
if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filePath, []byte("hello"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
var opened []string
app.externalOpen = newTestExternalOpenService(func(path string) error {
opened = append(opened, path)
return nil
})
if errStr := app.OpenVaultPathExternal("files.plugin", "Docs/one.txt"); errStr != "" {
t.Fatalf("OpenVaultPathExternal: %s", errStr)
}
if len(opened) != 1 || opened[0] != filePath {
t.Fatalf("opened = %#v, want %q", opened, filePath)
}
if errStr := app.OpenVaultPathExternal("files.plugin", ".verstak/vault.json"); errStr == "" || !strings.Contains(errStr, "reserved-path") {
t.Fatalf("reserved path error = %q, want reserved-path", errStr)
}
if len(opened) != 1 {
t.Fatalf("reserved path should not open, opened = %#v", opened)
}
}
func TestFilesBridgeShowInFolderUsesVaultPathPolicyAndPermission(t *testing.T) {
app, root := newFilesTestApp(t, []string{"files.openExternal"})
filePath := filepath.Join(root, "Docs", "one.txt")
if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filePath, []byte("hello"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
var shown []string
app.externalOpen = newTestExternalOpenService(func(path string) error {
shown = append(shown, path)
return nil
})
if errStr := app.ShowVaultPathInFolder("files.plugin", "Docs/one.txt"); errStr != "" {
t.Fatalf("ShowVaultPathInFolder: %s", errStr)
}
if len(shown) != 1 || shown[0] != filePath {
t.Fatalf("shown = %#v, want %q", shown, filePath)
}
}
func TestFilesBridgePermissions(t *testing.T) {
cases := []struct {
name string
@@ -411,6 +482,18 @@ func TestFilesBridgePermissions(t *testing.T) {
call: func(app *App) string { _, errStr := app.TrashVaultPath("files.plugin", "one.txt"); return errStr },
wantPhrase: "files.delete",
},
{
name: "open external requires openExternal",
perms: []string{"files.read", "files.write", "files.delete"},
call: func(app *App) string { return app.OpenVaultPathExternal("files.plugin", "one.txt") },
wantPhrase: "files.openExternal",
},
{
name: "show in folder requires openExternal",
perms: []string{"files.read", "files.write", "files.delete"},
call: func(app *App) string { return app.ShowVaultPathInFolder("files.plugin", "one.txt") },
wantPhrase: "files.openExternal",
},
}
for _, tc := range cases {