Skip to content

Commit 135685b

Browse files
author
DoSun
committed
fix(workspace): normalize stored directory paths
Store workspace directory paths in a canonical format and normalize lookup paths so notifications can resolve Windows-style workspace paths reliably.
1 parent 189c832 commit 135685b

4 files changed

Lines changed: 126 additions & 39 deletions

File tree

server/internal/notification/handlers_test.go

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"net/http/httptest"
88
"testing"
99

10-
"github.com/costrict/costrict-web/server/internal/models"
1110
"github.com/costrict/costrict-web/server/internal/middleware"
1211
"github.com/costrict/costrict-web/server/internal/systemrole"
1312
"github.com/gin-gonic/gin"
@@ -24,7 +23,8 @@ func setupNotificationTestDB(t *testing.T) *gorm.DB {
2423
}
2524
stmts := []string{
2625
`CREATE TABLE users (
27-
id TEXT PRIMARY KEY,
26+
id INTEGER PRIMARY KEY,
27+
subject_id TEXT,
2828
username TEXT NOT NULL,
2929
display_name TEXT,
3030
email TEXT,
@@ -96,14 +96,11 @@ func setupNotificationTestDB(t *testing.T) *gorm.DB {
9696
t.Fatalf("migrate test db: %v", err)
9797
}
9898
}
99-
seedUsers := []models.User{
100-
{ID: "u1", Username: "u1", IsActive: true},
101-
{ID: "u2", Username: "u2", IsActive: true},
99+
if err := db.Exec(`INSERT INTO users (id, subject_id, username, is_active) VALUES (?, ?, ?, ?)`, 1, "u1", "u1", true).Error; err != nil {
100+
t.Fatalf("seed user u1: %v", err)
102101
}
103-
for _, user := range seedUsers {
104-
if err := db.Create(&user).Error; err != nil {
105-
t.Fatalf("seed user: %v", err)
106-
}
102+
if err := db.Exec(`INSERT INTO users (id, subject_id, username, is_active) VALUES (?, ?, ?, ?)`, 2, "u2", "u2", true).Error; err != nil {
103+
t.Fatalf("seed user u2: %v", err)
107104
}
108105
return db
109106
}
@@ -196,3 +193,53 @@ func TestAdminNotificationRoutesRequirePlatformAdmin(t *testing.T) {
196193
t.Fatalf("expected 200, got %d, body=%s", w.Code, w.Body.String())
197194
}
198195
}
196+
197+
func TestGetWorkspaceIDNormalizesWindowsPath(t *testing.T) {
198+
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
199+
if err != nil {
200+
t.Fatalf("open test db: %v", err)
201+
}
202+
203+
stmts := []string{
204+
`CREATE TABLE devices (
205+
id TEXT PRIMARY KEY,
206+
device_id TEXT NOT NULL,
207+
deleted_at DATETIME
208+
)`,
209+
`CREATE TABLE workspaces (
210+
id TEXT PRIMARY KEY,
211+
device_id TEXT NOT NULL,
212+
deleted_at DATETIME
213+
)`,
214+
`CREATE TABLE workspace_directories (
215+
id TEXT PRIMARY KEY,
216+
workspace_id TEXT NOT NULL,
217+
path TEXT NOT NULL,
218+
deleted_at DATETIME
219+
)`,
220+
}
221+
for _, stmt := range stmts {
222+
if err := db.Exec(stmt).Error; err != nil {
223+
t.Fatalf("migrate test db: %v", err)
224+
}
225+
}
226+
227+
if err := db.Exec(`INSERT INTO devices (id, device_id) VALUES (?, ?)`, "dev-uuid-1", "device-1").Error; err != nil {
228+
t.Fatalf("seed device: %v", err)
229+
}
230+
if err := db.Exec(`INSERT INTO workspaces (id, device_id) VALUES (?, ?)`, "ws-1", "dev-uuid-1").Error; err != nil {
231+
t.Fatalf("seed workspace: %v", err)
232+
}
233+
if err := db.Exec(`INSERT INTO workspace_directories (id, workspace_id, path) VALUES (?, ?, ?)`, "wd-1", "ws-1", "D:/DEV/myclaw").Error; err != nil {
234+
t.Fatalf("seed workspace directory: %v", err)
235+
}
236+
237+
svc := NewNotificationService(db, "")
238+
workspaceID, err := svc.getWorkspaceID("device-1", `D:\DEV\myclaw`)
239+
if err != nil {
240+
t.Fatalf("get workspace id: %v", err)
241+
}
242+
if workspaceID != "ws-1" {
243+
t.Fatalf("expected workspace id ws-1, got %s", workspaceID)
244+
}
245+
}

server/internal/notification/service.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/costrict/costrict-web/server/internal/models"
1111
"github.com/costrict/costrict-web/server/internal/notification/sender"
12+
"github.com/costrict/costrict-web/server/internal/pathutil"
1213
"gorm.io/gorm"
1314
)
1415

@@ -63,18 +64,28 @@ func (s *NotificationService) TriggerMessage(userID, eventType string, msg sende
6364
// 因此需要先 JOIN devices 表进行转换,不能直接用 deviceID 匹配 workspaces.device_id。
6465
func (s *NotificationService) getWorkspaceID(deviceID, path string) (string, error) {
6566
var workspaceID string
67+
normalizedPath := pathutil.NormalizeWorkspacePath(path)
6668
err := s.db.Table("workspace_directories wd").
6769
Select("w.id").
6870
Joins("JOIN workspaces w ON w.id = wd.workspace_id").
69-
Joins("JOIN devices d ON d.id::text = w.device_id").
70-
Where("wd.path = ? AND d.device_id = ?", path, deviceID).
71+
Joins("JOIN devices d ON CAST(d.id AS TEXT) = w.device_id").
72+
Where("wd.path = ? AND d.device_id = ?", normalizedPath, deviceID).
7173
Where("wd.deleted_at IS NULL AND w.deleted_at IS NULL AND d.deleted_at IS NULL").
7274
Scan(&workspaceID).Error
75+
if err == nil && workspaceID == "" && normalizedPath != path {
76+
err = s.db.Table("workspace_directories wd").
77+
Select("w.id").
78+
Joins("JOIN workspaces w ON w.id = wd.workspace_id").
79+
Joins("JOIN devices d ON CAST(d.id AS TEXT) = w.device_id").
80+
Where("wd.path = ? AND d.device_id = ?", path, deviceID).
81+
Where("wd.deleted_at IS NULL AND w.deleted_at IS NULL AND d.deleted_at IS NULL").
82+
Scan(&workspaceID).Error
83+
}
7384
if err != nil {
7485
return "", err
7586
}
7687
if workspaceID == "" {
77-
return "", fmt.Errorf("workspace not found for deviceID=%s, path=%s", deviceID, path)
88+
return "", fmt.Errorf("workspace not found for deviceID=%s, path=%s", deviceID, normalizedPath)
7889
}
7990
return workspaceID, nil
8091
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package pathutil
2+
3+
import (
4+
"path/filepath"
5+
"strings"
6+
)
7+
8+
// NormalizeWorkspacePath standardizes workspace paths so they can be
9+
// consistently stored and matched across different path separator styles.
10+
func NormalizeWorkspacePath(p string) string {
11+
p = strings.TrimSpace(p)
12+
if p == "" {
13+
return ""
14+
}
15+
16+
p = filepath.Clean(p)
17+
p = filepath.ToSlash(p)
18+
19+
if p == "/" {
20+
return p
21+
}
22+
23+
if len(p) == 3 && p[1] == ':' && p[2] == '/' {
24+
return p
25+
}
26+
27+
return strings.TrimSuffix(p, "/")
28+
}

server/internal/services/workspace_service.go

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package services
22

3-
import (
4-
"encoding/json"
5-
"errors"
6-
"fmt"
7-
8-
"github.com/costrict/costrict-web/server/internal/models"
9-
"gorm.io/datatypes"
10-
"gorm.io/gorm"
11-
)
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/costrict/costrict-web/server/internal/models"
9+
"github.com/costrict/costrict-web/server/internal/pathutil"
10+
"gorm.io/datatypes"
11+
"gorm.io/gorm"
12+
)
1213

1314
var (
1415
ErrWorkspaceNotFound = errors.New("workspace not found")
@@ -176,14 +177,14 @@ func (s *WorkspaceService) CreateWorkspace(userID string, req CreateWorkspaceReq
176177

177178
// 创建工作目录
178179
hasDefault := false
179-
for i, dirReq := range req.Directories {
180-
directory := &models.WorkspaceDirectory{
181-
WorkspaceID: workspace.ID,
182-
Name: dirReq.Name,
183-
Path: dirReq.Path,
184-
IsDefault: dirReq.IsDefault,
185-
OrderIndex: i,
186-
}
180+
for i, dirReq := range req.Directories {
181+
directory := &models.WorkspaceDirectory{
182+
WorkspaceID: workspace.ID,
183+
Name: dirReq.Name,
184+
Path: pathutil.NormalizeWorkspacePath(dirReq.Path),
185+
IsDefault: dirReq.IsDefault,
186+
OrderIndex: i,
187+
}
187188
if dirReq.Settings != nil {
188189
directory.Settings = toDatatypesJSON(dirReq.Settings)
189190
}
@@ -417,13 +418,13 @@ func (s *WorkspaceService) AddDirectory(workspaceID, userID string, req CreateDi
417418
}
418419
}
419420

420-
directory := &models.WorkspaceDirectory{
421-
WorkspaceID: workspaceID,
422-
Name: req.Name,
423-
Path: req.Path,
424-
IsDefault: req.IsDefault,
425-
OrderIndex: maxOrder + 1,
426-
}
421+
directory := &models.WorkspaceDirectory{
422+
WorkspaceID: workspaceID,
423+
Name: req.Name,
424+
Path: pathutil.NormalizeWorkspacePath(req.Path),
425+
IsDefault: req.IsDefault,
426+
OrderIndex: maxOrder + 1,
427+
}
427428

428429
if req.Settings != nil {
429430
directory.Settings = toDatatypesJSON(req.Settings)
@@ -468,9 +469,9 @@ func (s *WorkspaceService) UpdateDirectory(workspaceID, directoryID, userID stri
468469
updates["name"] = req.Name
469470
}
470471

471-
if req.Path != "" {
472-
updates["path"] = req.Path
473-
}
472+
if req.Path != "" {
473+
updates["path"] = pathutil.NormalizeWorkspacePath(req.Path)
474+
}
474475

475476
if req.Settings != nil {
476477
updates["settings"] = req.Settings

0 commit comments

Comments
 (0)