package main import ( "encoding/json" "fmt" "os" "sort" "sync" "sync/atomic" "time" ) // URLRecord represents a stored link. type URLRecord struct { Code string `json:"code"` OriginalURL string `json:"original_url"` VisitCount int64 `json:"visit_count"` CreatedAt time.Time `json:"created_at"` } // Store is a simple JSON-file-backed key-value store. type Store struct { mu sync.RWMutex file string links map[string]*URLRecord // code -> record } // NewStore opens (or creates) the JSON-backed store. func NewStore(dbPath string) (*Store, error) { s := &Store{ file: dbPath, links: make(map[string]*URLRecord), } data, err := os.ReadFile(dbPath) if err == nil { var records []*URLRecord if err := json.Unmarshal(data, &records); err == nil { for _, rec := range records { s.links[rec.Code] = rec } } } else if !os.IsNotExist(err) { return nil, fmt.Errorf("open store: %w", err) } return s, nil } // Create inserts a new URL with the given code. Returns an error if the code // already exists. func (s *Store) Create(code, originalURL string) error { s.mu.Lock() defer s.mu.Unlock() if _, exists := s.links[code]; exists { return fmt.Errorf("code %q already exists", code) } s.links[code] = &URLRecord{ Code: code, OriginalURL: originalURL, CreatedAt: time.Now(), } return s.persist() } // FindByCode looks up a URL record by its short code. func (s *Store) FindByCode(code string) (*URLRecord, error) { s.mu.RLock() defer s.mu.RUnlock() rec, ok := s.links[code] if !ok { return nil, nil } return rec, nil } // IncrementVisit atomically bumps the visit counter for the given code. func (s *Store) IncrementVisit(code string) error { s.mu.Lock() defer s.mu.Unlock() rec, ok := s.links[code] if !ok { return fmt.Errorf("code %q not found", code) } atomic.AddInt64(&rec.VisitCount, 1) return s.persist() } // Close is a no-op for the JSON store. func (s *Store) Close() error { return nil } // ListAll returns all records sorted by CreatedAt descending (newest first). func (s *Store) ListAll() ([]*URLRecord, error) { s.mu.RLock() defer s.mu.RUnlock() records := make([]*URLRecord, 0, len(s.links)) for _, rec := range s.links { records = append(records, rec) } sort.Slice(records, func(i, j int) bool { return records[i].CreatedAt.After(records[j].CreatedAt) }) return records, nil } // Update changes the OriginalURL of the record identified by code. func (s *Store) Update(code, newURL string) error { s.mu.Lock() defer s.mu.Unlock() rec, ok := s.links[code] if !ok { return fmt.Errorf("code %q not found", code) } rec.OriginalURL = newURL return s.persist() } // Delete removes the record identified by code. func (s *Store) Delete(code string) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.links[code]; !ok { return fmt.Errorf("code %q not found", code) } delete(s.links, code) return s.persist() } func (s *Store) persist() error { records := make([]*URLRecord, 0, len(s.links)) for _, rec := range s.links { records = append(records, rec) } data, err := json.MarshalIndent(records, "", " ") if err != nil { return err } return os.WriteFile(s.file, data, 0644) }