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