Skip to content
This repository has been archived by the owner on Aug 31, 2020. It is now read-only.

Commit

Permalink
Static handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
kylelemons committed May 24, 2013
1 parent 884dd09 commit 1d3d036
Show file tree
Hide file tree
Showing 2 changed files with 251 additions and 0 deletions.
222 changes: 222 additions & 0 deletions static/static.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package static

import (
"bytes"
"net/http"
"path/filepath"
"strings"
"sync"
"time"

"code.google.com/p/go.exp/fsnotify"
"kylelemons.net/go/daemon"
)

type fileData struct {
mime string
data []byte
}

func serve(w http.ResponseWriter, r *http.Request, file string, get func(string) (*fileData, time.Time), put func(string, time.Time, *fileData)) {
// Check the cache
data, touched := get(file)
if data != nil {
w.Header().Set("Content-Type", data.mime)
http.ServeContent(w, r, file, touched, bytes.NewReader(data.data))
return
}

// Serve the file
now := time.Now()
save := saveResp{
ResponseWriter: w,
}
http.ServeFile(&save, r, file)

// Store it in the cache
go put(file, now, &fileData{
mime: w.Header().Get("Content-Type"),
data: save.buf.Bytes(),
})
}

func watch(path string, stop chan bool, taint func(file string)) {
// Start the filesystem notifications
watch, err := fsnotify.NewWatcher()
if err != nil {
daemon.Fatal.Printf("fsnotify failed: %s", err)
}
defer watch.Close()
watch.Watch(path)

for {
select {
case ev := <-watch.Event:
daemon.Verbose.Printf("static(%q): event: %q", path, ev)
taint(ev.Name)
case err := <-watch.Error:
daemon.Verbose.Printf("static(%q): error: %s", path, err)
return
case <-stop:
daemon.Verbose.Printf("static(%q): closing", path)
return
}
}
}

// A DirCache is an http.Handler for serving static files.
//
// Files within the directory are cached; if a file is changed,
// an inotify mechanism will invalidate the cache.
//
// There is no limit to how much data will be cached.
type DirCache struct {
dir string
strip string

lock sync.RWMutex
data map[string]*fileData
touch map[string]time.Time

stop chan bool
}

// Dir returns a DirCache serving files in the given directory.
func Dir(dir string) *DirCache {
d := &DirCache{
dir: dir,
data: make(map[string]*fileData),
touch: make(map[string]time.Time),
stop: make(chan bool),
}

// Start the filesystem watcher
go watch(dir, d.stop, d.taint)

return d
}

// Strip causes all requests to strip the given prefix from the request URI.
func (d *DirCache) Strip(prefix string) *DirCache {
d.strip = prefix
return d
}

func (d *DirCache) taint(file string) {
d.lock.Lock()
defer d.lock.Unlock()

d.touch[file] = time.Now()
delete(d.data, file)
}

func (d *DirCache) put(file string, t time.Time, data *fileData) {
d.lock.Lock()
defer d.lock.Unlock()

if len(data.data) == 0 || data.mime == "" {
return
}

if !d.touch[file].Before(t) {
daemon.Verbose.Printf("static(%q): skipping update of %q (file has been modified)", d.dir, file)
return
}

d.touch[file] = t
d.data[file] = data
daemon.Info.Printf("static(%q): caching %q (%s)", d.dir, file, data.mime)
}

func (d *DirCache) get(file string) (*fileData, time.Time) {
d.lock.RLock()
defer d.lock.RUnlock()

return d.data[file], d.touch[file]
}

// Close should be called to clean up the cached resources and stop
// the inotify watcher if this DirCache is no longer necessary.
func (d *DirCache) Close() {
close(d.stop)
}

type saveResp struct {
buf bytes.Buffer
http.ResponseWriter
}

func (w *saveResp) Write(b []byte) (n int, err error) {
w.buf.Write(b)
return w.ResponseWriter.Write(b)
}

// ServeHTTP is part of the http.Handler interface.
func (d *DirCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, d.strip)
clean := filepath.Clean(filepath.FromSlash(path))
file := filepath.Join(d.dir, clean)

serve(w, r, file, d.get, d.put)
}

// A FileCache is an http.Handler for serving a single static file.
//
// The file's contents will be cached; if the file is changed,
// an inotify mechanism will invalidate the cache.
//
// There is no limit to how much data will be cached.
type FileCache struct {
file string

lock sync.RWMutex
data *fileData
touch time.Time

stop chan bool
}

// File returns an http.Handler for serving a single static file.
func File(file string) *FileCache {
f := &FileCache{
file: file,
stop: make(chan bool),
}

go watch(file, f.stop, f.taint)

return f
}

func (f *FileCache) get(string) (*fileData, time.Time) {
f.lock.RLock()
defer f.lock.RUnlock()

return f.data, f.touch
}

func (f *FileCache) put(file string, t time.Time, data *fileData) {
f.lock.Lock()
defer f.lock.Unlock()

if !f.touch.Before(t) {
daemon.Verbose.Printf("static(%q): skipping update (file has been modified)", f.file)
return
}

f.touch = t
f.data = data
daemon.Info.Printf("static(%q): caching file (%s)", f.file, data.mime)
}

func (f *FileCache) taint(file string) {
f.lock.Lock()
defer f.lock.Unlock()

f.touch = time.Now()
f.data = nil
}

func (f *FileCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
serve(w, r, f.file, f.get, f.put)
}
29 changes: 29 additions & 0 deletions static/static_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package static

import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
)

func TestDir(t *testing.T) {
victim, err := ioutil.TempDir("", "statictest-")
if err != nil {
t.Fatalf("tempdir: %s", err)
}
defer os.RemoveAll(victim)

t.Logf("Using temporary directory: %q", victim)

dir := Dir(victim)
defer dir.Close()

testFile := filepath.Join(victim, "test")
ioutil.WriteFile(testFile, nil, 0644)
ioutil.WriteFile(testFile, []byte("foo"), 0644)
os.Remove(testFile)

time.Sleep(10 * time.Millisecond)
}

0 comments on commit 1d3d036

Please sign in to comment.