зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
0dcdd65989
Коммит
b732cf15e1
|
@ -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");
|
||||
|
|
Загрузка…
Ссылка в новой задаче