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