Ver Fonte

Refactor store into its own file.

Dmitry Chestnykh há 14 anos atrás
pai
commit
aa0588b4c9
4 ficheiros alterados com 126 adições e 70 exclusões
  1. 1 0
      Makefile
  2. 29 69
      captcha.go
  3. 1 1
      cmd/main.go
  4. 95 0
      store.go

+ 1 - 0
Makefile

@@ -3,6 +3,7 @@ include $(GOROOT)/src/Make.inc
 TARG=github.com/dchest/captcha
 TARG=github.com/dchest/captcha
 GOFILES=\
 GOFILES=\
 	captcha.go\
 	captcha.go\
+	store.go\
 	font.go\
 	font.go\
 	image.go
 	image.go
 
 

+ 29 - 69
captcha.go

@@ -8,47 +8,19 @@ import (
 	crand "crypto/rand"
 	crand "crypto/rand"
 	"github.com/dchest/uniuri"
 	"github.com/dchest/uniuri"
 	"io"
 	"io"
-	"container/list"
-	"sync"
 )
 )
 
 
-const (
-	// Expiration time for captchas
-	Expiration = 2 * 60 // 2 minutes
-	// The number of captchas created that triggers garbage collection
-	CollectNum = 100
-)
-
-// expValue stores timestamp and id of captchas. It is used in a list inside
-// storage for indexing generated captchas by timestamp to enable garbage
-// collection of expired captchas.
-type expValue struct {
-	timestamp int64
-	id        string
-}
-
-// storage is an internal storage for captcha ids and their values.
-type storage struct {
-	mu  sync.RWMutex
-	ids map[string][]byte
-	exp *list.List
-	// Number of items stored after last collection
-	colNum int
-}
+// Standard number of numbers in captcha
+const StdLength = 6
 
 
-func newStore() *storage {
-	s := new(storage)
-	s.ids = make(map[string][]byte)
-	s.exp = list.New()
-	return s
-}
-
-var store = newStore()
+var globalStore = newStore()
 
 
 func init() {
 func init() {
 	rand.Seed(time.Seconds())
 	rand.Seed(time.Seconds())
 }
 }
 
 
+// randomNumbers return a byte slice of the given length containing random
+// numbers in range 0-9.
 func randomNumbers(length int) []byte {
 func randomNumbers(length int) []byte {
 	n := make([]byte, length)
 	n := make([]byte, length)
 	if _, err := io.ReadFull(crand.Reader, n); err != nil {
 	if _, err := io.ReadFull(crand.Reader, n); err != nil {
@@ -62,28 +34,30 @@ func randomNumbers(length int) []byte {
 
 
 // New creates a new captcha of the given length, saves it in the internal
 // New creates a new captcha of the given length, saves it in the internal
 // storage, and returns its id.
 // storage, and returns its id.
-func New(length int) string {
-	ns := randomNumbers(length)
-	id := uniuri.New()
-	store.mu.Lock()
-	defer store.mu.Unlock()
-	store.ids[id] = ns
-	store.exp.PushBack(expValue{time.Seconds(), id})
-	store.colNum++
-	if store.colNum > CollectNum {
-		go Collect()
-		store.colNum = 0
+func New(length int) (id string) {
+	id = uniuri.New()
+	globalStore.saveCaptcha(id, randomNumbers(length))
+	return
+}
+
+// Reload generates new numbers for the given captcha id.  This function does
+// nothing if there is no captcha with the given id.
+//
+// After calling this function, the image presented to a user must be refreshed
+// to show the new captcha (WriteImage will write the new one).
+func Reload(id string) {
+	oldns := globalStore.getNumbers(id)
+	if oldns == nil {
+		return
 	}
 	}
-	return id
+	globalStore.saveCaptcha(id, randomNumbers(len(oldns)))
 }
 }
 
 
 // WriteImage writes PNG-encoded captcha image of the given width and height
 // WriteImage writes PNG-encoded captcha image of the given width and height
 // with the given captcha id into the io.Writer.
 // with the given captcha id into the io.Writer.
 func WriteImage(w io.Writer, id string, width, height int) os.Error {
 func WriteImage(w io.Writer, id string, width, height int) os.Error {
-	store.mu.RLock()
-	defer store.mu.RUnlock()
-	ns, ok := store.ids[id]
-	if !ok {
+	ns := globalStore.getNumbers(id)
+	if ns == nil {
 		return os.NewError("captcha id not found")
 		return os.NewError("captcha id not found")
 	}
 	}
 	return NewImage(ns, width, height).PNGEncode(w)
 	return NewImage(ns, width, height).PNGEncode(w)
@@ -95,13 +69,10 @@ func WriteImage(w io.Writer, id string, width, height int) os.Error {
 // The function deletes the captcha with the given id from the internal
 // The function deletes the captcha with the given id from the internal
 // storage, so that the same captcha can't be used anymore.
 // storage, so that the same captcha can't be used anymore.
 func Verify(id string, numbers []byte) bool {
 func Verify(id string, numbers []byte) bool {
-	store.mu.Lock()
-	defer store.mu.Unlock()
-	realns, ok := store.ids[id]
-	if !ok {
+	realns := globalStore.getNumbersClear(id)
+	if realns == nil {
 		return false
 		return false
 	}
 	}
-	store.ids[id] = nil, false
 	return bytes.Equal(numbers, realns)
 	return bytes.Equal(numbers, realns)
 }
 }
 
 
@@ -109,20 +80,9 @@ func Verify(id string, numbers []byte) bool {
 // storage. It is called automatically by New function every CollectNum
 // storage. It is called automatically by New function every CollectNum
 // generated captchas, but still exported to enable freeing memory manually if
 // generated captchas, but still exported to enable freeing memory manually if
 // needed.
 // needed.
+//
+// Collection is launched in a new goroutine, so this function returns
+// immediately.
 func Collect() {
 func Collect() {
-	now := time.Seconds()
-	store.mu.Lock()
-	defer store.mu.Unlock()
-	for e := store.exp.Front(); e != nil; e = e.Next() {
-		ev, ok := e.Value.(expValue)
-		if !ok {
-			return
-		}
-		if ev.timestamp+Expiration < now {
-			store.ids[ev.id] = nil, false
-			store.exp.Remove(e)
-		} else {
-			return
-		}
-	}
+	go globalStore.collect()
 }
 }

+ 1 - 1
cmd/main.go

@@ -6,6 +6,6 @@ import (
 )
 )
 
 
 func main() {
 func main() {
-	img, _ := captcha.NewRandomImage(captcha.StdWidth, captcha.StdHeight)
+	img, _ := captcha.NewRandomImage(captcha.StdLength, captcha.StdWidth, captcha.StdHeight)
 	img.PNGEncode(os.Stdout)
 	img.PNGEncode(os.Stdout)
 }
 }

+ 95 - 0
store.go

@@ -0,0 +1,95 @@
+package captcha
+
+import (
+	"container/list"
+	"sync"
+	"time"
+)
+
+const (
+	// Expiration time for captchas
+	Expiration = 2 * 60 // 2 minutes
+	// The number of captchas created that triggers garbage collection
+	CollectNum = 100
+)
+
+// expValue stores timestamp and id of captchas. It is used in a list inside
+// store for indexing generated captchas by timestamp to enable garbage
+// collection of expired captchas.
+type expValue struct {
+	timestamp int64
+	id        string
+}
+
+// store is an internal store for captcha ids and their values.
+type store struct {
+	mu  sync.RWMutex
+	ids map[string][]byte
+	exp *list.List
+	// Number of items stored after last collection
+	colNum int
+}
+
+// newStore initializes and returns a new store.
+func newStore() *store {
+	s := new(store)
+	s.ids = make(map[string][]byte)
+	s.exp = list.New()
+	return s
+}
+
+// saveCaptcha saves the captcha id and the corresponding numbers.
+func (s *store) saveCaptcha(id string, ns []byte) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.ids[id] = ns
+	s.exp.PushBack(expValue{time.Seconds(), id})
+	s.colNum++
+	if s.colNum > CollectNum {
+		go s.collect()
+		s.colNum = 0
+	}
+}
+
+// getNumbers returns the numbers for the given id.
+func (s *store) getNumbers(id string) (ns []byte) {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	ns, _ = s.ids[id]
+	return
+}
+
+// getNumbersClear returns the numbers for the given id, and removes them from
+// the store.
+func (s *store) getNumbersClear(id string) (ns []byte) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	ns, ok := s.ids[id]
+	if !ok {
+		return
+	}
+	s.ids[id] = nil, false
+	// XXX(dchest) Index (s.exp) will be cleaned when collecting expired
+	// captchas.  Can't clean it here, because we don't store reference to
+	// expValue in the map. Maybe store it?
+	return
+}
+
+// collect deletes expired captchas from the store.
+func (s *store) collect() {
+	now := time.Seconds()
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	for e := s.exp.Front(); e != nil; e = e.Next() {
+		ev, ok := e.Value.(expValue)
+		if !ok {
+			return
+		}
+		if ev.timestamp+Expiration < now {
+			s.ids[ev.id] = nil, false
+			s.exp.Remove(e)
+		} else {
+			return
+		}
+	}
+}