1// Copyright 2014 Google Inc. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Package fs contains an HTTP file system that works with zip contents. 16package fs 17 18import ( 19 "archive/zip" 20 "bytes" 21 "errors" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/http" 26 "os" 27 "path" 28 "sort" 29 "strings" 30 "time" 31) 32 33var zipData string 34 35// file holds unzipped read-only file contents and file metadata. 36type file struct { 37 os.FileInfo 38 data []byte 39 fs *statikFS 40} 41 42type statikFS struct { 43 files map[string]file 44 dirs map[string][]string 45} 46 47// Register registers zip contents data, later used to initialize 48// the statik file system. 49func Register(data string) { 50 zipData = data 51} 52 53// New creates a new file system with the registered zip contents data. 54// It unzips all files and stores them in an in-memory map. 55func New() (http.FileSystem, error) { 56 if zipData == "" { 57 return nil, errors.New("statik/fs: no zip data registered") 58 } 59 zipReader, err := zip.NewReader(strings.NewReader(zipData), int64(len(zipData))) 60 if err != nil { 61 return nil, err 62 } 63 files := make(map[string]file, len(zipReader.File)) 64 dirs := make(map[string][]string) 65 fs := &statikFS{files: files, dirs: dirs} 66 for _, zipFile := range zipReader.File { 67 fi := zipFile.FileInfo() 68 f := file{FileInfo: fi, fs: fs} 69 f.data, err = unzip(zipFile) 70 if err != nil { 71 return nil, fmt.Errorf("statik/fs: error unzipping file %q: %s", zipFile.Name, err) 72 } 73 files["/"+zipFile.Name] = f 74 } 75 for fn := range files { 76 // go up directories recursively in order to care deep directory 77 for dn := path.Dir(fn); dn != fn; { 78 if _, ok := files[dn]; !ok { 79 files[dn] = file{FileInfo: dirInfo{dn}, fs: fs} 80 } else { 81 break 82 } 83 fn, dn = dn, path.Dir(dn) 84 } 85 } 86 for fn := range files { 87 dn := path.Dir(fn) 88 if fn != dn { 89 fs.dirs[dn] = append(fs.dirs[dn], path.Base(fn)) 90 } 91 } 92 for _, s := range fs.dirs { 93 sort.Strings(s) 94 } 95 return fs, nil 96} 97 98var _ = os.FileInfo(dirInfo{}) 99 100type dirInfo struct { 101 name string 102} 103 104func (di dirInfo) Name() string { return path.Base(di.name) } 105func (di dirInfo) Size() int64 { return 0 } 106func (di dirInfo) Mode() os.FileMode { return 0755 | os.ModeDir } 107func (di dirInfo) ModTime() time.Time { return time.Time{} } 108func (di dirInfo) IsDir() bool { return true } 109func (di dirInfo) Sys() interface{} { return nil } 110 111func unzip(zf *zip.File) ([]byte, error) { 112 rc, err := zf.Open() 113 if err != nil { 114 return nil, err 115 } 116 defer rc.Close() 117 return ioutil.ReadAll(rc) 118} 119 120// Open returns a file matching the given file name, or os.ErrNotExists if 121// no file matching the given file name is found in the archive. 122// If a directory is requested, Open returns the file named "index.html" 123// in the requested directory, if that file exists. 124func (fs *statikFS) Open(name string) (http.File, error) { 125 name = strings.Replace(name, "//", "/", -1) 126 if f, ok := fs.files[name]; ok { 127 return newHTTPFile(f), nil 128 } 129 return nil, os.ErrNotExist 130} 131 132func newHTTPFile(file file) *httpFile { 133 if file.IsDir() { 134 return &httpFile{file: file, isDir: true} 135 } 136 return &httpFile{file: file, reader: bytes.NewReader(file.data)} 137} 138 139// httpFile represents an HTTP file and acts as a bridge 140// between file and http.File. 141type httpFile struct { 142 file 143 144 reader *bytes.Reader 145 isDir bool 146 dirIdx int 147} 148 149// Read reads bytes into p, returns the number of read bytes. 150func (f *httpFile) Read(p []byte) (n int, err error) { 151 if f.reader == nil && f.isDir { 152 return 0, io.EOF 153 } 154 return f.reader.Read(p) 155} 156 157// Seek seeks to the offset. 158func (f *httpFile) Seek(offset int64, whence int) (ret int64, err error) { 159 return f.reader.Seek(offset, whence) 160} 161 162// Stat stats the file. 163func (f *httpFile) Stat() (os.FileInfo, error) { 164 return f, nil 165} 166 167// IsDir returns true if the file location represents a directory. 168func (f *httpFile) IsDir() bool { 169 return f.isDir 170} 171 172// Readdir returns an empty slice of files, directory 173// listing is disabled. 174func (f *httpFile) Readdir(count int) ([]os.FileInfo, error) { 175 var fis []os.FileInfo 176 if !f.isDir { 177 return fis, nil 178 } 179 di, ok := f.FileInfo.(dirInfo) 180 if !ok { 181 return nil, fmt.Errorf("failed to read directory: %q", f.Name()) 182 } 183 184 // If count is positive, the specified number of files will be returned, 185 // and if negative, all remaining files will be returned. 186 // The reading position of which file is returned is held in dirIndex. 187 fnames := f.file.fs.dirs[di.name] 188 flen := len(fnames) 189 190 // If dirIdx reaches the end and the count is a positive value, 191 // an io.EOF error is returned. 192 // In other cases, no error will be returned even if, for example, 193 // you specified more counts than the number of remaining files. 194 start := f.dirIdx 195 if start >= flen && count > 0 { 196 return fis, io.EOF 197 } 198 var end int 199 if count < 0 { 200 end = flen 201 } else { 202 end = start + count 203 } 204 if end > flen { 205 end = flen 206 } 207 for i := start; i < end; i++ { 208 fis = append(fis, f.file.fs.files[path.Join(di.name, fnames[i])].FileInfo) 209 } 210 f.dirIdx += len(fis) 211 return fis, nil 212} 213 214func (f *httpFile) Close() error { 215 return nil 216} 217