1 // Written in the D programming language.
2 
3 /**
4 
5 This module contains cross-platform file access utilities
6 
7 
8 
9 Synopsis:
10 
11 ----
12 import exlib.files;
13 ----
14 
15 Copyright: Vadim Lopatin, 2014
16 License:   Boost License 1.0
17 Authors:   Vadim Lopatin, coolreader.org@gmail.com
18 */
19 module dlangui.core.files;
20 
21 import std.algorithm;
22 
23 private import dlangui.core.logger;
24 private import std.process;
25 private import std.path;
26 private import std.file;
27 private import std.utf;
28 
29 version (Windows) {
30     /// path delimiter (\ for windows, / for others)
31     immutable char PATH_DELIMITER = '\\';
32 } else {
33     /// path delimiter (\ for windows, / for others)
34     immutable char PATH_DELIMITER = '/';
35 }
36 
37 /// Filesystem root entry / bookmark types
38 enum RootEntryType : uint {
39     /// filesystem root
40     ROOT,
41     /// current user home
42     HOME,
43     /// removable drive
44     REMOVABLE,
45     /// fixed drive
46     FIXED,
47     /// network
48     NETWORK,
49     /// cd rom
50     CDROM,
51     /// sd card
52     SDCARD,
53     /// custom bookmark
54     BOOKMARK,
55 }
56 
57 /// Filesystem root entry item
58 struct RootEntry {
59     private RootEntryType _type;
60     private string _path;
61     private dstring _display;
62     this(RootEntryType type, string path, dstring display = null) {
63         _type = type;
64         _path = path;
65         _display = display;
66         if (display is null) {
67             _display = toUTF32(baseName(path));
68         }
69     }
70     /// Returns type
71     @property RootEntryType type() { return _type; }
72     /// Returns path
73     @property string path() { return _path; }
74     /// Returns display label
75     @property dstring label() { return _display; }
76     /// Returns icon resource id
77     @property string icon() {
78         switch (type) {
79             case RootEntryType.NETWORK:
80                 return "folder-network";
81             case RootEntryType.BOOKMARK:
82                 return "folder-bookmark";
83             case RootEntryType.CDROM:
84                 return "drive-optical";
85             case RootEntryType.FIXED:
86                 return "drive-harddisk";
87             case RootEntryType.HOME:
88                 return "user-home";
89             case RootEntryType.ROOT:
90                 return "computer";
91             case RootEntryType.SDCARD:
92                 return "media-flash-sd-mmc";
93             case RootEntryType.REMOVABLE:
94                 return "device-removable-media";
95             default:
96                 return "folder-blue";
97         }
98     }
99 }
100 
101 /// Returns 
102 @property RootEntry homeEntry() {
103     return RootEntry(RootEntryType.HOME, homePath);
104 }
105 
106 /// returns array of system root entries
107 @property RootEntry[] getRootPaths() {
108     RootEntry[] res;
109     res ~= RootEntry(RootEntryType.HOME, homePath);
110     version (posix) {
111         res ~= RootEntry(RootEntryType.ROOT, "/", "File System"d);
112     }
113     version (Windows) {
114         import win32.windows;
115         uint mask = GetLogicalDrives();
116         for (int i = 0; i < 26; i++) {
117             if (mask & (1 << i)) {
118                 char letter = cast(char)('A' + i);
119                 string path = "" ~ letter ~ ":\\";
120                 dstring display = ""d ~ letter ~ ":"d;
121                 // detect drive type
122                 RootEntryType type;
123                 uint wtype = GetDriveTypeA(("" ~ path).ptr);
124                 //Log.d("Drive ", path, " type ", wtype);
125                 switch (wtype) {
126                     case DRIVE_REMOVABLE:
127                         type = RootEntryType.REMOVABLE;
128                         break;
129                     case DRIVE_REMOTE:
130                         type = RootEntryType.NETWORK;
131                         break;
132                     case DRIVE_CDROM:
133                         type = RootEntryType.CDROM;
134                         break;
135                     default:
136                         type = RootEntryType.FIXED;
137                         break;
138                 }
139                 res ~= RootEntry(type, path, display);
140             }
141         }
142     }
143     return res;
144 }
145 
146 /// returns true if directory is root directory (e.g. / or C:\)
147 bool isRoot(string path) {
148     string root = rootName(path);
149     if (path.equal(root))
150         return true;
151     return false;
152 }
153 
154 /// returns parent directory for specified path
155 string parentDir(string path) {
156     return buildNormalizedPath(path, "..");
157 }
158 
159 /// check filename with pattern (currently only *.ext pattern is supported)
160 bool filterFilename(string filename, string pattern) {
161 	if (pattern.equal("*.*"))
162 		return true; // matches any
163 	if (pattern.length < 3)
164 		return false;
165 	if (pattern[0] != '*' || pattern[1] != '.')
166 		return false;
167 	return filename.endsWith(pattern[1..$]);
168 }
169 
170 /// Filters file name by pattern list
171 bool filterFilename(string filename, string[] filters) {
172 	if (filters.length == 0)
173 		return true; // no filters - show all
174 	foreach(pattern; filters) {
175 		if (filterFilename(filename, pattern))
176 			return true;
177 	}
178     return false;
179 }
180 
181 /** List directory content 
182     
183     Optionally filters file names by filter.
184 
185     Result will be placed into entries array.
186 
187     Returns true if directory exists and listed successfully, false otherwise.
188 */
189 bool listDirectory(string dir, bool includeDirs, bool includeFiles, bool showHiddenFiles, string[] filters, ref DirEntry[] entries) {
190 
191     entries.length = 0;
192 
193     if (!isDir(dir)) {
194         return false;
195     }
196 
197     if (!isRoot(dir) && includeDirs) {
198         entries ~= DirEntry(appendPath(dir, ".."));
199     }
200 
201     try {
202         DirEntry[] dirs;
203         DirEntry[] files;
204         foreach (DirEntry e; dirEntries(dir, SpanMode.shallow)) {
205             string fn = baseName(e.name);
206             if (!showHiddenFiles && fn.startsWith("."))
207                 continue;
208             if (e.isDir) {
209                 dirs ~= e;
210             } else if (e.isFile) {
211                 files ~= e;
212             }
213         }
214         if (includeDirs)
215             foreach(DirEntry e; dirs)
216                 entries ~= e;
217         if (includeFiles)
218             foreach(DirEntry e; files)
219                 if (filterFilename(e.name, filters))
220                     entries ~= e;
221         return true;
222     } catch (FileException e) {
223         return false;
224     }
225 
226 }
227 
228 /** Returns true if char ch is / or \ slash */
229 bool isPathDelimiter(char ch) {
230     return ch == '/' || ch == '\\';
231 }
232 
233 /// Returns current directory
234 @property string currentDir() {
235     return getcwd();
236 }
237 
238 /** Returns current executable path only, including last path delimiter - removes executable name from result of std.file.thisExePath() */
239 @property string exePath() {
240     string path = thisExePath();
241     int lastSlash = 0;
242     for (int i = 0; i < path.length; i++)
243         if (path[i] == PATH_DELIMITER)
244             lastSlash = i;
245     return path[0 .. lastSlash + 1];
246 }
247 
248 /// Returns user's home directory
249 @property string homePath() {
250     string path;
251     version (Windows) {
252         path = environment.get("USERPROFILE");
253         if (path is null)
254             path = environment.get("HOME");
255     } else {
256         path = environment.get("HOME");
257     }
258     if (path is null)
259         path = "."; // fallback to current directory
260     return path;
261 }
262 
263 /** 
264     
265     Returns application data directory
266 
267     On unix, it will return path to subdirectory in home directory - e.g. /home/user/.subdir if ".subdir" is passed as a paramter.
268 
269     On windows, it will return path to subdir in APPDATA directory - e.g. C:\Users\User\AppData\Roaming\.subdir.
270 
271  */
272 string appDataPath(string subdir = null) {
273     string path;
274     version (Windows) {
275         path = environment.get("APPDATA");
276     }
277     if (path is null)
278         path = homePath;
279     if (subdir !is null) {
280         path ~= PATH_DELIMITER;
281         path ~= subdir;
282     }
283     return path;
284 }
285 
286 /// Converts path delimiters to standard for platform inplace in buffer(e.g. / to \ on windows, \ to / on posix), returns buf
287 char[] convertPathDelimiters(char[] buf) {
288     foreach(ref ch; buf) {
289         version (Windows) {
290             if (ch == '/')
291                 ch = '\\';
292         } else {
293             if (ch == '\\')
294                 ch = '/';
295         }
296     }
297     return buf;
298 }
299 
300 /** Converts path delimiters to standard for platform (e.g. / to \ on windows, \ to / on posix) */
301 string convertPathDelimiters(string src) {
302     char[] buf = src.dup;
303     return cast(string)convertPathDelimiters(buf);
304 }
305 
306 /** Appends file path parts with proper delimiters e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config" */
307 string appendPath(string[] pathItems ...) {
308     char[] buf;
309     foreach (s; pathItems) {
310         if (buf.length && !isPathDelimiter(buf[$-1]))
311             buf ~= PATH_DELIMITER;
312         buf ~= s;
313     }
314     return convertPathDelimiters(buf).dup;
315 }
316 
317 /**  Appends file path parts with proper delimiters (as well converts delimiters inside path to system) to buffer e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config" */
318 char[] appendPath(char[] buf, string[] pathItems ...) {
319     foreach (s; pathItems) {
320         if (buf.length && !isPathDelimiter(buf[$-1]))
321             buf ~= PATH_DELIMITER;
322         buf ~= s;
323     }
324     return convertPathDelimiters(buf);
325 }
326 
327 /** Split path into elements, e.g. /home/user/dir1 -> ["home", "user", "dir1"], "c:\dir1\dir2" -> ["c:", "dir1", "dir2"] */
328 string[] splitPath(string path) {
329 	string[] res;
330 	int start = 0;
331 	for (int i = 0; i <= path.length; i++) {
332 		char ch = i < path.length ? path[i] : 0;
333 		if (ch == '\\' || ch == '/' || ch == 0) {
334 			if (start < i)
335 				res ~= path[start .. i].dup;
336 			start = i + 1;
337 		}
338 	}
339 	return res;
340 }