Changeset 0.21.1 (#86)
This commit is contained in:
@@ -42,6 +42,7 @@ type Stores struct {
|
||||
Projects ProjectStore
|
||||
Notifications NotificationStore
|
||||
NotifPrefs NotificationPreferenceStore
|
||||
Workspaces WorkspaceStore
|
||||
}
|
||||
|
||||
// =========================================
|
||||
@@ -510,6 +511,33 @@ type NotificationPreferenceStore interface {
|
||||
Delete(ctx context.Context, userID, notifType string) error
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// WORKSPACE STORE (v0.21.0)
|
||||
// =========================================
|
||||
|
||||
type WorkspaceStore interface {
|
||||
// CRUD
|
||||
Create(ctx context.Context, w *models.Workspace) error
|
||||
GetByID(ctx context.Context, id string) (*models.Workspace, error)
|
||||
Update(ctx context.Context, id string, patch models.WorkspacePatch) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// Ownership lookup
|
||||
GetByOwner(ctx context.Context, ownerType, ownerID string) (*models.Workspace, error)
|
||||
ListByOwner(ctx context.Context, ownerType, ownerID string) ([]models.Workspace, error)
|
||||
|
||||
// File index
|
||||
UpsertFile(ctx context.Context, f *models.WorkspaceFile) error
|
||||
DeleteFile(ctx context.Context, workspaceID, path string) error
|
||||
DeleteFilesByPrefix(ctx context.Context, workspaceID, prefix string) error
|
||||
GetFile(ctx context.Context, workspaceID, path string) (*models.WorkspaceFile, error)
|
||||
ListFiles(ctx context.Context, workspaceID, prefix string, recursive bool) ([]models.WorkspaceFile, error)
|
||||
DeleteAllFiles(ctx context.Context, workspaceID string) error
|
||||
|
||||
// Stats
|
||||
GetStats(ctx context.Context, workspaceID string) (*models.WorkspaceStats, error)
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// SHARED TYPES
|
||||
// =========================================
|
||||
|
||||
@@ -35,5 +35,6 @@ func NewStores(db *sql.DB) store.Stores {
|
||||
Projects: NewProjectStore(),
|
||||
Notifications: NewNotificationStore(),
|
||||
NotifPrefs: NewNotificationPreferenceStore(),
|
||||
Workspaces: NewWorkspaceStore(),
|
||||
}
|
||||
}
|
||||
|
||||
272
server/store/postgres/workspace.go
Normal file
272
server/store/postgres/workspace.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
)
|
||||
|
||||
type WorkspaceStore struct{}
|
||||
|
||||
func NewWorkspaceStore() *WorkspaceStore { return &WorkspaceStore{} }
|
||||
|
||||
// ── columns ────────────────────────────────
|
||||
|
||||
const workspaceCols = `id, owner_type, owner_id, name, root_path, max_bytes, status, created_at, updated_at`
|
||||
const workspaceFileCols = `id, workspace_id, path, is_directory, content_type, size_bytes, sha256, created_at, updated_at`
|
||||
|
||||
// ── scanners ───────────────────────────────
|
||||
|
||||
func scanWorkspace(row interface{ Scan(dest ...interface{}) error }) (*models.Workspace, error) {
|
||||
var w models.Workspace
|
||||
var maxBytes sql.NullInt64
|
||||
err := row.Scan(
|
||||
&w.ID, &w.OwnerType, &w.OwnerID, &w.Name,
|
||||
&w.RootPath, &maxBytes, &w.Status,
|
||||
&w.CreatedAt, &w.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if maxBytes.Valid {
|
||||
w.MaxBytes = &maxBytes.Int64
|
||||
}
|
||||
return &w, nil
|
||||
}
|
||||
|
||||
func scanWorkspaceFile(row interface{ Scan(dest ...interface{}) error }) (*models.WorkspaceFile, error) {
|
||||
var f models.WorkspaceFile
|
||||
var contentType sql.NullString
|
||||
var sha256 sql.NullString
|
||||
err := row.Scan(
|
||||
&f.ID, &f.WorkspaceID, &f.Path, &f.IsDirectory,
|
||||
&contentType, &f.SizeBytes, &sha256,
|
||||
&f.CreatedAt, &f.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if contentType.Valid {
|
||||
f.ContentType = contentType.String
|
||||
}
|
||||
if sha256.Valid {
|
||||
f.SHA256 = sha256.String
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
// ── Workspace CRUD ─────────────────────────
|
||||
|
||||
func (s *WorkspaceStore) Create(ctx context.Context, w *models.Workspace) error {
|
||||
// Accept pre-generated ID or let Postgres generate one
|
||||
if w.ID != "" {
|
||||
return DB.QueryRowContext(ctx, `
|
||||
INSERT INTO workspaces (id, owner_type, owner_id, name, root_path, max_bytes, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING created_at, updated_at`,
|
||||
w.ID, w.OwnerType, w.OwnerID, w.Name, w.RootPath,
|
||||
w.MaxBytes, w.Status,
|
||||
).Scan(&w.CreatedAt, &w.UpdatedAt)
|
||||
}
|
||||
return DB.QueryRowContext(ctx, `
|
||||
INSERT INTO workspaces (owner_type, owner_id, name, root_path, max_bytes, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, created_at, updated_at`,
|
||||
w.OwnerType, w.OwnerID, w.Name, w.RootPath,
|
||||
w.MaxBytes, w.Status,
|
||||
).Scan(&w.ID, &w.CreatedAt, &w.UpdatedAt)
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) GetByID(ctx context.Context, id string) (*models.Workspace, error) {
|
||||
row := DB.QueryRowContext(ctx,
|
||||
`SELECT `+workspaceCols+` FROM workspaces WHERE id = $1`, id)
|
||||
return scanWorkspace(row)
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) Update(ctx context.Context, id string, patch models.WorkspacePatch) error {
|
||||
b := NewUpdate("workspaces")
|
||||
if patch.Name != nil {
|
||||
b.Set("name", *patch.Name)
|
||||
}
|
||||
if patch.MaxBytes != nil {
|
||||
b.Set("max_bytes", *patch.MaxBytes)
|
||||
}
|
||||
if patch.Status != nil {
|
||||
b.Set("status", *patch.Status)
|
||||
}
|
||||
if !b.HasSets() {
|
||||
return nil
|
||||
}
|
||||
b.Set("updated_at", time.Now().UTC())
|
||||
b.Where("id", id)
|
||||
_, err := b.Exec(DB)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) Delete(ctx context.Context, id string) error {
|
||||
_, err := DB.ExecContext(ctx, `DELETE FROM workspaces WHERE id = $1`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// ── Ownership lookup ───────────────────────
|
||||
|
||||
func (s *WorkspaceStore) GetByOwner(ctx context.Context, ownerType, ownerID string) (*models.Workspace, error) {
|
||||
row := DB.QueryRowContext(ctx,
|
||||
`SELECT `+workspaceCols+` FROM workspaces
|
||||
WHERE owner_type = $1 AND owner_id = $2 AND status != 'deleting'
|
||||
ORDER BY created_at DESC LIMIT 1`,
|
||||
ownerType, ownerID)
|
||||
return scanWorkspace(row)
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) ListByOwner(ctx context.Context, ownerType, ownerID string) ([]models.Workspace, error) {
|
||||
rows, err := DB.QueryContext(ctx,
|
||||
`SELECT `+workspaceCols+` FROM workspaces
|
||||
WHERE owner_type = $1 AND owner_id = $2 AND status != 'deleting'
|
||||
ORDER BY created_at DESC`,
|
||||
ownerType, ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []models.Workspace
|
||||
for rows.Next() {
|
||||
w, err := scanWorkspace(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, *w)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ── File index ─────────────────────────────
|
||||
|
||||
func (s *WorkspaceStore) UpsertFile(ctx context.Context, f *models.WorkspaceFile) error {
|
||||
return DB.QueryRowContext(ctx, `
|
||||
INSERT INTO workspace_files (workspace_id, path, is_directory, content_type, size_bytes, sha256)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (workspace_id, path) DO UPDATE SET
|
||||
is_directory = EXCLUDED.is_directory,
|
||||
content_type = EXCLUDED.content_type,
|
||||
size_bytes = EXCLUDED.size_bytes,
|
||||
sha256 = EXCLUDED.sha256,
|
||||
updated_at = NOW()
|
||||
RETURNING id, created_at, updated_at`,
|
||||
f.WorkspaceID, f.Path, f.IsDirectory,
|
||||
models.NullString(&f.ContentType), f.SizeBytes,
|
||||
models.NullString(&f.SHA256),
|
||||
).Scan(&f.ID, &f.CreatedAt, &f.UpdatedAt)
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) DeleteFile(ctx context.Context, workspaceID, path string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`DELETE FROM workspace_files WHERE workspace_id = $1 AND path = $2`,
|
||||
workspaceID, path)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) DeleteFilesByPrefix(ctx context.Context, workspaceID, prefix string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`DELETE FROM workspace_files WHERE workspace_id = $1 AND path LIKE $2`,
|
||||
workspaceID, prefix+"%")
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) GetFile(ctx context.Context, workspaceID, path string) (*models.WorkspaceFile, error) {
|
||||
row := DB.QueryRowContext(ctx,
|
||||
`SELECT `+workspaceFileCols+` FROM workspace_files
|
||||
WHERE workspace_id = $1 AND path = $2`,
|
||||
workspaceID, path)
|
||||
return scanWorkspaceFile(row)
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) ListFiles(ctx context.Context, workspaceID, prefix string, recursive bool) ([]models.WorkspaceFile, error) {
|
||||
var query string
|
||||
var args []interface{}
|
||||
|
||||
if recursive {
|
||||
// All files under prefix (recursive)
|
||||
if prefix == "" {
|
||||
query = `SELECT ` + workspaceFileCols + ` FROM workspace_files
|
||||
WHERE workspace_id = $1 ORDER BY path`
|
||||
args = []interface{}{workspaceID}
|
||||
} else {
|
||||
query = `SELECT ` + workspaceFileCols + ` FROM workspace_files
|
||||
WHERE workspace_id = $1 AND path LIKE $2 ORDER BY path`
|
||||
args = []interface{}{workspaceID, prefix + "%"}
|
||||
}
|
||||
} else {
|
||||
// Direct children only: match prefix but no additional slashes
|
||||
if prefix == "" {
|
||||
// Root level: no slash in path
|
||||
query = `SELECT ` + workspaceFileCols + ` FROM workspace_files
|
||||
WHERE workspace_id = $1 AND path NOT LIKE '%/%' ORDER BY path`
|
||||
args = []interface{}{workspaceID}
|
||||
} else {
|
||||
// Children of prefix: starts with prefix, no additional slash after prefix
|
||||
query = fmt.Sprintf(`SELECT `+workspaceFileCols+` FROM workspace_files
|
||||
WHERE workspace_id = $1 AND path LIKE $2
|
||||
AND substring(path FROM %d) NOT LIKE '%%/%%'
|
||||
ORDER BY path`, len(prefix)+1)
|
||||
args = []interface{}{workspaceID, prefix + "%"}
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := DB.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []models.WorkspaceFile
|
||||
for rows.Next() {
|
||||
f, err := scanWorkspaceFile(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, *f)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) DeleteAllFiles(ctx context.Context, workspaceID string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`DELETE FROM workspace_files WHERE workspace_id = $1`, workspaceID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ── Stats ──────────────────────────────────
|
||||
|
||||
func (s *WorkspaceStore) GetStats(ctx context.Context, workspaceID string) (*models.WorkspaceStats, error) {
|
||||
stats := &models.WorkspaceStats{WorkspaceID: workspaceID}
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN NOT is_directory THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN is_directory THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(size_bytes), 0)
|
||||
FROM workspace_files WHERE workspace_id = $1`,
|
||||
workspaceID,
|
||||
).Scan(&stats.FileCount, &stats.DirCount, &stats.TotalBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fetch quota
|
||||
var maxBytes sql.NullInt64
|
||||
err = DB.QueryRowContext(ctx,
|
||||
`SELECT max_bytes FROM workspaces WHERE id = $1`, workspaceID,
|
||||
).Scan(&maxBytes)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
if maxBytes.Valid {
|
||||
stats.MaxBytes = &maxBytes.Int64
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
@@ -35,5 +35,6 @@ func NewStores(db *sql.DB) store.Stores {
|
||||
Projects: NewProjectStore(),
|
||||
Notifications: NewNotificationStore(),
|
||||
NotifPrefs: NewNotificationPreferenceStore(),
|
||||
Workspaces: NewWorkspaceStore(),
|
||||
}
|
||||
}
|
||||
|
||||
274
server/store/sqlite/workspace.go
Normal file
274
server/store/sqlite/workspace.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
type WorkspaceStore struct{}
|
||||
|
||||
func NewWorkspaceStore() *WorkspaceStore { return &WorkspaceStore{} }
|
||||
|
||||
// ── columns ────────────────────────────────
|
||||
|
||||
const workspaceCols = `id, owner_type, owner_id, name, root_path, max_bytes, status, created_at, updated_at`
|
||||
const workspaceFileCols = `id, workspace_id, path, is_directory, content_type, size_bytes, sha256, created_at, updated_at`
|
||||
|
||||
// ── scanners ───────────────────────────────
|
||||
|
||||
func scanWorkspace(row interface{ Scan(dest ...interface{}) error }) (*models.Workspace, error) {
|
||||
var w models.Workspace
|
||||
var maxBytes sql.NullInt64
|
||||
err := row.Scan(
|
||||
&w.ID, &w.OwnerType, &w.OwnerID, &w.Name,
|
||||
&w.RootPath, &maxBytes, &w.Status,
|
||||
st(&w.CreatedAt), st(&w.UpdatedAt),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if maxBytes.Valid {
|
||||
w.MaxBytes = &maxBytes.Int64
|
||||
}
|
||||
return &w, nil
|
||||
}
|
||||
|
||||
func scanWorkspaceFile(row interface{ Scan(dest ...interface{}) error }) (*models.WorkspaceFile, error) {
|
||||
var f models.WorkspaceFile
|
||||
var contentType sql.NullString
|
||||
var sha256 sql.NullString
|
||||
err := row.Scan(
|
||||
&f.ID, &f.WorkspaceID, &f.Path, &f.IsDirectory,
|
||||
&contentType, &f.SizeBytes, &sha256,
|
||||
st(&f.CreatedAt), st(&f.UpdatedAt),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if contentType.Valid {
|
||||
f.ContentType = contentType.String
|
||||
}
|
||||
if sha256.Valid {
|
||||
f.SHA256 = sha256.String
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
// ── Workspace CRUD ─────────────────────────
|
||||
|
||||
func (s *WorkspaceStore) Create(ctx context.Context, w *models.Workspace) error {
|
||||
if w.ID == "" {
|
||||
w.ID = store.NewID()
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
w.CreatedAt = now
|
||||
w.UpdatedAt = now
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO workspaces (id, owner_type, owner_id, name, root_path, max_bytes, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
w.ID, w.OwnerType, w.OwnerID, w.Name, w.RootPath,
|
||||
w.MaxBytes, w.Status,
|
||||
now.Format(timeFmt), now.Format(timeFmt),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) GetByID(ctx context.Context, id string) (*models.Workspace, error) {
|
||||
row := DB.QueryRowContext(ctx,
|
||||
`SELECT `+workspaceCols+` FROM workspaces WHERE id = ?`, id)
|
||||
return scanWorkspace(row)
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) Update(ctx context.Context, id string, patch models.WorkspacePatch) error {
|
||||
b := NewUpdate("workspaces")
|
||||
if patch.Name != nil {
|
||||
b.Set("name", *patch.Name)
|
||||
}
|
||||
if patch.MaxBytes != nil {
|
||||
b.Set("max_bytes", *patch.MaxBytes)
|
||||
}
|
||||
if patch.Status != nil {
|
||||
b.Set("status", *patch.Status)
|
||||
}
|
||||
if !b.HasSets() {
|
||||
return nil
|
||||
}
|
||||
b.SetExpr("updated_at", nowExpr)
|
||||
b.Where("id", id)
|
||||
_, err := b.Exec(DB)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) Delete(ctx context.Context, id string) error {
|
||||
_, err := DB.ExecContext(ctx, `DELETE FROM workspaces WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// ── Ownership lookup ───────────────────────
|
||||
|
||||
func (s *WorkspaceStore) GetByOwner(ctx context.Context, ownerType, ownerID string) (*models.Workspace, error) {
|
||||
row := DB.QueryRowContext(ctx,
|
||||
`SELECT `+workspaceCols+` FROM workspaces
|
||||
WHERE owner_type = ? AND owner_id = ? AND status != 'deleting'
|
||||
ORDER BY created_at DESC LIMIT 1`,
|
||||
ownerType, ownerID)
|
||||
return scanWorkspace(row)
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) ListByOwner(ctx context.Context, ownerType, ownerID string) ([]models.Workspace, error) {
|
||||
rows, err := DB.QueryContext(ctx,
|
||||
`SELECT `+workspaceCols+` FROM workspaces
|
||||
WHERE owner_type = ? AND owner_id = ? AND status != 'deleting'
|
||||
ORDER BY created_at DESC`,
|
||||
ownerType, ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []models.Workspace
|
||||
for rows.Next() {
|
||||
w, err := scanWorkspace(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, *w)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ── File index ─────────────────────────────
|
||||
|
||||
func (s *WorkspaceStore) UpsertFile(ctx context.Context, f *models.WorkspaceFile) error {
|
||||
if f.ID == "" {
|
||||
f.ID = store.NewID()
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
f.CreatedAt = now
|
||||
f.UpdatedAt = now
|
||||
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO workspace_files (id, workspace_id, path, is_directory, content_type, size_bytes, sha256, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (workspace_id, path) DO UPDATE SET
|
||||
is_directory = excluded.is_directory,
|
||||
content_type = excluded.content_type,
|
||||
size_bytes = excluded.size_bytes,
|
||||
sha256 = excluded.sha256,
|
||||
updated_at = datetime('now')`,
|
||||
f.ID, f.WorkspaceID, f.Path, f.IsDirectory,
|
||||
models.NullString(&f.ContentType), f.SizeBytes,
|
||||
models.NullString(&f.SHA256),
|
||||
now.Format(timeFmt), now.Format(timeFmt),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) DeleteFile(ctx context.Context, workspaceID, path string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`DELETE FROM workspace_files WHERE workspace_id = ? AND path = ?`,
|
||||
workspaceID, path)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) DeleteFilesByPrefix(ctx context.Context, workspaceID, prefix string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`DELETE FROM workspace_files WHERE workspace_id = ? AND path LIKE ?`,
|
||||
workspaceID, prefix+"%")
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) GetFile(ctx context.Context, workspaceID, path string) (*models.WorkspaceFile, error) {
|
||||
row := DB.QueryRowContext(ctx,
|
||||
`SELECT `+workspaceFileCols+` FROM workspace_files
|
||||
WHERE workspace_id = ? AND path = ?`,
|
||||
workspaceID, path)
|
||||
return scanWorkspaceFile(row)
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) ListFiles(ctx context.Context, workspaceID, prefix string, recursive bool) ([]models.WorkspaceFile, error) {
|
||||
var query string
|
||||
var args []interface{}
|
||||
|
||||
if recursive {
|
||||
if prefix == "" {
|
||||
query = `SELECT ` + workspaceFileCols + ` FROM workspace_files
|
||||
WHERE workspace_id = ? ORDER BY path`
|
||||
args = []interface{}{workspaceID}
|
||||
} else {
|
||||
query = `SELECT ` + workspaceFileCols + ` FROM workspace_files
|
||||
WHERE workspace_id = ? AND path LIKE ? ORDER BY path`
|
||||
args = []interface{}{workspaceID, prefix + "%"}
|
||||
}
|
||||
} else {
|
||||
if prefix == "" {
|
||||
query = `SELECT ` + workspaceFileCols + ` FROM workspace_files
|
||||
WHERE workspace_id = ? AND path NOT LIKE '%/%' ORDER BY path`
|
||||
args = []interface{}{workspaceID}
|
||||
} else {
|
||||
// Direct children: starts with prefix, no slash after prefix
|
||||
query = fmt.Sprintf(`SELECT `+workspaceFileCols+` FROM workspace_files
|
||||
WHERE workspace_id = ? AND path LIKE ?
|
||||
AND substr(path, %d) NOT LIKE '%%/%%'
|
||||
ORDER BY path`, len(prefix)+1)
|
||||
args = []interface{}{workspaceID, prefix + "%"}
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := DB.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []models.WorkspaceFile
|
||||
for rows.Next() {
|
||||
f, err := scanWorkspaceFile(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, *f)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *WorkspaceStore) DeleteAllFiles(ctx context.Context, workspaceID string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`DELETE FROM workspace_files WHERE workspace_id = ?`, workspaceID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ── Stats ──────────────────────────────────
|
||||
|
||||
func (s *WorkspaceStore) GetStats(ctx context.Context, workspaceID string) (*models.WorkspaceStats, error) {
|
||||
stats := &models.WorkspaceStats{WorkspaceID: workspaceID}
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN NOT is_directory THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN is_directory THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(size_bytes), 0)
|
||||
FROM workspace_files WHERE workspace_id = ?`,
|
||||
workspaceID,
|
||||
).Scan(&stats.FileCount, &stats.DirCount, &stats.TotalBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var maxBytes sql.NullInt64
|
||||
err = DB.QueryRowContext(ctx,
|
||||
`SELECT max_bytes FROM workspaces WHERE id = ?`, workspaceID,
|
||||
).Scan(&maxBytes)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
if maxBytes.Valid {
|
||||
stats.MaxBytes = &maxBytes.Int64
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
Reference in New Issue
Block a user