package upload import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // ErrSessionNotFound wird geliefert, wenn eine angefragte Upload-Sitzung // nicht existiert. var ErrSessionNotFound = errors.New("upload: sitzung nicht gefunden") // Session ist der Zustand eines laufenden oder abgeschlossenen Uploads // (Akzeptanzkriterium 1: Grundlage fuer Fortsetzung nach Abbruch). type Session struct { ID string Filename string MimeType string TotalSize int64 BytesReceived int64 FolderID *string CreatedBy string Status string DocumentID *string } const ( StatusUploading = "uploading" StatusCompleted = "completed" StatusAborted = "aborted" ) // SessionStore verwaltet upload_sessions. type SessionStore struct { pool *pgxpool.Pool } func NewSessionStore(pool *pgxpool.Pool) *SessionStore { return &SessionStore{pool: pool} } func (s *SessionStore) Create(ctx context.Context, filename, mimeType string, totalSize int64, folderID *string, createdBy string) (string, error) { var id string err := s.pool.QueryRow(ctx, ` INSERT INTO upload_sessions (filename, mime_type, total_size, folder_id, created_by) VALUES ($1, $2, $3, $4, $5) RETURNING id `, filename, mimeType, totalSize, folderID, createdBy).Scan(&id) if err != nil { return "", fmt.Errorf("upload: sitzung anlegen: %w", err) } return id, nil } func (s *SessionStore) Get(ctx context.Context, sessionID string) (*Session, error) { var sess Session err := s.pool.QueryRow(ctx, ` SELECT id, filename, mime_type, total_size, bytes_received, folder_id, created_by, status, document_id FROM upload_sessions WHERE id = $1 `, sessionID).Scan(&sess.ID, &sess.Filename, &sess.MimeType, &sess.TotalSize, &sess.BytesReceived, &sess.FolderID, &sess.CreatedBy, &sess.Status, &sess.DocumentID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrSessionNotFound } return nil, fmt.Errorf("upload: sitzung lesen: %w", err) } return &sess, nil } // RecordChunk aktualisiert bytes_received auf das Maximum aus dem // bisherigen und dem neu gemeldeten Wert (Akzeptanzkriterium 1: ein // erneut zugestellter/uebersprungener Chunk darf den Fortschritt nie // zurueckdrehen — GREATEST statt blindem Ueberschreiben). func (s *SessionStore) RecordChunk(ctx context.Context, sessionID string, bytesReceivedNow int64) error { tag, err := s.pool.Exec(ctx, ` UPDATE upload_sessions SET bytes_received = GREATEST(bytes_received, $2), updated_at = now() WHERE id = $1 AND status = 'uploading' `, sessionID, bytesReceivedNow) if err != nil { return fmt.Errorf("upload: fortschritt aktualisieren: %w", err) } if tag.RowsAffected() == 0 { return ErrSessionNotFound } return nil } func (s *SessionStore) MarkCompleted(ctx context.Context, sessionID, documentID string) error { tag, err := s.pool.Exec(ctx, ` UPDATE upload_sessions SET status = 'completed', document_id = $2, updated_at = now() WHERE id = $1 `, sessionID, documentID) if err != nil { return fmt.Errorf("upload: sitzung abschliessen: %w", err) } if tag.RowsAffected() == 0 { return ErrSessionNotFound } return nil }