1 /*
2 * The Doomsday Engine Project -- libcore
3 *
4 * Copyright © 2009-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
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/Folder"
21
22 #include "de/App"
23 #include "de/Async"
24 #include "de/DirectoryFeed"
25 #include "de/FS"
26 #include "de/Feed"
27 #include "de/Guard"
28 #include "de/LogBuffer"
29 #include "de/NumberValue"
30 #include "de/ScriptedInfo"
31 #include "de/ScriptSystem"
32 #include "de/Task"
33 #include "de/TaskPool"
34 #include "de/UnixInfo"
35
36 namespace de {
37
38 FolderPopulationAudience audienceForFolderPopulation; // public
39
40 namespace internal {
41
42 static TaskPool populateTasks;
43 static bool enableBackgroundPopulation = true;
44
45 /// Forwards internal folder population notifications to the public audience.
46 struct PopulationNotifier : DENG2_OBSERVES(TaskPool, Done)
47 {
PopulationNotifierde::internal::PopulationNotifier48 PopulationNotifier() { populateTasks.audienceForDone() += this; }
taskPoolDonede::internal::PopulationNotifier49 void taskPoolDone(TaskPool &) { notify(); }
notifyde::internal::PopulationNotifier50 void notify() { DENG2_FOR_AUDIENCE(FolderPopulation, i) i->folderPopulationFinished(); }
51 };
52
53 static PopulationNotifier populationNotifier;
54
55 } // namespace internal
56
DENG2_PIMPL(Folder)57 DENG2_PIMPL(Folder)
58 {
59 /// A map of file names to file instances.
60 Contents contents;
61
62 /// Feeds provide content for the folder.
63 Feeds feeds;
64
65 Impl(Public *i) : Base(i) {}
66
67 void add(File *file)
68 {
69 contents.insert(file->name().toLower(), file);
70 file->setParent(thisPublic);
71 }
72
73 void destroy(String path, File *file)
74 {
75 Feed *originFeed = file->originFeed();
76
77 // This'll close it and remove it from the index.
78 delete file;
79
80 // The origin feed will remove the original data of the file (e.g., the native file).
81 if (originFeed)
82 {
83 originFeed->destroyFile(path);
84 }
85 }
86
87 QList<Folder *> subfolders() const
88 {
89 DENG2_GUARD_FOR(self(), G);
90 QList<Folder *> subs;
91 for (Contents::const_iterator i = contents.begin(); i != contents.end(); ++i)
92 {
93 if (Folder *folder = maybeAs<Folder>(i.value()))
94 {
95 subs << folder;
96 }
97 }
98 return subs;
99 }
100
101 static void destroyRecursive(Folder &folder)
102 {
103 foreach (Folder *sub, folder.subfolders())
104 {
105 destroyRecursive(*sub);
106 }
107 folder.destroyAllFiles();
108 }
109 };
110
Folder(String const & name)111 Folder::Folder(String const &name) : File(name), d(new Impl(this))
112 {
113 setStatus(Type::Folder);
114 objectNamespace().addSuperRecord(ScriptSystem::builtInClass(QStringLiteral("Folder")));
115 }
116
~Folder()117 Folder::~Folder()
118 {
119 DENG2_GUARD(this);
120
121 DENG2_FOR_AUDIENCE2(Deletion, i) i->fileBeingDeleted(*this);
122 audienceForDeletion().clear();
123
124 deindex();
125
126 // Empty the contents.
127 clear();
128
129 // Destroy all feeds that remain.
130 while (!d->feeds.isEmpty())
131 {
132 delete d->feeds.takeLast();
133 }
134 }
135
describe() const136 String Folder::describe() const
137 {
138 // As a special case, plain native directories should be described as such.
139 if (auto const *direcFeed = primaryFeedMaybeAs<DirectoryFeed>())
140 {
141 return String("directory \"%1\"").arg(direcFeed->nativePath().pretty());
142 }
143
144 String desc;
145 if (name().isEmpty())
146 {
147 desc = "root folder";
148 }
149 else
150 {
151 desc = String("folder \"%1\"").arg(name());
152 }
153
154 String const feedDesc = describeFeeds();
155 if (!feedDesc.isEmpty())
156 {
157 desc += String(" (%1)").arg(feedDesc);
158 }
159
160 return desc;
161 }
162
describeFeeds() const163 String Folder::describeFeeds() const
164 {
165 DENG2_GUARD(this);
166
167 String desc;
168
169 if (d->feeds.size() == 1)
170 {
171 desc += String("contains %1 file%2 from %3")
172 .arg(d->contents.size())
173 .arg(DENG2_PLURAL_S(d->contents.size()))
174 .arg(d->feeds.front()->description());
175 }
176 else if (d->feeds.size() > 1)
177 {
178 desc += String("contains %1 file%2 from %3 feed%4")
179 .arg(d->contents.size())
180 .arg(DENG2_PLURAL_S(d->contents.size()))
181 .arg(d->feeds.size())
182 .arg(DENG2_PLURAL_S(d->feeds.size()));
183
184 int n = 0;
185 DENG2_FOR_EACH_CONST(Feeds, i, d->feeds)
186 {
187 desc += String("; feed #%2 is %3")
188 .arg(n + 1)
189 .arg((*i)->description());
190 ++n;
191 }
192 }
193
194 return desc;
195 }
196
clear()197 void Folder::clear()
198 {
199 DENG2_GUARD(this);
200
201 if (d->contents.empty()) return;
202
203 // Destroy all the file objects.
204 for (Contents::iterator i = d->contents.begin(); i != d->contents.end(); ++i)
205 {
206 i.value()->setParent(0);
207 delete i.value();
208 }
209 d->contents.clear();
210 }
211
populate(PopulationBehaviors behavior)212 void Folder::populate(PopulationBehaviors behavior)
213 {
214 fileSystem().changeBusyLevel(+1);
215
216 LOG_AS("Folder");
217 {
218 DENG2_GUARD(this);
219
220 // Prune the existing files first.
221 QMutableMapIterator<String, File *> iter(d->contents);
222 while (iter.hasNext())
223 {
224 iter.next();
225
226 // By default we will NOT prune if there are no feeds attached to the folder.
227 // In this case the files were probably created manually, so we shouldn't
228 // touch them.
229 bool mustPrune = false;
230
231 File *file = iter.value();
232 if (file->mode() & DontPrune)
233 {
234 // Skip this one, it should be kept as-is until manually deleted.
235 continue;
236 }
237 Feed *originFeed = file->originFeed();
238
239 // If the file has a designated feed, ask it about pruning.
240 if (originFeed && originFeed->prune(*file))
241 {
242 LOG_RES_XVERBOSE("Pruning \"%s\" due to origin feed %s", file->path() << originFeed->description());
243 mustPrune = true;
244 }
245 else if (!originFeed)
246 {
247 // There is no designated feed, ask all feeds of this folder.
248 // If even one of the feeds thinks that the file is out of date,
249 // it will be pruned.
250 for (Feeds::iterator f = d->feeds.begin(); f != d->feeds.end(); ++f)
251 {
252 if ((*f)->prune(*file))
253 {
254 LOG_RES_XVERBOSE("Pruning %s due to non-origin feed %s", file->path() << (*f)->description());
255 mustPrune = true;
256 break;
257 }
258 }
259 }
260
261 if (mustPrune)
262 {
263 // It needs to go.
264 file->setParent(nullptr);
265 iter.remove();
266 delete file;
267 }
268 }
269 }
270
271 auto populationTask = [this, behavior]() {
272 Feed::PopulatedFiles newFiles;
273
274 // Populate with new/updated ones.
275 for (int i = d->feeds.size() - 1; i >= 0; --i)
276 {
277 newFiles.append(d->feeds.at(i)->populate(*this));
278 }
279
280 // Insert and index all new files atomically.
281 {
282 DENG2_GUARD(this);
283 for (File *i : newFiles)
284 {
285 if (i)
286 {
287 std::unique_ptr<File> file(i);
288 if (!d->contents.contains(i->name().toLower()))
289 {
290 d->add(file.release());
291 fileSystem().index(*i);
292 }
293 }
294 }
295 newFiles.clear();
296 }
297
298 if (behavior & PopulateFullTree)
299 {
300 // Call populate on subfolders.
301 for (Folder *folder : d->subfolders())
302 {
303 folder->populate(behavior | PopulateCalledRecursively);
304 }
305 }
306
307 fileSystem().changeBusyLevel(-1);
308 };
309
310 if (internal::enableBackgroundPopulation)
311 {
312 if (behavior & PopulateAsync)
313 {
314 internal::populateTasks.start(populationTask, TaskPool::MediumPriority);
315 }
316 else
317 {
318 populationTask();
319 }
320 }
321 else
322 {
323 // Only synchronous population is enabled.
324 populationTask();
325
326 // Each population gets an individual notification since they're done synchronously.
327 // However, only notify once a full hierarchy of populations has finished.
328 if (!(behavior & PopulateCalledRecursively))
329 {
330 internal::populationNotifier.notify();
331 }
332 }
333 }
334
contents() const335 Folder::Contents Folder::contents() const
336 {
337 DENG2_GUARD(this);
338 return d->contents;
339 }
340
forContents(std::function<LoopResult (String,File &)> func) const341 LoopResult Folder::forContents(std::function<LoopResult (String, File &)> func) const
342 {
343 DENG2_GUARD(this);
344
345 for (Contents::const_iterator i = d->contents.constBegin(); i != d->contents.constEnd(); ++i)
346 {
347 if (auto result = func(i.key(), *i.value()))
348 {
349 return result;
350 }
351 }
352 return LoopContinue;
353 }
354
subfolders() const355 QList<Folder *> Folder::subfolders() const
356 {
357 return d->subfolders();
358 }
359
createFile(String const & newPath,FileCreationBehavior behavior)360 File &Folder::createFile(String const &newPath, FileCreationBehavior behavior)
361 {
362 String path = newPath.fileNamePath();
363 if (!path.empty())
364 {
365 // Locate the folder where the file will be created in.
366 return locate<Folder>(path).createFile(newPath.fileName(), behavior);
367 }
368
369 verifyWriteAccess();
370
371 if (behavior == ReplaceExisting && has(newPath))
372 {
373 try
374 {
375 destroyFile(newPath);
376 }
377 catch (Feed::RemoveError const &er)
378 {
379 LOG_RES_WARNING("Failed to replace %s: existing file could not be removed.\n")
380 << newPath << er.asText();
381 }
382 }
383
384 // The first feed able to create a file will get the honors.
385 for (Feeds::iterator i = d->feeds.begin(); i != d->feeds.end(); ++i)
386 {
387 File *file = (*i)->createFile(newPath);
388 if (file)
389 {
390 // Allow writing to the new file. Don't prune the file since we will be
391 // actively modifying it ourselves (otherwise it would get pruned after
392 // a flush modifies the underlying data).
393 file->setMode(Write | DontPrune);
394
395 add(file);
396 fileSystem().index(*file);
397 return *file;
398 }
399 }
400
401 /// @throw NewFileError All feeds of this folder failed to create a file.
402 throw NewFileError("Folder::createFile", "Unable to create new file '" + newPath +
403 "' in " + description());
404 }
405
replaceFile(String const & newPath)406 File &Folder::replaceFile(String const &newPath)
407 {
408 return createFile(newPath, ReplaceExisting);
409 }
410
destroyFile(String const & removePath)411 void Folder::destroyFile(String const &removePath)
412 {
413 DENG2_GUARD(this);
414
415 String path = removePath.fileNamePath();
416 if (!path.empty())
417 {
418 // Locate the folder where the file will be removed.
419 return locate<Folder>(path).destroyFile(removePath.fileName());
420 }
421
422 verifyWriteAccess();
423
424 d->destroy(removePath, &locate<File>(removePath));
425 }
426
tryDestroyFile(String const & removePath)427 bool Folder::tryDestroyFile(String const &removePath)
428 {
429 try
430 {
431 if (has(removePath))
432 {
433 destroyFile(removePath);
434 return true;
435 }
436 }
437 catch (Error const &)
438 {
439 // This shouldn't happen.
440 }
441 return false;
442 }
443
destroyAllFiles()444 void Folder::destroyAllFiles()
445 {
446 DENG2_GUARD(this);
447
448 verifyWriteAccess();
449
450 foreach (File *file, d->contents)
451 {
452 file->setParent(nullptr);
453 d->destroy(file->name(), file);
454 }
455 d->contents.clear();
456 }
457
destroyAllFilesRecursively()458 void Folder::destroyAllFilesRecursively()
459 {
460 Impl::destroyRecursive(*this);
461 }
462
has(String const & name) const463 bool Folder::has(String const &name) const
464 {
465 if (name.isEmpty()) return false;
466
467 // Check if we were given a path rather than just a name.
468 String path = name.fileNamePath();
469 if (!path.empty())
470 {
471 Folder *folder = tryLocate<Folder>(path);
472 if (folder)
473 {
474 return folder->has(name.fileName());
475 }
476 return false;
477 }
478
479 DENG2_GUARD(this);
480 return (d->contents.find(name.lower()) != d->contents.end());
481 }
482
add(File * file)483 File &Folder::add(File *file)
484 {
485 DENG2_ASSERT(file != 0);
486
487 if (has(file->name()))
488 {
489 /// @throw DuplicateNameError All file names in a folder must be unique.
490 throw DuplicateNameError("Folder::add", "Folder cannot contain two files with the same name: '" +
491 file->name() + "'");
492 }
493 DENG2_GUARD(this);
494 d->add(file);
495 return *file;
496 }
497
remove(String name)498 File *Folder::remove(String name)
499 {
500 DENG2_GUARD(this);
501
502 String const key = name.toLower();
503 DENG2_ASSERT(d->contents.contains(key));
504
505 File *removed = d->contents.take(key);
506 removed->setParent(nullptr);
507 return removed;
508 }
509
remove(char const * nameUtf8)510 File *Folder::remove(char const *nameUtf8)
511 {
512 return remove(String(nameUtf8));
513 }
514
remove(File & file)515 File *Folder::remove(File &file)
516 {
517 return remove(file.name());
518 }
519
tryGetChild(String const & name) const520 filesys::Node const *Folder::tryGetChild(String const &name) const
521 {
522 DENG2_GUARD(this);
523
524 Contents::const_iterator found = d->contents.find(name.toLower());
525 if (found != d->contents.end())
526 {
527 return found.value();
528 }
529 return nullptr;
530 }
531
root()532 Folder &Folder::root()
533 {
534 return FS::get().root();
535 }
536
waitForPopulation(WaitBehavior waitBehavior)537 void Folder::waitForPopulation(WaitBehavior waitBehavior)
538 {
539 if (waitBehavior == OnlyInBackground && App::inMainThread())
540 {
541 DENG2_ASSERT(!App::inMainThread());
542 throw Error("Folder::waitForPopulation", "Not allowed to block the main thread");
543 }
544 Time startedAt;
545 {
546 internal::populateTasks.waitForDone();
547 }
548 const auto elapsed = startedAt.since();
549 if (elapsed > .01)
550 {
551 LOG_MSG("Waited for %.3f seconds for file system to be ready") << elapsed;
552 }
553 }
554
afterPopulation(std::function<void ()> func)555 AsyncTask *Folder::afterPopulation(std::function<void ()> func)
556 {
557 if (!isPopulatingAsync())
558 {
559 func();
560 return nullptr;
561 }
562
563 return async([] ()
564 {
565 waitForPopulation();
566 return 0;
567 },
568 [func] (int)
569 {
570 func();
571 });
572 }
573
isPopulatingAsync()574 bool Folder::isPopulatingAsync()
575 {
576 return !internal::populateTasks.isDone();
577 }
578
checkDefaultSettings()579 void Folder::checkDefaultSettings()
580 {
581 String mtEnabled;
582 if (App::app().unixInfo().defaults("fs:multithreaded", mtEnabled))
583 {
584 internal::enableBackgroundPopulation = !ScriptedInfo::isFalse(mtEnabled);
585 }
586 }
587
tryFollowPath(PathRef const & path) const588 filesys::Node const *Folder::tryFollowPath(PathRef const &path) const
589 {
590 // Absolute paths refer to the file system root.
591 if (path.isAbsolute())
592 {
593 return fileSystem().root().tryFollowPath(path.subPath(Rangei(1, path.segmentCount())));
594 }
595
596 return Node::tryFollowPath(path);
597 }
598
tryLocateFile(String const & path) const599 File *Folder::tryLocateFile(String const &path) const
600 {
601 if (filesys::Node const *node = tryFollowPath(Path(path)))
602 {
603 return const_cast<File *>(maybeAs<File>(node));
604 }
605 return nullptr;
606 }
607
attach(Feed * feed)608 void Folder::attach(Feed *feed)
609 {
610 if (feed)
611 {
612 DENG2_GUARD(this);
613 d->feeds.push_back(feed);
614 }
615 }
616
detach(Feed & feed)617 Feed *Folder::detach(Feed &feed)
618 {
619 DENG2_GUARD(this);
620
621 d->feeds.removeOne(&feed);
622 return &feed;
623 }
624
setPrimaryFeed(Feed & feed)625 void Folder::setPrimaryFeed(Feed &feed)
626 {
627 DENG2_GUARD(this);
628
629 d->feeds.removeOne(&feed);
630 d->feeds.push_front(&feed);
631 }
632
primaryFeed() const633 Feed *Folder::primaryFeed() const
634 {
635 DENG2_GUARD(this);
636
637 if (d->feeds.isEmpty()) return nullptr;
638 return d->feeds.front();
639 }
640
clearFeeds()641 void Folder::clearFeeds()
642 {
643 DENG2_GUARD(this);
644
645 while (!d->feeds.empty())
646 {
647 delete detach(*d->feeds.front());
648 }
649 }
650
feeds() const651 Folder::Feeds Folder::feeds() const
652 {
653 DENG2_GUARD(this);
654
655 return d->feeds;
656 }
657
contentsAsText() const658 String Folder::contentsAsText() const
659 {
660 QList<File const *> files;
661 forContents([&files] (String, File &f)
662 {
663 files << &f;
664 return LoopContinue;
665 });
666 return File::fileListAsText(files);
667 }
668
669 } // namespace de
670