Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#include "GetFilesHelper.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/Directory.h"
#include "mozilla/dom/FileBlobImpl.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/UnionTypes.h"
#include "mozilla/dom/IPCBlobUtils.h"
#include "mozilla/ipc/IPCStreamUtils.h"
#include "FileSystemUtils.h"
#include "nsNetCID.h"
#include "nsProxyRelease.h"
namespace mozilla::dom {
namespace {
// This class is used in the DTOR of GetFilesHelper to release resources in the
// correct thread.
class ReleaseRunnable final : public Runnable {
public:
static void MaybeReleaseOnMainThread(
nsTArray<RefPtr<Promise>>&& aPromises,
nsTArray<RefPtr<GetFilesCallback>>&& aCallbacks) {
if (NS_IsMainThread()) {
return;
}
RefPtr<ReleaseRunnable> runnable =
new ReleaseRunnable(std::move(aPromises), std::move(aCallbacks));
FileSystemUtils::DispatchRunnable(nullptr, runnable.forget());
}
NS_IMETHOD
Run() override {
MOZ_ASSERT(NS_IsMainThread());
mPromises.Clear();
mCallbacks.Clear();
return NS_OK;
}
private:
ReleaseRunnable(nsTArray<RefPtr<Promise>>&& aPromises,
nsTArray<RefPtr<GetFilesCallback>>&& aCallbacks)
: Runnable("dom::ReleaseRunnable"),
mPromises(std::move(aPromises)),
mCallbacks(std::move(aCallbacks)) {}
nsTArray<RefPtr<Promise>> mPromises;
nsTArray<RefPtr<GetFilesCallback>> mCallbacks;
};
} // namespace
///////////////////////////////////////////////////////////////////////////////
// GetFilesHelper Base class
already_AddRefed<GetFilesHelper> GetFilesHelper::Create(
const nsTArray<OwningFileOrDirectory>& aFilesOrDirectory,
bool aRecursiveFlag, ErrorResult& aRv) {
RefPtr<GetFilesHelper> helper;
if (XRE_IsParentProcess()) {
helper = new GetFilesHelper(aRecursiveFlag);
} else {
helper = new GetFilesHelperChild(aRecursiveFlag);
}
nsAutoString directoryPath;
for (uint32_t i = 0; i < aFilesOrDirectory.Length(); ++i) {
const OwningFileOrDirectory& data = aFilesOrDirectory[i];
if (data.IsFile()) {
if (!helper->mTargetBlobImplArray.AppendElement(data.GetAsFile()->Impl(),
fallible)) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return nullptr;
}
} else {
MOZ_ASSERT(data.IsDirectory());
// We support the upload of only 1 top-level directory from our
// directory picker. This means that we cannot have more than 1
// Directory object in aFilesOrDirectory array.
MOZ_ASSERT(directoryPath.IsEmpty());
RefPtr<Directory> directory = data.GetAsDirectory();
MOZ_ASSERT(directory);
aRv = directory->GetFullRealPath(directoryPath);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
}
}
// No directories to explore.
if (directoryPath.IsEmpty()) {
helper->mListingCompleted = true;
return helper.forget();
}
MOZ_ASSERT(helper->mTargetBlobImplArray.IsEmpty());
helper->SetDirectoryPath(directoryPath);
helper->Work(aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
return helper.forget();
}
GetFilesHelper::GetFilesHelper(bool aRecursiveFlag)
: Runnable("GetFilesHelper"),
GetFilesHelperBase(aRecursiveFlag),
mListingCompleted(false),
mErrorResult(NS_OK),
mMutex("GetFilesHelper::mMutex"),
mCanceled(false) {}
GetFilesHelper::~GetFilesHelper() {
ReleaseRunnable::MaybeReleaseOnMainThread(std::move(mPromises),
std::move(mCallbacks));
}
void GetFilesHelper::AddPromise(Promise* aPromise) {
MOZ_ASSERT(aPromise);
// Still working.
if (!mListingCompleted) {
mPromises.AppendElement(aPromise);
return;
}
MOZ_ASSERT(mPromises.IsEmpty());
ResolveOrRejectPromise(aPromise);
}
void GetFilesHelper::AddCallback(GetFilesCallback* aCallback) {
MOZ_ASSERT(aCallback);
// Still working.
if (!mListingCompleted) {
mCallbacks.AppendElement(aCallback);
return;
}
MOZ_ASSERT(mCallbacks.IsEmpty());
RunCallback(aCallback);
}
void GetFilesHelper::Unlink() {
mPromises.Clear();
mCallbacks.Clear();
{
MutexAutoLock lock(mMutex);
mCanceled = true;
}
Cancel();
}
void GetFilesHelper::Traverse(nsCycleCollectionTraversalCallback& cb) {
GetFilesHelper* tmp = this;
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromises);
}
void GetFilesHelper::Work(ErrorResult& aRv) {
nsCOMPtr<nsIEventTarget> target =
do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
MOZ_ASSERT(target);
aRv = target->Dispatch(this, NS_DISPATCH_NORMAL);
}
NS_IMETHODIMP
GetFilesHelper::Run() {
MOZ_ASSERT(!mDirectoryPath.IsEmpty());
MOZ_ASSERT(!mListingCompleted);
// First step is to retrieve the list of file paths.
// This happens in the I/O thread.
if (!NS_IsMainThread()) {
RunIO();
// If this operation has been canceled, we don't have to go back to
// main-thread.
if (IsCanceled()) {
return NS_OK;
}
RefPtr<Runnable> runnable = this;
return FileSystemUtils::DispatchRunnable(nullptr, runnable.forget());
}
// We are here, but we should not do anything on this thread because, in the
// meantime, the operation has been canceled.
if (IsCanceled()) {
return NS_OK;
}
OperationCompleted();
return NS_OK;
}
void GetFilesHelper::OperationCompleted() {
// We mark the operation as completed here.
mListingCompleted = true;
// Let's process the pending promises.
nsTArray<RefPtr<Promise>> promises = std::move(mPromises);
for (uint32_t i = 0; i < promises.Length(); ++i) {
ResolveOrRejectPromise(promises[i]);
}
// Let's process the pending callbacks.
nsTArray<RefPtr<GetFilesCallback>> callbacks = std::move(mCallbacks);
for (uint32_t i = 0; i < callbacks.Length(); ++i) {
RunCallback(callbacks[i]);
}
}
void GetFilesHelper::RunIO() {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(!mDirectoryPath.IsEmpty());
MOZ_ASSERT(!mListingCompleted);
nsCOMPtr<nsIFile> file;
mErrorResult = NS_NewLocalFile(mDirectoryPath, getter_AddRefs(file));
if (NS_WARN_IF(NS_FAILED(mErrorResult))) {
return;
}
nsAutoString leafName;
mErrorResult = file->GetLeafName(leafName);
if (NS_WARN_IF(NS_FAILED(mErrorResult))) {
return;
}
nsAutoString domPath;
domPath.AssignLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
domPath.Append(leafName);
mErrorResult = ExploreDirectory(domPath, file);
}
nsresult GetFilesHelperBase::ExploreDirectory(const nsAString& aDOMPath,
nsIFile* aFile) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(aFile);
// We check if this operation has to be terminated at each recursion.
if (IsCanceled()) {
return NS_OK;
}
nsCOMPtr<nsIDirectoryEnumerator> entries;
nsresult rv = aFile->GetDirectoryEntries(getter_AddRefs(entries));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
for (;;) {
nsCOMPtr<nsIFile> currFile;
if (NS_WARN_IF(NS_FAILED(entries->GetNextFile(getter_AddRefs(currFile)))) ||
!currFile) {
break;
}
bool isLink, isSpecial, isFile, isDir;
if (NS_WARN_IF(NS_FAILED(currFile->IsSymlink(&isLink)) ||
NS_FAILED(currFile->IsSpecial(&isSpecial))) ||
isSpecial ||
// Although we allow explicit individual selection of symlinks via the
// file picker, we do not process symlinks in directory traversal. Our
// specific policy decision is documented at
isLink) {
continue;
}
if (NS_WARN_IF(NS_FAILED(currFile->IsFile(&isFile)) ||
NS_FAILED(currFile->IsDirectory(&isDir))) ||
!(isFile || isDir)) {
continue;
}
// The new domPath
nsAutoString domPath;
domPath.Assign(aDOMPath);
if (!aDOMPath.EqualsLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL)) {
domPath.AppendLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
}
nsAutoString leafName;
if (NS_WARN_IF(NS_FAILED(currFile->GetLeafName(leafName)))) {
continue;
}
domPath.Append(leafName);
if (isFile) {
RefPtr<BlobImpl> blobImpl = new FileBlobImpl(currFile);
blobImpl->SetDOMPath(domPath);
if (!mTargetBlobImplArray.AppendElement(blobImpl, fallible)) {
return NS_ERROR_OUT_OF_MEMORY;
}
continue;
}
MOZ_ASSERT(isDir);
if (!mRecursiveFlag) {
continue;
}
// Recursive.
rv = ExploreDirectory(domPath, currFile);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
void GetFilesHelper::ResolveOrRejectPromise(Promise* aPromise) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mListingCompleted);
MOZ_ASSERT(aPromise);
Sequence<RefPtr<File>> files;
if (NS_SUCCEEDED(mErrorResult)) {
for (uint32_t i = 0; i < mTargetBlobImplArray.Length(); ++i) {
RefPtr<File> domFile =
File::Create(aPromise->GetParentObject(), mTargetBlobImplArray[i]);
if (NS_WARN_IF(!domFile)) {
mErrorResult = NS_ERROR_FAILURE;
files.Clear();
break;
}
if (!files.AppendElement(domFile, fallible)) {
mErrorResult = NS_ERROR_OUT_OF_MEMORY;
files.Clear();
break;
}
}
}
// Error propagation.
if (NS_FAILED(mErrorResult)) {
aPromise->MaybeReject(mErrorResult);
return;
}
aPromise->MaybeResolve(files);
}
void GetFilesHelper::RunCallback(GetFilesCallback* aCallback) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mListingCompleted);
MOZ_ASSERT(aCallback);
aCallback->Callback(mErrorResult, mTargetBlobImplArray);
}
///////////////////////////////////////////////////////////////////////////////
// GetFilesHelperChild class
void GetFilesHelperChild::Work(ErrorResult& aRv) {
ContentChild* cc = ContentChild::GetSingleton();
if (NS_WARN_IF(!cc)) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
aRv = nsID::GenerateUUIDInPlace(mUUID);
if (NS_WARN_IF(aRv.Failed())) {
return;
}
mPendingOperation = true;
cc->CreateGetFilesRequest(mDirectoryPath, mRecursiveFlag, mUUID, this);
}
void GetFilesHelperChild::Cancel() {
if (!mPendingOperation) {
return;
}
ContentChild* cc = ContentChild::GetSingleton();
if (NS_WARN_IF(!cc)) {
return;
}
mPendingOperation = false;
cc->DeleteGetFilesRequest(mUUID, this);
}
bool GetFilesHelperChild::AppendBlobImpl(BlobImpl* aBlobImpl) {
MOZ_ASSERT(mPendingOperation);
MOZ_ASSERT(aBlobImpl);
MOZ_ASSERT(aBlobImpl->IsFile());
return mTargetBlobImplArray.AppendElement(aBlobImpl, fallible);
}
void GetFilesHelperChild::Finished(nsresult aError) {
MOZ_ASSERT(mPendingOperation);
MOZ_ASSERT(NS_SUCCEEDED(mErrorResult));
mPendingOperation = false;
mErrorResult = aError;
OperationCompleted();
}
///////////////////////////////////////////////////////////////////////////////
// GetFilesHelperParent class
class GetFilesHelperParentCallback final : public GetFilesCallback {
public:
explicit GetFilesHelperParentCallback(GetFilesHelperParent* aParent)
: mParent(aParent) {
MOZ_ASSERT(aParent);
}
void Callback(nsresult aStatus,
const FallibleTArray<RefPtr<BlobImpl>>& aBlobImpls) override {
if (NS_FAILED(aStatus)) {
mParent->mContentParent->SendGetFilesResponseAndForget(
mParent->mUUID, GetFilesResponseFailure(aStatus));
return;
}
GetFilesResponseSuccess success;
nsTArray<IPCBlob>& ipcBlobs = success.blobs();
ipcBlobs.SetLength(aBlobImpls.Length());
for (uint32_t i = 0; i < aBlobImpls.Length(); ++i) {
nsresult rv = IPCBlobUtils::Serialize(aBlobImpls[i], ipcBlobs[i]);
if (NS_WARN_IF(NS_FAILED(rv))) {
mParent->mContentParent->SendGetFilesResponseAndForget(
mParent->mUUID, GetFilesResponseFailure(NS_ERROR_OUT_OF_MEMORY));
return;
}
}
mParent->mContentParent->SendGetFilesResponseAndForget(mParent->mUUID,
success);
}
private:
// Raw pointer because this callback is kept alive by this parent object.
GetFilesHelperParent* mParent;
};
GetFilesHelperParent::GetFilesHelperParent(const nsID& aUUID,
ContentParent* aContentParent,
bool aRecursiveFlag)
: GetFilesHelper(aRecursiveFlag),
mContentParent(aContentParent),
mUUID(aUUID) {}
GetFilesHelperParent::~GetFilesHelperParent() {
NS_ReleaseOnMainThread("GetFilesHelperParent::mContentParent",
mContentParent.forget());
}
/* static */
already_AddRefed<GetFilesHelperParent> GetFilesHelperParent::Create(
const nsID& aUUID, const nsAString& aDirectoryPath, bool aRecursiveFlag,
ContentParent* aContentParent, ErrorResult& aRv) {
MOZ_ASSERT(aContentParent);
RefPtr<GetFilesHelperParent> helper =
new GetFilesHelperParent(aUUID, aContentParent, aRecursiveFlag);
helper->SetDirectoryPath(aDirectoryPath);
helper->Work(aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
RefPtr<GetFilesHelperParentCallback> callback =
new GetFilesHelperParentCallback(helper);
helper->AddCallback(callback);
return helper.forget();
}
} // namespace mozilla::dom