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