mirror of
https://github.com/Dvorinka/excalidraw-full.git
synced 2026-06-03 22:02:57 +00:00
更新项目架构与存储适配器,添加用户认证功能
本次提交包含以下主要更改: 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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user