зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1650227: Implement IOUtils move method r=barret,Gijs
This patch introduces a move method to the IOUtils interface, which allows for renaming/moving files or directories on disk. Source and destination files may be specified either by an absolute path, or a relative path from the current working directory. This method has well-defined behaviour similar to the POSIX mv command (except that this may create missing directories as necessary). The behaviour is briefly summarized below: 1. If the source is a file that exists: a. If the destination is a file that does not exist, the source is renamed (and re-parented as a child of the destination parent directory). The destination parent directory will be created if necessary. b. If the destination is a file that does exist, the destination is replaced with the source (unless the noOverwrite option is true). 2. If the source is a directory that exists: a. If the destination is a directory, then the source directory is re-parented such that it becomes a child of the destination. b. If the destination does not exist, then the source is renamed, creating additional directories if needed. c. If the destination is a file, then an error occurs. 3. If the source does not exist, an error occurs. Differential Revision: https://phabricator.services.mozilla.com/D82202
This commit is contained in:
Родитель
16439a0ec2
Коммит
0dcdd65989
|
@ -25,6 +25,17 @@ namespace IOUtils {
|
|||
* @param data Data to write to the file at path.
|
||||
*/
|
||||
Promise<unsigned long long> writeAtomic(DOMString path, Uint8Array data, optional WriteAtomicOptions options = {});
|
||||
/**
|
||||
* Moves the file from |sourcePath| to |destPath|, creating necessary parents.
|
||||
* If |destPath| is a directory, then the source file will be moved into the
|
||||
* destination directory.
|
||||
*
|
||||
* @param sourcePath An absolute file path identifying the file or directory
|
||||
* to move.
|
||||
* @param destPath An absolute file path identifying the destination
|
||||
* directory and/or file name.
|
||||
*/
|
||||
Promise<void> move(DOMString sourcePath, DOMString destPath, optional MoveOptions options = {});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -52,3 +63,13 @@ dictionary WriteAtomicOptions {
|
|||
*/
|
||||
boolean flush = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options to be passed to the |IOUtils.move| method.
|
||||
*/
|
||||
dictionary MoveOptions {
|
||||
/**
|
||||
* If true, fail if the destination already exists.
|
||||
*/
|
||||
boolean noOverwrite = false;
|
||||
};
|
||||
|
|
|
@ -8,15 +8,21 @@
|
|||
|
||||
#include "mozilla/dom/IOUtils.h"
|
||||
#include "mozilla/dom/Promise.h"
|
||||
#include "mozilla/ErrorNames.h"
|
||||
#include "mozilla/Result.h"
|
||||
#include "mozilla/ResultExtensions.h"
|
||||
#include "mozilla/Services.h"
|
||||
#include "mozilla/Span.h"
|
||||
#include "nspr/prio.h"
|
||||
#include "nspr/private/pprio.h"
|
||||
#include "nspr/prtypes.h"
|
||||
#include "nsDirectoryServiceDefs.h"
|
||||
#include "nsIFile.h"
|
||||
#include "nsIGlobalObject.h"
|
||||
#include "nsReadableUtils.h"
|
||||
#include "nsString.h"
|
||||
#include "nsThreadManager.h"
|
||||
#include "SpecialSystemDirectory.h"
|
||||
|
||||
#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
|
||||
# include <fcntl.h>
|
||||
|
@ -45,6 +51,42 @@
|
|||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
// static helper functions
|
||||
|
||||
/**
|
||||
* Platform-specific (e.g. Windows, Unix) implementations of XPCOM APIs may
|
||||
* report I/O errors inconsistently. For convenience, this function will attempt
|
||||
* to match a |nsresult| against known results which imply a file cannot be
|
||||
* found.
|
||||
*
|
||||
* @see nsLocalFileWin.cpp
|
||||
* @see nsLocalFileUnix.cpp
|
||||
*/
|
||||
static bool IsFileNotFound(nsresult aResult) {
|
||||
switch (aResult) {
|
||||
case NS_ERROR_FILE_NOT_FOUND:
|
||||
case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static nsCString FormatErrorMessage(nsresult aError,
|
||||
const char* const aMessage) {
|
||||
const char* errName = GetStaticErrorName(aError);
|
||||
if (errName) {
|
||||
return nsPrintfCString("%s: %s", aMessage, errName);
|
||||
}
|
||||
// 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.
|
||||
return nsPrintfCString("%s: 0x%" PRIX32, aMessage,
|
||||
static_cast<uint32_t>(aError));
|
||||
}
|
||||
|
||||
// IOUtils implementation
|
||||
|
||||
/* static */
|
||||
StaticDataMutex<StaticRefPtr<nsISerialEventTarget>>
|
||||
IOUtils::sBackgroundEventTarget("sBackgroundEventTarget");
|
||||
|
@ -61,6 +103,9 @@ already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal,
|
|||
NS_ENSURE_TRUE(!!promise, nullptr);
|
||||
REJECT_IF_SHUTTING_DOWN(promise);
|
||||
|
||||
RefPtr<nsISerialEventTarget> bg = GetBackgroundEventTarget();
|
||||
REJECT_IF_NULL_EVENT_TARGET(bg, promise);
|
||||
|
||||
// Process arguments.
|
||||
uint32_t toRead = 0;
|
||||
if (aMaxBytes.WasPassed()) {
|
||||
|
@ -76,10 +121,6 @@ already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal,
|
|||
|
||||
NS_ConvertUTF16toUTF8 path(aPath);
|
||||
|
||||
// Do the IO on a background thread and return the result to this thread.
|
||||
RefPtr<nsISerialEventTarget> bg = GetBackgroundEventTarget();
|
||||
REJECT_IF_NULL_EVENT_TARGET(bg, promise);
|
||||
|
||||
InvokeAsync(
|
||||
bg, __func__,
|
||||
[path, toRead]() {
|
||||
|
@ -151,6 +192,9 @@ already_AddRefed<Promise> IOUtils::WriteAtomic(
|
|||
NS_ENSURE_TRUE(!!promise, nullptr);
|
||||
REJECT_IF_SHUTTING_DOWN(promise);
|
||||
|
||||
RefPtr<nsISerialEventTarget> bg = GetBackgroundEventTarget();
|
||||
REJECT_IF_NULL_EVENT_TARGET(bg, promise);
|
||||
|
||||
// Process arguments.
|
||||
aData.ComputeState();
|
||||
FallibleTArray<uint8_t> toWrite;
|
||||
|
@ -179,10 +223,6 @@ already_AddRefed<Promise> IOUtils::WriteAtomic(
|
|||
|
||||
NS_ConvertUTF16toUTF8 path(aPath);
|
||||
|
||||
// Do the IO on a background thread and return the result to this thread.
|
||||
RefPtr<nsISerialEventTarget> bg = GetBackgroundEventTarget();
|
||||
REJECT_IF_NULL_EVENT_TARGET(bg, promise);
|
||||
|
||||
InvokeAsync(bg, __func__,
|
||||
[path, flags, noOverwrite, toWrite = std::move(toWrite)]() {
|
||||
MOZ_ASSERT(!NS_IsMainThread());
|
||||
|
@ -227,6 +267,80 @@ already_AddRefed<Promise> IOUtils::WriteAtomic(
|
|||
return promise.forget();
|
||||
}
|
||||
|
||||
/* static */
|
||||
already_AddRefed<Promise> IOUtils::Move(GlobalObject& aGlobal,
|
||||
const nsAString& aSourcePath,
|
||||
const nsAString& aDestPath,
|
||||
const MoveOptions& aOptions) {
|
||||
RefPtr<Promise> promise = CreateJSPromise(aGlobal);
|
||||
NS_ENSURE_TRUE(!!promise, nullptr);
|
||||
REJECT_IF_SHUTTING_DOWN(promise);
|
||||
|
||||
RefPtr<nsISerialEventTarget> bg = GetBackgroundEventTarget();
|
||||
REJECT_IF_NULL_EVENT_TARGET(bg, promise);
|
||||
|
||||
// Process arguments.
|
||||
bool noOverwrite = false;
|
||||
if (aOptions.IsAnyMemberPresent()) {
|
||||
noOverwrite = aOptions.mNoOverwrite;
|
||||
}
|
||||
|
||||
InvokeAsync(bg, __func__,
|
||||
[srcPathString = nsAutoString(aSourcePath),
|
||||
destPathString = nsAutoString(aDestPath), noOverwrite]() {
|
||||
nsresult rv =
|
||||
MoveSync(srcPathString, destPathString, noOverwrite);
|
||||
if (NS_FAILED(rv)) {
|
||||
return IOMoveMozPromise::CreateAndReject(rv, __func__);
|
||||
}
|
||||
return IOMoveMozPromise::CreateAndResolve(true, __func__);
|
||||
})
|
||||
->Then(
|
||||
GetCurrentSerialEventTarget(), __func__,
|
||||
[promise = RefPtr(promise)](const bool&) {
|
||||
AutoJSAPI jsapi;
|
||||
if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
|
||||
promise->MaybeReject(NS_ERROR_UNEXPECTED);
|
||||
return;
|
||||
}
|
||||
promise->MaybeResolveWithUndefined();
|
||||
},
|
||||
[promise = RefPtr(promise)](const nsresult& aError) {
|
||||
switch (aError) {
|
||||
case NS_ERROR_FILE_ACCESS_DENIED:
|
||||
promise->MaybeRejectWithInvalidAccessError("Access denied");
|
||||
break;
|
||||
case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
|
||||
case NS_ERROR_FILE_NOT_FOUND:
|
||||
promise->MaybeRejectWithNotFoundError(
|
||||
"Source file does not exist");
|
||||
break;
|
||||
case NS_ERROR_FILE_ALREADY_EXISTS:
|
||||
promise->MaybeRejectWithNoModificationAllowedError(
|
||||
"Destination file exists and overwrites are not allowed");
|
||||
break;
|
||||
case NS_ERROR_FILE_READ_ONLY:
|
||||
promise->MaybeRejectWithNoModificationAllowedError(
|
||||
"Destination is read only");
|
||||
break;
|
||||
case NS_ERROR_FILE_DESTINATION_NOT_DIR:
|
||||
promise->MaybeRejectWithInvalidAccessError(
|
||||
"Source is a directory but destination is not");
|
||||
break;
|
||||
case NS_ERROR_FILE_UNRECOGNIZED_PATH:
|
||||
promise->MaybeRejectWithOperationError(
|
||||
"Only absolute file paths are permitted");
|
||||
break;
|
||||
default: {
|
||||
promise->MaybeRejectWithUnknownError(
|
||||
FormatErrorMessage(aError, "Unexpected error moving file"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return promise.forget();
|
||||
}
|
||||
|
||||
/* static */
|
||||
already_AddRefed<nsISerialEventTarget> IOUtils::GetBackgroundEventTarget() {
|
||||
if (sShutdownStarted) {
|
||||
|
@ -387,6 +501,68 @@ nsresult IOUtils::WriteSync(PRFileDesc* aFd, const nsTArray<uint8_t>& aBytes,
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
/* static */
|
||||
nsresult IOUtils::MoveSync(const nsAString& aSourcePath,
|
||||
const nsAString& aDestPath, bool noOverwrite) {
|
||||
MOZ_ASSERT(!NS_IsMainThread());
|
||||
nsresult rv = NS_OK;
|
||||
|
||||
nsCOMPtr<nsIFile> srcFile = new nsLocalFile();
|
||||
MOZ_TRY(srcFile->InitWithPath(aSourcePath)); // Fails if not absolute.
|
||||
MOZ_TRY(srcFile->Normalize()); // Fails if path does not exist.
|
||||
|
||||
nsCOMPtr<nsIFile> destFile = new nsLocalFile();
|
||||
MOZ_TRY(destFile->InitWithPath(aDestPath));
|
||||
rv = destFile->Normalize();
|
||||
// Normalize can fail for a number of reasons, including if the file doesn't
|
||||
// exist. It is expected that the file might not exist for a number of calls
|
||||
// (e.g. if we want to rename a file to a new location).
|
||||
if (!IsFileNotFound(rv)) { // Deliberately ignore "not found" errors.
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
// Case 1: Destination is an existing directory. Move source into dest.
|
||||
bool destIsDir = false;
|
||||
bool destExists = true;
|
||||
|
||||
rv = destFile->IsDirectory(&destIsDir);
|
||||
if (NS_SUCCEEDED(rv) && destIsDir) {
|
||||
return srcFile->MoveTo(destFile, EmptyString());
|
||||
}
|
||||
if (IsFileNotFound(rv)) {
|
||||
// It's ok if the file doesn't exist. Case 2 handles this below.
|
||||
destExists = false;
|
||||
} else {
|
||||
// Bail out early for any other kind of error though.
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
// Case 2: Destination is a file which may or may not exist. Try to rename the
|
||||
// source to the destination. This will fail if the source is a not a
|
||||
// regular file.
|
||||
if (noOverwrite && destExists) {
|
||||
return NS_ERROR_FILE_ALREADY_EXISTS;
|
||||
}
|
||||
if (destExists && !destIsDir) {
|
||||
// If the source file is a directory, but the target is a file, abort early.
|
||||
// Different implementations of |MoveTo| seem to handle this error case
|
||||
// differently (or not at all), so we explicitly handle it here.
|
||||
bool srcIsDir = false;
|
||||
MOZ_TRY(srcFile->IsDirectory(&srcIsDir));
|
||||
if (srcIsDir) {
|
||||
return NS_ERROR_FILE_DESTINATION_NOT_DIR;
|
||||
}
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIFile> destDir;
|
||||
nsAutoString destName;
|
||||
MOZ_TRY(destFile->GetLeafName(destName));
|
||||
MOZ_TRY(destFile->GetParent(getter_AddRefs(destDir)));
|
||||
|
||||
// NB: if destDir doesn't exist, then MoveTo will create it.
|
||||
return srcFile->MoveTo(destDir, destName);
|
||||
}
|
||||
|
||||
NS_IMPL_ISUPPORTS(IOUtilsShutdownBlocker, nsIAsyncShutdownBlocker);
|
||||
|
||||
NS_IMETHODIMP IOUtilsShutdownBlocker::GetName(nsAString& aName) {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#include "nspr/prio.h"
|
||||
#include "nsIAsyncShutdown.h"
|
||||
#include "nsISerialEventTarget.h"
|
||||
#include "nsLocalFile.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
|
@ -51,6 +52,11 @@ class IOUtils final {
|
|||
GlobalObject& aGlobal, const nsAString& aPath, const Uint8Array& aData,
|
||||
const WriteAtomicOptions& aOptions);
|
||||
|
||||
static already_AddRefed<Promise> Move(GlobalObject& aGlobal,
|
||||
const nsAString& aSourcePath,
|
||||
const nsAString& aDestPath,
|
||||
const MoveOptions& aOptions);
|
||||
|
||||
private:
|
||||
~IOUtils() = default;
|
||||
|
||||
|
@ -96,12 +102,19 @@ class IOUtils final {
|
|||
static nsresult WriteSync(PRFileDesc* aFd, const nsTArray<uint8_t>& aBytes,
|
||||
uint32_t& aResult);
|
||||
|
||||
static nsresult MoveSync(const nsAString& aSource, const nsAString& aDest,
|
||||
bool noOverwrite);
|
||||
|
||||
using IOReadMozPromise =
|
||||
mozilla::MozPromise<nsTArray<uint8_t>, const nsCString,
|
||||
/* IsExclusive */ true>;
|
||||
|
||||
using IOWriteMozPromise =
|
||||
mozilla::MozPromise<uint32_t, const nsCString, /* IsExclusive */ true>;
|
||||
|
||||
using IOMoveMozPromise =
|
||||
mozilla::MozPromise<bool /* ignored */, const nsresult,
|
||||
/* IsExclusive */ true>;
|
||||
};
|
||||
|
||||
class IOUtilsShutdownBlocker : public nsIAsyncShutdownBlocker {
|
||||
|
|
|
@ -14,11 +14,14 @@ importScripts("resource://gre/modules/ObjectUtils.jsm");
|
|||
importScripts("resource://gre/modules/osfile.jsm");
|
||||
|
||||
self.onmessage = async function(msg) {
|
||||
const tmpDir = OS.Constants.Path.tmpDir;
|
||||
|
||||
// IOUtils functionality is the same when called from the main thread, or a
|
||||
// web worker. These tests are a modified subset of the main thread tests, and
|
||||
// serve as a sanity check that the implementation is thread-safe.
|
||||
// serve as a confidence check that the implementation is thread-safe.
|
||||
await test_api_is_available_on_worker();
|
||||
await test_full_read_and_write();
|
||||
await test_move_file();
|
||||
|
||||
finish();
|
||||
info("test_ioutils_worker.xhtml: Test finished");
|
||||
|
@ -28,6 +31,7 @@ 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 bytes = Uint8Array.of(...new Array(50).keys());
|
||||
|
@ -56,6 +60,21 @@ self.onmessage = async function(msg) {
|
|||
|
||||
cleanup(tmpFileName);
|
||||
}
|
||||
|
||||
async function test_move_file() {
|
||||
const src = OS.Path.join(tmpDir, "test_move_file_src.tmp");
|
||||
const dest = OS.Path.join(tmpDir, "test_move_file_dest.tmp");
|
||||
const bytes = Uint8Array.of(...new Array(50).keys());
|
||||
await self.IOUtils.writeAtomic(src, bytes);
|
||||
|
||||
await self.IOUtils.move(src, dest);
|
||||
ok(
|
||||
!OS.File.exists(src) && OS.File.exists(dest),
|
||||
"IOUtils::move can move files from a worker"
|
||||
);
|
||||
|
||||
cleanup(dest);
|
||||
}
|
||||
};
|
||||
|
||||
function cleanup(...files) {
|
||||
|
|
|
@ -24,11 +24,14 @@
|
|||
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
|
||||
const tmpDir = OS.Constants.Path.tmpDir;
|
||||
|
||||
add_task(async function test_api_is_available_on_window() {
|
||||
ok(window.IOUtils, "IOUtils is present on the window");
|
||||
});
|
||||
|
||||
add_task(async function test_read_failure() {
|
||||
// TODO: Stop using relative paths.
|
||||
await Assert.rejects(
|
||||
window.IOUtils.read("does_not_exist.txt"),
|
||||
/Could not open file/,
|
||||
|
@ -37,6 +40,7 @@
|
|||
});
|
||||
|
||||
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 untouchableContents = new TextEncoder().encode("Can't touch this!\n");
|
||||
|
@ -66,6 +70,7 @@
|
|||
});
|
||||
|
||||
add_task(async function test_partial_read() {
|
||||
// TODO: Stop using relative paths.
|
||||
const tmpFileName = "test_ioutils_partial_read.tmp";
|
||||
const bytes = Uint8Array.of(...new Array(50).keys());
|
||||
const bytesWritten = await window.IOUtils.writeAtomic(tmpFileName, bytes);
|
||||
|
@ -92,6 +97,7 @@
|
|||
});
|
||||
|
||||
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";
|
||||
|
@ -115,7 +121,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 bytes = Uint8Array.of(...new Array(50).keys());
|
||||
const bytesWritten = await window.IOUtils.writeAtomic(tmpFileName, bytes);
|
||||
|
@ -126,6 +134,7 @@
|
|||
);
|
||||
|
||||
// Read it back.
|
||||
info("Test reading a binary file");
|
||||
let fileContents = await window.IOUtils.read(tmpFileName);
|
||||
ok(
|
||||
ObjectUtils.deepEqual(bytes, fileContents) &&
|
||||
|
@ -141,18 +150,220 @@
|
|||
"IOUtils::read can read entire file when requested maxBytes is too large"
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
await cleanup(tmpFileName);
|
||||
});
|
||||
|
||||
add_task(async function test_move_relative_path() {
|
||||
const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_move_relative_path.tmp");
|
||||
const dest = "relative_to_cwd.tmp";
|
||||
await createFile(tmpFileName, "source");
|
||||
|
||||
info("Test moving a file to a relative destination");
|
||||
await Assert.rejects(
|
||||
window.IOUtils.move(tmpFileName, dest),
|
||||
/Only absolute file paths are permitted/,
|
||||
"IOUtils::move only works with absolute paths"
|
||||
);
|
||||
ok(
|
||||
await fileHasTextContents(tmpFileName, "source"),
|
||||
"IOUtils::move doesn't change source file when move fails"
|
||||
);
|
||||
|
||||
cleanup(tmpFileName);
|
||||
});
|
||||
|
||||
add_task(async function test_move_rename() {
|
||||
// Set up.
|
||||
const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_move_src.tmp");
|
||||
const destFileName = OS.Path.join(tmpDir, "test_ioutils_move_dest.tmp");
|
||||
await createFile(tmpFileName, "dest");
|
||||
// Test.
|
||||
info("Test move to new file in same directory");
|
||||
await window.IOUtils.move(tmpFileName, destFileName);
|
||||
info(`moved ${tmpFileName} to ${destFileName}`);
|
||||
ok(
|
||||
!await fileExists(tmpFileName)
|
||||
&& await fileHasTextContents(destFileName, "dest"),
|
||||
"IOUtils::move can move source to dest in same directory"
|
||||
)
|
||||
|
||||
// Set up.
|
||||
info("Test move to existing file with no overwrite");
|
||||
await createFile(tmpFileName, "source");
|
||||
// Test.
|
||||
await Assert.rejects(
|
||||
window.IOUtils.move(tmpFileName, destFileName, { noOverwrite: true }),
|
||||
/Destination file exists and overwrites are not allowed/,
|
||||
"IOUtils::move will refuse to move a file if overwrites are disabled"
|
||||
);
|
||||
ok(
|
||||
await fileExists(tmpFileName)
|
||||
&& await fileHasTextContents(destFileName, "dest"),
|
||||
"Failed IOUtils::move doesn't move the source file"
|
||||
);
|
||||
|
||||
// Test.
|
||||
info("Test move to existing file with overwrite");
|
||||
await window.IOUtils.move(tmpFileName, destFileName, { noOverwrite: false });
|
||||
ok(!await fileExists(tmpFileName), "IOUtils::move moved source");
|
||||
ok(
|
||||
await fileHasTextContents(destFileName, "source"),
|
||||
"IOUtils::move overwrote the destination with the source"
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
await cleanup(tmpFileName, destFileName);
|
||||
});
|
||||
|
||||
add_task(async function test_move_to_dir() {
|
||||
// Set up.
|
||||
info("Test move and rename to non-existing directory");
|
||||
const tmpFileName = OS.Path.join(tmpDir, "test_move_to_dir.tmp");
|
||||
const destDir = OS.Path.join(tmpDir, "test_move_to_dir.tmp.d");
|
||||
const dest = OS.Path.join(destDir, "dest.tmp");
|
||||
await createFile(tmpFileName);
|
||||
// Test.
|
||||
ok(!await OS.File.exists(destDir), "Expected path not to exist");
|
||||
await window.IOUtils.move(tmpFileName, dest);
|
||||
ok(
|
||||
!await fileExists(tmpFileName) && await fileExists(dest),
|
||||
"IOUtils::move creates non-existing parents if needed"
|
||||
);
|
||||
|
||||
// Set up.
|
||||
info("Test move and rename to existing directory.")
|
||||
await createFile(tmpFileName);
|
||||
// Test.
|
||||
ok(await OS.File.exists(destDir), "Expected path to exist");
|
||||
await window.IOUtils.move(tmpFileName, dest);
|
||||
ok(
|
||||
!await fileExists(tmpFileName)
|
||||
&& await fileExists(dest),
|
||||
"IOUtils::move can move/rename a file into an existing dir"
|
||||
);
|
||||
|
||||
// Set up.
|
||||
info("Test move to existing directory without specifying leaf name.")
|
||||
await createFile(tmpFileName);
|
||||
// Test.
|
||||
await window.IOUtils.move(tmpFileName, destDir);
|
||||
ok(await OS.File.exists(destDir), "Expected path to exist");
|
||||
ok(
|
||||
!await fileExists(tmpFileName)
|
||||
&& await fileExists(OS.Path.join(destDir, OS.Path.basename(tmpFileName))),
|
||||
"IOUtils::move can move a file into an existing dir"
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
await OS.File.removeDir(destDir);
|
||||
});
|
||||
|
||||
add_task(async function test_move_dir() {
|
||||
// Set up.
|
||||
info("Test rename an empty directory");
|
||||
const srcDir = OS.Path.join(tmpDir, "test_move_dir.tmp.d");
|
||||
const destDir = OS.Path.join(tmpDir, "test_move_dir_dest.tmp.d");
|
||||
await OS.File.makeDir(srcDir);
|
||||
// Test.
|
||||
await window.IOUtils.move(srcDir, destDir);
|
||||
ok(
|
||||
!await OS.File.exists(srcDir) && await OS.File.exists(destDir),
|
||||
"IOUtils::move can rename directories"
|
||||
);
|
||||
|
||||
// Set up.
|
||||
info("Test move directory and its content into another directory");
|
||||
await OS.File.makeDir(srcDir);
|
||||
await createFile(OS.Path.join(srcDir, "file.tmp"), "foo");
|
||||
// Test.
|
||||
await window.IOUtils.move(srcDir, destDir);
|
||||
const destFile = OS.Path.join(destDir, OS.Path.basename(srcDir), "file.tmp");
|
||||
ok(
|
||||
!await OS.File.exists(srcDir)
|
||||
&& await OS.File.exists(destDir)
|
||||
&& await OS.File.exists(OS.Path.join(destDir, OS.Path.basename(srcDir)))
|
||||
&& await fileHasTextContents(destFile, "foo"),
|
||||
"IOUtils::move can move a directory and its contents into another one"
|
||||
)
|
||||
|
||||
// Clean up.
|
||||
await OS.File.removeDir(destDir);
|
||||
});
|
||||
|
||||
add_task(async function test_move_failures() {
|
||||
// Set up.
|
||||
info("Test attempt to rename a non-existent source file");
|
||||
const notExistsSrc = OS.Path.join(tmpDir, "not_exists_src.tmp");
|
||||
const notExistsDest = OS.Path.join(tmpDir, "not_exists_dest.tmp");
|
||||
// Test.
|
||||
await Assert.rejects(
|
||||
window.IOUtils.move(notExistsSrc, notExistsDest),
|
||||
/Source file does not exist/,
|
||||
"IOUtils::move throws if source file does not exist"
|
||||
);
|
||||
ok(
|
||||
!await fileExists(notExistsSrc) && !await fileExists(notExistsDest),
|
||||
"IOUtils::move fails if source file does not exist"
|
||||
);
|
||||
|
||||
// Set up.
|
||||
info("Test attempt to move a directory to a file");
|
||||
const destFile = OS.Path.join(tmpDir, "test_move_failures_file_dest.tmp");
|
||||
const srcDir = OS.Path.join(tmpDir, "test_move_failure_src.tmp.d");
|
||||
await createFile(destFile);
|
||||
await OS.File.makeDir(srcDir);
|
||||
// Test.
|
||||
await Assert.rejects(
|
||||
window.IOUtils.move(srcDir, destFile),
|
||||
/Source is a directory but destination is not/,
|
||||
"IOUtils::move throws if try to move dir into an existing file"
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
await cleanup(destFile);
|
||||
await OS.File.removeDir(srcDir);
|
||||
});
|
||||
|
||||
|
||||
// Utility functions.
|
||||
|
||||
async function createFile(location, contents = "") {
|
||||
if (typeof contents === "string") {
|
||||
contents = new TextEncoder().encode(contents);
|
||||
}
|
||||
await window.IOUtils.writeAtomic(location, contents);
|
||||
const exists = await fileExists(location);
|
||||
ok(exists, `Created temporary file at: ${location}`);
|
||||
}
|
||||
|
||||
async function fileHasTextContents(location, expectedContents) {
|
||||
if (typeof expectedContents !== "string") {
|
||||
throw new TypeError("expectedContents must be a string");
|
||||
}
|
||||
info(`opening ${location} for reading`);
|
||||
const bytes = await window.IOUtils.read(location);
|
||||
const contents = new TextDecoder().decode(bytes);
|
||||
return contents === expectedContents;
|
||||
}
|
||||
|
||||
async function fileExists(file) {
|
||||
try {
|
||||
await window.IOUtils.read(file);
|
||||
} catch (ex) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function cleanup(...files) {
|
||||
for (const file of files) {
|
||||
await OS.File.remove(file);
|
||||
const exists = await OS.File.exists(file);
|
||||
const exists = await fileExists(file);
|
||||
ok(!exists, `Removed temporary file: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче