package storage import ( "encoding/binary" "fmt" "io" "os" "git.dws.rip/DWS/onyx/internal/models" ) // OplogReader handles reading entries from the oplog file type OplogReader struct { path string } // NewOplogReader creates a new oplog reader for the given file path func NewOplogReader(path string) *OplogReader { return &OplogReader{ path: path, } } // ReadLastEntry reads the last (most recent) entry in the oplog func (r *OplogReader) ReadLastEntry() (*models.OplogEntry, error) { file, err := os.Open(r.path) if err != nil { return nil, fmt.Errorf("failed to open oplog file: %w", err) } defer file.Close() var lastEntry *models.OplogEntry // Read through all entries to find the last one for { // Read entry length (4 bytes) var entryLen uint32 err := binary.Read(file, binary.LittleEndian, &entryLen) if err != nil { if err == io.EOF { break } return nil, fmt.Errorf("failed to read entry length: %w", err) } // Read the entry data entryData := make([]byte, entryLen) n, err := file.Read(entryData) if err != nil { return nil, fmt.Errorf("failed to read entry data: %w", err) } if n != int(entryLen) { return nil, fmt.Errorf("incomplete entry data read: expected %d bytes, got %d", entryLen, n) } // Deserialize the entry entry, err := models.DeserializeOplogEntry(entryData) if err != nil { return nil, fmt.Errorf("failed to deserialize entry: %w", err) } lastEntry = entry } if lastEntry == nil { return nil, fmt.Errorf("oplog is empty") } return lastEntry, nil } // ReadEntry reads a specific entry by ID func (r *OplogReader) ReadEntry(id uint64) (*models.OplogEntry, error) { file, err := os.Open(r.path) if err != nil { return nil, fmt.Errorf("failed to open oplog file: %w", err) } defer file.Close() // Read through all entries to find the one with matching ID for { // Read entry length (4 bytes) var entryLen uint32 err := binary.Read(file, binary.LittleEndian, &entryLen) if err != nil { if err == io.EOF { break } return nil, fmt.Errorf("failed to read entry length: %w", err) } // Read the entry data entryData := make([]byte, entryLen) n, err := file.Read(entryData) if err != nil { return nil, fmt.Errorf("failed to read entry data: %w", err) } if n != int(entryLen) { return nil, fmt.Errorf("incomplete entry data read: expected %d bytes, got %d", entryLen, n) } // Deserialize the entry entry, err := models.DeserializeOplogEntry(entryData) if err != nil { return nil, fmt.Errorf("failed to deserialize entry: %w", err) } if entry.ID == id { return entry, nil } } return nil, fmt.Errorf("entry with ID %d not found", id) } // GetUndoStack returns a stack of entries that can be undone (in reverse order) func (r *OplogReader) GetUndoStack() ([]*models.OplogEntry, error) { entries, err := r.ReadAllEntries() if err != nil { return nil, err } // Filter out entries that have already been undone // For now, we return all entries in reverse order // In the future, we might track undone entries separately var undoStack []*models.OplogEntry for i := len(entries) - 1; i >= 0; i-- { undoStack = append(undoStack, entries[i]) } return undoStack, nil } // ReadAllEntries reads all entries from the oplog in order func (r *OplogReader) ReadAllEntries() ([]*models.OplogEntry, error) { file, err := os.Open(r.path) if err != nil { return nil, fmt.Errorf("failed to open oplog file: %w", err) } defer file.Close() var entries []*models.OplogEntry // Read through all entries for { // Read entry length (4 bytes) var entryLen uint32 err := binary.Read(file, binary.LittleEndian, &entryLen) if err != nil { if err == io.EOF { break } return nil, fmt.Errorf("failed to read entry length: %w", err) } // Read the entry data entryData := make([]byte, entryLen) n, err := file.Read(entryData) if err != nil { return nil, fmt.Errorf("failed to read entry data: %w", err) } if n != int(entryLen) { return nil, fmt.Errorf("incomplete entry data read: expected %d bytes, got %d", entryLen, n) } // Deserialize the entry entry, err := models.DeserializeOplogEntry(entryData) if err != nil { return nil, fmt.Errorf("failed to deserialize entry: %w", err) } entries = append(entries, entry) } return entries, nil } // Count returns the total number of entries in the oplog func (r *OplogReader) Count() (int, error) { entries, err := r.ReadAllEntries() if err != nil { return 0, err } return len(entries), nil } // IsEmpty checks if the oplog is empty func (r *OplogReader) IsEmpty() (bool, error) { file, err := os.Open(r.path) if err != nil { return false, fmt.Errorf("failed to open oplog file: %w", err) } defer file.Close() stat, err := file.Stat() if err != nil { return false, fmt.Errorf("failed to stat file: %w", err) } return stat.Size() == 0, nil }