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 return n.Open() 62 } 63 64 func (d Dir) get(name string) (namedNode, error) { 65 name = path.Clean(name) 66 if len(name) > 0 && name[0] == '/' { 67 name = name[1:] 68 } 69 n := namedNode{"", d.d} 70 if len(name) > 0 { 71 for _, part := range strings.Split(name, "/") { 72 nd, ok := n.Node.(dir) 73 if !ok { 74 return namedNode{}, fs.ErrInvalid 75 } 76 dn, ok := nd.contents[part] 77 if !ok { 78 return namedNode{}, fs.ErrNotExist 79 } 80 n = namedNode{part, dn} 81 } 82 } 83 return n, nil 84 } 85 86 // Mkdir creates the named directory, and any parent directories required. 87 // 88 // modTime is the modification time of the directory, used in caching 89 // mechanisms. 90 // 91 // index specifies whether or not the directory allows a directory listing. 92 // NB: if the directory contains an index.html file, then that will be 93 // displayed instead, regardless the value of index. 94 // 95 // All directories created will be given the specified modification time and 96 // index bool. 97 // 98 // Directories already existing will not be modified. 99 func (d Dir) Mkdir(name string, modTime time.Time, index bool) error { 100 _, err := d.makePath(path.Clean(name), modTime, index) 101 return err 102 } 103 104 func (d Dir) makePath(name string, modTime time.Time, index bool) (dir, error) { 105 if len(name) > 0 && name[0] == '/' { 106 name = name[1:] 107 } 108 td := d.d 109 for _, part := range strings.Split(name, "/") { 110 if part == "" { 111 continue 112 } 113 n, ok := td.contents[part] 114 if ok { 115 switch f := n.(type) { 116 case dir: 117 td = f 118 default: 119 return dir{}, fs.ErrInvalid 120 } 121 } else { 122 nd := dir{ 123 index, 124 make(map[string]Node), 125 modTime, 126 } 127 td.contents[part] = nd 128 td = nd 129 } 130 } 131 return td, nil 132 } 133 134 // Create places a Node into the directory tree. 135 // 136 // Any non-existent directories will be created automatically, setting the 137 // modTime to that of the Node and the index to false. 138 // 139 // If you want to specify alternate modTime/index values for the directories, 140 // then you should create them first with Mkdir 141 func (d Dir) Create(name string, n Node) error { 142 dname, fname := path.Split(name) 143 dn, err := d.makePath(dname, n.ModTime(), false) 144 if err != nil { 145 return nil 146 } 147 if _, ok := dn.contents[fname]; ok { 148 return fs.ErrExist 149 } 150 dn.contents[fname] = n 151 return nil 152 } 153 154 // Remove will remove a node from the tree. 155 // 156 // It will remove files and any directories, whether they are empty or not. 157 // 158 // Caution: httpdir does no internal locking, so you should provide your own if 159 // you intend to call this method. 160 func (d Dir) Remove(name string) error { 161 dname, fname := path.Split(name) 162 nn, err := d.get(dname) 163 if err != nil { 164 return err 165 } 166 if nd, ok := nn.Node.(dir); ok { 167 return nd.Remove(fname) 168 } 169 return fs.ErrInvalid 170 } 171 172 // Node represents a data file in the tree 173 type Node interface { 174 Size() int64 175 Mode() fs.FileMode 176 ModTime() time.Time 177 Open() (File, error) 178 } 179 180 type namedNode struct { 181 name string 182 Node 183 } 184 185 func (n namedNode) Name() string { 186 return n.name 187 } 188 189 func (n namedNode) IsDir() bool { 190 return n.Mode().IsDir() 191 } 192 193 func (n namedNode) Sys() interface{} { 194 return n.Node 195 } 196 197 func (n namedNode) Open() (http.File, error) { 198 f, err := n.Node.Open() 199 if err != nil { 200 return nil, err 201 } 202 return wrapped{n, f}, nil 203 } 204 205 // File represents an opened data Node 206 type File interface { 207 io.Reader 208 io.Seeker 209 io.Closer 210 Readdir(int) ([]fs.FileInfo, error) 211 } 212 213 type wrapped struct { 214 fs.FileInfo 215 File 216 } 217 218 func (w wrapped) Stat() (fs.FileInfo, error) { 219 return w.FileInfo, nil 220 } 221