package main import ( "bufio" "encoding/json" "fmt" "log" "os" "path/filepath" "sort" "sync" "time" ) // Apunte es una linea del registro. El registro solo crece: cada cambio se // escribe al final y nunca se reescribe nada. Al arrancar se relee entero para // reconstruir el estado en memoria, asi que el fichero es a la vez la base de // datos y el acta de la asamblea. type Apunte struct { Ts time.Time `json:"ts"` Tipo string `json:"tipo"` Por string `json:"por,omitempty"` Cuenta *Cuenta `json:"cuenta,omitempty"` Evento *Evento `json:"evento,omitempty"` Excepcion *Excepcion `json:"excepcion,omitempty"` } const ( ApunteCuenta = "cuenta" ApunteEvento = "evento" ApunteExcepcion = "excepcion" ) // Almacen aisla el resto del programa de como se guardan los datos. Hoy es un // registro de texto; si algun dia hace falta SQL, se cambia solo esta pieza. type Almacen interface { Cuentas() []Cuenta BuscarCuenta(usuario string) (Cuenta, bool) GuardarCuenta(por string, c Cuenta) error Eventos() []Evento BuscarEvento(id string) (Evento, bool) GuardarEvento(por string, e Evento) error Excepciones() map[Dia]Turno GuardarExcepcion(por string, x Excepcion) error } type Registro struct { mu sync.RWMutex f *os.File ruta string cuentas map[string]Cuenta eventos map[string]Evento excepciones map[Dia]Turno } func AbrirRegistro(dir string) (*Registro, error) { if err := os.MkdirAll(dir, 0o750); err != nil { return nil, err } ruta := filepath.Join(dir, "registro.jsonl") r := &Registro{ ruta: ruta, cuentas: map[string]Cuenta{}, eventos: map[string]Evento{}, excepciones: map[Dia]Turno{}, } if err := r.releer(); err != nil { return nil, err } f, err := os.OpenFile(ruta, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o640) if err != nil { return nil, err } r.f = f return r, nil } func (r *Registro) releer() error { f, err := os.Open(r.ruta) if os.IsNotExist(err) { return nil } if err != nil { return err } defer f.Close() sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024) n := 0 for sc.Scan() { n++ linea := sc.Bytes() if len(linea) == 0 { continue } var a Apunte if err := json.Unmarshal(linea, &a); err != nil { return fmt.Errorf("%s linea %d: %w", r.ruta, n, err) } r.aplicar(a) } return sc.Err() } func (r *Registro) aplicar(a Apunte) { switch a.Tipo { case ApunteCuenta: if a.Cuenta != nil { r.cuentas[a.Cuenta.Usuario] = *a.Cuenta } case ApunteEvento: if a.Evento == nil { return } if _, err := a.Evento.Desde.Time(); err != nil { log.Printf("evento %q ignorado: %v", a.Evento.ID, err) return } if _, err := a.Evento.Hasta.Time(); err != nil { log.Printf("evento %q ignorado: %v", a.Evento.ID, err) return } r.eventos[a.Evento.ID] = *a.Evento case ApunteExcepcion: if a.Excepcion != nil { if _, err := a.Excepcion.Viernes.Time(); err != nil { log.Printf("excepcion de finde ignorada: %v", err) return } if a.Excepcion.Turno == "" { // Turno vacio significa volver a la alternancia automatica. delete(r.excepciones, a.Excepcion.Viernes) } else { r.excepciones[a.Excepcion.Viernes] = a.Excepcion.Turno } } } } // anotar escribe el apunte al final del fichero y lo sincroniza a disco antes // de dar la operacion por buena. Con el candado ya tomado por quien llama. func (r *Registro) anotar(a Apunte) error { a.Ts = time.Now() b, err := json.Marshal(a) if err != nil { return err } b = append(b, '\n') if _, err := r.f.Write(b); err != nil { return err } if err := r.f.Sync(); err != nil { return err } r.aplicar(a) return nil } func (r *Registro) Cerrar() error { r.mu.Lock() defer r.mu.Unlock() if r.f == nil { return nil } return r.f.Close() } func (r *Registro) Cuentas() []Cuenta { r.mu.RLock() defer r.mu.RUnlock() out := make([]Cuenta, 0, len(r.cuentas)) for _, c := range r.cuentas { out = append(out, c) } sort.Slice(out, func(i, j int) bool { return out[i].Usuario < out[j].Usuario }) return out } func (r *Registro) BuscarCuenta(usuario string) (Cuenta, bool) { r.mu.RLock() defer r.mu.RUnlock() c, ok := r.cuentas[usuario] return c, ok } func (r *Registro) GuardarCuenta(por string, c Cuenta) error { r.mu.Lock() defer r.mu.Unlock() return r.anotar(Apunte{Tipo: ApunteCuenta, Por: por, Cuenta: &c}) } func (r *Registro) Eventos() []Evento { r.mu.RLock() defer r.mu.RUnlock() out := make([]Evento, 0, len(r.eventos)) for _, e := range r.eventos { out = append(out, e) } sort.Slice(out, func(i, j int) bool { if out[i].Desde != out[j].Desde { return out[i].Desde < out[j].Desde } return out[i].ID < out[j].ID }) return out } func (r *Registro) BuscarEvento(id string) (Evento, bool) { r.mu.RLock() defer r.mu.RUnlock() e, ok := r.eventos[id] return e, ok } func (r *Registro) GuardarEvento(por string, e Evento) error { r.mu.Lock() defer r.mu.Unlock() return r.anotar(Apunte{Tipo: ApunteEvento, Por: por, Evento: &e}) } func (r *Registro) Excepciones() map[Dia]Turno { r.mu.RLock() defer r.mu.RUnlock() out := make(map[Dia]Turno, len(r.excepciones)) for k, v := range r.excepciones { out[k] = v } return out } func (r *Registro) GuardarExcepcion(por string, x Excepcion) error { r.mu.Lock() defer r.mu.Unlock() return r.anotar(Apunte{Tipo: ApunteExcepcion, Por: por, Excepcion: &x}) }