From 245497999e081015bd57480edfb3da432936bad0 Mon Sep 17 00:00:00 2001 From: Fendy Date: Wed, 26 Aug 2026 21:41:47 +0800 Subject: [PATCH] feat: store layer with file CRUD --- internal/store/files.go | 71 ++++++++++++++++++++++++++++++++++++ internal/store/files_test.go | 36 ++++++++++++++++++ internal/store/store.go | 53 +++++++++++++++++++++++++++ internal/store/store_test.go | 22 +++++++++++ 4 files changed, 182 insertions(+) create mode 100644 internal/store/files.go create mode 100644 internal/store/files_test.go create mode 100644 internal/store/store.go create mode 100644 internal/store/store_test.go diff --git a/internal/store/files.go b/internal/store/files.go new file mode 100644 index 0000000..45dd71a --- /dev/null +++ b/internal/store/files.go @@ -0,0 +1,71 @@ +package store + +import "database/sql" + +type File struct { + ID string + OriginalName string + StorageName string + Dir string + Size int64 + CreatedAt int64 +} + +func (s *Store) FileCreate(f *File) error { + _, err := s.db.Exec( + `INSERT INTO files (id, original_name, storage_name, dir, size, created_at) + VALUES (?, ?, ?, ?, ?, ?)`, + f.ID, f.OriginalName, f.StorageName, f.Dir, f.Size, f.CreatedAt, + ) + return err +} + +func (s *Store) FileGet(id string) (*File, error) { + row := s.db.QueryRow( + `SELECT id, original_name, storage_name, dir, size, created_at + FROM files WHERE id = ?`, id, + ) + return scanFile(row) +} + +func (s *Store) FileList(dir string) ([]*File, error) { + query := `SELECT id, original_name, storage_name, dir, size, created_at FROM files` + var args []any + if dir != "" { + query += ` WHERE dir = ?` + args = []any{dir} + } + query += ` ORDER BY created_at DESC` + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var files []*File + for rows.Next() { + f, err := scanFile(rows) + if err != nil { + return nil, err + } + files = append(files, f) + } + return files, rows.Err() +} + +func (s *Store) FileDelete(id string) error { + _, err := s.db.Exec(`DELETE FROM files WHERE id = ?`, id) + return err +} + +func scanFile(row interface { + Scan(dest ...any) error +}) (*File, error) { + f := &File{} + err := row.Scan(&f.ID, &f.OriginalName, &f.StorageName, &f.Dir, &f.Size, &f.CreatedAt) + if err == sql.ErrNoRows { + return nil, err + } + return f, err +} diff --git a/internal/store/files_test.go b/internal/store/files_test.go new file mode 100644 index 0000000..67e5529 --- /dev/null +++ b/internal/store/files_test.go @@ -0,0 +1,36 @@ +package store + +import ( + "path/filepath" + "testing" +) + +func TestFileCreate(t *testing.T) { + dir := t.TempDir() + s, err := Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatal(err) + } + defer s.Close() + + f := &File{ + ID: "test-id", + OriginalName: "test.pdf", + StorageName: "storage-uuid", + Dir: "inbox", + Size: 1024, + CreatedAt: 1750000000, + } + + if err := s.FileCreate(f); err != nil { + t.Fatalf("FileCreate failed: %v", err) + } + + got, err := s.FileGet("test-id") + if err != nil { + t.Fatalf("FileGet failed: %v", err) + } + if got.OriginalName != "test.pdf" { + t.Errorf("OriginalName = %q, want %q", got.OriginalName, "test.pdf") + } +} diff --git a/internal/store/store.go b/internal/store/store.go new file mode 100644 index 0000000..9fe36b0 --- /dev/null +++ b/internal/store/store.go @@ -0,0 +1,53 @@ +package store + +import ( + "database/sql" + _ "modernc.org/sqlite" +) + +type Store struct { + db *sql.DB +} + +func Open(dbPath string) (*Store, error) { + db, err := sql.Open("sqlite", dbPath) + if err != nil { + return nil, err + } + + if err := migrate(db); err != nil { + db.Close() + return nil, err + } + + return &Store{db: db}, nil +} + +func (s *Store) Close() error { + return s.db.Close() +} + +func migrate(db *sql.DB) error { + schema := ` + CREATE TABLE IF NOT EXISTS files ( + id TEXT PRIMARY KEY, + original_name TEXT NOT NULL, + storage_name TEXT NOT NULL UNIQUE, + dir TEXT NOT NULL CHECK (dir IN ('inbox','outbox')), + size INTEGER NOT NULL, + created_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS shares ( + id TEXT PRIMARY KEY, + file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE, + token TEXT NOT NULL UNIQUE, + password_hash TEXT, + expires_at INTEGER, + created_at INTEGER NOT NULL, + last_accessed_at INTEGER + ); + ` + _, err := db.Exec(schema) + return err +} diff --git a/internal/store/store_test.go b/internal/store/store_test.go new file mode 100644 index 0000000..73301a0 --- /dev/null +++ b/internal/store/store_test.go @@ -0,0 +1,22 @@ +package store + +import ( + "os" + "path/filepath" + "testing" +) + +func TestOpen(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "test.db") + + s, err := Open(dbPath) + if err != nil { + t.Fatalf("Open failed: %v", err) + } + defer s.Close() + + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + t.Error("Database file not created") + } +}