diff --git a/assets/assets.go b/assets/assets.go new file mode 100644 index 00000000..8230008f --- /dev/null +++ b/assets/assets.go @@ -0,0 +1,262 @@ +// Code generated by go-bindata. +// sources: +// assets/source/styles.css +// assets/source/template.tmpl +// DO NOT EDIT! + +package assets + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _assetsSourceStylesCss = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x94\x55\xdd\x4e\xe3\x3c\x10\xbd\x6e\xa5\xbe\x83\x25\x84\x04\x9f\x48\x95\x14\xca\x27\xd2\x9b\x95\xf6\x1d\xf6\x7e\x12\x4f\x1a\x0b\xc7\x8e\x6c\x97\x96\x45\xfb\xee\x3b\x76\xea\xc6\xa1\x61\x57\x4b\x90\x48\xe6\xcf\x33\xe7\x1c\x0f\xff\xb1\x0f\xd6\x03\xe7\x42\xed\x4b\x96\xef\x58\x07\x66\x2f\x54\x78\xfd\xb5\x5a\xae\x96\x95\xe6\xef\xec\x63\xb5\x5c\x34\x5a\xb9\xac\x81\x4e\xc8\xf7\x92\x59\x50\x36\xb3\x68\x44\xb3\x23\x97\xc3\x93\xcb\x0c\x2a\x4e\x06\x5f\x46\xf7\x4e\x74\xe2\x27\xda\x1e\x91\x53\x40\x28\x04\xa1\x4a\xad\xa5\x36\x25\xbb\xc9\xf3\x67\xe4\x8f\x97\x64\x8e\xb5\x36\xe0\x84\xa6\x93\x95\x56\x78\x49\x2a\x5b\xfd\x86\xe6\x61\xb5\x6c\x0b\x76\xfe\x98\xd4\x79\x2c\x5e\xea\xa6\x89\xe1\x2d\x02\x0f\xd1\x37\xf6\xd0\xd1\x24\x43\xe7\xe7\xf1\x32\x89\x8d\x2b\xd9\xf6\x76\x97\xd8\x8c\xd8\xb7\xd1\x18\x4a\xb8\xb6\x6c\x84\xb1\x2e\xab\x5b\x21\x39\x95\x72\x3c\x35\x7c\x55\x30\xe6\x4a\x98\xa6\x8e\xdf\x93\xcc\xab\x63\x87\xce\x27\x31\x4e\xf7\x25\xdb\x6c\xfb\x53\xda\x6f\xa5\x9d\xd3\x5d\xc9\x8a\xb3\xbd\x82\xfa\x75\x6f\xf4\x41\xf1\x2c\x42\xd2\x6c\xfc\x73\x29\x5c\x8c\xec\x59\xe2\x84\x4a\xe6\x43\x6a\x30\x1d\x71\xe8\x44\x69\xd3\x81\xf4\xe6\x63\x2b\x1c\x66\xb6\x87\x1a\xbd\xf9\x68\xa0\xf7\x66\x8f\x7c\x23\xf5\x31\x3b\x95\xac\x15\x9c\xa3\xba\x90\x17\x5d\x25\x43\x29\x45\x6f\x85\x4d\x0e\x9f\xd0\x2e\x54\x4b\x12\x71\xa9\x3b\xe1\xf4\x4a\x09\x07\xaf\x28\x29\x46\x39\x74\x20\x54\x08\xe5\xc2\xf6\x12\x48\x88\x95\xd4\xf5\x6b\x74\xaf\x3b\x74\xf0\x79\xdc\x62\x93\x8c\x1b\xf5\xfb\x03\x0d\x07\x05\x0f\x9f\x84\x5c\x69\x43\x27\x8e\x20\xf7\x27\x66\xb5\x14\x9c\xdd\xbc\x7c\xf7\xcf\xee\x33\x3d\xc5\x9f\xe9\x19\xbb\xca\x08\xd4\x2e\xb4\x36\x5c\xb0\xa8\x80\x02\xbb\x8b\x7a\xa0\x92\x18\x42\x8e\x82\x93\x94\x58\x91\xe7\xb7\x49\x57\x84\xa1\x84\xde\xd2\x48\xf1\xed\x92\x39\xe0\x37\xd3\x3d\x07\xdb\x22\xb5\xcf\xc1\x3f\x63\x7c\xa9\xb4\xbb\x4b\x75\x7d\x9f\xf0\x30\xa7\x29\xfa\xc1\x7a\xd4\x79\x10\xf7\x48\x1a\x48\xb1\x27\xbe\xfc\x7d\x48\xd0\xf0\x13\x50\x0f\xf9\x98\x76\xad\xef\xbf\x00\x38\x21\xf2\x79\x30\xcd\x0b\x34\x9e\x30\xd1\x5b\x25\x61\x54\x07\x39\xed\xdb\x3e\xb8\x69\x52\x27\x6a\x90\xb1\xf1\x8e\x04\x2d\x47\x38\xf9\x95\x86\x9e\x46\x3a\x67\xf6\xc1\x99\xaf\x6d\xfe\x0f\x9b\x20\xc5\x2d\x68\xe1\x8b\xfa\xb1\xe5\x5e\x5b\x31\xdc\x0a\xa8\x48\x93\x07\x97\xb6\xbb\x56\xd0\xe1\xc0\xc9\x7a\xaf\x0f\x7d\x2a\xb4\x61\x49\x15\xeb\xff\xb7\x41\x6a\x8b\x23\xa9\x24\xab\x0c\xc2\x2b\xe1\xe3\xff\x50\x17\x72\x72\xc3\x3d\xa2\xd1\xe7\xa3\xaf\x40\xef\x0d\x66\x29\xec\x8d\xd6\x6e\xba\xbc\x4a\xf6\xe4\xa9\x9f\xac\x9a\xe9\x75\x4c\xe7\xaf\x51\x51\x7e\xac\xf6\xad\x43\x2e\x80\xdd\x75\x70\xca\xce\xc8\x3e\xe7\x54\xe8\x3e\x1c\xb0\xa6\xe5\x83\x97\x9b\x32\x2e\x82\xf3\x7f\x8d\x45\x28\xb1\x98\x21\x29\xb2\x04\x07\xa7\x93\xc0\xb6\x54\xae\x1d\xc2\xee\x36\xf7\x0f\x43\x6e\x6a\x1a\x92\xe7\x16\xf7\x62\x96\x44\x5f\x98\x7e\x7f\x07\x00\x00\xff\xff\x8c\x4d\x45\x42\x58\x07\x00\x00") + +func assetsSourceStylesCssBytes() ([]byte, error) { + return bindataRead( + _assetsSourceStylesCss, + "assets/source/styles.css", + ) +} + +func assetsSourceStylesCss() (*asset, error) { + bytes, err := assetsSourceStylesCssBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "assets/source/styles.css", size: 1880, mode: os.FileMode(438), modTime: time.Unix(1465578069, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _assetsSourceTemplateTmpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\xdd\x6f\xe3\xb8\x11\x7f\x4e\x81\xfe\x0f\x3c\xdd\xe1\xce\x06\x56\x94\x48\x7d\x67\xe5\x14\x77\xbb\xd7\xed\x01\xfb\x71\xe8\x66\x1f\xda\x97\x03\x2d\xd1\xb6\x7a\xb2\xe4\x52\xb2\x13\x6f\x90\xff\xbd\x33\xa2\x64\xcb\x8e\x92\x38\xdd\xbd\xc5\x16\x28\x1c\x44\xa4\x34\x1c\xce\xfc\xe6\x8b\xa4\x14\x7f\xf3\xf2\xdd\x8b\xcb\x7f\xfc\xfa\x33\x59\xd4\xcb\xfc\xe2\xcf\x7f\x8a\xdb\xeb\x59\xbc\x90\x22\xc5\xc6\x59\x5c\x67\x75\x2e\x2f\x6e\x6e\xe8\x5b\xb1\x94\xb7\xb7\xb1\xa5\x6f\x34\xcf\x96\xb2\x16\xa4\x80\xfb\x13\x63\x93\xc9\xab\x55\xa9\x6a\x83\x24\x65\x51\xcb\xa2\x9e\x18\x57\x59\x5a\x2f\x26\xa9\xdc\x64\x89\x34\x9b\xce\x33\x92\x15\x59\x9d\x89\xdc\xac\x12\x91\xcb\x09\xa3\xb6\x01\x9c\x90\x17\x21\x71\x9e\x15\xbf\x13\x25\xf3\x89\x51\xd5\xdb\x5c\x56\x0b\x29\x81\xdd\x42\xc9\xd9\xc4\xb0\x7e\x9b\x65\xb9\x5c\x8a\x42\xcc\xa5\xfa\x2d\x83\x19\x54\x21\x72\x4b\x13\xd2\xa4\xaa\x90\x4f\xdc\x74\x1b\x86\xb1\xd5\xb5\xcf\x62\xab\x53\x26\x9e\x96\xe9\x56\x4b\x5e\x6d\xe6\x64\x23\x55\x95\x95\xc5\xc4\x60\x94\x19\xe4\x7a\x99\x17\xd5\xc4\x58\xd4\xf5\xea\xdc\xb2\xae\xae\xae\xe8\x95\x43\x4b\x35\xb7\xb8\x6d\xdb\x16\xd0\xb7\x24\xe7\xd7\x28\xe7\x10\x21\x8b\xa2\xc8\x6a\x9e\x82\xd4\x32\x9b\x2f\x00\x03\xdb\x20\x1a\x06\x6c\x35\x22\x4d\x8c\x55\x59\x01\x08\x65\x71\x4e\xc4\xb4\x2a\xf3\x75\x2d\x9f\x1b\x8d\x54\x67\x71\x2a\x67\x95\x6e\x9e\xc5\xdf\x98\x26\xf9\x6b\x99\xa7\x52\x11\xd3\xec\x6e\x02\x77\x29\xd4\x2b\x25\xd2\x0c\x30\x26\x59\x3a\x31\x66\x06\xd9\xf2\x89\xe1\xbb\x30\xc3\xbc\x7d\xf0\x01\x60\x06\x65\xd6\x95\x54\xef\x57\x22\x91\xef\x8a\x0f\x95\x04\x05\x80\x8e\xbb\x2e\x0d\xdd\x3d\xe9\xa5\x12\x45\x35\x2b\xd5\x72\x62\x2c\x45\xad\xb2\xeb\x11\x8d\x02\x87\x45\xc4\x86\x1f\x18\x88\x39\x1e\x31\xa9\x67\xfb\x11\x5c\x99\x43\xfd\x20\x1a\xc3\x8c\x6c\x62\xb8\xcc\xa3\x81\x07\x5c\xd9\x8e\x6b\x2b\x26\xe0\x5b\x97\x2b\x82\xff\xcc\xa4\xcc\x4b\x35\x31\xbe\x9d\x3a\x69\x3a\x4b\x0d\x52\xce\x66\x95\x6c\x90\xb1\x1e\xa0\xf6\xa3\x64\x4f\xca\x76\xa4\xb1\x75\x08\xc0\x03\xb0\x48\x0d\x8b\x17\x80\x0e\xfe\x69\xc8\x38\x21\xb5\x9d\x41\x64\x6a\x6c\xe6\xa2\x96\x23\xfb\x19\x6f\xd5\x77\x5c\x9f\xda\x9d\xfa\x8e\x4f\xb9\xff\x90\xfa\x30\xd5\xa9\xba\x3b\x7e\xf4\x14\xdd\xe7\xda\x0b\x1a\x4f\x31\x48\x3d\x24\xb4\xc9\x7d\x90\xd5\x07\xfb\x45\x0e\x75\xfc\xf1\x5e\xd0\x79\x7f\x40\x67\x7f\xdb\xf7\xed\xd6\x01\xba\x36\xf7\x5d\xca\x09\x0b\x6c\x1a\x39\xfb\xe1\xc3\xe3\x19\x75\x03\x27\x6c\xfd\xa7\x69\x9a\x1e\x07\xa8\x1c\x98\x1f\xe4\x38\x60\x70\x16\xaf\x44\xbd\x20\x10\xdc\x79\x67\x75\x50\x66\x19\x85\xd4\xe5\x2e\x71\x5c\x87\x06\x61\x62\x32\xb0\x21\x30\x34\xb9\x4d\x42\x1a\x71\xbc\x72\x7b\xe3\x86\xd4\x23\x0e\x50\x80\x7c\xcc\xf6\x40\xd4\x04\x66\x6c\x48\x91\x8a\x34\x54\xf0\xb7\xe0\x41\x44\x39\x4f\x5a\x2e\x70\xc7\x6c\x9f\x03\xa3\x0d\xf8\x74\x88\x93\xd8\x7a\x16\xb3\x9d\x40\xff\x2d\xe0\x66\x40\x19\x4f\xcc\x80\x7a\x6e\x10\x9a\x90\x2a\x42\xd7\x35\x23\x1a\x44\x5e\x68\xfa\xe0\xf3\xa1\x63\x32\x9b\x3a\x81\x87\xe3\x1d\xc6\x37\xa6\x47\x7d\xde\xf4\x3c\x9f\xdf\xcb\xd6\x61\x34\xe0\x1f\xf7\x7e\x00\x48\x28\x99\xd4\x44\x5d\x83\xcd\x39\x0d\x43\xf0\x2c\xb5\x6d\xda\x90\x57\xf6\x09\x85\xf3\x80\xf2\x70\x97\x55\x1c\x00\x14\xdd\x05\x28\x5d\x3b\x6a\x9a\xc0\xc0\x73\x01\x3e\x20\x6a\x61\x45\x87\xb2\x86\x21\x5f\xab\x7c\xf4\xad\x1c\x1f\xa1\xee\xfd\x1f\xf5\xd3\x51\x0f\x86\x51\x6f\x90\x9d\x8d\x7b\x73\xc5\xd6\x7c\x17\x77\xbb\xa6\x6e\xf5\x53\x3e\xd4\xb9\x87\x13\xbe\x78\x28\xcf\x24\x53\xfc\x9d\x9a\x6a\x66\x36\xfe\xf6\xd4\xd4\x71\x23\xee\x3c\x38\x44\x72\xfc\x7d\x62\x72\x4e\xdb\x9a\x15\xfa\x94\x61\x0a\xc5\xa2\x79\xae\x2b\xfd\xb7\xe2\xd1\x64\x8d\xd9\x97\x03\xee\xe1\x70\xb2\xee\xd2\x18\x0f\x21\x45\xeb\x2c\xe6\xb0\x26\xfd\xf8\x36\x38\x2f\x27\x6e\xe8\x51\xc6\xc6\x3a\xeb\x3b\xa1\xdd\x94\x7f\xe0\x19\xb8\xe0\xb1\xde\x5e\x9f\x01\xc1\x13\x2d\x38\x0f\x03\x1a\xb8\xff\x95\xe0\xcc\x8f\xa8\xfb\x70\xfd\x45\xc1\xf9\x5e\xf0\x60\x58\x70\x9f\x73\xea\x38\x9d\xe4\x8c\xfa\xee\x83\x92\x4f\xb5\xe4\x2e\x03\x4e\xee\x69\x10\x43\x51\x63\x8f\x40\x1c\x41\x8a\xd6\x92\x72\x58\xfd\x44\xf7\x40\xec\xd8\x10\x5a\x5a\x52\x1f\x90\x8b\x1e\xac\x94\xb3\xd9\xec\x64\xf7\x45\xd2\xe6\x4e\x09\xb2\x67\xf5\xb6\x59\x66\x3d\xb9\x74\x42\xc4\xdd\x5b\x38\x83\x10\x3c\x94\x98\x9c\xf9\xd4\x8b\x0e\x0a\x27\x86\xb9\xa9\xd6\xb8\xa2\x93\x1b\x59\x94\x69\x6a\x0c\x15\x53\x16\x05\x81\xa7\x21\xd2\x4d\x27\x84\xd4\xe4\x41\xca\xe4\xd4\x0f\xfb\xb5\xb4\x49\xcb\x98\x87\xa1\x5c\x82\x4f\x06\xc4\xe3\x1e\xf5\x36\x60\x85\xc0\x89\xc8\xd0\x65\x01\x79\xd8\xe3\x21\x19\xba\x6c\x4c\x4d\x73\x78\xc9\x4d\xfd\xb4\xed\x2e\x76\xdd\xfe\xe5\x23\x82\xaa\xca\xdf\xe5\x1e\x56\x58\x35\x40\x22\xde\xdd\x6f\xf3\x20\xe4\x3b\xdf\x8d\x76\x75\x06\xcd\xd1\x4b\x77\xf3\xe3\x92\xb3\xd7\x8d\x35\xba\xf9\xe1\xd7\xa8\x1c\x28\x02\x2b\xfe\x87\x55\x6d\x92\x7b\x3a\xbe\x5b\x55\x1b\x15\x9d\xd0\x27\x50\x85\x60\x48\x82\x55\x8a\x41\x21\xe2\x34\x8a\xa0\x3c\x71\x87\x72\xdb\x87\x9e\xe7\x87\x1c\x05\x62\x36\x33\x21\x38\xa0\x74\x39\xb0\x58\x0a\xec\xd0\xc5\x11\x91\x0f\x6b\x2d\x28\x5d\x50\xbb\x5c\xb8\x89\x24\x2e\x0f\x98\xe9\x78\x34\x0c\x82\xbc\x83\xa2\xd1\xf2\xe3\x1d\x49\x23\x0f\x8a\xd2\xaa\x3e\x90\x35\xb9\x4f\x56\x8f\x45\x10\xe4\xc4\x73\x02\x98\xc4\xb7\x5d\x1b\x66\x76\x60\xd2\x05\xd6\x4a\x3f\xcc\x6d\x6a\x07\x9e\x87\xa2\xf2\xc8\x4f\x50\x0f\x17\x92\x2a\x14\x64\x07\xd4\x71\xc0\x85\xa1\xc0\xba\x20\x71\x14\x9a\xb0\x38\x64\x2e\x08\x09\x0c\x43\xdf\x74\x02\x0a\x55\x1a\x28\x43\x06\xbc\xa1\x7e\x06\xa1\x67\x7a\x20\x72\x10\x98\x3e\x44\x00\xc0\x00\x10\x71\xe6\xde\x51\x80\x1d\xc9\x3e\x7d\x7a\x11\xfd\xb0\x22\x42\xa9\xf2\xaa\x57\x48\x75\xac\xaf\x57\x66\xf3\xe0\xfe\x78\xc7\x45\x0b\xc4\x3b\xa4\x31\xc6\x7b\xf1\xde\x20\x36\x10\xde\x9c\xbb\xac\xcd\x80\x0c\x06\x81\xf3\x82\x91\xfc\x80\x30\x58\x30\x3b\xde\xf8\x58\x37\x5b\xaf\xb4\x60\x21\xee\x62\x84\xbb\x90\x04\x6c\xb6\x30\x79\x48\x43\x1e\xb4\x97\x9c\xc1\x42\x82\xb9\x68\xfb\xc8\x87\x44\x31\xd0\x23\xba\xf7\xb1\x97\xe1\x8e\x10\x78\x59\x5e\x15\xf7\x60\x90\xc2\xa3\x3f\x0c\x05\xf3\x10\x06\xee\x05\xcd\x82\xff\x8b\xc2\xd0\x34\x76\x5b\xe9\x18\x37\xef\x2d\x38\xcd\xb9\x86\x54\x2d\xcd\x82\xb5\xc3\x6e\x6e\x40\xa5\xb9\x24\xdf\x81\xbf\x3d\x23\xdf\xe1\x89\x06\x39\x9f\x10\xfa\x93\x02\xf2\x44\xad\x97\xd3\x37\x62\x75\x7b\x1b\x8b\xf6\x34\xe2\xe6\x06\x29\x6f\x6f\x8d\x0b\x68\x15\xed\xb9\x88\x80\x4e\x36\x23\x85\x66\x43\x0c\xcb\xb8\xbd\xb5\x6e\x6e\x64\x91\xde\xde\xb6\x97\x56\xb4\x76\x5e\x7d\x30\xd1\x4a\x13\x2f\x45\x56\x74\xa7\x00\xd9\x86\x24\xb9\xa8\xa0\x2e\xe3\x09\x4b\x07\x7f\x73\x1f\x0d\x58\xad\x97\x4b\xa1\xb6\xbd\x22\xba\x12\x45\x7f\x84\x99\xd5\x72\x69\x5c\xc4\xd3\xe6\xdc\x66\xbd\x7c\x99\xa9\x0a\x45\x9c\x5e\x90\x34\xc3\x05\x6f\xa9\x1a\x59\xe5\xbf\x09\x23\x7b\x82\x2d\x88\x99\x57\xa0\x4c\x26\xab\x56\x62\x00\x0f\x78\x9f\x3c\x11\x2e\x5e\xbb\x99\xb0\xac\x76\x88\xe8\x59\xda\xa7\xc3\xbc\x6f\x6e\x4c\xa2\x89\x21\x94\x7e\x01\xb6\xd5\xeb\x6c\x09\xec\xd3\xcb\xb2\x05\xee\xfe\xf9\x47\xe5\x8c\x5c\x2d\xb2\x64\x41\xca\x22\xdf\x12\x2d\xce\x31\x8f\x46\x28\xa1\x24\x40\x50\xad\x72\xb1\x95\xe9\xf8\xae\x04\x7b\x33\xa1\x0b\x65\x9b\x9d\x37\xed\x9a\x3d\xe3\xe4\x59\x55\x67\xc5\x7c\x67\x9f\x5a\x4c\x61\xed\x2e\x54\x26\xcc\x54\x56\x89\xca\xa6\x32\x9d\x6e\x07\xec\x55\xef\x8e\xd7\x9a\x9e\xda\x67\xb7\x7a\xb1\xcf\xd2\x2d\x22\xa2\x48\xc9\x08\x2c\x45\xdf\x97\xaa\x26\x06\xba\x9b\x31\x26\x23\x00\x8a\xbe\x53\x78\x3a\x64\xe0\x64\xc6\x78\x87\x12\xb0\xe9\x1c\xf5\x2f\x15\x8c\x99\xe0\x90\xef\x4b\xa4\x9d\x20\x69\x67\x94\x01\x9c\xbf\xcf\xb1\x3d\x19\x40\xaf\xb5\x99\x71\x81\xc7\x80\xa4\x39\x3f\xeb\xd2\x35\x58\x60\xb7\x51\xa2\x2e\xf6\x0e\x4f\xd6\xf0\x68\xf0\xa7\x12\x36\x48\xcd\x89\x00\xc7\xba\x06\x71\x4f\x7c\x6a\x3b\x5e\x18\xc2\x36\xeb\x22\x86\xe5\xe7\xe1\x5a\x7a\x97\xa7\x2f\x62\x0b\x1e\x5e\xe8\x20\xc6\x20\x3b\x80\x07\xdd\xf5\x34\x8c\xc4\xc9\x10\x89\xff\x0d\x84\x7a\x59\xfc\x71\x8c\xbe\x9c\xe2\x77\xa7\xdf\x47\x14\x06\x52\xcf\xc1\x4f\x70\xf6\x2a\xfb\xf8\x54\x67\xc7\x21\x9f\xcd\xd9\xdf\x03\xb3\xaf\xdd\xd9\x07\x30\x7a\xc4\xd9\x7b\x10\x7d\xb2\xcd\xbf\x0c\x42\x9f\xc9\xd9\x3f\xb7\xe2\x4f\x73\xf6\xae\x6c\x2c\xb2\x54\x62\xa5\x30\x1e\xf3\xfe\x3a\x7b\x72\xaa\xc7\x21\x9f\xcd\xfb\xdf\x94\x69\x36\xcb\x64\xfa\xb5\x47\xc0\x00\x4e\x8f\x44\x40\x0f\xa6\x4f\x76\x84\x2f\x87\xd2\x67\x8a\x82\x3f\x42\xf9\xd3\x23\x01\xda\xaa\xd7\x3e\x5c\x09\xed\x5e\xd0\xed\x63\x82\xbe\x10\xc5\xab\xf2\xc3\x6a\xbf\x08\x3c\x58\x2f\xa5\x17\x77\xd5\xa4\xb4\x17\x5a\x87\x8b\xc6\x79\xb9\x5e\x19\x17\xaf\x4a\xb2\x5e\x1d\x2e\xfe\x50\x18\xd1\xdb\x65\xf6\x18\xe3\x24\xdf\x2f\x53\x51\x2d\x9e\x1f\xdf\xbf\x1b\xd2\x03\x84\x7d\x8d\x8f\xa1\xc1\xbe\xde\x7b\x68\x8c\x9f\xa0\x25\x98\xe5\xc3\xdf\x5f\xa3\x11\xf6\xba\x76\x98\xfd\x52\xc1\x8a\xbe\xe7\x01\x67\x07\xbe\x49\xbd\xbe\x77\xb2\x47\x9c\xb3\xd9\xc8\x85\xa1\xed\x10\x8e\x47\x7b\x41\xe4\x7a\x43\xce\xd9\xbe\x80\x3a\x70\xcc\x43\xc1\x8e\xbc\xf2\x53\x64\xe2\x3e\x9e\x55\x38\x21\x23\x3c\xa2\xcc\x75\x03\x7c\xf9\x36\x20\x13\x9e\xec\x3d\x24\x51\xdf\x49\x8f\x3c\xa5\x59\x43\xf6\xdf\x7b\x9f\xe8\x2d\xf7\x98\x00\x9d\x25\x15\xb0\x63\xd1\x81\x67\x98\x6c\xd0\x57\x06\x80\x3a\x1e\x09\x22\x61\xed\xd1\xbb\x4f\xfa\xb7\xf5\x52\x14\xba\x7f\x97\xcf\x41\x0c\x0e\x79\x6b\x8c\xb9\x00\xb9\x4b\x6c\x34\xbc\x1b\x86\x10\xd6\x97\xf8\xc4\xe0\x78\x4e\x83\x07\x50\xfc\x92\x79\xe7\xb6\x7b\x6e\x7b\xff\x34\xfa\x53\xef\x28\x6d\x66\xd9\x1c\x5f\x96\xfb\xc4\x76\x34\x25\xf9\xf5\x0d\x31\xed\xe0\xdc\xb6\x0d\xfd\xe1\xc0\x12\xcd\x70\x5a\x60\xc0\xb3\x5e\x32\x80\x1e\x0a\x7c\xbc\x1b\x8b\xad\xdd\xae\x39\x9e\x95\x65\xdd\xed\xec\xdf\x4b\xb5\x81\x84\x7c\x95\x41\xd1\x85\x78\x69\xbe\x29\x28\xca\x72\x25\x0b\xa8\x0d\x45\x09\xce\x21\x95\xc2\xb7\xa5\xda\x4f\xf0\x55\x7e\x75\x6e\x59\x89\x48\xd3\x6d\x85\x63\x15\x4d\x4a\xd8\x57\xbe\xc0\x1b\x68\x69\xaa\x67\xeb\xcd\x11\xe3\xfe\x6e\x55\x93\x7a\xbb\x02\xdc\x6a\x79\x5d\x5b\xff\x12\x1b\xa1\xef\xb6\x21\x39\x5b\x17\x09\xbe\xe8\x27\x79\x99\x88\x1c\x6c\xf4\xb2\x05\x7a\x24\xf1\x4b\x88\x54\x5e\x3f\x83\xdd\xe2\x98\xdc\x68\x25\xc1\x6b\x46\x92\x22\xab\x17\xfa\x13\x0a\x32\x99\x4c\xc8\x1a\xe8\x66\x59\x01\xbb\xd5\x8e\xee\x4c\xc9\x7a\xad\x8a\xe7\xba\xd7\x02\xb6\x11\x8a\xa4\x64\x02\x99\xfc\x8a\xe0\x34\xc0\x69\x2e\xeb\x1f\xeb\x1a\x76\xa1\x6b\xe8\xfe\xd0\x19\xf9\x87\xf1\xf8\xf9\x7e\xbe\xac\x7a\x2b\xde\x8e\xd2\xf1\x9e\xf9\x11\x93\x9e\x38\xdd\xb8\x7b\x06\x1e\x8a\xd5\xc9\xd5\x5e\x8e\xf4\x22\x29\xad\xcb\xd7\x88\x8a\x7c\x0f\x12\x16\xf3\x51\xcb\x5c\x93\xa3\x32\x28\xeb\x6b\xd8\x60\x03\xf1\x8f\x4a\x89\x2d\x5d\xa9\xb2\x2e\x11\x6e\x5a\xe5\x59\x22\x29\x0c\xce\x47\x69\x99\xac\x97\xc0\x12\x95\xfd\x39\x97\xd8\xac\x7e\xda\x5e\x8a\x39\x46\xed\xa8\x5d\x19\xb4\xbc\x3b\x8e\x74\x56\xaa\x9f\x45\xb2\x18\x1d\x9b\x45\xd3\x41\xa4\x37\x46\xd4\xdf\x8e\xb4\x4e\x18\x5b\xed\xa7\x31\xff\x09\x00\x00\xff\xff\x4b\xd9\xdc\xaa\x34\x23\x00\x00") + +func assetsSourceTemplateTmplBytes() ([]byte, error) { + return bindataRead( + _assetsSourceTemplateTmpl, + "assets/source/template.tmpl", + ) +} + +func assetsSourceTemplateTmpl() (*asset, error) { + bytes, err := assetsSourceTemplateTmplBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "assets/source/template.tmpl", size: 9012, mode: os.FileMode(438), modTime: time.Unix(1465578153, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "assets/source/styles.css": assetsSourceStylesCss, + "assets/source/template.tmpl": assetsSourceTemplateTmpl, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} +var _bintree = &bintree{nil, map[string]*bintree{ + "assets": &bintree{nil, map[string]*bintree{ + "source": &bintree{nil, map[string]*bintree{ + "styles.css": &bintree{assetsSourceStylesCss, map[string]*bintree{}}, + "template.tmpl": &bintree{assetsSourceTemplateTmpl, map[string]*bintree{}}, + }}, + }}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} + diff --git a/assets/source/styles.css b/assets/source/styles.css new file mode 100644 index 00000000..4cd347a7 --- /dev/null +++ b/assets/source/styles.css @@ -0,0 +1,151 @@ +* { padding: 0; margin: 0; } + +body { + font-family: sans-serif; + text-rendering: optimizespeed; +} + +a { + color: #006ed3; + text-decoration: none; +} + +a:hover, +h1 a:hover { + color: #319cff; +} + +header, +#summary { + padding-left: 5%; + padding-right: 5%; +} + +th:first-child, +td:first-child { + padding-left: 5%; +} + +th:last-child, +td:last-child { + padding-right: 5%; +} + +header { + padding-top: 25px; + padding-bottom: 15px; + background-color: #f2f2f2; +} + +h1 { + font-size: 20px; + font-weight: normal; + white-space: nowrap; + overflow-x: hidden; + text-overflow: ellipsis; +} + +h1 a { + color: inherit; +} + +h1 a:hover { + text-decoration: underline; +} + +main { + display: block; +} + +.meta { + font-size: 12px; + font-family: Verdana, sans-serif; + border-bottom: 1px solid #9C9C9C; + padding-top: 15px; + padding-bottom: 15px; +} + +.meta-item { + margin-right: 1em; +} + +table { + width: 100%; + border-collapse: collapse; +} + +tr { + border-bottom: 1px dashed #dadada; +} + +tr:not(:first-child):hover { + background-color: #ffffec; +} + +th, +td { + text-align: left; + padding: 10px 0; +} + +th { + padding-top: 15px; + padding-bottom: 15px; + font-size: 16px; + white-space: nowrap; +} + +th a { + color: black; +} + +th svg { + vertical-align: middle; +} + +td { + font-size: 14px; +} + +td:first-child { + width: 50%; +} + +th:last-child, +td:last-child { + text-align: right; +} + +td:first-child svg { + position: absolute; +} + +td .name, +td .goup { + margin-left: 1.75em; + word-break: break-all; + overflow-wrap: break-word; + white-space: pre-wrap; +} + +footer { + padding: 40px 20px; + font-size: 12px; + text-align: center; +} + +@media (max-width: 600px) { + .hideable { + display: none; + } + + td:first-child { + width: auto; + } + + th:nth-child(2), + td:nth-child(2) { + padding-right: 5%; + text-align: right; + } +} diff --git a/assets/source/template.tmpl b/assets/source/template.tmpl new file mode 100644 index 00000000..797ee967 --- /dev/null +++ b/assets/source/template.tmpl @@ -0,0 +1,175 @@ + + + + {{.Name}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ {{range $url, $name := .BreadcrumbMap}}{{$name}}{{if ne $url "/"}}/{{end}}{{end}} +

+
+
+
+
+ {{.NumDirs}} director{{if eq 1 .NumDirs}}y{{else}}ies{{end}} + {{.NumFiles}} file{{if ne 1 .NumFiles}}s{{end}} + {{- if ne 0 .ItemsLimitedTo}} + (of which only {{.ItemsLimitedTo}} are displayed) + {{- end}} +
+
+
+ + + + + + + + + + {{- if .CanGoUp}} + + + + + + {{- end}} + {{- range .Items}} + + + {{- if .IsDir}} + + {{- else}} + + {{- end}} + + + {{- end}} + +
+ {{- if and (eq .Sort "name") (ne .Order "desc")}} + Name + {{- else if and (eq .Sort "name") (ne .Order "asc")}} + Name + {{- else}} + Name + {{- end}} + + {{- if and (eq .Sort "size") (ne .Order "desc")}} + Size + {{- else if and (eq .Sort "size") (ne .Order "asc")}} + Size + {{- else}} + Size + {{- end}} + + {{- if and (eq .Sort "time") (ne .Order "desc")}} + Modified + {{- else if and (eq .Sort "time") (ne .Order "asc")}} + Modified + {{- else}} + Modified + {{- end}} +
+ + Go up + +
+ + {{- if .IsDir}} + + {{- else}} + + {{- end}} + {{.Name}} + + {{.HumanSize}}
+
+
+ + + + diff --git a/filemanager.go b/filemanager.go index 9bf92182..5bb0659c 100644 --- a/filemanager.go +++ b/filemanager.go @@ -1,16 +1,354 @@ +//go:generate go get github.com/jteeuwen/go-bindata +//go:generate go install github.com/jteeuwen/go-bindata/go-bindata +//go:generate go-bindata -pkg assets -o assets/assets.go assets/source/... + +// Package filemanager provides middleware for managing files in a directory +// when directory path is requested instead of a specific file. Based on browse +// middleware. package filemanager import ( + "bytes" + "encoding/json" + "mime" "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "text/template" + "time" + "github.com/dustin/go-humanize" + "github.com/hacdias/caddy-filemanager/assets" "github.com/mholt/caddy/caddyhttp/httpserver" + "github.com/mholt/caddy/caddyhttp/staticfiles" ) +// FileManager is an http.Handler that can show a file listing when +// directories in the given paths are specified. type FileManager struct { - Next httpserver.Handler + Next httpserver.Handler + Configs []Config + IgnoreIndexes bool } +// Config is a configuration for browsing in a particular path. +type Config struct { + PathScope string + Root http.FileSystem + Variables interface{} + Template *template.Template +} + +// A Listing is the context used to fill out a template. +type Listing struct { + // The name of the directory (the last element of the path) + Name string + + // The full path of the request + Path string + + // Whether the parent directory is browsable + CanGoUp bool + + // The items (files and folders) in the path + Items []FileInfo + + // The number of directories in the listing + NumDirs int + + // The number of files (items that aren't directories) in the listing + NumFiles int + + // Which sorting order is used + Sort string + + // And which order + Order string + + // If ≠0 then Items have been limited to that many elements + ItemsLimitedTo int + + // Optional custom variables for use in browse templates + User interface{} + + httpserver.Context +} + +// BreadcrumbMap returns l.Path where every element is a map +// of URLs and path segment names. +func (l Listing) BreadcrumbMap() map[string]string { + result := map[string]string{} + + if len(l.Path) == 0 { + return result + } + + // skip trailing slash + lpath := l.Path + if lpath[len(lpath)-1] == '/' { + lpath = lpath[:len(lpath)-1] + } + + parts := strings.Split(lpath, "/") + for i, part := range parts { + if i == 0 && part == "" { + // Leading slash (root) + result["/"] = "/" + continue + } + result[strings.Join(parts[:i+1], "/")] = part + } + + return result +} + +// FileInfo is the info about a particular file or directory +type FileInfo struct { + IsDir bool + Name string + Size int64 + URL string + ModTime time.Time + Mode os.FileMode +} + +// HumanSize returns the size of the file as a human-readable string +// in IEC format (i.e. power of 2 or base 1024). +func (fi FileInfo) HumanSize() string { + return humanize.IBytes(uint64(fi.Size)) +} + +// HumanModTime returns the modified time of the file as a human-readable string. +func (fi FileInfo) HumanModTime(format string) string { + return fi.ModTime.Format(format) +} + +func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (Listing, bool) { + var ( + fileinfos []FileInfo + dirCount, fileCount int + hasIndexFile bool + ) + + for _, f := range files { + name := f.Name() + + for _, indexName := range staticfiles.IndexPages { + if name == indexName { + hasIndexFile = true + break + } + } + + if f.IsDir() { + name += "/" + dirCount++ + } else { + fileCount++ + } + + url := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name + + fileinfos = append(fileinfos, FileInfo{ + IsDir: f.IsDir(), + Name: f.Name(), + Size: f.Size(), + URL: url.String(), + ModTime: f.ModTime().UTC(), + Mode: f.Mode(), + }) + } + + return Listing{ + Name: path.Base(urlPath), + Path: urlPath, + CanGoUp: canGoUp, + Items: fileinfos, + NumDirs: dirCount, + NumFiles: fileCount, + }, hasIndexFile +} + +// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. +// If so, control is handed over to ServeListing. func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - + var bc *Config + // See if there's a browse configuration to match the path + for i := range f.Configs { + if httpserver.Path(r.URL.Path).Matches(f.Configs[i].PathScope) { + bc = &f.Configs[i] + goto inScope + } + } return f.Next.ServeHTTP(w, r) +inScope: + + // Browse works on existing directories; delegate everything else + requestedFilepath, err := bc.Root.Open(r.URL.Path) + if err != nil { + switch { + case os.IsPermission(err): + return http.StatusForbidden, err + case os.IsExist(err): + return http.StatusNotFound, err + default: + return f.Next.ServeHTTP(w, r) + } + } + defer requestedFilepath.Close() + + info, err := requestedFilepath.Stat() + if err != nil { + switch { + case os.IsPermission(err): + return http.StatusForbidden, err + case os.IsExist(err): + return http.StatusGone, err + default: + return f.Next.ServeHTTP(w, r) + } + } + if !info.IsDir() { + return f.Next.ServeHTTP(w, r) + } + + // Do not reply to anything else because it might be nonsensical + switch r.Method { + case http.MethodGet, http.MethodHead: + // proceed, noop + case "PROPFIND", http.MethodOptions: + return http.StatusNotImplemented, nil + default: + return f.Next.ServeHTTP(w, r) + } + + // Browsing navigation gets messed up if browsing a directory + // that doesn't end in "/" (which it should, anyway) + if !strings.HasSuffix(r.URL.Path, "/") { + http.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect) + return 0, nil + } + + return f.ServeListing(w, r, requestedFilepath, bc) +} + +func (f FileManager) loadDirectoryContents(requestedFilepath http.File, urlPath string) (*Listing, bool, error) { + files, err := requestedFilepath.Readdir(-1) + if err != nil { + return nil, false, err + } + + // Determine if user can browse up another folder + var canGoUp bool + curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/")) + for _, other := range f.Configs { + if strings.HasPrefix(curPathDir, other.PathScope) { + canGoUp = true + break + } + } + + // Assemble listing of directory contents + listing, hasIndex := directoryListing(files, canGoUp, urlPath) + + return &listing, hasIndex, nil +} + +// ServeListing returns a formatted view of 'requestedFilepath' contents'. +func (f FileManager) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath http.File, bc *Config) (int, error) { + listing, containsIndex, err := f.loadDirectoryContents(requestedFilepath, r.URL.Path) + if err != nil { + switch { + case os.IsPermission(err): + return http.StatusForbidden, err + case os.IsExist(err): + return http.StatusGone, err + default: + return http.StatusInternalServerError, err + } + } + if containsIndex && !f.IgnoreIndexes { // directory isn't browsable + return f.Next.ServeHTTP(w, r) + } + listing.Context = httpserver.Context{ + Root: bc.Root, + Req: r, + URL: r.URL, + } + listing.User = bc.Variables + + // Copy the query values into the Listing struct + var limit int + listing.Sort, listing.Order, limit, err = f.handleSortOrder(w, r, bc.PathScope) + if err != nil { + return http.StatusBadRequest, err + } + + listing.applySort() + + if limit > 0 && limit <= len(listing.Items) { + listing.Items = listing.Items[:limit] + listing.ItemsLimitedTo = limit + } + + var buf *bytes.Buffer + acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) + switch { + case strings.Contains(acceptHeader, "application/json"): + if buf, err = f.formatAsJSON(listing, bc); err != nil { + return http.StatusInternalServerError, err + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + default: // There's no 'application/json' in the 'Accept' header; browse normally + if buf, err = f.formatAsHTML(listing, bc); err != nil { + return http.StatusInternalServerError, err + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + } + + buf.WriteTo(w) + + return http.StatusOK, nil +} + +// serveAssets handles the /{admin}/assets requests +func serveAssets(w http.ResponseWriter, r *http.Request) (int, error) { + filename := strings.Replace(r.URL.Path, assetsURL, "", 1) + file, err := assets.Asset(filename) + + if err != nil { + return 404, nil + } + + // Get the file extension ant its mime type + extension := filepath.Ext(filename) + mime := mime.TypeByExtension(extension) + + // Write the header with the Content-Type and write the file + // content to the buffer + w.Header().Set("Content-Type", mime) + w.Write(file) + return 200, nil +} + +func (f FileManager) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) { + marsh, err := json.Marshal(listing.Items) + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + _, err = buf.Write(marsh) + return buf, err +} + +func (f FileManager) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) { + buf := new(bytes.Buffer) + err := bc.Template.Execute(buf, listing) + return buf, err } diff --git a/setup.go b/setup.go index df33496b..1d5615e3 100644 --- a/setup.go +++ b/setup.go @@ -1,10 +1,18 @@ package filemanager import ( + "fmt" + "io/ioutil" + "net/http" + "text/template" + + "github.com/hacdias/caddy-filemanager/assets" "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/httpserver" ) +const assetsURL = "/_filemanager_internal/" + func init() { caddy.RegisterPlugin("filemanager", caddy.Plugin{ ServerType: "http", @@ -12,18 +20,92 @@ func init() { }) } -// setup configures the middlware. +// setup configures a new Browse middleware instance. func setup(c *caddy.Controller) error { - cnf := httpserver.GetConfig(c.Key) + configs, err := fileManagerParse(c) + if err != nil { + return err + } - // parse config + f := FileManager{ + Configs: configs, + IgnoreIndexes: false, + } - mid := func(next httpserver.Handler) httpserver.Handler { - return FileManager{ - Next: next, + httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + f.Next = next + return f + }) + + return nil +} + +func fileManagerParse(c *caddy.Controller) ([]Config, error) { + var configs []Config + + cfg := httpserver.GetConfig(c.Key) + + appendCfg := func(bc Config) error { + for _, c := range configs { + if c.PathScope == bc.PathScope { + return fmt.Errorf("duplicate browsing config for %s", c.PathScope) + } + } + configs = append(configs, bc) + return nil + } + + for c.Next() { + var bc Config + + // First argument is directory to allow browsing; default is site root + if c.NextArg() { + bc.PathScope = c.Val() + } else { + bc.PathScope = "/" + } + bc.Root = http.Dir(cfg.Root) + theRoot, err := bc.Root.Open("/") // catch a missing path early + if err != nil { + return configs, err + } + defer theRoot.Close() + _, err = theRoot.Readdir(-1) + if err != nil { + return configs, err + } + + var tplBytes []byte + + // Second argument would be the template file to use + var tplText string + if c.NextArg() { + tplBytes, err = ioutil.ReadFile(c.Val()) + if err != nil { + return configs, err + } + tplText = string(tplBytes) + } else { + tplBytes, err = assets.Asset(assetsURL + "template.tmpl") + if err != nil { + return configs, err + } + tplText = string(tplBytes) + } + + // Build the template + tpl, err := template.New("listing").Parse(tplText) + if err != nil { + return configs, err + } + bc.Template = tpl + + // Save configuration + err = appendCfg(bc) + if err != nil { + return configs, err } } - cnf.AddMiddleware(mid) - return nil + return configs, nil } diff --git a/sort.go b/sort.go new file mode 100644 index 00000000..02199159 --- /dev/null +++ b/sort.go @@ -0,0 +1,112 @@ +package filemanager + +import ( + "net/http" + "sort" + "strconv" + "strings" +) + +// handleSortOrder gets and stores for a Listing the 'sort' and 'order', +// and reads 'limit' if given. The latter is 0 if not given. +// +// This sets Cookies. +func (f FileManager) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) { + sort, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), r.URL.Query().Get("limit") + + // If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies + switch sort { + case "": + sort = "name" + if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { + sort = sortCookie.Value + } + case "name", "size", "type": + http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil}) + } + + switch order { + case "": + order = "asc" + if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { + order = orderCookie.Value + } + case "asc", "desc": + http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil}) + } + + if limitQuery != "" { + limit, err = strconv.Atoi(limitQuery) + if err != nil { // if the 'limit' query can't be interpreted as a number, return err + return + } + } + + return +} + +// Implement sorting for Listing +type byName Listing +type bySize Listing +type byTime Listing + +// By Name +func (l byName) Len() int { return len(l.Items) } +func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } + +// Treat upper and lower case equally +func (l byName) Less(i, j int) bool { + return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) +} + +// By Size +func (l bySize) Len() int { return len(l.Items) } +func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } + +const directoryOffset = -1 << 31 // = math.MinInt32 +func (l bySize) Less(i, j int) bool { + iSize, jSize := l.Items[i].Size, l.Items[j].Size + if l.Items[i].IsDir { + iSize = directoryOffset + iSize + } + if l.Items[j].IsDir { + jSize = directoryOffset + jSize + } + return iSize < jSize +} + +// By Time +func (l byTime) Len() int { return len(l.Items) } +func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } +func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) } + +// Add sorting method to "Listing" +// it will apply what's in ".Sort" and ".Order" +func (l Listing) applySort() { + // Check '.Order' to know how to sort + if l.Order == "desc" { + switch l.Sort { + case "name": + sort.Sort(sort.Reverse(byName(l))) + case "size": + sort.Sort(sort.Reverse(bySize(l))) + case "time": + sort.Sort(sort.Reverse(byTime(l))) + default: + // If not one of the above, do nothing + return + } + } else { // If we had more Orderings we could add them here + switch l.Sort { + case "name": + sort.Sort(byName(l)) + case "size": + sort.Sort(bySize(l)) + case "time": + sort.Sort(byTime(l)) + default: + // If not one of the above, do nothing + return + } + } +}