1 // Package httpdir provides an in-memory implementation of http.FileSystem. 2 package httpdir // import "vimagination.zapto.org/httpdir" 3 4 import ( 5 "io" 6 "io/fs" 7 "net/http" 8 "path" 9 "strings" 10 "time" 11 ) 12 13 // Convenient FileMode constants. 14 const ( 15 ModeDir fs.FileMode = fs.ModeDir | 0o755 16 ModeFile fs.FileMode = 0o644 17 ) 18 19 // Default is the Dir used by the top-level functions. 20 var Default = New(time.Now()) 21 22 // Mkdir is a convenience function for Default.Mkdir. 23 func Mkdir(name string, modTime time.Time, index bool) error { 24 return Default.Mkdir(name, modTime, index) 25 } 26 27 // Create is a convenience function for Default.Create. 28 func Create(name string, n Node) error { 29 return Default.Create(name, n) 30 } 31 32 // Remove is a convenience function for Default.Remove. 33 func Remove(name string) error { 34 return Default.Remove(name) 35 } 36 37 // Dir is the start of a simple in-memory filesystem tree. 38 type Dir struct { 39 d dir 40 } 41 42 // New creates a new, initialised, Dir. 43 func New(t time.Time) Dir { 44 return Dir{ 45 dir{ 46 modTime: t, 47 contents: make(map[string]Node), 48 }, 49 } 50 } 51 52 // Open returns the file, or directory, specified by the given name. 53 // 54 // This method is the implementation of http.FileSystem and isn't intended to 55 // be used by clients of this package. 56 func (d Dir) Open(name string) (http.File, error) { 57 n, err := d.get(name) 58 if err != nil { 59 return nil, err 60 } 61 62 return n.Open() 63 } 64 65 func (d Dir) get(name string) (namedNode, error) { 66 name = path.Clean(name) 67 if len(name) > 0 && name[0] == '/' { 68 name = name[1:] 69 } 70 71 n := namedNode{"", d.d} 72 73 if len(name) > 0 { 74 for _, part := range strings.Split(name, "/") { 75 nd, ok := n.Node.(dir) 76 if !ok { 77 return namedNode{}, fs.ErrInvalid 78 } 79 80 dn, ok := nd.contents[part] 81 if !ok { 82 return namedNode{}, fs.ErrNotExist 83 } 84 85 n = namedNode{part, dn} 86 } 87 } 88 89 return n, nil 90 } 91 92 // Mkdir creates the named directory, and any parent directories required. 93 // 94 // modTime is the modification time of the directory, used in caching 95 // mechanisms. 96 // 97 // index specifies whether or not the directory allows a directory listing. 98 // NB: if the directory contains an index.html file, then that will be 99 // displayed instead, regardless the value of index. 100 // 101 // All directories created will be given the specified modification time and 102 // index bool. 103 // 104 // Directories already existing will not be modified. 105 func (d Dir) Mkdir(name string, modTime time.Time, index bool) error { 106 _, err := d.makePath(path.Clean(name), modTime, index) 107 108 return err 109 } 110 111 func (d Dir) makePath(name string, modTime time.Time, index bool) (dir, error) { 112 name = strings.TrimPrefix(name, "/") 113 td := d.d 114 115 for _, part := range strings.Split(name, "/") { 116 if part == "" { 117 continue 118 } 119 120 if n, ok := td.contents[part]; ok { 121 switch f := n.(type) { 122 case dir: 123 td = f 124 default: 125 return dir{}, fs.ErrInvalid 126 } 127 } else { 128 nd := dir{ 129 index: index, 130 contents: make(map[string]Node), 131 modTime: modTime, 132 } 133 134 td.contents[part] = nd 135 td = nd 136 } 137 } 138 139 return td, nil 140 } 141 142 // Create places a Node into the directory tree. 143 // 144 // Any non-existent directories will be created automatically, setting the 145 // modTime to that of the Node and the index to false. 146 // 147 // If you want to specify alternate modTime/index values for the directories, 148 // then you should create them first with Mkdir. 149 func (d Dir) Create(name string, n Node) error { 150 dname, fname := path.Split(name) 151 152 dn, err := d.makePath(dname, n.ModTime(), false) 153 if err != nil { 154 return err 155 } 156 157 if _, ok := dn.contents[fname]; ok { 158 return fs.ErrExist 159 } 160 161 dn.contents[fname] = n 162 163 return nil 164 } 165 166 // Remove will remove a node from the tree. 167 // 168 // It will remove files and any directories, whether they are empty or not. 169 // 170 // Caution: httpdir does no internal locking, so you should provide your own if 171 // you intend to call this method. 172 func (d Dir) Remove(name string) error { 173 dname, fname := path.Split(name) 174 175 nn, err := d.get(dname) 176 if err != nil { 177 return err 178 } 179 180 if nd, ok := nn.Node.(dir); ok { 181 return nd.Remove(fname) 182 } 183 184 return fs.ErrInvalid 185 } 186 187 // Node represents a data file in the tree. 188 type Node interface { 189 Size() int64 190 Mode() fs.FileMode 191 ModTime() time.Time 192 Open() (File, error) 193 } 194 195 type namedNode struct { 196 name string 197 Node 198 } 199 200 func (n namedNode) Name() string { 201 return n.name 202 } 203 204 func (n namedNode) IsDir() bool { 205 return n.Mode().IsDir() 206 } 207 208 func (n namedNode) Sys() interface{} { 209 return n.Node 210 } 211 212 func (n namedNode) Open() (http.File, error) { 213 f, err := n.Node.Open() 214 if err != nil { 215 return nil, err 216 } 217 218 return wrapped{n, f}, nil 219 } 220 221 // File represents an opened data Node. 222 type File interface { 223 io.Reader 224 io.Seeker 225 io.Closer 226 Readdir(int) ([]fs.FileInfo, error) 227 } 228 229 type wrapped struct { 230 fs.FileInfo 231 File 232 } 233 234 func (w wrapped) Stat() (fs.FileInfo, error) { 235 return w.FileInfo, nil 236 } 237