package db import ( "database/sql" "fmt" "os" "path/filepath" "app_compartir/internal/models" _ "modernc.org/sqlite" ) type Database struct { *sql.DB } func Init(dbPath, uploadDir string) (*Database, error) { if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { return nil, fmt.Errorf("error creando directorio de bd: %w", err) } if err := os.MkdirAll(uploadDir, 0755); err != nil { return nil, fmt.Errorf("error creando directorio de uploads: %w", err) } conn, err := sql.Open("sqlite", dbPath+"?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)") if err != nil { return nil, fmt.Errorf("error abriendo sqlite: %w", err) } d := &Database{DB: conn} if err := d.migrate(); err != nil { return nil, fmt.Errorf("error ejecutando migraciones: %w", err) } return d, nil } func (d *Database) migrate() error { schema := ` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, provider TEXT NOT NULL, provider_id TEXT NOT NULL, email TEXT, name TEXT, avatar_url TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS sessions ( token TEXT PRIMARY KEY, user_id TEXT, is_guest BOOLEAN NOT NULL DEFAULT 1, guest_id TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS shares ( id TEXT PRIMARY KEY, user_id TEXT, user_display TEXT NOT NULL DEFAULT 'Invitado', is_guest BOOLEAN NOT NULL DEFAULT 1, type TEXT NOT NULL DEFAULT 'file', title TEXT NOT NULL, stored_path TEXT, file_size INTEGER NOT NULL DEFAULT 0, mime_type TEXT NOT NULL DEFAULT 'text/plain', text_content TEXT, views_count INTEGER NOT NULL DEFAULT 0, downloads_count INTEGER NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ); CREATE INDEX IF NOT EXISTS idx_shares_expired_guest ON shares(is_guest, expires_at); CREATE INDEX IF NOT EXISTS idx_shares_user_id ON shares(user_id); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at); ` _, err := d.Exec(schema) return err } func (d *Database) CreateShare(s *models.Share) error { query := ` INSERT INTO shares ( id, user_id, user_display, is_guest, type, title, stored_path, file_size, mime_type, text_content, views_count, downloads_count, created_at, expires_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` _, err := d.Exec(query, s.ID, s.UserID, s.UserDisplay, s.IsGuest, s.Type, s.Title, s.StoredPath, s.FileSize, s.MimeType, s.TextContent, s.ViewsCount, s.DownloadsCount, s.CreatedAt, s.ExpiresAt, ) return err } func (d *Database) GetShare(id string) (*models.Share, error) { query := ` SELECT id, user_id, user_display, is_guest, type, title, stored_path, file_size, mime_type, text_content, views_count, downloads_count, created_at, expires_at FROM shares WHERE id = ?` row := d.QueryRow(query, id) s := &models.Share{} var expiresAt sql.NullTime var textContent, storedPath sql.NullString var userID sql.NullString err := row.Scan( &s.ID, &userID, &s.UserDisplay, &s.IsGuest, &s.Type, &s.Title, &storedPath, &s.FileSize, &s.MimeType, &textContent, &s.ViewsCount, &s.DownloadsCount, &s.CreatedAt, &expiresAt, ) if err != nil { return nil, err } if userID.Valid { s.UserID = &userID.String } if storedPath.Valid { s.StoredPath = storedPath.String } if textContent.Valid { s.TextContent = textContent.String } if expiresAt.Valid { s.ExpiresAt = &expiresAt.Time } return s, nil } func (d *Database) IncrementViews(id string) error { _, err := d.Exec("UPDATE shares SET views_count = views_count + 1 WHERE id = ?", id) return err } func (d *Database) IncrementDownloads(id string) error { _, err := d.Exec("UPDATE shares SET downloads_count = downloads_count + 1 WHERE id = ?", id) return err } func (d *Database) GetUserShares(userID string) ([]*models.Share, error) { query := ` SELECT id, user_id, user_display, is_guest, type, title, stored_path, file_size, mime_type, text_content, views_count, downloads_count, created_at, expires_at FROM shares WHERE user_id = ? ORDER BY created_at DESC` rows, err := d.Query(query, userID) if err != nil { return nil, err } defer rows.Close() var list []*models.Share for rows.Next() { s := &models.Share{} var expiresAt sql.NullTime var textContent, storedPath sql.NullString var uID sql.NullString if err := rows.Scan( &s.ID, &uID, &s.UserDisplay, &s.IsGuest, &s.Type, &s.Title, &storedPath, &s.FileSize, &s.MimeType, &textContent, &s.ViewsCount, &s.DownloadsCount, &s.CreatedAt, &expiresAt, ); err != nil { return nil, err } if uID.Valid { s.UserID = &uID.String } if storedPath.Valid { s.StoredPath = storedPath.String } if textContent.Valid { s.TextContent = textContent.String } if expiresAt.Valid { s.ExpiresAt = &expiresAt.Time } list = append(list, s) } return list, nil } func (d *Database) DeleteShare(id string, userID *string) error { if userID != nil { _, err := d.Exec("DELETE FROM shares WHERE id = ? AND user_id = ?", id, *userID) return err } _, err := d.Exec("DELETE FROM shares WHERE id = ?", id) return err } // GetExpiredGuestShares obtiene SOLO los shares marcados como guest cuya fecha de expiración ya pasó. // Los usuarios registrados (is_guest = 0) NUNCA se seleccionan aquí. func (d *Database) GetExpiredGuestShares() ([]*models.Share, error) { query := ` SELECT id, stored_path FROM shares WHERE is_guest = 1 AND expires_at IS NOT NULL AND expires_at <= CURRENT_TIMESTAMP` rows, err := d.Query(query) if err != nil { return nil, err } defer rows.Close() var list []*models.Share for rows.Next() { s := &models.Share{} var storedPath sql.NullString if err := rows.Scan(&s.ID, &storedPath); err != nil { return nil, err } if storedPath.Valid { s.StoredPath = storedPath.String } list = append(list, s) } return list, nil } func (d *Database) PurgeShare(id string) error { _, err := d.Exec("DELETE FROM shares WHERE id = ? AND is_guest = 1", id) return err } func (d *Database) UpsertUser(u *models.User) error { query := ` INSERT INTO users (id, provider, provider_id, email, name, avatar_url, created_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET email = excluded.email, name = excluded.name, avatar_url = excluded.avatar_url` _, err := d.Exec(query, u.ID, u.Provider, u.ProviderID, u.Email, u.Name, u.AvatarURL, u.CreatedAt) return err } func (d *Database) GetUser(id string) (*models.User, error) { query := `SELECT id, provider, provider_id, email, name, avatar_url, created_at FROM users WHERE id = ?` row := d.QueryRow(query, id) u := &models.User{} var email, name, avatar sql.NullString if err := row.Scan(&u.ID, &u.Provider, &u.ProviderID, &email, &name, &avatar, &u.CreatedAt); err != nil { return nil, err } if email.Valid { u.Email = email.String } if name.Valid { u.Name = name.String } if avatar.Valid { u.AvatarURL = avatar.String } return u, nil } func (d *Database) CreateSession(s *models.Session) error { query := ` INSERT INTO sessions (token, user_id, is_guest, guest_id, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)` _, err := d.Exec(query, s.Token, s.UserID, s.IsGuest, s.GuestID, s.CreatedAt, s.ExpiresAt) return err } func (d *Database) GetSession(token string) (*models.Session, error) { query := ` SELECT s.token, s.user_id, s.is_guest, s.guest_id, s.created_at, s.expires_at, u.id, u.provider, u.provider_id, u.email, u.name, u.avatar_url, u.created_at FROM sessions s LEFT JOIN users u ON s.user_id = u.id WHERE s.token = ? AND s.expires_at > CURRENT_TIMESTAMP` row := d.QueryRow(query, token) s := &models.Session{} var userID sql.NullString var uID, provider, provID, email, name, avatar sql.NullString var uCreated sql.NullTime err := row.Scan( &s.Token, &userID, &s.IsGuest, &s.GuestID, &s.CreatedAt, &s.ExpiresAt, &uID, &provider, &provID, &email, &name, &avatar, &uCreated, ) if err != nil { return nil, err } if userID.Valid { s.UserID = &userID.String } if uID.Valid { s.User = &models.User{ ID: uID.String, Provider: provider.String, ProviderID: provID.String, Email: email.String, Name: name.String, AvatarURL: avatar.String, CreatedAt: uCreated.Time, } } return s, nil } func (d *Database) DeleteSession(token string) error { _, err := d.Exec("DELETE FROM sessions WHERE token = ?", token) return err } func (d *Database) PurgeExpiredSessions() error { _, err := d.Exec("DELETE FROM sessions WHERE expires_at <= CURRENT_TIMESTAMP") return err }