comparison mde/file/paths.d @ 134:7ababdf97748

Moved mde.setup.paths to mde.file.paths and paths.mdeReader to mde.file.mergetag.Reader.MTMultiReader.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 29 Jan 2009 14:59:45 +0000
parents mde/setup/paths.d@1b1e2297e2fc
children bc697a218716
comparison
equal deleted inserted replaced
133:9fd705793568 134:7ababdf97748
1 /* LICENSE BLOCK
2 Part of mde: a Modular D game-oriented Engine
3 Copyright © 2007-2008 Diggory Hardy
4
5 This program is free software: you can redistribute it and/or modify it under the terms
6 of the GNU General Public License as published by the Free Software Foundation, either
7 version 2 of the License, or (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
10 without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License
14 along with this program. If not, see <http://www.gnu.org/licenses/>. */
15
16 /** Resource paths module.
17 *
18 * Internally, most mde code using files doesn't use absolute paths but paths
19 * relative to mde directory. Further, mergetag files are usually not read
20 * just from one file, but are the result of merging multiple files from the
21 * same relative paths in different directories, including the root data
22 * directory, possibly a directory in /etc, a directory for files specific to
23 * the user accont, and possibly other directories.
24 *
25 * This module transforms relative paths to absolute paths, allowing for
26 * multiple files to be read and merged.
27 *
28 * The base paths from which relative files are read are slit into two groups:
29 * data paths, and conf paths (usually the conf paths refer to a "conf"
30 * directory within the data paths). */
31 /* Implementation note:
32 * All paths are stored internally as strings, rather than as an instance of FilePath/FilePath once
33 * the FilePath has served its immediate purpose, since it's more convenient and creating new
34 * FilePaths for adjusted paths should be no slower than mutating existing ones. */
35 module mde.file.paths;
36
37 import mde.exception;
38 import mde.file.mergetag.Reader;
39 import mde.file.mergetag.Writer;
40 import mde.file.mergetag.DataSet;
41 import mde.file.mergetag.exception;
42
43 import tango.io.Console;
44 import tango.io.FilePath;
45 version (linux) {
46 import tango.io.FileScan;
47 import tango.util.container.SortedMap;
48 import tango.sys.Environment;
49 } else version (Windows)
50 import tango.sys.win32.SpecialPath;
51
52 debug {
53 import tango.util.log.Log : Log, Logger;
54 private Logger logger;
55 static this() {
56 logger = Log.getLogger ("mde.file.paths");
57 }
58 }
59
60 /** Order to read files in.
61 *
62 * Values: HIGH_LOW, LOW_HIGH, HIGH_ONLY. */
63 enum PRIORITY : byte { HIGH_LOW, LOW_HIGH, HIGH_ONLY }
64
65 /** This struct has one instance for each "directory".
66 *
67 * It is the only item within this module that you should need to interact with.
68 *
69 * In the case of confDir, the user path is guaranteed to exist (as highest priority path). */
70 struct mdeDirectory
71 {
72 /** Creates an MT reader for each file.
73 *
74 * Params:
75 * file = The file path and name relative to the mdeDirectory, without a suffix
76 * (e.g. "options")
77 * readOrder = Read the highest priority or lowest priority files first? For correct merging,
78 * this should be LOW_HIGH when newly-read items override old ones (as is the case
79 * with DefaultData) and HIGH_LOW when the first-read items survive. Thus override
80 * order needs to be the same for each section, except the header which is always
81 * read with LOW_HIGH order.
82 * Alternately, for files which shouldn't be
83 * merged where only the highest priority file should be read, pass HIGH_ONLY.
84 * ds = The dataset, as for mergetag. Note: all actual readers share one dataset.
85 * rdHeader = Read the headers for each file and merge if rdHeader == true.
86 */
87 IReader makeMTReader (char[] file, PRIORITY readOrder, DataSet ds = null, bool rdHeader = false)
88 {
89 FilePath[] files = getFiles (file, readOrder);
90 if (files is null)
91 throw new NoFileException ("Unable to find the file: "~file~"[.mtt|mtb]");
92
93 return new MTMultiReader (files, ds, rdHeader);
94 }
95
96 /** Creates an MT writer for file deciding on the best path to use.
97 *
98 * Params:
99 * file = The file path and name relative to the mdeDirectory, without a suffix
100 * (e.g. "options")
101 * ds = The dataset, as for mergetag.
102 */
103 IWriter makeMTWriter (char[] file, DataSet ds = null)
104 {
105 // FIXME: use highest priority writable path
106 return makeWriter (paths[pathsLen-1] ~ file, ds, WriterMethod.Text);
107 }
108
109 /** Returns a string listing the file name or names (if readOrder is not HIGH_ONLY and multiple
110 * matches are found), or "no file found". Intended for user output only. */
111 char[] getFileName (char[] file, PRIORITY readOrder)
112 {
113 FilePath[] files = getFiles (file, readOrder);
114 if (files is null)
115 return "no file found";
116
117 char[] ret = files[0].toString;
118 foreach (f; files[1..$])
119 ret ~= ", " ~ f.toString;
120 return ret;
121 }
122
123 /// Print all paths found.
124 static void printPaths () {
125 Cout ("Data paths found:");
126 dataDir.coutPaths;
127 Cout ("\nConf paths found:");
128 confDir.coutPaths;
129 Cout ("\nLog file directory:\n\t")(logDir);
130 version (Windows) {
131 Cout ("\nFont directory:\n\t")(fontDir).newline;
132 } else version (linux) {
133 Cout ("\nFont files found:");
134 foreach (f,p; fontFiles)
135 Cout ("\n\t")(f)("\t")(p[0..$-1]);
136 Cout.newline;
137 }
138 }
139
140 private:
141 FilePath[] getFiles (char[] filename, PRIORITY readOrder)
142 {
143 FilePath[] ret;
144
145 if (readOrder == PRIORITY.LOW_HIGH) {
146 for (size_t i = 0; i < pathsLen; ++i) {
147 FilePath file = findFile (paths[i]~filename);
148 if (file !is null)
149 ret ~= file;
150 }
151 } else {
152 assert (readOrder == PRIORITY.HIGH_LOW ||
153 readOrder == PRIORITY.HIGH_ONLY );
154
155 for (int i = pathsLen - 1; i >= 0; --i) {
156 FilePath file = findFile (paths[i]~filename);
157 if (file !is null)
158 ret ~= file;
159 if (readOrder == PRIORITY.HIGH_ONLY) break;
160 }
161 }
162 return ret;
163 }
164
165 // Unconditionally add a path
166 void addPath (char[] path) {
167 assert (pathsLen < MAX_PATHS);
168 paths[pathsLen++] = path~'/';
169 }
170
171 // Test a path and add if is a folder.
172 bool tryPath (char[] path, bool create = false) {
173 assert (pathsLen < MAX_PATHS);
174 FilePath fp = FilePath (path);
175 if (fp.exists && fp.isFolder) {
176 paths[pathsLen++] = path~'/';
177 return true;
178 } else if (create) {
179 try {
180 fp.create;
181 paths[pathsLen++] = fp.toString~'/';
182 return true;
183 } catch (Exception e) {
184 // No logging avaiable yet: Use Stdout/Cout
185 Cout ("Creating path "~path~" failed:" ~ e.msg).newline;
186 }
187 }
188 return false;
189 }
190
191 void coutPaths () {
192 if (pathsLen) {
193 for (size_t i = 0; i < pathsLen; ++i)
194 Cout ("\n\t" ~ paths[i]);
195 } else
196 Cout ("[none]");
197 }
198
199 // Use a static array to store all possible paths with separate length counters.
200 // Lowest priority paths are first.
201 char[][MAX_PATHS] paths;
202 ubyte pathsLen = 0;
203 }
204
205 /** These are the actual instances, one for each of the data and conf "directories". */
206 mdeDirectory dataDir, confDir;
207 char[] logDir; /// Directory for log files
208
209 //BEGIN Path resolution
210 /** Find at least one path for each required directory. */
211 debug (mdeUnitTest) {
212 /** In unittest mode, add paths unittest/data and unittest/conf before init runs. */
213 static this () {
214 dataDir.tryPath ("unittest/data");
215 confDir.tryPath ("unittest/conf");
216 if (!(dataDir.pathsLen && confDir.pathsLen))
217 throw new mdeException ("Fatal: unittest/data and unittest/conf don't both exist");
218 // Don't bother with real paths or logDir or font dir(s) for unittest.
219 }
220 }
221 version (linux) {
222 SortedMap!(char[],char[]) fontFiles; // key is file name, value is CString path
223 /** Get the actual path of a font file, or throw NoFileException if not found.
224 *
225 * Returns a C string (null terminated). */
226 char[] getFontPath (char[] file) {
227 char[] ret;
228 if (fontFiles.get (file, ret))
229 return ret;
230 throw new NoFileException ("Unable to find font file: "~file);
231 }
232
233 // base-path not used on posix
234 void resolvePaths (char[] base = "data") {
235 // Home directory:
236 char[] HOME = Environment.get("HOME", ".");
237
238 // Base paths:
239 // Static data (must exist):
240 FilePath staticPath =
241 findPath (false, "/usr/share/games/mde", "/usr/local/share/games/mde", base);
242 // Config (can just use defaults if necessary, so long as we can save afterwards):
243 FilePath userPath = findPath (true, HOME~"/.config/mde", HOME~"/.mde");
244
245 // Static data paths:
246 dataDir.addPath (staticPath.toString); // we know this is valid anyway
247 dataDir.tryPath (userPath.toString ~ "/data");
248 if (extraDataPath) dataDir.tryPath (extraDataPath);
249 if (!dataDir.pathsLen) throw new mdeException ("Fatal: no data path found!");
250
251 // Configuration paths:
252 confDir.tryPath (staticPath.toString ~ "/conf");
253 confDir.tryPath ("/etc/mde");
254 confDir.tryPath (userPath.toString ~ "/conf", true);
255 if (extraConfPath) confDir.tryPath (extraConfPath);
256 if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!");
257
258 // Logging path:
259 logDir = userPath.toString;
260
261 // Font paths:
262 auto fs = new FileScan;
263 // Scan for directories containing truetype and type1 fonts:
264 fs.sweep ("/usr/share/fonts", delegate bool(FilePath fp, bool isDir)
265 { return isDir || fp.suffix == ".ttf" || fp.suffix == ".pfb"; },
266 true);
267 fontFiles = new SortedMap!(char[],char[]);
268 foreach (fp; fs.files)
269 fontFiles.add (fp.file, fp.cString); // both strings should be slices of same memory
270 }
271 } else version (Windows) {
272 char[] fontDir;
273 /** Get the actual path of a font file, or throw NoFileException if not found.
274 *
275 * Returns a C string (null terminated). */
276 char[] getFontPath (char[] file) {
277 FilePath path = new FilePath (fontDir~file);
278 if (path.exists && !path.isFolder)
279 return path.cString;
280 throw new NoFileException ("Unable to find font file: "~file);
281 }
282
283 void resolvePaths (char[] base = "./") {
284 //FIXME: Get base path from registry
285
286 // Base paths:
287 FilePath installPath = findPath (false, base);
288 FilePath staticPath = findPath (false, installPath.toString);
289 FilePath userPath = findPath (true, getSpecialPath(CSIDL_LOCAL_APPDATA) ~ "/mde");
290
291 // Static data paths:
292 dataDir.addPath (staticPath.toString); // we know this is valid anyway
293 dataDir.tryPath (userPath.toString ~ "/data");
294 if (extraDataPath) dataDir.tryPath (extraDataPath);
295 if (!dataDir.pathsLen) throw new mdeException ("Fatal: no data path found!");
296
297 // Configuration paths:
298 confDir.tryPath (staticPath.toString ~ "/conf");
299 confDir.tryPath (installPath.append("user").toString);
300 confDir.tryPath (userPath.toString ~ "/conf", true);
301 if (extraConfPath) confDir.tryPath (extraConfPath);
302 if (!confDir.pathsLen) throw new mdeException ("Fatal: no conf path found!");
303
304 // Logging path:
305 logDir = userPath.toString;
306
307 // Font path:
308 fontDir = getSpecialPath (CSIDL_FONTS) ~ "/"; // append separator
309 }
310 } else {
311 static assert (false, "Platform is not linux or Windows: no support for paths on this platform yet!");
312 }
313
314 /// For command line args: these paths are added if non-null, with highest priority.
315 char[] extraDataPath, extraConfPath;
316
317 private {
318 class PathException : mdeException {
319 this(char[] msg) {
320 super (msg);
321 }
322 }
323
324 // The maximum number of paths for any one "directory".
325 const MAX_PATHS = 4;
326 static assert (MTMultiReader.MAX_READERS == MAX_PATHS, "MAX_PATHS not all equal");
327
328 /* Try each path in succession, returning the first to exist and be a folder.
329 * If none are valid and create is true, will try creating each in turn.
330 * If still none are valid, throws. */
331 FilePath findPath (bool create, char[][] paths ...) {
332 FilePath[] fps;
333 fps.length = paths.length;
334 foreach (i,path; paths) {
335 FilePath pv = new FilePath (path);
336 if (pv.exists && pv.isFolder) return pv; // got a valid path
337 fps[i] = pv;
338 }
339 if (create) { // try to create a folder, using each path in turn until succesful
340 foreach (fp; fps) {
341 try {
342 return fp.create;
343 }
344 catch (Exception e) {}
345 }
346 }
347 // no valid path...
348 char[] msg = "Unable to find"~(create ? " or create" : "")~" a required path! The following were tried:";
349 foreach (path; paths) msg ~= " \"" ~ path ~ '\"';
350 throw new PathException (msg);
351 }
352 }
353 //END Path resolution
354
355 /// Thrown when makeMTReader couldn't find a file.
356 class NoFileException : MTFileIOException {
357 this (char[] msg) {
358 super(msg);
359 }
360 }