You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

149 lines
3.2 KiB

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)
}