1 /** @file archivefeed.cpp Archive 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/ArchiveFeed"
21 #include "de/ArchiveEntryFile"
22 #include "de/ArchiveFolder"
23 #include "de/ByteArrayFile"
24 #include "de/ZipArchive"
25 #include "de/Writer"
26 #include "de/Folder"
27 #include "de/FS"
28 #include "de/LogBuffer"
29 
30 namespace de {
31 
DENG2_PIMPL(ArchiveFeed)32 DENG2_PIMPL(ArchiveFeed)
33 , DENG2_OBSERVES(File, Deletion)
34 {
35     /// File where the archive is stored (in a serialized format).
36     File *file;
37 
38     /// The archive can be physically stored here, as Archive doesn't make a
39     /// copy of the buffer.
40     Block serializedArchive;
41 
42     Archive *arch;
43 
44     /// Mount point within the archive for this feed.
45     String basePath;
46 
47     /// The feed whose archive this feed is using.
48     ArchiveFeed *parentFeed;
49 
50     bool allowWrite;
51 
52     /// All the entries of this archive that are populated as files.
53     /// Subfeeds use the parent's entry table.
54     typedef LockableT<PointerSetT<ArchiveEntryFile>> EntryTable;
55     EntryTable entries;
56 
57     Impl(Public *feed, File &f)
58         : Base(feed)
59         , file(&f)
60         , arch(0)
61         , parentFeed(0)
62         , allowWrite(f.mode().testFlag(File::Write)) // write access depends on file
63     {
64         // If the file happens to be a byte array file, we can use it
65         // directly to store the Archive.
66         if (auto *bytes = maybeAs<IByteArray>(f))
67         {
68             //qDebug() << "Loading" << f.description() << f.metaId().asHexadecimalText();
69 
70             LOG_RES_XVERBOSE("Source %s is a byte array", f.description());
71 
72             arch = new ZipArchive(*bytes, f.metaId());
73         }
74         else
75         {
76             LOG_RES_XVERBOSE("Source %s is a stream", f.description());
77 
78             // The file is just a stream, so we can't rely on the file
79             // acting as the physical storage location for Archive.
80             f >> serializedArchive;
81             arch = new ZipArchive(serializedArchive);
82         }
83 
84         file->audienceForDeletion() += this;
85     }
86 
87     Impl(Public *feed, ArchiveFeed &parentFeed, String const &path)
88         : Base(feed)
89         , file(parentFeed.d->file)
90         , arch(0)
91         , basePath(path)
92         , parentFeed(&parentFeed)
93         , allowWrite(isWriteAllowed())
94     {
95         file->audienceForDeletion() += this;
96     }
97 
98     ~Impl()
99     {
100         if (arch)
101         {
102             writeIfModified();
103             delete arch;
104         }
105     }
106 
107     bool isWriteAllowed() const
108     {
109         if (parentFeed)
110         {
111             return parentFeed->d->isWriteAllowed();
112         }
113         return allowWrite;
114     }
115 
116     void writeIfModified()
117     {
118         if (!file || !arch || !file->mode().testFlag(File::Write))
119         {
120             return;
121         }
122 
123         // If modified, the archive is written back to the file.
124         if (arch->modified())
125         {
126             LOG_RES_VERBOSE("Updating archive in ") << file->description();
127 
128             // Make sure we have either a compressed or uncompressed version of
129             // each entry in memory before destroying the source file.
130             arch->cache();
131 
132             file->clear();
133             Writer(*file) << *arch;
134             file->flush();
135         }
136         else
137         {
138             LOG_RES_VERBOSE("Not updating archive in %s (not changed)") << file->description();
139         }
140     }
141 
142     void fileBeingDeleted(File const &deleted)
143     {
144         if (file == &deleted)
145         {
146             // Ensure that changes are saved and detach from the file.
147             writeIfModified();
148             file = 0;
149         }
150         else
151         {
152             auto &table = entryTable();
153             DENG2_GUARD(table);
154             table.value.remove(&deleted.as<ArchiveEntryFile>());
155         }
156     }
157 
158     Archive &archive()
159     {
160         if (parentFeed)
161         {
162             return parentFeed->archive();
163         }
164         return *arch;
165     }
166 
167     EntryTable &entryTable()
168     {
169         return parentFeed? parentFeed->d->entries : entries;
170     }
171 
172     void addToEntryTable(ArchiveEntryFile *file)
173     {
174         auto &table = entryTable();
175         DENG2_GUARD(table);
176         table.value.insert(file);
177         file->audienceForDeletion() += this;
178     }
179 
180     PopulatedFiles populate(Folder const &folder)
181     {
182         PopulatedFiles populated;
183 
184         // Get a list of the files in this directory.
185         Archive::Names names;
186         archive().listFiles(names, basePath);
187 
188         DENG2_FOR_EACH(Archive::Names, i, names)
189         {
190             if (folder.has(*i))
191             {
192                 // Already has an entry for this, skip it (wasn't pruned so it's OK).
193                 continue;
194             }
195 
196             String entry = basePath / *i;
197 
198             std::unique_ptr<ArchiveEntryFile> archFile(new ArchiveEntryFile(*i, archive(), entry));
199             addToEntryTable(archFile.get());
200 
201             // Write access is inherited from the main source file.
202             if (allowWrite) archFile->setMode(File::Write);
203 
204             // Use the status of the entry within the archive.
205             archFile->setStatus(archive().entryStatus(entry));
206 
207             // Interpret the entry contents.
208             File *f = folder.fileSystem().interpret(archFile.release());
209 
210             // We will decide on pruning this.
211             f->setOriginFeed(thisPublic);
212 
213             populated << f;
214         }
215 
216         // Also create subfolders by inheriting from this feed. They will get
217         // populated later.
218         archive().listFolders(names, basePath);
219         for (Archive::Names::iterator i = names.begin(); i != names.end(); ++i)
220         {
221             folder.fileSystem().makeFolder(folder.path() / *i, FS::InheritPrimaryFeed);
222         }
223         return populated;
224     }
225 };
226 
ArchiveFeed(File & archiveFile)227 ArchiveFeed::ArchiveFeed(File &archiveFile)
228     : d(new Impl(this, archiveFile))
229 {}
230 
ArchiveFeed(ArchiveFeed & parentFeed,String const & basePath)231 ArchiveFeed::ArchiveFeed(ArchiveFeed &parentFeed, String const &basePath)
232     : d(new Impl(this, parentFeed, basePath))
233 {}
234 
~ArchiveFeed()235 ArchiveFeed::~ArchiveFeed()
236 {
237     LOG_AS("~ArchiveFeed");
238     d.reset();
239 }
240 
description() const241 String ArchiveFeed::description() const
242 {
243     return "archive in " + (d->file? d->file->description() : "(deleted file)");
244 }
245 
populate(Folder const & folder)246 Feed::PopulatedFiles ArchiveFeed::populate(Folder const &folder)
247 {
248     LOG_AS("ArchiveFeed::populate");
249 
250     return d->populate(folder);
251 }
252 
prune(File & file) const253 bool ArchiveFeed::prune(File &file) const
254 {
255     LOG_AS("ArchiveFeed::prune");
256 
257     ArchiveEntryFile *entryFile = maybeAs<ArchiveEntryFile>(file);
258     if (entryFile && &entryFile->archive() == &archive())
259     {
260         if (!archive().hasEntry(entryFile->entryPath()))
261         {
262             LOG_RES_VERBOSE("%s removed from archive") << file.description();
263             return true; // It's gone...
264         }
265 
266         // Prune based on entry status.
267         if (archive().entryStatus(entryFile->entryPath()).modifiedAt !=
268            file.status().modifiedAt)
269         {
270             LOG_RES_XVERBOSE("%s has been modified (arch:%s != file:%s)",
271                              file.description()
272                              << archive().entryStatus(entryFile->entryPath()).modifiedAt.asText()
273                              << file.status().modifiedAt.asText());
274             return true;
275         }
276     }
277     return false;
278 }
279 
createFile(String const & name)280 File *ArchiveFeed::createFile(String const &name)
281 {
282     String newEntry = d->basePath / name;
283     if (archive().hasEntry(newEntry))
284     {
285         /// @throw AlreadyExistsError  The entry @a name already exists in the archive.
286         throw AlreadyExistsError("ArchiveFeed::createFile", name + ": already exists");
287     }
288     // Add an empty entry.
289     archive().add(newEntry, Block());
290     auto *file = new ArchiveEntryFile(name, archive(), newEntry);
291     d->addToEntryTable(file);
292     file->setOriginFeed(this);
293     return file;
294 }
295 
destroyFile(String const & name)296 void ArchiveFeed::destroyFile(String const &name)
297 {
298     archive().remove(d->basePath / name);
299 }
300 
newSubFeed(String const & name)301 Feed *ArchiveFeed::newSubFeed(String const &name)
302 {
303     return new ArchiveFeed(*this, d->basePath / name);
304 }
305 
archive()306 Archive &ArchiveFeed::archive()
307 {
308     return d->archive();
309 }
310 
archive() const311 Archive const &ArchiveFeed::archive() const
312 {
313     return d->archive();
314 }
315 
basePath() const316 String const &ArchiveFeed::basePath() const
317 {
318     return d->basePath;
319 }
320 
archiveSourceFile() const321 File const &ArchiveFeed::archiveSourceFile() const
322 {
323     if (d->file)
324     {
325         return *d->file;
326     }
327     throw InvalidSourceError("ArchiveFeed::archiveSourceFile", "Archive source file is missing");
328 }
329 
rewriteFile()330 void ArchiveFeed::rewriteFile()
331 {
332     if (d->parentFeed)
333     {
334         DENG2_ASSERT(!d->arch);
335         d->parentFeed->rewriteFile();
336     }
337     else
338     {
339         DENG2_ASSERT(d->arch != 0);
340         d->writeIfModified();
341     }
342 }
343 
uncache()344 void ArchiveFeed::uncache()
345 {
346     auto &table = d->entryTable();
347     DENG2_GUARD(table);
348     for (auto *entry : table.value)
349     {
350         entry->uncache();
351     }
352 }
353 
uncacheAllEntries(StringList folderTypes)354 void ArchiveFeed::uncacheAllEntries(StringList folderTypes) // static
355 {
356     if (Folder::isPopulatingAsync()) return; // Never mind.
357 
358     for (String const &folderType : folderTypes)
359     {
360         foreach (File *file, FileSystem::get().indexFor(folderType).files())
361         {
362             if (auto *feed = file->as<Folder>().primaryFeedMaybeAs<ArchiveFeed>())
363             {
364                 feed->uncache();
365             }
366         }
367     }
368 }
369 
370 } // namespace de
371