1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "mozilla/dom/cache/FileUtils.h"
8
9 #include "mozilla/dom/quota/FileStreams.h"
10 #include "mozilla/SnappyCompressOutputStream.h"
11 #include "mozilla/Unused.h"
12 #include "nsIFile.h"
13 #include "nsIUUIDGenerator.h"
14 #include "nsNetCID.h"
15 #include "nsISimpleEnumerator.h"
16 #include "nsServiceManagerUtils.h"
17 #include "nsString.h"
18 #include "nsThreadUtils.h"
19
20 namespace mozilla {
21 namespace dom {
22 namespace cache {
23
24 using mozilla::dom::quota::FileInputStream;
25 using mozilla::dom::quota::FileOutputStream;
26 using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
27
28 namespace {
29
30 enum BodyFileType
31 {
32 BODY_FILE_FINAL,
33 BODY_FILE_TMP
34 };
35
36 nsresult
37 BodyIdToFile(nsIFile* aBaseDir, const nsID& aId, BodyFileType aType,
38 nsIFile** aBodyFileOut);
39
40 } // namespace
41
42 // static
43 nsresult
BodyCreateDir(nsIFile * aBaseDir)44 BodyCreateDir(nsIFile* aBaseDir)
45 {
46 MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
47
48 nsCOMPtr<nsIFile> aBodyDir;
49 nsresult rv = aBaseDir->Clone(getter_AddRefs(aBodyDir));
50 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
51
52 rv = aBodyDir->Append(NS_LITERAL_STRING("morgue"));
53 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
54
55 rv = aBodyDir->Create(nsIFile::DIRECTORY_TYPE, 0755);
56 if (rv == NS_ERROR_FILE_ALREADY_EXISTS) {
57 return NS_OK;
58 }
59 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
60
61 return rv;
62 }
63
64 // static
65 nsresult
BodyDeleteDir(nsIFile * aBaseDir)66 BodyDeleteDir(nsIFile* aBaseDir)
67 {
68 MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
69
70 nsCOMPtr<nsIFile> aBodyDir;
71 nsresult rv = aBaseDir->Clone(getter_AddRefs(aBodyDir));
72 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
73
74 rv = aBodyDir->Append(NS_LITERAL_STRING("morgue"));
75 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
76
77 rv = aBodyDir->Remove(/* recursive = */ true);
78 if (rv == NS_ERROR_FILE_NOT_FOUND ||
79 rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
80 rv = NS_OK;
81 }
82 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
83
84 return rv;
85 }
86
87 // static
88 nsresult
BodyGetCacheDir(nsIFile * aBaseDir,const nsID & aId,nsIFile ** aCacheDirOut)89 BodyGetCacheDir(nsIFile* aBaseDir, const nsID& aId, nsIFile** aCacheDirOut)
90 {
91 MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
92 MOZ_DIAGNOSTIC_ASSERT(aCacheDirOut);
93
94 *aCacheDirOut = nullptr;
95
96 nsresult rv = aBaseDir->Clone(aCacheDirOut);
97 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
98 MOZ_DIAGNOSTIC_ASSERT(*aCacheDirOut);
99
100 rv = (*aCacheDirOut)->Append(NS_LITERAL_STRING("morgue"));
101 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
102
103 // Some file systems have poor performance when there are too many files
104 // in a single directory. Mitigate this issue by spreading the body
105 // files out into sub-directories. We use the last byte of the ID for
106 // the name of the sub-directory.
107 nsAutoString subDirName;
108 subDirName.AppendInt(aId.m3[7]);
109 rv = (*aCacheDirOut)->Append(subDirName);
110 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
111
112 rv = (*aCacheDirOut)->Create(nsIFile::DIRECTORY_TYPE, 0755);
113 if (rv == NS_ERROR_FILE_ALREADY_EXISTS) {
114 return NS_OK;
115 }
116 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
117
118 return rv;
119 }
120
121 // static
122 nsresult
BodyStartWriteStream(const QuotaInfo & aQuotaInfo,nsIFile * aBaseDir,nsIInputStream * aSource,void * aClosure,nsAsyncCopyCallbackFun aCallback,nsID * aIdOut,nsISupports ** aCopyContextOut)123 BodyStartWriteStream(const QuotaInfo& aQuotaInfo,
124 nsIFile* aBaseDir, nsIInputStream* aSource,
125 void* aClosure,
126 nsAsyncCopyCallbackFun aCallback, nsID* aIdOut,
127 nsISupports** aCopyContextOut)
128 {
129 MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
130 MOZ_DIAGNOSTIC_ASSERT(aSource);
131 MOZ_DIAGNOSTIC_ASSERT(aClosure);
132 MOZ_DIAGNOSTIC_ASSERT(aCallback);
133 MOZ_DIAGNOSTIC_ASSERT(aIdOut);
134 MOZ_DIAGNOSTIC_ASSERT(aCopyContextOut);
135
136 nsresult rv;
137 nsCOMPtr<nsIUUIDGenerator> idGen =
138 do_GetService("@mozilla.org/uuid-generator;1", &rv);
139 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
140
141 rv = idGen->GenerateUUIDInPlace(aIdOut);
142 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
143
144 nsCOMPtr<nsIFile> finalFile;
145 rv = BodyIdToFile(aBaseDir, *aIdOut, BODY_FILE_FINAL,
146 getter_AddRefs(finalFile));
147 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
148
149 bool exists;
150 rv = finalFile->Exists(&exists);
151 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
152 if (NS_WARN_IF(exists)) { return NS_ERROR_FILE_ALREADY_EXISTS; }
153
154 nsCOMPtr<nsIFile> tmpFile;
155 rv = BodyIdToFile(aBaseDir, *aIdOut, BODY_FILE_TMP, getter_AddRefs(tmpFile));
156 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
157
158 rv = tmpFile->Exists(&exists);
159 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
160 if (NS_WARN_IF(exists)) { return NS_ERROR_FILE_ALREADY_EXISTS; }
161
162 nsCOMPtr<nsIOutputStream> fileStream =
163 FileOutputStream::Create(PERSISTENCE_TYPE_DEFAULT, aQuotaInfo.mGroup,
164 aQuotaInfo.mOrigin, tmpFile);
165 if (NS_WARN_IF(!fileStream)) { return NS_ERROR_UNEXPECTED; }
166
167 RefPtr<SnappyCompressOutputStream> compressed =
168 new SnappyCompressOutputStream(fileStream);
169
170 nsCOMPtr<nsIEventTarget> target =
171 do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
172
173 rv = NS_AsyncCopy(aSource, compressed, target, NS_ASYNCCOPY_VIA_WRITESEGMENTS,
174 compressed->BlockSize(), aCallback, aClosure,
175 true, true, // close streams
176 aCopyContextOut);
177 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
178
179 return rv;
180 }
181
182 // static
183 void
BodyCancelWrite(nsIFile * aBaseDir,nsISupports * aCopyContext)184 BodyCancelWrite(nsIFile* aBaseDir, nsISupports* aCopyContext)
185 {
186 MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
187 MOZ_DIAGNOSTIC_ASSERT(aCopyContext);
188
189 nsresult rv = NS_CancelAsyncCopy(aCopyContext, NS_ERROR_ABORT);
190 Unused << NS_WARN_IF(NS_FAILED(rv));
191
192 // The partially written file must be cleaned up after the async copy
193 // makes its callback.
194 }
195
196 // static
197 nsresult
BodyFinalizeWrite(nsIFile * aBaseDir,const nsID & aId)198 BodyFinalizeWrite(nsIFile* aBaseDir, const nsID& aId)
199 {
200 MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
201
202 nsCOMPtr<nsIFile> tmpFile;
203 nsresult rv = BodyIdToFile(aBaseDir, aId, BODY_FILE_TMP, getter_AddRefs(tmpFile));
204 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
205
206 nsCOMPtr<nsIFile> finalFile;
207 rv = BodyIdToFile(aBaseDir, aId, BODY_FILE_FINAL, getter_AddRefs(finalFile));
208 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
209
210 nsAutoString finalFileName;
211 rv = finalFile->GetLeafName(finalFileName);
212 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
213
214 rv = tmpFile->RenameTo(nullptr, finalFileName);
215 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
216
217 return rv;
218 }
219
220 // static
221 nsresult
BodyOpen(const QuotaInfo & aQuotaInfo,nsIFile * aBaseDir,const nsID & aId,nsIInputStream ** aStreamOut)222 BodyOpen(const QuotaInfo& aQuotaInfo, nsIFile* aBaseDir, const nsID& aId,
223 nsIInputStream** aStreamOut)
224 {
225 MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
226 MOZ_DIAGNOSTIC_ASSERT(aStreamOut);
227
228 nsCOMPtr<nsIFile> finalFile;
229 nsresult rv = BodyIdToFile(aBaseDir, aId, BODY_FILE_FINAL,
230 getter_AddRefs(finalFile));
231 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
232
233 bool exists;
234 rv = finalFile->Exists(&exists);
235 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
236 if (NS_WARN_IF(!exists)) { return NS_ERROR_FILE_NOT_FOUND; }
237
238 nsCOMPtr<nsIInputStream> fileStream =
239 FileInputStream::Create(PERSISTENCE_TYPE_DEFAULT, aQuotaInfo.mGroup,
240 aQuotaInfo.mOrigin, finalFile);
241 if (NS_WARN_IF(!fileStream)) { return NS_ERROR_UNEXPECTED; }
242
243 fileStream.forget(aStreamOut);
244
245 return rv;
246 }
247
248 // static
249 nsresult
BodyDeleteFiles(nsIFile * aBaseDir,const nsTArray<nsID> & aIdList)250 BodyDeleteFiles(nsIFile* aBaseDir, const nsTArray<nsID>& aIdList)
251 {
252 nsresult rv = NS_OK;
253
254 for (uint32_t i = 0; i < aIdList.Length(); ++i) {
255 nsCOMPtr<nsIFile> tmpFile;
256 rv = BodyIdToFile(aBaseDir, aIdList[i], BODY_FILE_TMP,
257 getter_AddRefs(tmpFile));
258 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
259
260 rv = tmpFile->Remove(false /* recursive */);
261 if (rv == NS_ERROR_FILE_NOT_FOUND ||
262 rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
263 rv = NS_OK;
264 }
265
266 // Only treat file deletion as a hard failure in DEBUG builds. Users
267 // can unfortunately hit this on windows if anti-virus is scanning files,
268 // etc.
269 MOZ_ASSERT(NS_SUCCEEDED(rv));
270
271 nsCOMPtr<nsIFile> finalFile;
272 rv = BodyIdToFile(aBaseDir, aIdList[i], BODY_FILE_FINAL,
273 getter_AddRefs(finalFile));
274 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
275
276 rv = finalFile->Remove(false /* recursive */);
277 if (rv == NS_ERROR_FILE_NOT_FOUND ||
278 rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
279 rv = NS_OK;
280 }
281
282 // Again, only treat removal as hard failure in debug build.
283 MOZ_ASSERT(NS_SUCCEEDED(rv));
284 }
285
286 return NS_OK;
287 }
288
289 namespace {
290
291 nsresult
BodyIdToFile(nsIFile * aBaseDir,const nsID & aId,BodyFileType aType,nsIFile ** aBodyFileOut)292 BodyIdToFile(nsIFile* aBaseDir, const nsID& aId, BodyFileType aType,
293 nsIFile** aBodyFileOut)
294 {
295 MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
296 MOZ_DIAGNOSTIC_ASSERT(aBodyFileOut);
297
298 *aBodyFileOut = nullptr;
299
300 nsresult rv = BodyGetCacheDir(aBaseDir, aId, aBodyFileOut);
301 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
302 MOZ_DIAGNOSTIC_ASSERT(*aBodyFileOut);
303
304 char idString[NSID_LENGTH];
305 aId.ToProvidedString(idString);
306
307 NS_ConvertASCIItoUTF16 fileName(idString);
308
309 if (aType == BODY_FILE_FINAL) {
310 fileName.AppendLiteral(".final");
311 } else {
312 fileName.AppendLiteral(".tmp");
313 }
314
315 rv = (*aBodyFileOut)->Append(fileName);
316 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
317
318 return rv;
319 }
320
321 } // namespace
322
323 nsresult
BodyDeleteOrphanedFiles(nsIFile * aBaseDir,nsTArray<nsID> & aKnownBodyIdList)324 BodyDeleteOrphanedFiles(nsIFile* aBaseDir, nsTArray<nsID>& aKnownBodyIdList)
325 {
326 MOZ_DIAGNOSTIC_ASSERT(aBaseDir);
327
328 // body files are stored in a directory structure like:
329 //
330 // /morgue/01/{01fdddb2-884d-4c3d-95ba-0c8062f6c325}.final
331 // /morgue/02/{02fdddb2-884d-4c3d-95ba-0c8062f6c325}.tmp
332
333 nsCOMPtr<nsIFile> dir;
334 nsresult rv = aBaseDir->Clone(getter_AddRefs(dir));
335 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
336
337 // Add the root morgue directory
338 rv = dir->Append(NS_LITERAL_STRING("morgue"));
339 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
340
341 nsCOMPtr<nsISimpleEnumerator> entries;
342 rv = dir->GetDirectoryEntries(getter_AddRefs(entries));
343 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
344
345 // Iterate over all the intermediate morgue subdirs
346 bool hasMore = false;
347 while (NS_SUCCEEDED(rv = entries->HasMoreElements(&hasMore)) && hasMore) {
348 nsCOMPtr<nsISupports> entry;
349 rv = entries->GetNext(getter_AddRefs(entry));
350 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
351
352 nsCOMPtr<nsIFile> subdir = do_QueryInterface(entry);
353
354 bool isDir = false;
355 rv = subdir->IsDirectory(&isDir);
356 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
357
358 // If a file got in here somehow, try to remove it and move on
359 if (NS_WARN_IF(!isDir)) {
360 rv = subdir->Remove(false /* recursive */);
361 MOZ_ASSERT(NS_SUCCEEDED(rv));
362 continue;
363 }
364
365 nsCOMPtr<nsISimpleEnumerator> subEntries;
366 rv = subdir->GetDirectoryEntries(getter_AddRefs(subEntries));
367 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
368
369 // Now iterate over all the files in the subdir
370 bool subHasMore = false;
371 while(NS_SUCCEEDED(rv = subEntries->HasMoreElements(&subHasMore)) &&
372 subHasMore) {
373 nsCOMPtr<nsISupports> subEntry;
374 rv = subEntries->GetNext(getter_AddRefs(subEntry));
375 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
376
377 nsCOMPtr<nsIFile> file = do_QueryInterface(subEntry);
378
379 nsAutoCString leafName;
380 rv = file->GetNativeLeafName(leafName);
381 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
382
383 // Delete all tmp files regardless of known bodies. These are
384 // all considered orphans.
385 if (StringEndsWith(leafName, NS_LITERAL_CSTRING(".tmp"))) {
386 // remove recursively in case its somehow a directory
387 rv = file->Remove(true /* recursive */);
388 MOZ_ASSERT(NS_SUCCEEDED(rv));
389 continue;
390 }
391
392 nsCString suffix(NS_LITERAL_CSTRING(".final"));
393
394 // Otherwise, it must be a .final file. If its not, then just
395 // skip it.
396 if (NS_WARN_IF(!StringEndsWith(leafName, suffix) ||
397 leafName.Length() != NSID_LENGTH - 1 + suffix.Length())) {
398 continue;
399 }
400
401 // Finally, parse the uuid out of the name. If its fails to parse,
402 // the ignore the file.
403 nsID id;
404 if (NS_WARN_IF(!id.Parse(leafName.BeginReading()))) {
405 continue;
406 }
407
408 if (!aKnownBodyIdList.Contains(id)) {
409 // remove recursively in case its somehow a directory
410 rv = file->Remove(true /* recursive */);
411 MOZ_ASSERT(NS_SUCCEEDED(rv));
412 }
413 }
414 }
415
416 return rv;
417 }
418
419 namespace {
420
421 nsresult
GetMarkerFileHandle(const QuotaInfo & aQuotaInfo,nsIFile ** aFileOut)422 GetMarkerFileHandle(const QuotaInfo& aQuotaInfo, nsIFile** aFileOut)
423 {
424 MOZ_DIAGNOSTIC_ASSERT(aFileOut);
425
426 nsCOMPtr<nsIFile> marker;
427 nsresult rv = aQuotaInfo.mDir->Clone(getter_AddRefs(marker));
428 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
429
430 rv = marker->Append(NS_LITERAL_STRING("cache"));
431 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
432
433 rv = marker->Append(NS_LITERAL_STRING("context_open.marker"));
434 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
435
436 marker.forget(aFileOut);
437
438 return rv;
439 }
440
441 } // namespace
442
443 nsresult
CreateMarkerFile(const QuotaInfo & aQuotaInfo)444 CreateMarkerFile(const QuotaInfo& aQuotaInfo)
445 {
446 nsCOMPtr<nsIFile> marker;
447 nsresult rv = GetMarkerFileHandle(aQuotaInfo, getter_AddRefs(marker));
448 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
449
450 rv = marker->Create(nsIFile::NORMAL_FILE_TYPE, 0644);
451 if (rv == NS_ERROR_FILE_ALREADY_EXISTS) {
452 rv = NS_OK;
453 }
454
455 // Note, we don't need to fsync here. We only care about actually
456 // writing the marker if later modifications to the Cache are
457 // actually flushed to the disk. If the OS crashes before the marker
458 // is written then we are ensured no other changes to the Cache were
459 // flushed either.
460
461 return rv;
462 }
463
464 nsresult
DeleteMarkerFile(const QuotaInfo & aQuotaInfo)465 DeleteMarkerFile(const QuotaInfo& aQuotaInfo)
466 {
467 nsCOMPtr<nsIFile> marker;
468 nsresult rv = GetMarkerFileHandle(aQuotaInfo, getter_AddRefs(marker));
469 if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
470
471 rv = marker->Remove(/* recursive = */ false);
472 if (rv == NS_ERROR_FILE_NOT_FOUND ||
473 rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
474 rv = NS_OK;
475 }
476
477 // Again, no fsync is necessary. If the OS crashes before the file
478 // removal is flushed, then the Cache will search for stale data on
479 // startup. This will cause the next Cache access to be a bit slow, but
480 // it seems appropriate after an OS crash.
481
482 return NS_OK;
483 }
484
485 bool
MarkerFileExists(const QuotaInfo & aQuotaInfo)486 MarkerFileExists(const QuotaInfo& aQuotaInfo)
487 {
488 nsCOMPtr<nsIFile> marker;
489 nsresult rv = GetMarkerFileHandle(aQuotaInfo, getter_AddRefs(marker));
490 if (NS_WARN_IF(NS_FAILED(rv))) { return false; }
491
492 bool exists = false;
493 rv = marker->Exists(&exists);
494 if (NS_WARN_IF(NS_FAILED(rv))) { return false; }
495
496 return exists;
497 }
498
499 } // namespace cache
500 } // namespace dom
501 } // namespace mozilla
502