1 /** @file directoryfeed.cpp Directory Feed.
2 *
3 * @author Copyright © 2009-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4 * @author Copyright © 2013 Daniel Swanson <danij@dengine.net>
5 *
6 * @par License
7 * LGPL: http://www.gnu.org/licenses/lgpl.html
8 *
9 * <small>This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU Lesser General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or (at your
12 * option) any later version. This program is distributed in the hope that it
13 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
15 * General Public License for more details. You should have received a copy of
16 * the GNU Lesser General Public License along with this program; if not, see:
17 * http://www.gnu.org/licenses</small>
18 */
19
20 #include "de/DirectoryFeed"
21 #include "de/Folder"
22 #include "de/NativeFile"
23 #include "de/FS"
24 #include "de/Date"
25 #include "de/App"
26
27 #include <QDir>
28 #include <QFileInfo>
29
30 namespace de {
31
32 static String const fileStatusSuffix = ".doomsday_file_status";
33
DENG2_PIMPL_NOREF(DirectoryFeed)34 DENG2_PIMPL_NOREF(DirectoryFeed)
35 {
36 NativePath nativePath;
37 Flags mode;
38 String namePattern;
39 };
40
DirectoryFeed(NativePath const & nativePath,Flags const & mode)41 DirectoryFeed::DirectoryFeed(NativePath const &nativePath, Flags const &mode)
42 : d(new Impl)
43 {
44 d->nativePath = nativePath;
45 d->mode = mode;
46 }
47
setNamePattern(const String & namePattern)48 void DirectoryFeed::setNamePattern(const String &namePattern)
49 {
50 d->namePattern = namePattern;
51 }
52
description() const53 String DirectoryFeed::description() const
54 {
55 String desc;
56 if (d->namePattern)
57 {
58 desc = "files matching \"" + d->namePattern + "\" in ";
59 }
60 desc += "directory \"" + d->nativePath.pretty() + "\"";
61 return desc;
62 }
63
nativePath() const64 const NativePath &DirectoryFeed::nativePath() const
65 {
66 return d->nativePath;
67 }
68
populate(Folder const & folder)69 Feed::PopulatedFiles DirectoryFeed::populate(Folder const &folder)
70 {
71 if (d->mode & AllowWrite)
72 {
73 // Automatically enable modifying the Folder.
74 const_cast<Folder &>(folder).setMode(File::Write);
75 }
76 if (d->mode.testFlag(CreateIfMissing) && !NativePath::exists(d->nativePath))
77 {
78 NativePath::createPath(d->nativePath);
79 }
80
81 QDir dir(d->nativePath);
82 if (!dir.isReadable())
83 {
84 /// @throw NotFoundError The native directory was not accessible.
85 throw NotFoundError("DirectoryFeed::populate", "Path '" + d->nativePath + "' inaccessible");
86 }
87 QStringList nameFilters;
88 if (d->namePattern)
89 {
90 nameFilters << d->namePattern;
91 }
92 else
93 {
94 nameFilters << "*";
95 }
96 QDir::Filters dirFlags = QDir::Files | QDir::NoDotAndDotDot;
97 if (d->mode.testFlag(PopulateNativeSubfolders))
98 {
99 dirFlags |= QDir::Dirs;
100 }
101 PopulatedFiles populated;
102 foreach (QFileInfo entry, dir.entryInfoList(nameFilters, dirFlags))
103 {
104 if (entry.isDir())
105 {
106 populateSubFolder(folder, entry.fileName());
107 }
108 else
109 {
110 if (!entry.fileName().endsWith(fileStatusSuffix)) // ignore meta files
111 {
112 populateFile(folder, entry.fileName(), populated);
113 }
114 }
115 }
116 return populated;
117 }
118
populateSubFolder(Folder const & folder,String const & entryName)119 void DirectoryFeed::populateSubFolder(Folder const &folder, String const &entryName)
120 {
121 LOG_AS("DirectoryFeed::populateSubFolder");
122
123 if (entryName != "." && entryName != "..")
124 {
125 Folder *subFolder = nullptr;
126 if (!folder.has(entryName))
127 {
128 subFolder = &folder.fileSystem()
129 .makeFolderWithFeed(folder.path() / entryName,
130 newSubFeed(entryName),
131 Folder::PopulateFullTree,
132 FS::DontInheritFeeds);
133 }
134 else
135 {
136 // Use the previously populated subfolder.
137 subFolder = &folder.locate<Folder>(entryName);
138 }
139
140 if (d->mode & AllowWrite)
141 {
142 subFolder->setMode(File::Write);
143 }
144 else
145 {
146 subFolder->setMode(File::ReadOnly);
147 }
148 }
149 }
150
populateFile(Folder const & folder,String const & entryName,PopulatedFiles & populated)151 void DirectoryFeed::populateFile(Folder const &folder, String const &entryName,
152 PopulatedFiles &populated)
153 {
154 try
155 {
156 if (folder.has(entryName))
157 {
158 // Already has an entry for this, skip it (wasn't pruned so it's OK).
159 return;
160 }
161
162 NativePath const entryPath = d->nativePath / entryName;
163
164 // Open the native file.
165 std::unique_ptr<NativeFile> nativeFile(new NativeFile(entryName, entryPath));
166 nativeFile->setStatus(fileStatus(entryPath));
167 if (d->mode & AllowWrite)
168 {
169 nativeFile->setMode(File::Write);
170 }
171
172 File *file = folder.fileSystem().interpret(nativeFile.release());
173
174 // We will decide on pruning this.
175 file->setOriginFeed(this);
176
177 populated << file;
178 }
179 catch (StatusError const &er)
180 {
181 LOG_WARNING("Error with \"%s\" in %s: %s")
182 << entryName
183 << folder.description()
184 << er.asText();
185 }
186 }
187
prune(File & file) const188 bool DirectoryFeed::prune(File &file) const
189 {
190 LOG_AS("DirectoryFeed::prune");
191
192 /// Rules for pruning:
193 /// - A file sourced by NativeFile will be pruned if it's out of sync with the hard
194 /// drive version (size, time of last modification).
195 if (NativeFile *nativeFile = maybeAs<NativeFile>(file.source()))
196 {
197 try
198 {
199 if (fileStatus(nativeFile->nativePath()) != nativeFile->status())
200 {
201 // It's not up to date.
202 LOG_RES_MSG("Pruning \"%s\": status has changed") << nativeFile->nativePath();
203 return true;
204 }
205 }
206 catch (StatusError const &)
207 {
208 // Get rid of it.
209 return true;
210 }
211 }
212
213 /// - A Folder will be pruned if the corresponding directory does not exist (providing
214 /// a DirectoryFeed is the sole feed in the folder).
215 if (Folder *subFolder = maybeAs<Folder>(file))
216 {
217 if (subFolder->feeds().size() == 1)
218 {
219 DirectoryFeed *dirFeed = maybeAs<DirectoryFeed>(subFolder->feeds().front());
220 if (dirFeed && !dirFeed->d->nativePath.exists())
221 {
222 LOG_RES_NOTE("Pruning %s: no longer exists") << dirFeed->description(); //d->nativePath;
223 return true;
224 }
225 }
226 }
227
228 /// - Other types of Files will not be pruned.
229 return false;
230 }
231
createFile(String const & name)232 File *DirectoryFeed::createFile(String const &name)
233 {
234 NativePath newPath = d->nativePath / name;
235 /*if (NativePath::exists(newPath))
236 {
237 /// @throw AlreadyExistsError The file @a name already exists in the native directory.
238 //throw AlreadyExistsError("DirectoryFeed::createFile", name + ": already exists");
239 //qDebug() << "[DirectoryFeed] Overwriting" << newPath.toString();
240 }*/
241 File *file = new NativeFile(name, newPath);
242 file->setOriginFeed(this);
243 return file;
244 }
245
destroyFile(String const & name)246 void DirectoryFeed::destroyFile(String const &name)
247 {
248 NativePath path = d->nativePath / name;
249
250 if (!path.exists())
251 {
252 // The file doesn't exist in the native file system, we can ignore this.
253 return;
254 }
255 if (!path.destroy())
256 {
257 /// @throw RemoveError The file @a name exists but could not be removed.
258 throw RemoveError("DirectoryFeed::destroyFile", "Cannot remove \"" + name +
259 "\" in " + description());
260 }
261 }
262
newSubFeed(String const & name)263 Feed *DirectoryFeed::newSubFeed(String const &name)
264 {
265 NativePath subPath = d->nativePath / name;
266 if (d->mode.testFlag(CreateIfMissing) || (subPath.exists() && subPath.isReadable()))
267 {
268 return new DirectoryFeed(subPath, d->mode);
269 }
270 return nullptr;
271 }
272
changeWorkingDir(NativePath const & nativePath)273 void DirectoryFeed::changeWorkingDir(NativePath const &nativePath)
274 {
275 if (!App::setCurrentWorkPath(nativePath))
276 {
277 /// @throw WorkingDirError Changing to @a nativePath failed.
278 throw WorkingDirError("DirectoryFeed::changeWorkingDir",
279 "Failed to change to " + nativePath);
280 }
281 }
282
fileStatus(NativePath const & nativePath)283 File::Status DirectoryFeed::fileStatus(NativePath const &nativePath)
284 {
285 QFileInfo info(nativePath);
286 if (!info.exists())
287 {
288 /// @throw StatusError Determining the file status was not possible.
289 throw StatusError("DirectoryFeed::fileStatus", nativePath + " inaccessible");
290 }
291
292 // Get file status information.
293 File::Status st { info.isDir()? File::Type::Folder : File::Type::File,
294 dsize(info.size()),
295 info.lastModified() };
296
297 // Check for overridden status.
298 String const overrideName = nativePath + fileStatusSuffix;
299 if (QFileInfo().exists(overrideName))
300 {
301 QFile f(overrideName);
302 if (f.open(QFile::ReadOnly))
303 {
304 st.modifiedAt = Time::fromText(String::fromUtf8(f.readAll()), Time::ISOFormat);
305 }
306 }
307 return st;
308 }
309
setFileModifiedTime(NativePath const & nativePath,Time const & modifiedAt)310 void DirectoryFeed::setFileModifiedTime(NativePath const &nativePath, Time const &modifiedAt)
311 {
312 String const overrideName = nativePath + fileStatusSuffix;
313 if (!modifiedAt.isValid())
314 {
315 QFile::remove(overrideName);
316 return;
317 }
318 QFile f(overrideName);
319 if (f.open(QFile::WriteOnly | QFile::Truncate))
320 {
321 f.write(modifiedAt.asText(Time::ISOFormat).toUtf8());
322 }
323 }
324
manuallyPopulateSingleFile(NativePath const & nativePath,Folder & parentFolder)325 File &DirectoryFeed::manuallyPopulateSingleFile(NativePath const &nativePath,
326 Folder &parentFolder) // static
327 {
328 const bool isExisting = nativePath.exists();
329 Folder * parent = &parentFolder;
330
331 File::Status status;
332 if (isExisting)
333 {
334 status = fileStatus(nativePath);
335 }
336 else
337 {
338 status.modifiedAt = Time(); // file being created now
339 }
340
341 // If we're populating a .pack, the possible container .packs must be included as
342 // parent folders (in structure only, not all their contents). Otherwise the .pack
343 // identifier would not be the same.
344
345 if (parentFolder.extension() != ".pack" &&
346 nativePath.fileName().fileNameExtension() == ".pack")
347 {
348 // Extract the portion of the path containing the parent .packs.
349 int const last = nativePath.segmentCount() - 1;
350 Rangei packRange(last, last);
351 while (packRange.start > 0 &&
352 nativePath.segment(packRange.start - 1).toStringRef()
353 .endsWith(".pack", Qt::CaseInsensitive))
354 {
355 packRange.start--;
356 }
357 if (!packRange.isEmpty())
358 {
359 parent = &FS::get().makeFolder(parentFolder.path() /
360 nativePath.subPath(packRange).withSeparators('/'),
361 FS::DontInheritFeeds);
362 }
363 }
364
365 const String newFilePath = parent->path() / nativePath.fileName();
366
367 if (status.type() == File::Type::File)
368 {
369 parent->clear();
370 parent->clearFeeds();
371
372 auto *feed = new DirectoryFeed(nativePath.fileNamePath());
373 feed->setNamePattern(nativePath.fileName());
374 parent->attach(feed);
375 if (isExisting)
376 {
377 parent->populate();
378 }
379 else
380 {
381 parent->replaceFile(nativePath.fileName());
382 }
383 return FS::locate<File>(newFilePath);
384 }
385 else
386 {
387 return FS::get().makeFolderWithFeed(newFilePath,
388 new DirectoryFeed(nativePath),
389 Folder::PopulateFullTree,
390 FS::DontInheritFeeds | FS::PopulateNewFolder);
391 }
392 }
393
394 } // namespace de
395