This repository has been archived by the owner on Aug 31, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
884dd09
commit 1d3d036
Showing
2 changed files
with
251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |