Bug 1650898: Implement tmpPath and backupFile options for IOUtils::writeAtomic r=barret,Gijs

This patch refactors the existing IOUtils::writeAtomic method to add support for the `tmpPath` and `backupFile` options.

Differential Revision: https://phabricator.services.mozilla.com/D82601
This commit is contained in:
Keefer Rourke 2020-07-15 16:04:17 +00:00
Родитель 0dcdd65989
Коммит b732cf15e1
5 изменённых файлов: 331 добавлений и 120 удалений

Просмотреть файл

@ -14,12 +14,15 @@ namespace IOUtils {
*/
Promise<Uint8Array> read(DOMString path, optional unsigned long maxBytes);
/**
* Atomically write |data| to a file at |path|. This operation attempts to
* ensure that until the data is entirely written to disk, the destination
* file is not modified.
* Attempts to safely write |data| to a file at |path|.
*
* This is generally accomplished by writing to a temporary file, then
* performing an overwriting move.
* This operation can be made atomic by specifying the |tmpFile| option. If
* specified, then this method ensures that the destination file is not
* modified until the data is entirely written to the temporary file, after
* which point the |tmpFile| is moved to the specified |path|.
*
* The target file can also be backed up to a |backupFile| before any writes
* are performed to prevent data loss in case of corruption.
*
* @param path A forward-slash separated path.
* @param data Data to write to the file at path.
@ -47,8 +50,10 @@ dictionary WriteAtomicOptions {
*/
DOMString backupFile;
/**
* If specified, write the data to a file at |tmpPath|. Once the write is
* complete, the destination will be overwritten by a move.
* If specified, write the data to a file at |tmpPath| instead of directly to
* the destination. Once the write is complete, the destination will be
* overwritten by a move. Specifying this option will make the write a little
* slower, but also safer.
*/
DOMString tmpPath;
/**

Просмотреть файл

@ -19,6 +19,7 @@
#include "nsDirectoryServiceDefs.h"
#include "nsIFile.h"
#include "nsIGlobalObject.h"
#include "nsNativeCharsetUtils.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsThreadManager.h"
@ -72,6 +73,26 @@ static bool IsFileNotFound(nsresult aResult) {
}
}
/**
* Formats an error message and appends the error name to the end.
*/
template <typename... Args>
static nsCString FormatErrorMessage(nsresult aError, const char* const aMessage,
Args... aArgs) {
nsPrintfCString msg(aMessage, aArgs...);
if (const char* errName = GetStaticErrorName(aError)) {
msg.AppendPrintf(": %s", errName);
} else {
// In the exceptional case where there is no error name, print the literal
// integer value of the nsresult as an upper case hex value so it can be
// located easily in searchfox.
msg.AppendPrintf(": 0x%" PRIX32, static_cast<uint32_t>(aError));
}
return std::move(msg);
}
static nsCString FormatErrorMessage(nsresult aError,
const char* const aMessage) {
const char* errName = GetStaticErrorName(aError);
@ -119,52 +140,58 @@ already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal,
}
}
NS_ConvertUTF16toUTF8 path(aPath);
InvokeAsync(bg, __func__,
[path = nsAutoString(aPath), toRead]() {
MOZ_ASSERT(!NS_IsMainThread());
InvokeAsync(
bg, __func__,
[path, toRead]() {
MOZ_ASSERT(!NS_IsMainThread());
UniquePtr<PRFileDesc, PR_CloseDelete> fd =
OpenExistingSync(path, PR_RDONLY);
if (!fd) {
return IOReadMozPromise::CreateAndReject(
nsPrintfCString("Could not open file at %s",
NS_ConvertUTF16toUTF8(path).get()),
__func__);
}
uint32_t bufSize;
if (toRead == 0) { // maxBytes was unspecified.
// Limitation: We cannot read files that are larger than the
// max size of a TypedArray (UINT32_MAX bytes).
// Reject if the file is too big to be read.
PRFileInfo64 info;
if (PR_FAILURE == PR_GetOpenFileInfo64(fd.get(), &info)) {
return IOReadMozPromise::CreateAndReject(
nsPrintfCString("Could not get info for file at %s",
NS_ConvertUTF16toUTF8(path).get()),
__func__);
}
uint32_t fileSize = info.size;
if (fileSize > UINT32_MAX) {
return IOReadMozPromise::CreateAndReject(
nsPrintfCString("File at %s is too large to read",
NS_ConvertUTF16toUTF8(path).get()),
__func__);
}
bufSize = fileSize;
} else {
bufSize = toRead;
}
nsTArray<uint8_t> fileContents;
nsresult rv =
IOUtils::ReadSync(fd.get(), bufSize, fileContents);
UniquePtr<PRFileDesc, PR_CloseDelete> fd =
OpenExistingSync(path.get(), PR_RDONLY);
if (!fd) {
return IOReadMozPromise::CreateAndReject(
nsLiteralCString("Could not open file"), __func__);
}
uint32_t bufSize;
if (toRead == 0) { // maxBytes was unspecified.
// Limitation: We cannot read files that are larger than the max size
// of a TypedArray (UINT32_MAX bytes). Reject if the file is too
// big to be read.
PRFileInfo64 info;
if (PR_FAILURE == PR_GetOpenFileInfo64(fd.get(), &info)) {
return IOReadMozPromise::CreateAndReject(
nsLiteralCString("Could not get file info"), __func__);
}
uint32_t fileSize = info.size;
if (fileSize > UINT32_MAX) {
return IOReadMozPromise::CreateAndReject(
nsLiteralCString("File is too large to read"), __func__);
}
bufSize = fileSize;
} else {
bufSize = toRead;
}
nsTArray<uint8_t> fileContents;
nsresult er = IOUtils::ReadSync(fd.get(), bufSize, fileContents);
if (NS_SUCCEEDED(er)) {
return IOReadMozPromise::CreateAndResolve(std::move(fileContents),
__func__);
}
if (er == NS_ERROR_OUT_OF_MEMORY) {
return IOReadMozPromise::CreateAndReject(
nsLiteralCString("Out of memory"), __func__);
}
return IOReadMozPromise::CreateAndReject(
nsLiteralCString("Unexpected error reading file"), __func__);
})
if (NS_SUCCEEDED(rv)) {
return IOReadMozPromise::CreateAndResolve(
std::move(fileContents), __func__);
}
if (rv == NS_ERROR_OUT_OF_MEMORY) {
return IOReadMozPromise::CreateAndReject("Out of memory"_ns,
__func__);
}
return IOReadMozPromise::CreateAndReject(
nsPrintfCString("Unexpected error reading file at %s",
NS_ConvertUTF16toUTF8(path).get()),
__func__);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise = RefPtr(promise)](const nsTArray<uint8_t>& aBuf) {
@ -203,53 +230,100 @@ already_AddRefed<Promise> IOUtils::WriteAtomic(
return promise.forget();
}
// TODO: Implement tmpPath and backupFile options.
// The data to be written to file might be larger than can be written in any
// single call, so we must truncate the file and set the write mode to append
// to the file.
int32_t flags = PR_WRONLY | PR_TRUNCATE | PR_APPEND;
bool noOverwrite = false;
if (aOptions.IsAnyMemberPresent()) {
if (aOptions.mBackupFile.WasPassed() || aOptions.mTmpPath.WasPassed()) {
promise->MaybeRejectWithNotSupportedError(
"Unsupported options were passed");
return promise.forget();
}
if (aOptions.mFlush) {
flags |= PR_SYNC;
}
noOverwrite = aOptions.mNoOverwrite;
}
InvokeAsync(
bg, __func__,
[destPath = nsString(aPath), toWrite = std::move(toWrite), aOptions]() {
MOZ_ASSERT(!NS_IsMainThread());
NS_ConvertUTF16toUTF8 path(aPath);
// Check if the file exists and test it against the noOverwrite option.
const bool& noOverwrite = aOptions.mNoOverwrite;
bool exists = false;
{
UniquePtr<PRFileDesc, PR_CloseDelete> fd =
OpenExistingSync(destPath, PR_RDONLY);
exists = !!fd;
}
InvokeAsync(bg, __func__,
[path, flags, noOverwrite, toWrite = std::move(toWrite)]() {
MOZ_ASSERT(!NS_IsMainThread());
if (noOverwrite && exists) {
return IOWriteMozPromise::CreateAndReject(
nsPrintfCString("Refusing to overwrite the file at %s",
NS_ConvertUTF16toUTF8(destPath).get()),
__func__);
}
UniquePtr<PRFileDesc, PR_CloseDelete> fd =
OpenExistingSync(path.get(), flags);
if (noOverwrite && fd) {
return IOWriteMozPromise::CreateAndReject(
nsLiteralCString("Refusing to overwrite file"), __func__);
}
if (!fd) {
fd = CreateFileSync(path.get(), flags);
}
if (!fd) {
return IOWriteMozPromise::CreateAndReject(
nsLiteralCString("Could not open file"), __func__);
}
uint32_t result;
nsresult er = IOUtils::WriteSync(fd.get(), toWrite, result);
// If backupFile was specified, perform the backup as a move.
if (exists && aOptions.mBackupFile.WasPassed()) {
const nsString& backupFile(aOptions.mBackupFile.Value());
nsresult rv = MoveSync(destPath, backupFile, noOverwrite);
if (NS_FAILED(rv)) {
return IOWriteMozPromise::CreateAndReject(
FormatErrorMessage(rv,
"Failed to back up the file from %s to %s",
NS_ConvertUTF16toUTF8(destPath).get(),
NS_ConvertUTF16toUTF8(backupFile).get()),
__func__);
}
}
if (NS_SUCCEEDED(er)) {
return IOWriteMozPromise::CreateAndResolve(result, __func__);
}
return IOWriteMozPromise::CreateAndReject(
nsLiteralCString("Unexpected error writing file"),
__func__);
})
// If tmpPath was specified, we will write to there first, then perform
// a move to ensure the file ends up at the final requested destination.
nsString tmpPath = destPath;
if (aOptions.mTmpPath.WasPassed()) {
tmpPath = aOptions.mTmpPath.Value();
}
// The data to be written to file might be larger than can be
// written in any single call, so we must truncate the file and
// set the write mode to append to the file.
int32_t flags = PR_WRONLY | PR_TRUNCATE | PR_APPEND;
if (aOptions.mFlush) {
flags |= PR_SYNC;
}
// Try to perform the write and ensure that the file is closed before
// continuing.
uint32_t result = 0;
{
UniquePtr<PRFileDesc, PR_CloseDelete> fd =
OpenExistingSync(tmpPath, flags);
if (!fd) {
fd = CreateFileSync(tmpPath, flags);
}
if (!fd) {
return IOWriteMozPromise::CreateAndReject(
nsPrintfCString("Could not open the file at %s",
NS_ConvertUTF16toUTF8(tmpPath).get()),
__func__);
}
nsresult rv = IOUtils::WriteSync(fd.get(), toWrite, result);
if (NS_FAILED(rv)) {
return IOWriteMozPromise::CreateAndReject(
FormatErrorMessage(
rv,
"Unexpected error occurred while writing to the file at %s",
NS_ConvertUTF16toUTF8(tmpPath).get()),
__func__);
}
}
// The requested destination was written to, so there is nothing left to
// do.
if (destPath == tmpPath) {
return IOWriteMozPromise::CreateAndResolve(result, __func__);
}
// Otherwise, if tmpPath was specified and different from the destPath,
// then the operation is finished by performing a move.
nsresult rv = MoveSync(tmpPath, destPath, false);
if (NS_FAILED(rv)) {
return IOWriteMozPromise::CreateAndReject(
FormatErrorMessage(
rv, "Error moving temporary file at %s to destination at %s",
NS_ConvertUTF16toUTF8(tmpPath).get(),
NS_ConvertUTF16toUTF8(destPath).get()),
__func__);
}
return IOWriteMozPromise::CreateAndResolve(result, __func__);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise = RefPtr(promise)](const uint32_t& aBytesWritten) {
@ -415,20 +489,38 @@ already_AddRefed<Promise> IOUtils::CreateJSPromise(GlobalObject& aGlobal) {
/* static */
UniquePtr<PRFileDesc, PR_CloseDelete> IOUtils::OpenExistingSync(
const char* aPath, int32_t aFlags) {
const nsAString& aPath, int32_t aFlags) {
// Ensure that CREATE_FILE and EXCL flags were not included, as we do not
// want to create a new file.
MOZ_ASSERT((aFlags & (PR_CREATE_FILE | PR_EXCL)) == 0);
return UniquePtr<PRFileDesc, PR_CloseDelete>(PR_Open(aPath, aFlags, 0));
// We open the file descriptor through an nsLocalFile to ensure that the paths
// are interpreted/encoded correctly on all platforms.
RefPtr<nsLocalFile> file = new nsLocalFile();
nsresult rv = file->InitWithPath(aPath);
NS_ENSURE_SUCCESS(rv, nullptr);
PRFileDesc* fd;
rv = file->OpenNSPRFileDesc(aFlags, /* mode */ 0, &fd);
NS_ENSURE_SUCCESS(rv, nullptr);
return UniquePtr<PRFileDesc, PR_CloseDelete>(fd);
}
/* static */
UniquePtr<PRFileDesc, PR_CloseDelete> IOUtils::CreateFileSync(const char* aPath,
int32_t aFlags,
int32_t aMode) {
return UniquePtr<PRFileDesc, PR_CloseDelete>(
PR_Open(aPath, aFlags | PR_CREATE_FILE | PR_EXCL, aMode));
UniquePtr<PRFileDesc, PR_CloseDelete> IOUtils::CreateFileSync(
const nsAString& aPath, int32_t aFlags, int32_t aMode) {
// We open the file descriptor through an nsLocalFile to ensure that the paths
// are interpreted/encoded correctly on all platforms.
RefPtr<nsLocalFile> file = new nsLocalFile();
nsresult rv = file->InitWithPath(aPath);
NS_ENSURE_SUCCESS(rv, nullptr);
PRFileDesc* fd;
rv = file->OpenNSPRFileDesc(aFlags | PR_CREATE_FILE | PR_EXCL, aMode, &fd);
NS_ENSURE_SUCCESS(rv, nullptr);
return UniquePtr<PRFileDesc, PR_CloseDelete>(fd);
}
/* static */
@ -590,8 +682,8 @@ NS_IMETHODIMP IOUtilsShutdownBlocker::BlockShutdown(
*lockedBackgroundET = nullptr;
IOUtils::sBarrier = nullptr;
});
nsresult er = NS_DispatchToMainThread(mainThreadRunnable.forget());
NS_ENSURE_SUCCESS_VOID(er);
nsresult rv = NS_DispatchToMainThread(mainThreadRunnable.forget());
NS_ENSURE_SUCCESS_VOID(rv);
});
return et->Dispatch(backgroundRunnable.forget(),

Просмотреть файл

@ -78,23 +78,23 @@ class IOUtils final {
/**
* Opens an existing file at |path|.
*
* @param path The location of the file as a unix-style UTF-8 path string.
* @param path The location of the file as an absolute path string.
* @param flags PRIO flags, excluding |PR_CREATE| and |PR_EXCL|.
*/
static UniquePtr<PRFileDesc, PR_CloseDelete> OpenExistingSync(
const char* aPath, int32_t aFlags);
const nsAString& aPath, int32_t aFlags);
/**
* Creates a new file at |path|.
*
* @param aPath The location of the file as a unix-style UTF-8 path string.
* @param aPath The location of the file as an absolute path string.
* @param aFlags PRIO flags to be used in addition to |PR_CREATE| and
* |PR_EXCL|.
* @param aMode Optional file mode. Defaults to 0666 to allow the system
* umask to compute the best mode for the new file.
*/
static UniquePtr<PRFileDesc, PR_CloseDelete> CreateFileSync(
const char* aPath, int32_t aFlags, int32_t aMode = 0666);
const nsAString& aPath, int32_t aFlags, int32_t aMode = 0666);
static nsresult ReadSync(PRFileDesc* aFd, const uint32_t aBufSize,
nsTArray<uint8_t>& aResult);

Просмотреть файл

@ -31,9 +31,8 @@ self.onmessage = async function(msg) {
}
async function test_full_read_and_write() {
// TODO: Stop using relative paths.
// Write a file.
const tmpFileName = "test_ioutils_numbers.tmp";
const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_numbers.tmp");
const bytes = Uint8Array.of(...new Array(50).keys());
const bytesWritten = await self.IOUtils.writeAtomic(tmpFileName, bytes);
is(

Просмотреть файл

@ -31,18 +31,17 @@
});
add_task(async function test_read_failure() {
// TODO: Stop using relative paths.
const doesNotExist = OS.Path.join(tmpDir, "does_not_exist.tmp");
await Assert.rejects(
window.IOUtils.read("does_not_exist.txt"),
window.IOUtils.read(doesNotExist),
/Could not open file/,
"IOUtils::read rejects when file does not exist"
);
});
add_task(async function test_write_no_overwrite() {
// TODO: Stop using relative paths.
// Make a new file, and try to write to it with overwrites disabled.
const tmpFileName = "test_ioutils_overwrite.tmp";
const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_overwrite.tmp");
const untouchableContents = new TextEncoder().encode("Can't touch this!\n");
await window.IOUtils.writeAtomic(tmpFileName, untouchableContents);
@ -51,9 +50,13 @@
window.IOUtils.writeAtomic(tmpFileName, newContents, {
noOverwrite: true,
}),
/Refusing to overwrite file/,
/Refusing to overwrite the file at */,
"IOUtils::writeAtomic rejects writing to existing file if overwrites are disabled"
);
ok(
await fileHasBinaryContents(tmpFileName, untouchableContents),
"IOUtils::writeAtomic doesn't change target file when overwrite is refused"
);
const bytesWritten = await window.IOUtils.writeAtomic(
tmpFileName,
@ -69,9 +72,107 @@
await cleanup(tmpFileName);
});
add_task(async function test_write_with_backup() {
info("Test backup file option with non-existing file");
let fileContents = new TextEncoder().encode("Original file contents");
let destFileName = OS.Path.join(tmpDir, "test_write_with_backup_option.tmp");
let backupFileName = destFileName + ".backup";
let bytesWritten =
await window.IOUtils.writeAtomic(destFileName, fileContents, {
backupFile: backupFileName,
});
ok(
await fileHasTextContents(destFileName, "Original file contents"),
"IOUtils::writeAtomic creates a new file with the correct contents"
);
ok(
!await fileExists(backupFileName),
"IOUtils::writeAtomic does not create a backup if the target file does not exist"
);
is(
bytesWritten,
fileContents.length,
"IOUtils::writeAtomic correctly writes to a new file without performing a backup"
);
info("Test backup file option with existing destination");
let newFileContents = new TextEncoder().encode("New file contents");
ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
bytesWritten =
await window.IOUtils.writeAtomic(destFileName, newFileContents, {
backupFile: backupFileName,
});
ok(
await fileHasTextContents(backupFileName, "Original file contents"),
"IOUtils::writeAtomic can backup an existing file before writing"
);
ok(
await fileHasTextContents(destFileName, "New file contents"),
"IOUtils::writeAtomic can create the target with the correct contents"
);
is(
bytesWritten,
newFileContents.length,
"IOUtils::writeAtomic correctly writes to the target after taking a backup"
);
await cleanup(destFileName, backupFileName);
});
add_task(async function test_write_with_backup_and_tmp() {
info("Test backup with tmp and backup file options, non-existing destination");
let fileContents = new TextEncoder().encode("Original file contents");
let destFileName = OS.Path.join(tmpDir, "test_write_with_backup_and_tmp_options.tmp");
let backupFileName = destFileName + ".backup";
let tmpFileName = OS.Path.join(tmpDir, "temp_file.tmp");
let bytesWritten =
await window.IOUtils.writeAtomic(destFileName, fileContents, {
backupFile: backupFileName,
tmpPath: tmpFileName,
});
ok(!await fileExists(tmpFileName), "IOUtils::writeAtomic cleans up the tmpFile");
ok(
!await fileExists(backupFileName),
"IOUtils::writeAtomic does not create a backup if the target file does not exist"
);
ok(
await fileHasTextContents(destFileName, "Original file contents"),
"IOUtils::writeAtomic can write to the destination when a temporary file is used"
);
is(
bytesWritten,
fileContents.length,
"IOUtils::writeAtomic can copy tmp file to destination without performing a backup"
);
info("Test backup with tmp and backup file options, existing destination");
let newFileContents = new TextEncoder().encode("New file contents");
bytesWritten =
await window.IOUtils.writeAtomic(destFileName, newFileContents, {
backupFile: backupFileName,
tmpPath: tmpFileName,
});
ok(!await fileExists(tmpFileName), "IOUtils::writeAtomic cleans up the tmpFile");
ok(
await fileHasTextContents(backupFileName, "Original file contents"),
"IOUtils::writeAtomic can create a backup if the target file exists"
);
ok(
await fileHasTextContents(destFileName, "New file contents"),
"IOUtils::writeAtomic can write to the destination when a temporary file is used"
);
is(
bytesWritten,
newFileContents.length,
"IOUtils::writeAtomic IOUtils::writeAtomic can move tmp file to destination after performing a backup"
);
await cleanup(destFileName, backupFileName);
});
add_task(async function test_partial_read() {
// TODO: Stop using relative paths.
const tmpFileName = "test_ioutils_partial_read.tmp";
const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_partial_read.tmp");
const bytes = Uint8Array.of(...new Array(50).keys());
const bytesWritten = await window.IOUtils.writeAtomic(tmpFileName, bytes);
is(
@ -97,10 +198,9 @@
});
add_task(async function test_empty_read_and_write() {
// TODO: Stop using relative paths.
// Trying to write an empty file isn't very useful, but it should still
// succeed.
const tmpFileName = "test_ioutils_empty.tmp";
const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_empty.tmp");
const emptyByteArray = new Uint8Array(0);
const bytesWritten = await window.IOUtils.writeAtomic(
tmpFileName,
@ -121,10 +221,9 @@
});
add_task(async function test_full_read_and_write() {
// TODO: Stop using relative paths.
// Write a file.
info("Test writing to a new binary file");
const tmpFileName = "test_ioutils_numbers.tmp";
const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_numbers.tmp");
const bytes = Uint8Array.of(...new Array(50).keys());
const bytesWritten = await window.IOUtils.writeAtomic(tmpFileName, bytes);
is(
@ -313,6 +412,8 @@
const srcDir = OS.Path.join(tmpDir, "test_move_failure_src.tmp.d");
await createFile(destFile);
await OS.File.makeDir(srcDir);
ok(await OS.File.exists(srcDir), "Expected directory to exist");
ok(await OS.File.exists(destFile), "Expected file to exist");
// Test.
await Assert.rejects(
window.IOUtils.move(srcDir, destFile),
@ -328,6 +429,11 @@
// Utility functions.
Uint8Array.prototype.equals = function equals(other) {
if (this.byteLength !== other.byteLength) return false;
return this.every((val, i) => val === other[i]);
}
async function createFile(location, contents = "") {
if (typeof contents === "string") {
contents = new TextEncoder().encode(contents);
@ -337,6 +443,15 @@
ok(exists, `Created temporary file at: ${location}`);
}
async function fileHasBinaryContents(location, expectedContents) {
if (!(expectedContents instanceof Uint8Array)) {
throw new TypeError("expectedContents must be a byte array");
}
info(`opening ${location} for reading`);
const bytes = await window.IOUtils.read(location);
return bytes.equals(expectedContents);
}
async function fileHasTextContents(location, expectedContents) {
if (typeof expectedContents !== "string") {
throw new TypeError("expectedContents must be a string");