diff --git a/fixtures/site.zip b/fixtures/site.zip new file mode 100644 index 0000000..4114441 Binary files /dev/null and b/fixtures/site.zip differ diff --git a/main.go b/main.go index a1d6840..601c313 100644 --- a/main.go +++ b/main.go @@ -7,14 +7,14 @@ import ( "time" "flag" - "io/ioutil" "log" "os" "encoding/json" - "github.com/russross/blackfriday" "html/template" + "github.com/russross/blackfriday" + "net" "net/http" "path/filepath" @@ -46,19 +46,32 @@ type MenuEntry struct { } type Werc struct { - root string - conf WercConfig - tmpl *template.Template + root string + conf WercConfig + tmpl *template.Template + store Store } func New(root string) *Werc { w := new(Werc) w.root = root - w.tmpl = template.Must(template.ParseGlob(root + "/lib/*.html")) + w.tmpl = template.New("root") - b, err := ioutil.ReadFile(root + "/etc/config.json") + var err error + if strings.HasSuffix(root, ".zip") { + w.store, err = openZipStore(root) + } else { + w.store = &fileStore{root} + err = nil + } if err != nil { - log.Printf("no config.json in %s", root) + log.Printf("can't open root: %v", err) + return nil + } + + b, err := w.store.ReadFile("etc/config.json") + if err != nil { + log.Printf("error loading config.json: %v", err) return nil } err = json.Unmarshal(b, &w.conf) @@ -66,6 +79,17 @@ func New(root string) *Werc { log.Printf("%s: %s", root+"/config.json", err) return nil } + + // load templates + tmpls := []string{"base", "directory", "footer", "menu", "text", "topbar"} + for _, tn := range tmpls { + b, err := w.store.ReadFile(fmt.Sprintf("lib/%s.html", tn)) + if err != nil { + panic(err) + } + template.Must(w.tmpl.New(tn + ".html").Parse(string(b))) + } + return w } @@ -134,7 +158,7 @@ func (werc *Werc) genmenu(site, dir string) []MenuEntry { for i := range dirs { var sub []MenuEntry b := filepath.Join(base, dirs[i]) - fi, _ := ioutil.ReadDir(b) + fi, _ := werc.store.ReadDir(b) for _, f := range fi { newname, ok := okmenu(b, f) if !ok { @@ -172,23 +196,20 @@ func (werc *Werc) genmenu(site, dir string) []MenuEntry { return root } -func (werc *Werc) WercCommon(w http.ResponseWriter, r *http.Request, page *WercPage) { - site, _, _ := net.SplitHostPort(r.Host) - if site == "" { - site = werc.conf.MasterSite - } +func (werc *Werc) WercCommon(w http.ResponseWriter, r *http.Request, site string, page *WercPage) { path := r.URL.Path page.Menu = werc.genmenu(site, path) - conf := werc.root + "/sites/" + site + "/_werc/config.json" - b, err := ioutil.ReadFile(conf) - if err != nil { - log.Printf("%s: %s", conf, err) - } - err = json.Unmarshal(b, &page.Config) + conf := "sites/" + site + "/_werc/config.json" + b, err := werc.store.ReadFile(conf) if err != nil { log.Printf("%s: %s", conf, err) + } else { + err = json.Unmarshal(b, &page.Config) + if err != nil { + log.Printf("%s: %s", conf, err) + } } //log.Printf("root %+v", page.Menu) if err := werc.tmpl.ExecuteTemplate(w, "base.html", page); err != nil { @@ -221,7 +242,7 @@ func okmenu(base string, fi os.FileInfo) (string, bool) { return "", false } -func (werc *Werc) WercDir(w http.ResponseWriter, r *http.Request, dir string) { +func (werc *Werc) WercDir(w http.ResponseWriter, r *http.Request, site, dir string) { type DirEntry struct { Name string Fi os.FileInfo @@ -236,7 +257,7 @@ func (werc *Werc) WercDir(w http.ResponseWriter, r *http.Request, dir string) { data.Title = r.URL.Path buf := new(bytes.Buffer) - fi, err := ioutil.ReadDir(dir) + fi, err := werc.store.ReadDir(dir) if err != nil { http.Error(w, fmt.Sprintf("%s", err), 500) return @@ -249,30 +270,30 @@ func (werc *Werc) WercDir(w http.ResponseWriter, r *http.Request, dir string) { } werc.tmpl.ExecuteTemplate(buf, "directory.html", data) - werc.WercCommon(w, r, &WercPage{Title: ptitle(dir), Content: template.HTML(buf.String())}) + werc.WercCommon(w, r, site, &WercPage{Title: ptitle(dir), Content: template.HTML(buf.String())}) } -func (werc *Werc) WercMd(w http.ResponseWriter, r *http.Request, path string) { - b, err := ioutil.ReadFile(path) +func (werc *Werc) WercMd(w http.ResponseWriter, r *http.Request, site, path string) { + b, err := werc.store.ReadFile(path) if err != nil { http.Error(w, fmt.Sprintf("%s", err), 404) return } md := blackfriday.MarkdownBasic(b) - werc.WercCommon(w, r, &WercPage{Title: ptitle(path), Content: template.HTML(string(md))}) + werc.WercCommon(w, r, site, &WercPage{Title: ptitle(path), Content: template.HTML(string(md))}) } -func (werc *Werc) WercHTML(w http.ResponseWriter, r *http.Request, path string) { - b, err := ioutil.ReadFile(path) +func (werc *Werc) WercHTML(w http.ResponseWriter, r *http.Request, site, path string) { + b, err := werc.store.ReadFile(path) if err != nil { http.Error(w, fmt.Sprintf("%s", err), 404) return } - werc.WercCommon(w, r, &WercPage{Title: ptitle(path), Content: template.HTML(string(b))}) + werc.WercCommon(w, r, site, &WercPage{Title: ptitle(path), Content: template.HTML(string(b))}) } -func (werc *Werc) WercTXT(w http.ResponseWriter, r *http.Request, path string) { - b, err := ioutil.ReadFile(path) +func (werc *Werc) WercTXT(w http.ResponseWriter, r *http.Request, site, path string) { + b, err := werc.store.ReadFile(path) if err != nil { http.Error(w, fmt.Sprintf("%s", err), 404) return @@ -280,11 +301,25 @@ func (werc *Werc) WercTXT(w http.ResponseWriter, r *http.Request, path string) { buf := new(bytes.Buffer) werc.tmpl.ExecuteTemplate(buf, "text.html", string(b)) - werc.WercCommon(w, r, &WercPage{Title: ptitle(path), Content: template.HTML(buf.String())}) + werc.WercCommon(w, r, site, &WercPage{Title: ptitle(path), Content: template.HTML(buf.String())}) } -func (werc *Werc) Werc404(w http.ResponseWriter, r *http.Request) { - http.NotFound(w, r) +func (werc *Werc) Pub(w http.ResponseWriter, r *http.Request, path string) { + if strings.HasPrefix(path, "/") { + path = path[1:] + } + + b, err := werc.store.ReadFile(path) + if err != nil { + log.Printf("Pub: %v", err) + http.Error(w, err.Error(), 404) + return + } + + buf := bytes.NewReader(b) + http.ServeContent(w, r, filepath.Base(path), time.Now(), buf) + + log.Printf("pub sent %d bytes", len(b)) } func (werc *Werc) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -295,7 +330,14 @@ func (werc *Werc) ServeHTTP(w http.ResponseWriter, r *http.Request) { } path := r.URL.Path - base := werc.root + "/sites/" + site + // try pub first + if strings.HasPrefix(path, "/pub") { + werc.Pub(w, r, path) + return + } + +again: + base := "sites/" + site if strings.HasSuffix(path, "/index") { http.Redirect(w, r, strings.TrimSuffix(path, "/index"), http.StatusMovedPermanently) @@ -303,7 +345,7 @@ func (werc *Werc) ServeHTTP(w http.ResponseWriter, r *http.Request) { } if !strings.HasSuffix(path, "/") { - if st, err := os.Stat(base + path); err == nil { + if st, err := werc.store.Stat(base + path); err == nil { if st.Mode().IsDir() { http.Redirect(w, r, path+"/", http.StatusMovedPermanently) return @@ -312,12 +354,14 @@ func (werc *Werc) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // various format handling - sufferring := map[string]func(w http.ResponseWriter, r *http.Request, path string){ + sufferring := map[string]func(w http.ResponseWriter, r *http.Request, site, path string){ "md": werc.WercMd, "html": werc.WercHTML, "txt": werc.WercTXT, } + log.Printf("path %v", base) + for suf, handler := range sufferring { f := base if strings.HasSuffix(path, "/") { @@ -326,18 +370,18 @@ func (werc *Werc) ServeHTTP(w http.ResponseWriter, r *http.Request) { f = filepath.Join(f, path+"."+suf) } - if _, err := os.Stat(f); err == nil { + if _, err := werc.store.Stat(f); err == nil { log.Printf("%s %s", suf, f) - handler(w, r, f) + handler(w, r, site, f) return } } - if st, err := os.Stat(base + path); err == nil { + if st, err := werc.store.Stat(base + path); err == nil { if st.Mode().IsDir() { // directory handling log.Printf("d %s", base+path) - werc.WercDir(w, r, base+path) + werc.WercDir(w, r, site, base+path) return } else { // plain file handling @@ -347,7 +391,14 @@ func (werc *Werc) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - werc.Werc404(w, r) + if site != werc.conf.MasterSite { + site = werc.conf.MasterSite + goto again + } + + log.Printf("404 %s", path) + + http.NotFound(w, r) } func main() { @@ -355,7 +406,6 @@ func main() { w := New(*root) mux := http.NewServeMux() mux.Handle("/", w) - mux.Handle("/pub/", http.StripPrefix("/pub/", http.FileServer(http.Dir(filepath.Join(w.root, "pub"))))) s := &http.Server{ Addr: *listen, Handler: mux, diff --git a/store.go b/store.go new file mode 100644 index 0000000..8c2ee54 --- /dev/null +++ b/store.go @@ -0,0 +1,135 @@ +package main + +import ( + "archive/zip" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" +) + +type Store interface { + ReadFile(filename string) ([]byte, error) + ReadDir(dirname string) ([]os.FileInfo, error) + Stat(name string) (os.FileInfo, error) +} + +type fileStore struct { + base string +} + +func (f *fileStore) ReadFile(filename string) ([]byte, error) { + path := filepath.Join(f.base, filename) + return ioutil.ReadFile(path) +} + +func (f *fileStore) ReadDir(dirname string) ([]os.FileInfo, error) { + path := filepath.Join(f.base, dirname) + return ioutil.ReadDir(path) +} + +func (f *fileStore) Stat(name string) (os.FileInfo, error) { + path := filepath.Join(f.base, name) + return os.Stat(path) +} + +type zipStore struct { + archive *zip.ReadCloser + files map[string]*zip.File + dirs map[string][]*zip.File +} + +func openZipStore(file string) (*zipStore, error) { + r, err := zip.OpenReader(file) + if err != nil { + log.Fatal(err) + } + + zs := &zipStore{r, make(map[string]*zip.File), make(map[string][]*zip.File)} + + log.Printf("zip %q loading files...", file) + for _, file := range r.File { + zs.files[file.Name] = file + log.Printf("%v...", file.Name) + } + + log.Printf("zip %q loading directories...", file) + for _, dir := range r.File { + if !strings.HasSuffix(dir.Name, "/") { + continue + } + + var fi []*zip.File + for _, file := range zs.files { + name := file.Name + if name == dir.Name { + continue + } + + if strings.HasSuffix(name, "/") { + name = name[:len(name)-1] + } + + d, _ := filepath.Split(name) + + if d != dir.Name { + continue + } + + fi = append(fi, file) + + if strings.HasPrefix(file.Name, dir.Name) { + log.Printf("%v in dir %v", file.Name, dir.Name) + } + } + + zs.dirs[dir.Name] = fi + log.Printf("%v...", dir.Name) + } + + return zs, nil +} + +func (z *zipStore) ReadFile(filename string) ([]byte, error) { + log.Printf("read of %v", filename) + zf, ok := z.files[filename] + if !ok { + return nil, os.ErrNotExist + } + + zr, err := zf.Open() + if err != nil { + return nil, err + } + defer zr.Close() + zc, err := ioutil.ReadAll(zr) + if err != nil { + return nil, err + } + + return zc, nil +} + +func (z *zipStore) ReadDir(dirname string) ([]os.FileInfo, error) { + var fi []os.FileInfo + + dir, ok := z.dirs[dirname] + if !ok { + return nil, os.ErrNotExist + } + + for _, d := range dir { + fi = append(fi, d.FileInfo()) + } + + return fi, nil +} + +func (z *zipStore) Stat(name string) (os.FileInfo, error) { + log.Printf("stat %v", name) + if f, ok := z.files[name]; ok { + return f.FileInfo(), nil + } + return nil, os.ErrNotExist +} diff --git a/store_test.go b/store_test.go new file mode 100644 index 0000000..3df0424 --- /dev/null +++ b/store_test.go @@ -0,0 +1,24 @@ +package main + +import ( + "testing" + + "github.com/coreos/fleet/log" +) + +func TestZipStore(t *testing.T) { + file := "fixtures/site.zip" + zs, err := openZipStore(file) + if err != nil { + log.Error(err) + return + } + + ls, err := zs.ReadDir("site/foo/") + if err != nil { + log.Error(err) + return + } + + t.Logf("file %v", ls[0].Name()) +}