更新项目架构与存储适配器,添加用户认证功能

本次提交包含以下主要更改:
1. 更新 `.gitignore` 文件,添加对 `node_modules` 和环境变量文件的忽略。
2. 修改 `.gitmodules` 文件,替换为新的子模块 `cloudflare-worker`。
3. 新增 `ARCHITECTURE.md` 和 `PROJECT_REFACTOR_PLAN.md` 文档,详细描述项目架构和改造计划。
4. 实现用户认证功能,添加 GitHub OAuth 处理逻辑,支持 JWT 生成与解析。
5. 引入新的存储接口 `CanvasStore`,并实现相应的存储逻辑,支持用户画布的增删改查。
6. 更新 `main.go` 文件,整合新的认证与存储逻辑,优化路由设置。

这些更改旨在提升项目的可扩展性与用户体验,支持多用户环境下的画布管理与存储。
This commit is contained in:
Yuzhong Zhang
2025-07-05 23:13:17 +08:00
parent 61abbc612b
commit 94953a5eac
26 changed files with 2078 additions and 293 deletions
-71
View File
@@ -1,71 +0,0 @@
package aws
import (
"bytes"
"context"
"excalidraw-complete/core"
"fmt"
"io/ioutil"
"log"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/oklog/ulid/v2"
)
type documentStore struct {
s3Client *s3.Client
bucket string // Name of the S3 bucket
}
func NewDocumentStore(bucketName string) core.DocumentStore {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
log.Fatalf("unable to load SDK config, %v", err)
}
s3Client := s3.NewFromConfig(cfg)
return &documentStore{
s3Client: s3Client,
bucket: bucketName,
}
}
func (s *documentStore) FindID(ctx context.Context, id string) (*core.Document, error) {
resp, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(id),
})
if err != nil {
return nil, fmt.Errorf("failed to get document with id %s: %v", id, err)
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read document data: %v", err)
}
document := core.Document{
Data: *bytes.NewBuffer(data),
}
return &document, nil
}
func (s *documentStore) Create(ctx context.Context, document *core.Document) (string, error) {
id := ulid.Make().String()
_, err := s.s3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(id),
Body: bytes.NewReader(document.Data.Bytes()),
})
if err != nil {
return "", fmt.Errorf("failed to upload document: %v", err)
}
return id, nil
}
+160
View File
@@ -0,0 +1,160 @@
package aws
import (
"bytes"
"context"
"excalidraw-complete/core"
"fmt"
"io/ioutil"
"log"
"path"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/oklog/ulid/v2"
)
type s3Store struct {
s3Client *s3.Client
bucket string
}
// NewStore creates a new S3-based store.
func NewStore(bucketName string) *s3Store {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
log.Fatalf("unable to load SDK config, %v", err)
}
s3Client := s3.NewFromConfig(cfg)
return &s3Store{
s3Client: s3Client,
bucket: bucketName,
}
}
// DocumentStore implementation for anonymous sharing
func (s *s3Store) FindID(ctx context.Context, id string) (*core.Document, error) {
resp, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(id),
})
if err != nil {
return nil, fmt.Errorf("failed to get document with id %s: %v", id, err)
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read document data: %v", err)
}
document := core.Document{
Data: *bytes.NewBuffer(data),
}
return &document, nil
}
func (s *s3Store) Create(ctx context.Context, document *core.Document) (string, error) {
id := ulid.Make().String()
_, err := s.s3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(id),
Body: bytes.NewReader(document.Data.Bytes()),
})
if err != nil {
return "", fmt.Errorf("failed to upload document: %v", err)
}
return id, nil
}
// CanvasStore implementation for user-owned canvases
func (s *s3Store) getCanvasKey(userID, canvasID string) string {
return path.Join(userID, canvasID)
}
func (s *s3Store) List(ctx context.Context, userID string) ([]*core.Canvas, error) {
prefix := userID + "/"
output, err := s.s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: aws.String(s.bucket),
Prefix: aws.String(prefix),
})
if err != nil {
return nil, fmt.Errorf("failed to list canvases for user %s: %v", userID, err)
}
canvases := make([]*core.Canvas, 0, len(output.Contents))
for _, object := range output.Contents {
canvasID := path.Base(*object.Key)
canvas := &core.Canvas{
ID: canvasID,
UserID: userID,
Name: canvasID, // S3 doesn't have a native 'name' field, using ID.
UpdatedAt: *object.LastModified,
}
canvases = append(canvases, canvas)
}
return canvases, nil
}
func (s *s3Store) Get(ctx context.Context, userID, id string) (*core.Canvas, error) {
key := s.getCanvasKey(userID, id)
resp, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
if err != nil {
// A specific check for NoSuchKey can be useful here.
if bytes.Contains([]byte(err.Error()), []byte("NoSuchKey")) {
return nil, fmt.Errorf("canvas not found")
}
return nil, fmt.Errorf("failed to get canvas %s: %v", id, err)
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read canvas data: %v", err)
}
canvas := &core.Canvas{
ID: id,
UserID: userID,
Name: id,
Data: data,
UpdatedAt: *resp.LastModified,
}
return canvas, nil
}
func (s *s3Store) Save(ctx context.Context, canvas *core.Canvas) error {
key := s.getCanvasKey(canvas.UserID, canvas.ID)
_, err := s.s3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
Body: bytes.NewReader(canvas.Data),
})
if err != nil {
return fmt.Errorf("failed to save canvas %s: %v", canvas.ID, err)
}
return nil
}
func (s *s3Store) Delete(ctx context.Context, userID, id string) error {
key := s.getCanvasKey(userID, id)
_, err := s.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
if err != nil {
return fmt.Errorf("failed to delete canvas %s: %v", id, err)
}
return nil
}
-67
View File
@@ -1,67 +0,0 @@
package filesystem
import (
"bytes"
"context"
"excalidraw-complete/core"
"fmt"
"log"
"os"
"path/filepath"
"github.com/oklog/ulid/v2"
"github.com/sirupsen/logrus"
)
type documentStore struct {
basePath string // Directory where documents are stored.
}
func NewDocumentStore(basePath string) core.DocumentStore {
if err := os.MkdirAll(basePath, 0755); err != nil {
log.Fatalf("failed to create base directory: %v", err)
}
return &documentStore{basePath: basePath}
}
func (s *documentStore) FindID(ctx context.Context, id string) (*core.Document, error) {
filePath := filepath.Join(s.basePath, id)
log := logrus.WithField("document_id", id)
log.WithField("file_path", filePath).Info("Retrieving document by ID")
data, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
log.WithField("error", "document not found").Warn("Document with specified ID not found")
return nil, fmt.Errorf("document with id %s not found", id)
}
log.WithField("error", err).Error("Failed to retrieve document")
return nil, err
}
document := core.Document{
Data: *bytes.NewBuffer(data),
}
log.Info("Document retrieved successfully")
return &document, nil
}
func (s *documentStore) Create(ctx context.Context, document *core.Document) (string, error) {
id := ulid.Make().String()
filePath := filepath.Join(s.basePath, id)
log := logrus.WithFields(logrus.Fields{
"document_id": id,
"file_path": filePath,
})
log.Info("Creating new document")
if err := os.WriteFile(filePath, document.Data.Bytes(), 0644); err != nil {
log.WithField("error", err).Error("Failed to create document")
return "", err
}
log.Info("Document created successfully")
return id, nil
}
+190
View File
@@ -0,0 +1,190 @@
package filesystem
import (
"bytes"
"context"
"excalidraw-complete/core"
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/oklog/ulid/v2"
"github.com/sirupsen/logrus"
)
type fsStore struct {
basePath string
}
// NewStore creates a new filesystem-based store.
func NewStore(basePath string) *fsStore {
if err := os.MkdirAll(basePath, 0755); err != nil {
log.Fatalf("failed to create base directory: %v", err)
}
return &fsStore{basePath: basePath}
}
// DocumentStore implementation for anonymous sharing
func (s *fsStore) FindID(ctx context.Context, id string) (*core.Document, error) {
filePath := filepath.Join(s.basePath, id)
log := logrus.WithField("document_id", id)
log.WithField("file_path", filePath).Info("Retrieving document by ID")
data, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
log.WithField("error", "document not found").Warn("Document with specified ID not found")
return nil, fmt.Errorf("document with id %s not found", id)
}
log.WithError(err).Error("Failed to retrieve document")
return nil, err
}
document := core.Document{
Data: *bytes.NewBuffer(data),
}
log.Info("Document retrieved successfully")
return &document, nil
}
func (s *fsStore) Create(ctx context.Context, document *core.Document) (string, error) {
id := ulid.Make().String()
filePath := filepath.Join(s.basePath, id)
log := logrus.WithFields(logrus.Fields{
"document_id": id,
"file_path": filePath,
})
log.Info("Creating new document")
if err := os.WriteFile(filePath, document.Data.Bytes(), 0644); err != nil {
log.WithError(err).Error("Failed to create document")
return "", err
}
log.Info("Document created successfully")
return id, nil
}
// CanvasStore implementation for user-owned canvases
func (s *fsStore) getUserCanvasPath(userID string) string {
return filepath.Join(s.basePath, userID)
}
func (s *fsStore) List(ctx context.Context, userID string) ([]*core.Canvas, error) {
userPath := s.getUserCanvasPath(userID)
log := logrus.WithField("user_id", userID).WithField("path", userPath)
files, err := os.ReadDir(userPath)
if err != nil {
if os.IsNotExist(err) {
log.Info("User directory does not exist, returning empty list.")
return []*core.Canvas{}, nil
}
log.WithError(err).Error("Failed to read user directory")
return nil, err
}
canvases := make([]*core.Canvas, 0, len(files))
for _, file := range files {
if !file.IsDir() {
info, err := file.Info()
if err != nil {
log.WithError(err).Warn("Failed to get file info, skipping file")
continue
}
canvas := &core.Canvas{
ID: file.Name(),
UserID: userID,
Name: file.Name(),
UpdatedAt: info.ModTime(),
}
canvases = append(canvases, canvas)
}
}
log.Infof("Listed %d canvases", len(canvases))
return canvases, nil
}
func (s *fsStore) Get(ctx context.Context, userID, id string) (*core.Canvas, error) {
userPath := s.getUserCanvasPath(userID)
filePath := filepath.Join(userPath, id)
log := logrus.WithFields(logrus.Fields{"user_id": userID, "canvas_id": id, "path": filePath})
data, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
log.Warn("Canvas file not found")
return nil, fmt.Errorf("canvas %s not found", id)
}
log.WithError(err).Error("Failed to read canvas file")
return nil, err
}
info, err := os.Stat(filePath)
if err != nil {
log.WithError(err).Error("Failed to get file stats")
return nil, err
}
canvas := &core.Canvas{
ID: id,
UserID: userID,
Name: id,
Data: data,
UpdatedAt: info.ModTime(),
}
log.Info("Canvas retrieved successfully")
return canvas, nil
}
func (s *fsStore) Save(ctx context.Context, canvas *core.Canvas) error {
userPath := s.getUserCanvasPath(canvas.UserID)
filePath := filepath.Join(userPath, canvas.ID)
log := logrus.WithFields(logrus.Fields{"user_id": canvas.UserID, "canvas_id": canvas.ID, "path": filePath})
if err := os.MkdirAll(userPath, 0755); err != nil {
log.WithError(err).Error("Failed to create user directory")
return err
}
log.Info("Saving canvas")
err := os.WriteFile(filePath, canvas.Data, 0644)
if err != nil {
log.WithError(err).Error("Failed to write canvas file")
return err
}
// Set modification time for consistency, though WriteFile usually does this.
// We preserve created time logic in the storage layer if needed.
now := time.Now()
canvas.UpdatedAt = now
// A full implementation would handle CreatedAt by checking if the file exists first.
// For this KV-like store, we'll just update ModTime via WriteFile.
return nil
}
func (s *fsStore) Delete(ctx context.Context, userID, id string) error {
userPath := s.getUserCanvasPath(userID)
filePath := filepath.Join(userPath, id)
log := logrus.WithFields(logrus.Fields{"user_id": userID, "canvas_id": id, "path": filePath})
err := os.Remove(filePath)
if err != nil {
if os.IsNotExist(err) {
log.Warn("Canvas file not found for deletion, considered successful.")
return nil // If it doesn't exist, the goal is achieved.
}
log.WithError(err).Error("Failed to delete canvas file")
return err
}
log.Info("Canvas deleted successfully")
return nil
}
-41
View File
@@ -1,41 +0,0 @@
package memory
import (
"context"
"excalidraw-complete/core"
"fmt"
"github.com/oklog/ulid/v2"
"github.com/sirupsen/logrus"
)
var savedDocuments = make(map[string]core.Document)
type documentStore struct {
}
func NewDocumentStore() core.DocumentStore {
return &documentStore{}
}
func (s *documentStore) FindID(ctx context.Context, id string) (*core.Document, error) {
log := logrus.WithField("document_id", id)
if val, ok := savedDocuments[id]; ok {
log.Info("Document retrieved successfully")
return &val, nil
}
log.WithField("error", "document not found").Warn("Document with specified ID not found")
return nil, fmt.Errorf("document with id %s not found", id)
}
func (s *documentStore) Create(ctx context.Context, document *core.Document) (string, error) {
id := ulid.Make().String()
savedDocuments[id] = *document
log := logrus.WithFields(logrus.Fields{
"document_id": id,
"data_length": len(document.Data.Bytes()),
})
log.Info("Document created successfully")
return id, nil
}
+166
View File
@@ -0,0 +1,166 @@
package memory
import (
"context"
"excalidraw-complete/core"
"fmt"
"sync"
"time"
"github.com/oklog/ulid/v2"
"github.com/sirupsen/logrus"
)
var (
savedDocuments = make(map[string]core.Document)
// savedCanvases is a map where the key is userID, and the value is another map
// where the key is canvasID and the value is the canvas itself.
savedCanvases = make(map[string]map[string]*core.Canvas)
mu sync.RWMutex
)
// memStore implements both DocumentStore and CanvasStore for in-memory storage.
type memStore struct{}
// NewStore creates a new in-memory store.
func NewStore() *memStore {
return &memStore{}
}
// FindID retrieves a document by its ID. Part of the DocumentStore interface.
func (s *memStore) FindID(ctx context.Context, id string) (*core.Document, error) {
mu.RLock()
defer mu.RUnlock()
log := logrus.WithField("document_id", id)
if val, ok := savedDocuments[id]; ok {
log.Info("Document retrieved successfully")
return &val, nil
}
log.WithField("error", "document not found").Warn("Document with specified ID not found")
return nil, fmt.Errorf("document with id %s not found", id)
}
// Create stores a new document. Part of the DocumentStore interface.
func (s *memStore) Create(ctx context.Context, document *core.Document) (string, error) {
mu.Lock()
defer mu.Unlock()
id := ulid.Make().String()
savedDocuments[id] = *document
log := logrus.WithFields(logrus.Fields{
"document_id": id,
"data_length": len(document.Data.Bytes()),
})
log.Info("Document created successfully")
return id, nil
}
// List returns metadata for all canvases owned by a user. Part of the CanvasStore interface.
func (s *memStore) List(ctx context.Context, userID string) ([]*core.Canvas, error) {
mu.RLock()
defer mu.RUnlock()
userCanvases, ok := savedCanvases[userID]
if !ok {
return []*core.Canvas{}, nil // No canvases for this user, return empty slice
}
canvases := make([]*core.Canvas, 0, len(userCanvases))
for _, canvas := range userCanvases {
// Important: create a copy without the large `Data` field for the list view
listCanvas := &core.Canvas{
ID: canvas.ID,
UserID: canvas.UserID,
Name: canvas.Name,
CreatedAt: canvas.CreatedAt,
UpdatedAt: canvas.UpdatedAt,
}
canvases = append(canvases, listCanvas)
}
logrus.WithField("user_id", userID).Infof("Listed %d canvases", len(canvases))
return canvases, nil
}
// Get returns a single canvas by its ID, ensuring it belongs to the user. Part of the CanvasStore interface.
func (s *memStore) Get(ctx context.Context, userID, id string) (*core.Canvas, error) {
mu.RLock()
defer mu.RUnlock()
log := logrus.WithFields(logrus.Fields{"user_id": userID, "canvas_id": id})
userCanvases, ok := savedCanvases[userID]
if !ok {
log.Warn("User has no canvases")
return nil, fmt.Errorf("canvas with id %s not found for user %s", id, userID)
}
canvas, ok := userCanvases[id]
if !ok {
log.Warn("Canvas not found for user")
return nil, fmt.Errorf("canvas with id %s not found for user %s", id, userID)
}
log.Info("Canvas retrieved successfully")
return canvas, nil
}
// Save creates or updates a canvas for a user. Part of the CanvasStore interface.
func (s *memStore) Save(ctx context.Context, canvas *core.Canvas) error {
mu.Lock()
defer mu.Unlock()
log := logrus.WithFields(logrus.Fields{"user_id": canvas.UserID, "canvas_id": canvas.ID})
if canvas.UserID == "" {
return fmt.Errorf("UserID cannot be empty")
}
userCanvases, ok := savedCanvases[canvas.UserID]
if !ok {
userCanvases = make(map[string]*core.Canvas)
savedCanvases[canvas.UserID] = userCanvases
}
if canvas.ID == "" {
return fmt.Errorf("Canvas ID cannot be empty for save operation")
}
now := time.Now()
if existingCanvas, exists := userCanvases[canvas.ID]; exists {
canvas.CreatedAt = existingCanvas.CreatedAt
canvas.UpdatedAt = now
} else {
canvas.CreatedAt = now
canvas.UpdatedAt = now
}
userCanvases[canvas.ID] = canvas
log.Info("Canvas saved successfully")
return nil
}
// Delete removes a canvas, ensuring it belongs to the user. Part of the CanvasStore interface.
func (s *memStore) Delete(ctx context.Context, userID, id string) error {
mu.Lock()
defer mu.Unlock()
log := logrus.WithFields(logrus.Fields{"user_id": userID, "canvas_id": id})
userCanvases, ok := savedCanvases[userID]
if !ok {
log.Warn("User has no canvases to delete from")
return fmt.Errorf("user %s has no canvases", userID)
}
if _, ok := userCanvases[id]; !ok {
log.Warn("Canvas not found for deletion")
return fmt.Errorf("canvas with id %s not found for user %s", id, userID)
}
delete(userCanvases, id)
log.Info("Canvas deleted successfully")
return nil
}
-73
View File
@@ -1,73 +0,0 @@
package sqlite
import (
"bytes"
"context"
"excalidraw-complete/core"
"fmt"
"database/sql"
"log"
_ "github.com/mattn/go-sqlite3"
"github.com/oklog/ulid/v2"
"github.com/sirupsen/logrus"
)
var savedDocuments = make(map[string]core.Document)
type documentStore struct {
db *sql.DB
}
func NewDocumentStore(dataSourceName string) core.DocumentStore {
// db, err := sql.Open("sqlite3", ":memory:")
db, err := sql.Open("sqlite3", dataSourceName)
if err != nil {
log.Fatal(err)
}
sts := `CREATE TABLE IF NOT EXISTS documents (id TEXT PRIMARY KEY, data BLOB);`
_, err = db.Exec(sts)
if err != nil {
log.Fatal(err)
}
return &documentStore{db}
}
func (s *documentStore) FindID(ctx context.Context, id string) (*core.Document, error) {
log := logrus.WithField("document_id", id)
log.Debug("Retrieving document by ID")
var data []byte
err := s.db.QueryRowContext(ctx, "SELECT data FROM documents WHERE id = ?", id).Scan(&data)
if err != nil {
if err == sql.ErrNoRows {
log.WithField("error", "document not found").Warn("Document with specified ID not found")
return nil, fmt.Errorf("document with id %s not found", id)
}
log.WithField("error", err).Error("Failed to retrieve document")
return nil, err
}
document := core.Document{
Data: *bytes.NewBuffer(data),
}
log.Info("Document retrieved successfully")
return &document, nil
}
func (s *documentStore) Create(ctx context.Context, document *core.Document) (string, error) {
id := ulid.Make().String()
data := document.Data.Bytes()
log := logrus.WithFields(logrus.Fields{
"document_id": id,
"data_length": len(data),
})
_, err := s.db.ExecContext(ctx, "INSERT INTO documents (id, data) VALUES (?, ?)", id, data)
if err != nil {
log.WithField("error", err).Error("Failed to create document")
return "", err
}
log.Info("Document created successfully")
return id, nil
}
+157
View File
@@ -0,0 +1,157 @@
package sqlite
import (
"bytes"
"context"
"database/sql"
"excalidraw-complete/core"
"fmt"
"log"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/oklog/ulid/v2"
"github.com/sirupsen/logrus"
)
type sqliteStore struct {
db *sql.DB
}
// NewStore creates a new SQLite-based store.
func NewStore(dataSourceName string) *sqliteStore {
db, err := sql.Open("sqlite3", dataSourceName)
if err != nil {
log.Fatalf("failed to open sqlite database: %v", err)
}
// Initialize table for anonymous documents
docTableStmt := `CREATE TABLE IF NOT EXISTS documents (id TEXT PRIMARY KEY, data BLOB);`
if _, err = db.Exec(docTableStmt); err != nil {
log.Fatalf("failed to create documents table: %v", err)
}
// Initialize table for user-owned canvases
canvasTableStmt := `
CREATE TABLE IF NOT EXISTS canvases (
id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT,
data BLOB,
created_at DATETIME,
updated_at DATETIME,
PRIMARY KEY (user_id, id)
);`
if _, err = db.Exec(canvasTableStmt); err != nil {
log.Fatalf("failed to create canvases table: %v", err)
}
return &sqliteStore{db}
}
// DocumentStore implementation
func (s *sqliteStore) FindID(ctx context.Context, id string) (*core.Document, error) {
log := logrus.WithField("document_id", id)
log.Debug("Retrieving document by ID")
var data []byte
err := s.db.QueryRowContext(ctx, "SELECT data FROM documents WHERE id = ?", id).Scan(&data)
if err != nil {
if err == sql.ErrNoRows {
log.WithField("error", "document not found").Warn("Document with specified ID not found")
return nil, fmt.Errorf("document with id %s not found", id)
}
log.WithError(err).Error("Failed to retrieve document")
return nil, err
}
document := core.Document{
Data: *bytes.NewBuffer(data),
}
log.Info("Document retrieved successfully")
return &document, nil
}
func (s *sqliteStore) Create(ctx context.Context, document *core.Document) (string, error) {
id := ulid.Make().String()
data := document.Data.Bytes()
log := logrus.WithFields(logrus.Fields{
"document_id": id,
"data_length": len(data),
})
_, err := s.db.ExecContext(ctx, "INSERT INTO documents (id, data) VALUES (?, ?)", id, data)
if err != nil {
log.WithError(err).Error("Failed to create document")
return "", err
}
log.Info("Document created successfully")
return id, nil
}
// CanvasStore implementation
func (s *sqliteStore) List(ctx context.Context, userID string) ([]*core.Canvas, error) {
rows, err := s.db.QueryContext(ctx, "SELECT id, name, updated_at FROM canvases WHERE user_id = ?", userID)
if err != nil {
return nil, err
}
defer rows.Close()
var canvases []*core.Canvas
for rows.Next() {
var canvas core.Canvas
canvas.UserID = userID
if err := rows.Scan(&canvas.ID, &canvas.Name, &canvas.UpdatedAt); err != nil {
return nil, err
}
canvases = append(canvases, &canvas)
}
return canvases, nil
}
func (s *sqliteStore) Get(ctx context.Context, userID, id string) (*core.Canvas, error) {
var canvas core.Canvas
canvas.UserID = userID
canvas.ID = id
err := s.db.QueryRowContext(ctx, "SELECT name, data, created_at, updated_at FROM canvases WHERE user_id = ? AND id = ?", userID, id).Scan(&canvas.Name, &canvas.Data, &canvas.CreatedAt, &canvas.UpdatedAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("canvas not found")
}
return nil, err
}
return &canvas, nil
}
func (s *sqliteStore) Save(ctx context.Context, canvas *core.Canvas) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() // Rollback on any error
var exists bool
err = tx.QueryRowContext(ctx, "SELECT 1 FROM canvases WHERE user_id = ? AND id = ?", canvas.UserID, canvas.ID).Scan(&exists)
now := time.Now()
if err != nil && err != sql.ErrNoRows {
return err
}
if exists {
// Update
_, err = tx.ExecContext(ctx, "UPDATE canvases SET name = ?, data = ?, updated_at = ? WHERE user_id = ? AND id = ?", canvas.Name, canvas.Data, now, canvas.UserID, canvas.ID)
} else {
// Insert
_, err = tx.ExecContext(ctx, "INSERT INTO canvases (id, user_id, name, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", canvas.ID, canvas.UserID, canvas.Name, canvas.Data, now, now)
}
if err != nil {
return err
}
return tx.Commit()
}
func (s *sqliteStore) Delete(ctx context.Context, userID, id string) error {
_, err := s.db.ExecContext(ctx, "DELETE FROM canvases WHERE user_id = ? AND id = ?", userID, id)
return err
}
+21 -6
View File
@@ -11,9 +11,15 @@ import (
"github.com/sirupsen/logrus"
)
func GetStore() core.DocumentStore {
// Store is a union interface that includes all store types.
type Store interface {
core.DocumentStore
core.CanvasStore
}
func GetStore() Store {
storageType := os.Getenv("STORAGE_TYPE")
var store core.DocumentStore
var store Store
storageField := logrus.Fields{
"storageType": storageType,
@@ -22,18 +28,27 @@ func GetStore() core.DocumentStore {
switch storageType {
case "filesystem":
basePath := os.Getenv("LOCAL_STORAGE_PATH")
if basePath == "" {
basePath = "./data" // Default path
}
storageField["basePath"] = basePath
store = filesystem.NewDocumentStore(basePath)
store = filesystem.NewStore(basePath)
case "sqlite":
dataSourceName := os.Getenv("DATA_SOURCE_NAME")
if dataSourceName == "" {
dataSourceName = "excalidraw.db" // Default filename
}
storageField["dataSourceName"] = dataSourceName
store = sqlite.NewDocumentStore(dataSourceName)
store = sqlite.NewStore(dataSourceName)
case "s3":
bucketName := os.Getenv("S3_BUCKET_NAME")
if bucketName == "" {
logrus.Fatal("S3_BUCKET_NAME environment variable must be set for s3 storage type")
}
storageField["bucketName"] = bucketName
store = aws.NewDocumentStore(bucketName)
store = aws.NewStore(bucketName)
default:
store = memory.NewDocumentStore()
store = memory.NewStore()
storageField["storageType"] = "in-memory"
}
logrus.WithFields(storageField).Info("Use storage")