1 /* 2 Copyright (c) 2014-2021 Martin Cejp, Timur Gafarov 3 4 Boost Software License - Version 1.0 - August 17th, 2003 5 6 Permission is hereby granted, free of charge, to any person or organization 7 obtaining a copy of the software and accompanying documentation covered by 8 this license (the "Software") to use, reproduce, display, distribute, 9 execute, and transmit the Software, and to prepare derivative works of the 10 Software, and to permit third-parties to whom the Software is furnished to 11 do so, all subject to the following: 12 13 The copyright notices in the Software and this entire statement, including 14 the above license grant, this restriction and the following disclaimer, 15 must be included in all copies of the Software, in whole or in part, and 16 all derivative works of the Software, unless such copies or derivative 17 works are solely in the form of machine-executable object code generated by 18 a source language processor. 19 20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 DEALINGS IN THE SOFTWARE. 27 */ 28 29 /** 30 * Copyright: Martin Cejp, Timur Gafarov 2014-2021. 31 * License: $(LINK2 boost.org/LICENSE_1_0.txt, Boost License 1.0). 32 * Authors: Martin Cejp, Timur Gafarov 33 */ 34 module dlib.filesystem.filesystem; 35 36 import std.datetime; 37 import std.range; 38 import std.regex; 39 import std.algorithm; 40 import std..string; 41 42 import dlib.core.stream; 43 import dlib.filesystem.dirrange; 44 45 /// File size type 46 alias FileSize = StreamSize; 47 48 /// If a file is readable, FileStat.permissions will have PRead bits set 49 enum PRead = 0x01; 50 51 /// If a file is writable, FileStat.permissions will have PWrite bits set 52 enum PWrite = 0x02; 53 54 /// If a file is executable, FileStat.permissions will have PExecute bits set 55 enum PExecute = 0x04; 56 57 /// Holds general information about a file or directory. 58 struct FileStat 59 { 60 /// True if a file is not a directory 61 bool isFile; 62 63 /// True if a file is a directory 64 bool isDirectory; 65 66 /// File size. Valid only if isFile is true 67 FileSize sizeInBytes; 68 69 /// File creation date/time 70 SysTime creationTimestamp; 71 72 /// File modification date/time 73 SysTime modificationTimestamp; 74 75 /// Permissions of a file in a given filesystem. Bit combination of PRead, PWrite, PExecute 76 int permissions; 77 } 78 79 /// A filesystem entry - file or directory 80 struct DirEntry 81 { 82 /// Entry base name (relative to its containing directory) 83 string name; 84 85 /// True if an entry is a file 86 bool isFile; 87 88 /// True if an entry is a directory 89 bool isDirectory; 90 } 91 92 /// A directory in the file system. 93 interface Directory 94 { 95 /// 96 void close(); 97 98 /// Get directory contents as a range. 99 /// This range $(I should) be lazily evaluated when practical. 100 /// The entries "." and ".." are skipped. 101 InputRange!DirEntry contents(); 102 } 103 104 /// A filesystem limited to read access. 105 interface ReadOnlyFileSystem 106 { 107 /** Get file or directory stats. 108 Example: 109 --- 110 void printFileInfo(ReadOnlyFileSystem fs, string filename) 111 { 112 FileStat stat; 113 114 writef("'%s'\t", filename); 115 116 if (!fs.stat(filename, stat)) 117 { 118 writeln("ERROR"); 119 return; 120 } 121 122 if (stat.isFile) 123 writefln("%u", stat.sizeInBytes); 124 else if (stat.isDirectory) 125 writeln("DIR"); 126 127 writefln(" created: %s", to!string(stat.creationTimestamp)); 128 writefln(" modified: %s", to!string(stat.modificationTimestamp)); 129 } 130 --- 131 */ 132 bool stat(string filename, out FileStat stat); 133 134 /** Open a file for input. 135 Returns: a valid InputStream on success, null on failure 136 */ 137 InputStream openForInput(string filename); 138 139 /** Open a directory. 140 */ 141 Directory openDir(string path); 142 } 143 144 /// A file system with read/write access. 145 interface FileSystem: ReadOnlyFileSystem 146 { 147 // TODO: Use exceptions or not? 148 149 /// File access flags. 150 enum 151 { 152 read = 1, 153 write = 2, 154 } 155 156 /// File creation flags. 157 enum 158 { 159 create = 1, 160 truncate = 2, 161 } 162 163 // TODO: Keep it this way? (strongly-typed) 164 165 /** Open a file for output. 166 Returns: a valid OutputStream on success, null on failure 167 */ 168 OutputStream openForOutput(string filename, uint creationFlags); 169 170 /** Open a file for input & output. 171 Returns: a valid IOStream on success, null on failure 172 */ 173 IOStream openForIO(string filename, uint creationFlags); 174 175 //IOStream openFile(string filename, uint accessFlags, uint creationFlags); 176 177 /** Create a new directory. 178 Returns: true if a new directory was created 179 Examples: 180 --- 181 fs.createDir("New Directory", false); 182 fs.createDir("nested/directories/are/easy", true); 183 --- 184 */ 185 bool createDir(string path, bool recursive); 186 187 // BROKEN API. Must define semantics for non-atomic move cases (e.g. moving a file to a different drive) 188 //bool move(string path, string newPath); 189 190 /** Permanently delete a file or directory. 191 */ 192 bool remove(string path, bool recursive); 193 } 194 195 /** 196 Find files in the specified directory 197 198 Params: 199 rofs = filesystem to scan 200 baseDir = path to the base directory (if empty, defaults to current working directory) 201 recursive = if true, the search will recurse into subdirectories 202 203 Examples: 204 --- 205 void listImagesInDirectory(ReadOnlyFileSystem fs, string baseDir = "") 206 { 207 foreach (entry; fs.findFiles(baseDir, true) 208 .filter!(entry => entry.isFile) 209 .filter!(entry => !matchFirst(entry.name, `.*\.(gif|jpg|png)$`).empty)) 210 { 211 writefln("%s", entry.name); 212 } 213 } 214 --- 215 */ 216 InputRange!DirEntry findFiles(ReadOnlyFileSystem rofs, string baseDir, bool recursive) 217 { 218 // Do some magic so that we don't have to keep our own stack 219 220 import core.thread; 221 222 //baseDir = normalizePath(baseDir); 223 224 DirEntry entry; 225 226 // findFiles insists on calling us back (it's recursive), but we can trap it in a Fiber 227 auto search = new Fiber(delegate void() 228 { 229 findFiles(rofs, baseDir, recursive, delegate int(ref DirEntry de) 230 { 231 // save the data (D doesn't allow to yield it directly) 232 // and jump outside of the .call() (see below) 233 entry = de; 234 Fiber.yield(); 235 236 // after resuming, return to findFiles for another round 237 return 0; 238 }); 239 240 // state becomes TERM after we're resumed after returning the last entry 241 }); 242 243 return new DirRange(delegate bool(out DirEntry de) 244 { 245 // terminated before? 246 if (search.state == Fiber.State.TERM) 247 return false; 248 249 // jumps into our search delegate 250 search.call(); 251 252 // last entry had been returned last time? 253 // (even findFiles didn't know until we returned to it again) 254 if (search.state == Fiber.State.TERM) 255 return false; 256 257 de = entry; 258 return true; 259 }); 260 } 261 262 private int findFiles(ReadOnlyFileSystem rofs, string baseDir, bool recursive, int delegate(ref DirEntry entry) dg) 263 { 264 Directory dir = rofs.openDir(baseDir); 265 266 if (dir is null) 267 return 0; 268 269 int result = 0; 270 271 try 272 { 273 foreach (entry; dir.contents) 274 { 275 if (!baseDir.empty) 276 entry.name = baseDir ~ "/" ~ entry.name; 277 278 result = dg(entry); 279 280 if (result != 0) 281 return result; 282 283 if (recursive && entry.isDirectory) 284 { 285 result = findFiles(rofs, entry.name, recursive, dg); 286 287 if (result != 0) 288 return result; 289 } 290 } 291 } 292 293 finally 294 { 295 dir.close(); 296 } 297 298 return result; 299 } 300