266 lines
7.1 KiB
Go
266 lines
7.1 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"chat-switchboard/models"
|
|
"chat-switchboard/store"
|
|
)
|
|
|
|
// FileStore implements store.FileStore against the `files` table.
|
|
//
|
|
type FileStore struct{}
|
|
|
|
func NewFileStore() *FileStore { return &FileStore{} }
|
|
|
|
const fileCols = `id, channel_id, user_id, message_id, project_id, origin,
|
|
filename, content_type, size_bytes, storage_key, display_hint,
|
|
extracted_text, metadata, created_at, updated_at`
|
|
|
|
func scanFile(row interface{ Scan(dest ...interface{}) error }) (*models.File, error) {
|
|
var f models.File
|
|
var messageID sql.NullString
|
|
var projectID sql.NullString
|
|
var extractedText sql.NullString
|
|
var metadataJSON []byte
|
|
|
|
err := row.Scan(
|
|
&f.ID, &f.ChannelID, &f.UserID, &messageID, &projectID, &f.Origin,
|
|
&f.Filename, &f.ContentType, &f.SizeBytes,
|
|
&f.StorageKey, &f.DisplayHint,
|
|
&extractedText, &metadataJSON, st(&f.CreatedAt), st(&f.UpdatedAt),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
f.MessageID = NullableStringPtr(messageID)
|
|
f.ProjectID = NullableStringPtr(projectID)
|
|
if extractedText.Valid {
|
|
f.ExtractedText = &extractedText.String
|
|
}
|
|
if len(metadataJSON) > 0 {
|
|
json.Unmarshal(metadataJSON, &f.Metadata)
|
|
}
|
|
return &f, nil
|
|
}
|
|
|
|
func (s *FileStore) Create(ctx context.Context, f *models.File) error {
|
|
f.ID = store.NewID()
|
|
now := time.Now().UTC()
|
|
f.CreatedAt = now
|
|
f.UpdatedAt = now
|
|
if f.Origin == "" {
|
|
f.Origin = models.FileOriginUserUpload
|
|
}
|
|
if f.DisplayHint == "" {
|
|
f.DisplayHint = models.FileHintDownload
|
|
}
|
|
_, err := DB.ExecContext(ctx, `
|
|
INSERT INTO files (id, channel_id, user_id, message_id, project_id, origin,
|
|
filename, content_type, size_bytes, storage_key, display_hint,
|
|
extracted_text, metadata, created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
f.ID, f.ChannelID, f.UserID, models.NullString(f.MessageID),
|
|
models.NullString(f.ProjectID), f.Origin,
|
|
f.Filename, f.ContentType, f.SizeBytes,
|
|
f.StorageKey, f.DisplayHint, models.NullString(f.ExtractedText),
|
|
ToJSON(f.Metadata), now.Format(timeFmt), now.Format(timeFmt),
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *FileStore) GetByID(ctx context.Context, id string) (*models.File, error) {
|
|
row := DB.QueryRowContext(ctx, `SELECT `+fileCols+` FROM files WHERE id = ?`, id)
|
|
return scanFile(row)
|
|
}
|
|
|
|
func (s *FileStore) GetByChannel(ctx context.Context, channelID string, origin string) ([]models.File, error) {
|
|
var rows *sql.Rows
|
|
var err error
|
|
if origin != "" {
|
|
rows, err = DB.QueryContext(ctx,
|
|
`SELECT `+fileCols+` FROM files WHERE channel_id = ? AND origin = ? ORDER BY created_at`,
|
|
channelID, origin)
|
|
} else {
|
|
rows, err = DB.QueryContext(ctx,
|
|
`SELECT `+fileCols+` FROM files WHERE channel_id = ? ORDER BY created_at`,
|
|
channelID)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []models.File
|
|
for rows.Next() {
|
|
f, err := scanFile(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, *f)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *FileStore) GetByMessage(ctx context.Context, messageID string) ([]models.File, error) {
|
|
rows, err := DB.QueryContext(ctx,
|
|
`SELECT `+fileCols+` FROM files WHERE message_id = ? ORDER BY created_at`, messageID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []models.File
|
|
for rows.Next() {
|
|
f, err := scanFile(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, *f)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *FileStore) GetByProject(ctx context.Context, projectID string) ([]models.File, error) {
|
|
rows, err := DB.QueryContext(ctx,
|
|
`SELECT `+fileCols+` FROM files WHERE project_id = ? ORDER BY created_at`, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []models.File
|
|
for rows.Next() {
|
|
f, err := scanFile(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, *f)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *FileStore) GetByUser(ctx context.Context, userID string, page, perPage int) ([]models.File, int, error) {
|
|
var total int
|
|
err := DB.QueryRowContext(ctx,
|
|
`SELECT COUNT(*) FROM files WHERE user_id = ?`, userID).Scan(&total)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
offset := (page - 1) * perPage
|
|
rows, err := DB.QueryContext(ctx,
|
|
`SELECT `+fileCols+` FROM files WHERE user_id = ?
|
|
ORDER BY created_at DESC LIMIT ? OFFSET ?`, userID, perPage, offset)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []models.File
|
|
for rows.Next() {
|
|
f, err := scanFile(rows)
|
|
if err != nil {
|
|
return nil, total, err
|
|
}
|
|
out = append(out, *f)
|
|
}
|
|
return out, total, rows.Err()
|
|
}
|
|
|
|
func (s *FileStore) SetMessageID(ctx context.Context, fileID, messageID string) error {
|
|
_, err := DB.ExecContext(ctx,
|
|
`UPDATE files SET message_id = ?, updated_at = ? WHERE id = ?`,
|
|
messageID, time.Now().UTC().Format(timeFmt), fileID)
|
|
return err
|
|
}
|
|
|
|
func (s *FileStore) UpdateMetadata(ctx context.Context, id string, metadata map[string]interface{}) error {
|
|
metaJSON, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = DB.ExecContext(ctx,
|
|
`UPDATE files SET metadata = json_patch(metadata, ?), updated_at = ? WHERE id = ?`,
|
|
string(metaJSON), time.Now().UTC().Format(timeFmt), id)
|
|
return err
|
|
}
|
|
|
|
func (s *FileStore) SetExtractedText(ctx context.Context, id string, text string) error {
|
|
_, err := DB.ExecContext(ctx,
|
|
`UPDATE files SET extracted_text = ?, updated_at = ? WHERE id = ?`,
|
|
text, time.Now().UTC().Format(timeFmt), id)
|
|
return err
|
|
}
|
|
|
|
func (s *FileStore) Delete(ctx context.Context, id string) (*models.File, error) {
|
|
row := DB.QueryRowContext(ctx,
|
|
`DELETE FROM files WHERE id = ? RETURNING `+fileCols, id)
|
|
return scanFile(row)
|
|
}
|
|
|
|
func (s *FileStore) DeleteByChannel(ctx context.Context, channelID string) ([]string, error) {
|
|
rows, err := DB.QueryContext(ctx,
|
|
`DELETE FROM files WHERE channel_id = ? RETURNING storage_key`, channelID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var keys []string
|
|
for rows.Next() {
|
|
var key string
|
|
if err := rows.Scan(&key); err != nil {
|
|
return keys, err
|
|
}
|
|
keys = append(keys, key)
|
|
}
|
|
return keys, rows.Err()
|
|
}
|
|
|
|
func (s *FileStore) UserUsageBytes(ctx context.Context, userID string) (int64, error) {
|
|
var total sql.NullInt64
|
|
err := DB.QueryRowContext(ctx,
|
|
`SELECT COALESCE(SUM(size_bytes), 0) FROM files WHERE user_id = ?`,
|
|
userID).Scan(&total)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return total.Int64, nil
|
|
}
|
|
|
|
func (s *FileStore) ListOrphans(ctx context.Context, olderThan time.Duration) ([]models.File, error) {
|
|
cutoff := time.Now().Add(-olderThan)
|
|
rows, err := DB.QueryContext(ctx,
|
|
`SELECT `+fileCols+` FROM files
|
|
WHERE message_id IS NULL AND origin = 'user_upload' AND created_at < ?
|
|
ORDER BY created_at`, cutoff)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []models.File
|
|
for rows.Next() {
|
|
f, err := scanFile(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, *f)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// ── CS5c additions (v0.29.0) ──────────────────────────────────────────
|
|
|
|
func (s *FileStore) UpdateStorageKey(ctx context.Context, id, key string) error {
|
|
_, err := DB.ExecContext(ctx,
|
|
`UPDATE files SET storage_key = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
key, id)
|
|
return err
|
|
}
|