From ee9eaf88174476239e8b18993ff13c26b98417aa Mon Sep 17 00:00:00 2001 From: Fendy Date: Wed, 26 Aug 2026 21:49:26 +0800 Subject: [PATCH] feat: share CRUD with cascade delete --- internal/store/shares.go | 67 +++++++++++++++++++++++++++++++++ internal/store/shares_test.go | 70 +++++++++++++++++++++++++++++++++++ internal/store/store.go | 5 +++ 3 files changed, 142 insertions(+) create mode 100644 internal/store/shares.go create mode 100644 internal/store/shares_test.go diff --git a/internal/store/shares.go b/internal/store/shares.go new file mode 100644 index 0000000..ca0623c --- /dev/null +++ b/internal/store/shares.go @@ -0,0 +1,67 @@ +package store + +import "database/sql" + +type Share struct { + ID string + FileID string + Token string + PasswordHash string + ExpiresAt int64 + CreatedAt int64 + LastAccessedAt int64 +} + +func (s *Store) ShareCreate(sh *Share) error { + _, err := s.db.Exec( + `INSERT INTO shares (id, file_id, token, password_hash, expires_at, created_at, last_accessed_at) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + sh.ID, sh.FileID, sh.Token, sh.PasswordHash, sh.ExpiresAt, sh.CreatedAt, sh.LastAccessedAt, + ) + return err +} + +func (s *Store) ShareGetByToken(token string) (*Share, error) { + row := s.db.QueryRow( + `SELECT id, file_id, token, password_hash, expires_at, created_at, last_accessed_at + FROM shares WHERE token = ?`, token, + ) + return scanShare(row) +} + +func (s *Store) ShareList() ([]*Share, error) { + rows, err := s.db.Query( + `SELECT id, file_id, token, password_hash, expires_at, created_at, last_accessed_at + FROM shares ORDER BY created_at DESC`, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var shares []*Share + for rows.Next() { + sh, err := scanShare(rows) + if err != nil { + return nil, err + } + shares = append(shares, sh) + } + return shares, rows.Err() +} + +func (s *Store) ShareDelete(id string) error { + _, err := s.db.Exec(`DELETE FROM shares WHERE id = ?`, id) + return err +} + +func scanShare(row interface { + Scan(dest ...any) error +}) (*Share, error) { + sh := &Share{} + err := row.Scan(&sh.ID, &sh.FileID, &sh.Token, &sh.PasswordHash, &sh.ExpiresAt, &sh.CreatedAt, &sh.LastAccessedAt) + if err == sql.ErrNoRows { + return nil, err + } + return sh, err +} diff --git a/internal/store/shares_test.go b/internal/store/shares_test.go new file mode 100644 index 0000000..1249657 --- /dev/null +++ b/internal/store/shares_test.go @@ -0,0 +1,70 @@ +package store + +import ( + "path/filepath" + "testing" +) + +func TestShareCreate(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: "file-1", + OriginalName: "doc.pdf", + StorageName: "uuid-1", + Dir: "inbox", + Size: 2048, + CreatedAt: 1750000000, + } + if err := s.FileCreate(f); err != nil { + t.Fatal(err) + } + + share := &Share{ + ID: "share-1", + FileID: "file-1", + Token: "token-abc", + CreatedAt: 1750000000, + } + + if err := s.ShareCreate(share); err != nil { + t.Fatalf("ShareCreate failed: %v", err) + } + + got, err := s.ShareGetByToken("token-abc") + if err != nil { + t.Fatalf("ShareGetByToken failed: %v", err) + } + if got.FileID != "file-1" { + t.Errorf("FileID = %q, want %q", got.FileID, "file-1") + } +} + +func TestFileDeleteCascadesShares(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: "file-2", OriginalName: "x.pdf", StorageName: "uuid-2", Dir: "inbox", Size: 100, CreatedAt: 1750000000} + s.FileCreate(f) + + sh := &Share{ID: "share-2", FileID: "file-2", Token: "tok-2", CreatedAt: 1750000000} + s.ShareCreate(sh) + + if err := s.FileDelete("file-2"); err != nil { + t.Fatal(err) + } + + _, err = s.ShareGetByToken("tok-2") + if err == nil { + t.Error("Share should be deleted after file deletion") + } +} diff --git a/internal/store/store.go b/internal/store/store.go index 9fe36b0..6f9d3d8 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -15,6 +15,11 @@ func Open(dbPath string) (*Store, error) { return nil, err } + if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { + db.Close() + return nil, err + } + if err := migrate(db); err != nil { db.Close() return nil, err