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 td.contents[part] = nd 134 td = nd 135 } 136 } 137 138 return td, nil 139 } 140 141 // Create places a Node into the directory tree. 142 // 143 // Any non-existent directories will be created automatically, setting the 144 // modTime to that of the Node and the index to false. 145 // 146 // If you want to specify alternate modTime/index values for the directories, 147 // then you should create them first with Mkdir. 148 func (d Dir) Create(name string, n Node) error { 149 dname, fname := path.Split(name) 150 151 dn, err := d.makePath(dname, n.ModTime(), false) 152 if err != nil { 153 return err 154 } 155 156 if _, ok := dn.contents[fname]; ok { 157 return fs.ErrExist 158 } 159 160 dn.contents[fname] = n 161 162 return nil 163 } 164 165 // Remove will remove a node from the tree. 166 // 167 // It will remove files and any directories, whether they are empty or not. 168 // 169 // Caution: httpdir does no internal locking, so you should provide your own if 170 // you intend to call this method. 171 func (d Dir) Remove(name string) error { 172 dname, fname := path.Split(name) 173 174 nn, err := d.get(dname) 175 if err != nil { 176 return err 177 } 178 179 if nd, ok := nn.Node.(dir); ok { 180 return nd.Remove(fname) 181 } 182 183 return fs.ErrInvalid 184 } 185 186 // Node represents a data file in the tree. 187 type Node interface { 188 Size() int64 189 Mode() fs.FileMode 190 ModTime() time.Time 191 Open() (File, error) 192 } 193 194 type namedNode struct { 195 name string 196 Node 197 } 198 199 func (n namedNode) Name() string { 200 return n.name 201 } 202 203 func (n namedNode) IsDir() bool { 204 return n.Mode().IsDir() 205 } 206 207 func (n namedNode) Sys() interface{} { 208 return n.Node 209 } 210 211 func (n namedNode) Open() (http.File, error) { 212 f, err := n.Node.Open() 213 if err != nil { 214 return nil, err 215 } 216 217 return wrapped{n, f}, nil 218 } 219 220 // File represents an opened data Node. 221 type File interface { 222 io.Reader 223 io.Seeker 224 io.Closer 225 Readdir(int) ([]fs.FileInfo, error) 226 } 227 228 type wrapped struct { 229 fs.FileInfo 230 File 231 } 232 233 func (w wrapped) Stat() (fs.FileInfo, error) { 234 return w.FileInfo, nil 235 } 236