Commit 7a60462f authored by Xiaocheng Hu's avatar Xiaocheng Hu Committed by Commit Bot

Revert "Initial implementation of the FileSystemScanner"

This reverts commit 1e9bd7b2.

Reason for revert: Tests crash on MSan. See

https://ci.chromium.org/p/chromium/builders/ci/Linux%20ChromiumOS%20MSan%20Tests/17015

Original change's description:
> Initial implementation of the FileSystemScanner
> 
> This implements the initial version of FileSystemScanner
> (go/arc-fs-scanner).
> 
> Bug: b:145254635
> Test: unit_tests --gtest_filter="Arc*"
> Test: Manually populated a big file system and triggered different file
> Test: system events.
> Change-Id: I14bebb3719532afac54e1913229045fb2740a9ff
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1946177
> Reviewed-by: Hidehiko Abe <hidehiko@chromium.org>
> Reviewed-by: Mattias Nissler <mnissler@chromium.org>
> Commit-Queue: Umut Barış Öztunç <umutoztunc@google.com>
> Auto-Submit: Umut Barış Öztunç <umutoztunc@google.com>
> Cr-Commit-Position: refs/heads/master@{#727555}

TBR=hashimoto@chromium.org,yusukes@chromium.org,fukino@chromium.org,hidehiko@chromium.org,elijahtaylor@chromium.org,mnissler@chromium.org,kerrnel@chromium.org,risan@chromium.org,umutoztunc@google.com

Change-Id: I9b27f3bdd8d74586efdd6e141d0debc8b85b4a70
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: b:145254635
Bug: 1038049
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1981267Reviewed-by: default avatarXiaocheng Hu <xiaochengh@chromium.org>
Commit-Queue: Xiaocheng Hu <xiaochengh@chromium.org>
Cr-Commit-Position: refs/heads/master@{#727579}
parent 861452b2
......@@ -531,8 +531,6 @@ source_set("chromeos") {
"arc/file_system_watcher/arc_file_system_watcher_service.h",
"arc/file_system_watcher/arc_file_system_watcher_util.cc",
"arc/file_system_watcher/arc_file_system_watcher_util.h",
"arc/file_system_watcher/file_system_scanner.cc",
"arc/file_system_watcher/file_system_scanner.h",
"arc/fileapi/arc_content_file_system_async_file_util.cc",
"arc/fileapi/arc_content_file_system_async_file_util.h",
"arc/fileapi/arc_content_file_system_backend_delegate.cc",
......@@ -2597,7 +2595,6 @@ source_set("unit_tests") {
"arc/extensions/arc_support_message_host_unittest.cc",
"arc/file_system_watcher/arc_file_system_watcher_service_unittest.cc",
"arc/file_system_watcher/arc_file_system_watcher_util_unittest.cc",
"arc/file_system_watcher/file_system_scanner_unittest.cc",
"arc/fileapi/arc_content_file_system_async_file_util_unittest.cc",
"arc/fileapi/arc_content_file_system_file_stream_reader_unittest.cc",
"arc/fileapi/arc_content_file_system_file_stream_writer_unittest.cc",
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/arc/file_system_watcher/file_system_scanner.h"
#include <fts.h>
#include <sys/stat.h>
#include "base/files/file_enumerator.h"
#include "base/sequenced_task_runner.h"
#include "base/task/post_task.h"
#include "chrome/browser/chromeos/arc/file_system_watcher/arc_file_system_watcher_util.h"
#include "components/arc/mojom/file_system.mojom.h"
#include "components/arc/session/arc_bridge_service.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
using content::BrowserThread;
namespace arc {
namespace {
struct FTSCloser {
void operator()(FTS* fts) {
if (fts)
fts_close(fts);
}
};
// The scan interval for FileSystemScanner instances.
// This value needs to be bigger than 1 second since we subtract 1 second
// inside IsModified function to handle the timekeeper issue. In order to get
// more information about the issue, please check the comments above IsModified
// function.
//
// TODO(crbug/1037824): Measure the battery usage to find an optimal value for
// this.
constexpr base::TimeDelta kRegularScanInterval =
base::TimeDelta::FromSeconds(5);
// This value is used to handle the delay caused by timekeeper when reading the
// ctime values. For more information, please read the comments inside
// IsModified function.
//
// This value MUST NOT exceed |kRegularScanInterval|.
constexpr base::TimeDelta kCtimeCorrection = base::TimeDelta::FromSeconds(1);
// Returns ctime for the file |path| using stat(2).
// If stat fails for some reason, e.g., the file does not exists, then it
// returns base::Time().
base::Time GetLastChangedTime(const base::FilePath& path) {
struct stat st = {};
const int res = stat(path.value().c_str(), &st);
if (res < 0) {
DPLOG(ERROR) << "Couldn't stat " << path.value();
return base::Time();
}
return base::Time::FromTimeSpec(st.st_ctim);
}
// Returns true if ctime of |path| is after |previous_scan_time|.
bool IsModified(const base::FilePath& path,
base::Time previous_scan_time,
FileSystemScanner::GetLastChangeTimeCallback ctime_callback) {
// It is possible that ctime of the file might be before |previous_scan_time|
// even if the file has been modified after |previous_scan_time|.
//
// The reason for this is the concept of timekeeping in linux kernel. When
// writing to a file, ctime is updated using the value from the timekeeper
// which is updated by interrupts. However, when we use base::Time::Now() to
// update |previous_scan_time|, that call reads the system clock time directly
// instead of reading it from the timekeeper. Therefore, if the file is
// modified just after |previous_scan_time| is updated, it is possible
// that ctime is updated before an interrupt has been triggered.
//
// Thus, we add |kCtimeCorrection| to the ctime before the comparison to
// ensure we do not miss any modifications.
//
// Since |previous_scan_time| is updated at the beginning of a scan, it might
// be possible for a file to be identified as modified twice if it is modified
// after the |previous_scan_time| is stored but before the file is checked by
// the scan. It is expected and preferred compared to miss a file
// modification, e.g., if we store |previous_scan_time| at the end of a scan.
base::Time ctime = ctime_callback.Run(path) + kCtimeCorrection;
return previous_scan_time <= ctime;
}
// The following functions are made non-member functions because they can be
// executed on any thread other than the UI thread, and the FileSystemScanner is
// created on UI thread.
//
// It is not possible to PostTask an object's member function to a thread
// other than the one where the object is allocated, using weak pointers.
// Returns Android paths of all media files (recursively) under |directory|.
std::vector<std::string> FullScan(const base::FilePath& directory,
base::Time previous_scan_time,
const base::FilePath& cros_dir,
const base::FilePath& android_dir) {
std::vector<std::string> media_files;
base::FileEnumerator file_enum(directory, true /* recursive */,
base::FileEnumerator::FILES);
for (auto path = file_enum.Next(); !path.empty(); path = file_enum.Next()) {
if (!HasAndroidSupportedMediaExtension(path))
continue;
media_files.push_back(GetAndroidPath(path, cros_dir, android_dir).value());
}
return media_files;
}
// Iterates over the files in |cros_dir| and calls respective mojo functions
// for modified files and directories.
RegularScanResult RegularScan(
base::Time previous_scan_time,
const base::FilePath& cros_dir,
const base::FilePath& android_dir,
FileSystemScanner::GetLastChangeTimeCallback ctime_callback) {
char* argv[] = {const_cast<char*>(cros_dir.value().c_str()), nullptr};
std::unique_ptr<FTS, FTSCloser> fts{
fts_open(argv, FTS_PHYSICAL | FTS_NOCHDIR | FTS_XDEV, nullptr)};
if (!fts) {
LOG(ERROR) << "Regular scan failed because the path cannot be opened.";
return {};
}
RegularScanResult result;
// Keeps track of the last modified directory to force RequestMediaScan for
// all media files under a modified directory.
base::FilePath topmost_modified_dir;
// FileEnumerator does not guarantee the traversal ordering. Thus, fts_read is
// preferred here since it provides the preorder traversal which is important
// to cache |topmost_modified_dir|.
FTSENT* p;
while ((p = fts_read(fts.get())) != nullptr) {
base::FilePath path(p->fts_path);
if (p->fts_info == FTS_D) {
if (!IsModified(path, previous_scan_time, ctime_callback))
continue;
if (!topmost_modified_dir.IsParent(path))
topmost_modified_dir = path;
// Since the directory is modified, push it into the buffer to search for
// removed files and remove their entries from MediaStore.
result.modified_directories.push_back(
GetAndroidPath(path, cros_dir, android_dir).value());
} else if (p->fts_info == FTS_F &&
HasAndroidSupportedMediaExtension(path)) {
if (IsModified(path, previous_scan_time, ctime_callback) ||
topmost_modified_dir.IsParent(path)) {
// If it is modified or its under a directory which is modified, push it
// into the buffer to trigger media scan later.
result.modified_files.push_back(
GetAndroidPath(path, cros_dir, android_dir).value());
}
}
}
return result;
}
} // namespace
RegularScanResult::RegularScanResult() = default;
RegularScanResult::~RegularScanResult() = default;
RegularScanResult::RegularScanResult(RegularScanResult&&) = default;
RegularScanResult& RegularScanResult::operator=(RegularScanResult&&) = default;
FileSystemScanner::FileSystemScanner(const base::FilePath& cros_dir,
const base::FilePath& android_dir,
ArcBridgeService* arc_bridge_service)
: FileSystemScanner(cros_dir,
android_dir,
arc_bridge_service,
base::BindRepeating(&GetLastChangedTime)) {}
FileSystemScanner::FileSystemScanner(const base::FilePath& cros_dir,
const base::FilePath& android_dir,
ArcBridgeService* arc_bridge_service,
GetLastChangeTimeCallback ctime_callback)
: state_(State::kIdle),
cros_dir_(cros_dir),
android_dir_(android_dir),
arc_bridge_service_(arc_bridge_service),
scan_runner_(
base::CreateSequencedTaskRunner({base::ThreadPool(), base::MayBlock(),
base::TaskPriority::BEST_EFFORT})),
ctime_callback_(ctime_callback) {}
FileSystemScanner::~FileSystemScanner() = default;
void FileSystemScanner::Start() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
ScheduleFullScan();
}
// TODO(risan): Consider to rename this to "Post".
void FileSystemScanner::ScheduleFullScan() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Stop the timer to prevent scheduling regular scans before the full scan
// finishes.
timer_.Stop();
// Invalidate WeakPtrs to cancel callbacks to prevent the scanner being idle.
weak_ptr_factory_.InvalidateWeakPtrs();
state_ = State::kWaitingForScanToFinish;
// TODO(risan): We could skip a bunch of FullScan queued in the sequence if
// several SystemTimeClock change happened later (in separate CL). To do the
// skip, we could have a boolean variable full_scan_requested, and PostTask
// full_scan_requested from the OnFullScanFinished.
base::PostTaskAndReplyWithResult(
scan_runner_.get(), FROM_HERE,
base::BindOnce(&FullScan, cros_dir_, previous_scan_time_, cros_dir_,
android_dir_),
base::BindOnce(&FileSystemScanner::OnFullScanFinished,
weak_ptr_factory_.GetWeakPtr(), base::Time::Now()));
ReindexDirectory(android_dir_.value());
}
void FileSystemScanner::OnFullScanFinished(
base::Time current_scan_time,
std::vector<std::string> media_files) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(state_, State::kWaitingForScanToFinish);
if (!media_files.empty())
RequestMediaScan(media_files);
previous_scan_time_ = current_scan_time;
state_ = State::kIdle;
timer_.Start(FROM_HERE, kRegularScanInterval, this,
&FileSystemScanner::ScheduleRegularScan);
}
void FileSystemScanner::ScheduleRegularScan() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Do not schedule another scan if there is an ongoing scan happening.
if (state_ != State::kIdle)
return;
state_ = State::kWaitingForScanToFinish;
base::PostTaskAndReplyWithResult(
scan_runner_.get(), FROM_HERE,
base::BindOnce(&RegularScan, previous_scan_time_, cros_dir_, android_dir_,
ctime_callback_),
base::BindOnce(&FileSystemScanner::OnRegularScanFinished,
weak_ptr_factory_.GetWeakPtr(), base::Time::Now()));
}
void FileSystemScanner::OnRegularScanFinished(base::Time current_scan_time,
RegularScanResult result) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_NE(state_, State::kIdle);
if (!result.modified_files.empty())
RequestMediaScan(result.modified_files);
if (!result.modified_directories.empty())
RequestFileRemovalScan(result.modified_directories);
previous_scan_time_ = current_scan_time;
state_ = State::kIdle;
}
void FileSystemScanner::ReindexDirectory(const std::string& directory) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
auto* instance = ARC_GET_INSTANCE_FOR_METHOD(
arc_bridge_service_->file_system(), ReindexDirectory);
if (!instance) {
LOG(WARNING) << "Failed to call ReindexDirectory.";
return;
}
instance->ReindexDirectory(directory);
}
void FileSystemScanner::RequestFileRemovalScan(
const std::vector<std::string>& directories) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
auto* instance = ARC_GET_INSTANCE_FOR_METHOD(
arc_bridge_service_->file_system(), RequestFileRemovalScan);
if (!instance) {
LOG(WARNING) << "Failed to call RequestFileRemovalScan.";
return;
}
instance->RequestFileRemovalScan(directories);
}
void FileSystemScanner::RequestMediaScan(
const std::vector<std::string>& files) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
auto* instance = ARC_GET_INSTANCE_FOR_METHOD(
arc_bridge_service_->file_system(), RequestMediaScan);
if (!instance) {
LOG(WARNING) << "Failed to call RequestMediaScan.";
return;
}
instance->RequestMediaScan(files);
}
} // namespace arc
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_CHROMEOS_ARC_FILE_SYSTEM_WATCHER_FILE_SYSTEM_SCANNER_H_
#define CHROME_BROWSER_CHROMEOS_ARC_FILE_SYSTEM_WATCHER_FILE_SYSTEM_SCANNER_H_
#include <string>
#include <vector>
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
namespace arc {
class ArcBridgeService;
struct RegularScanResult {
RegularScanResult();
~RegularScanResult();
RegularScanResult(RegularScanResult&&);
RegularScanResult& operator=(RegularScanResult&&);
std::vector<std::string> modified_files;
std::vector<std::string> modified_directories;
};
// Periodical scanner to detect file system modifications in a directory. It is
// activated when FileSystemWatcher throws an error, e.g., the file system is
// too large.
// TODO(risan): Address all remaining feedbacks from next iterations of
// https://chromium-review.googlesource.com/c/chromium/src/+/1946177 before
// enabling this.
class FileSystemScanner {
public:
using GetLastChangeTimeCallback =
base::RepeatingCallback<base::Time(const base::FilePath& path)>;
// |cros_dir| is the directory that will be scanned by the scanner.
// |android_dir| is the respective path of |cros_dir| which is mounted to the
// Android container.
FileSystemScanner(const base::FilePath& cros_dir,
const base::FilePath& android_dir,
ArcBridgeService* arc_bridge_service);
// This constructor is only testing.
FileSystemScanner(const base::FilePath& cros_dir,
const base::FilePath& android_dir,
ArcBridgeService* arc_bridge_service,
GetLastChangeTimeCallback ctime_callback);
FileSystemScanner(const FileSystemScanner&) = delete;
FileSystemScanner& operator=(const FileSystemScanner&) = delete;
~FileSystemScanner();
// Starts scanning the directory.
void Start();
private:
// Schedules a full scan that triggers RequestMediaScan for all files and
// triggers ReindexDirectory for |android_dir_|. The reason for reindexing is
// to ensure that there are no indexes for non-existing files in MediaStore.
void ScheduleFullScan();
// Called after a full scan is finished. It updates |previous_scan_time_|
// accordingly. It also updates the state, so that a regular scan can be
// scheduled.
void OnFullScanFinished(base::Time current_scan_time,
std::vector<std::string> media_files);
// Schedules a regular scan if there is no ongoing scan at that time. Regular
// scan triggers RequestMediaScan only for the files that are modified after
// the previous (full or regular) scan. It also calls RequestFileRemovalScan
// for the modified directories to detect the removed files and directories
// under them. So that, their entries are removed from MediaStore.
void ScheduleRegularScan();
// Called after a regular scan is finished. It updates |previous_scan_time_|
// accordingly. It also updates the state, so that another regular scan can be
// done without skipping unless there is a full scan scheduled.
void OnRegularScanFinished(base::Time current_scan_time,
RegularScanResult result);
// Wrapper function that calls ReindexDirectory through mojo interface.
void ReindexDirectory(const std::string& directory_path);
// Wrapper function that calls RequestFileRemovalScan through mojo interface.
void RequestFileRemovalScan(const std::vector<std::string>& directory_paths);
// Wrapper function that calls RequestMediaScan through mojo interface.
void RequestMediaScan(const std::vector<std::string>& files);
// Internal state which is used to skip regular scans when there is an ongoing
// regular scan or there is a full scan scheduled and has not finished yet.
enum class State {
// Neither a regular scan nor full scan is happening. A scan can only be
// scheduled in this state.
kIdle,
// There is a scan which is already PostTask'ed but haven't finished yet.
// State will return to |kIdle| in the OnScanFinished function.
kWaitingForScanToFinish,
};
State state_;
const base::FilePath cros_dir_;
const base::FilePath android_dir_;
ArcBridgeService* const arc_bridge_service_;
// The timestamp of the start of the last scan.
base::Time previous_scan_time_;
// The task runner which runs the scans to avoid blocking the UI thread.
scoped_refptr<base::SequencedTaskRunner> scan_runner_;
// Calls ScheduleRegularScan every |kFileSystemScannerInterval|.
base::RepeatingTimer timer_;
GetLastChangeTimeCallback ctime_callback_;
base::WeakPtrFactory<FileSystemScanner> weak_ptr_factory_{this};
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest, ScheduleFullScan);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanCreateTopLevelFile);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanCreateTopLevelDirectory);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanModifyTopLevelFile);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanModifyTopLevelDirectory);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanRenameTopLevelFile);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanRenameTopLevelDirectory);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanDeleteTopLevelFile);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanDeleteTopLevelDirectory);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanCreateNestedFile);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanCreateNestedDirectory);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanModifyNestedFile);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanModifyNestedDirectory);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanRenameNestedFile);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanRenameNestedDirectory);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanDeleteNestedFile);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanDeleteNestedDirectory);
FRIEND_TEST_ALL_PREFIXES(ArcFileSystemScannerTest,
ScheduleRegularScanNoChange);
};
} // namespace arc
#endif // CHROME_BROWSER_CHROMEOS_ARC_FILE_SYSTEM_WATCHER_FILE_SYSTEM_SCANNER_H_
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/arc/file_system_watcher/file_system_scanner.h"
#include <unistd.h>
#include <string>
#include <vector>
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "chrome/browser/chromeos/arc/file_system_watcher/arc_file_system_watcher_util.h"
#include "chrome/browser/chromeos/arc/fileapi/arc_file_system_bridge.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/arc/arc_service_manager.h"
#include "components/arc/session/arc_bridge_service.h"
#include "components/arc/test/connection_holder_util.h"
#include "components/arc/test/fake_file_system_instance.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
using ::testing::AnyNumber;
using ::testing::Return;
namespace arc {
namespace {
constexpr char kTestingProfileName[] = "test-user";
class FileUtil {
public:
void SetLastChangeTime(const base::FilePath& path, base::Time ctime) {
ctimes_[path] = ctime;
}
base::Time GetLastChangeTime(const base::FilePath& path) {
return ctimes_[path];
}
private:
std::map<base::FilePath, base::Time> ctimes_;
};
} // namespace
class ArcFileSystemScannerTest : public ::testing::Test {
protected:
void SetUp() override {
android_dir_ = base::FilePath(FakeFileSystemInstance::kFakeAndroidPath);
// TODO(risan): ASSERT_TRUE inside this won't terminate. Instead, this
// should return boolean and we should return from this SetUp() when it is
// false.
CreateDummyFilesAndDirectories();
// Setting up profile.
profile_manager_ = std::make_unique<TestingProfileManager>(
TestingBrowserProcess::GetGlobal());
ASSERT_TRUE(profile_manager_->SetUp());
Profile* profile =
profile_manager_->CreateTestingProfile(kTestingProfileName);
// Setting up FakeFileSystemInstance.
auto ctime_callback = base::BindRepeating(&FileUtil::GetLastChangeTime,
base::Unretained(&file_util_));
file_system_instance_.SetGetLastChangeTimeCallback(ctime_callback);
file_system_instance_.SetCrosDir(temp_dir_.GetPath());
// Setting up ArcBridgeService and inject FakeFileSystemInstance.
arc_file_system_bridge_ =
std::make_unique<ArcFileSystemBridge>(profile, &arc_bridge_service_);
arc_bridge_service_.file_system()->SetInstance(&file_system_instance_);
WaitForInstanceReady(arc_bridge_service_.file_system());
ASSERT_TRUE(file_system_instance_.InitCalled());
// Setting up the FileSystemScanner.
file_system_scanner_ = std::make_unique<FileSystemScanner>(
temp_dir_.GetPath(), android_dir_, &arc_bridge_service_,
ctime_callback);
}
void TearDown() override {
arc_bridge_service_.file_system()->CloseInstance(&file_system_instance_);
arc_file_system_bridge_.reset();
profile_manager_.reset();
expected_media_store_.clear();
}
void CreateDummyFilesAndDirectories() {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
base::FilePath dir = temp_dir_.GetPath();
ASSERT_TRUE(CreateDirectory(dir, base::Time()));
ASSERT_TRUE(CreateFile(dir.AppendASCII("1.png"), base::Time()));
ASSERT_TRUE(CreateDirectory(dir.AppendASCII("a"), base::Time()));
ASSERT_TRUE(CreateFile(dir.AppendASCII("a/2.png"), base::Time()));
ASSERT_TRUE(CreateDirectory(dir.AppendASCII("a/b"), base::Time()));
ASSERT_TRUE(CreateFile(dir.AppendASCII("a/b/3.png"), base::Time()));
}
// TODO(risan): expected_media_store_ needs to be set by the callers
// instead of here. It should be called by the callers for explicitness.
void ModifyDirectory(const base::FilePath& path, base::Time ctime) {
expected_media_store_[GetAndroidPath(path, temp_dir_.GetPath(),
android_dir_)] = base::Time();
file_util_.SetLastChangeTime(path, ctime);
}
void ModifyFile(const base::FilePath& path, base::Time ctime) {
expected_media_store_[GetAndroidPath(path, temp_dir_.GetPath(),
android_dir_)] = ctime;
file_util_.SetLastChangeTime(path, ctime);
}
bool CreateFile(const base::FilePath& path, base::Time ctime) {
base::FilePath parent = path.DirName();
ModifyFile(path, ctime);
ModifyDirectory(parent, ctime);
return WriteFile(path, "42", sizeof("42")) == sizeof("42");
}
bool CreateDirectory(const base::FilePath& path, base::Time ctime) {
file_util_.SetLastChangeTime(path, ctime);
ModifyDirectory(path.DirName(), ctime);
return base::CreateDirectory(path);
}
bool RenameFile(const base::FilePath& old_path,
const base::FilePath& new_path,
base::Time ctime) {
DeleteFileRecursively(old_path, ctime);
return CreateFile(new_path, ctime);
}
bool RenameDirectory(const base::FilePath& old_path,
const base::FilePath& new_path,
base::Time ctime) {
base::FilePath android_old_path =
GetAndroidPath(old_path, temp_dir_.GetPath(), android_dir_);
base::FilePath android_new_path =
GetAndroidPath(new_path, temp_dir_.GetPath(), android_dir_);
// Collect all files to be renamed recursively under the |old_path|.
std::vector<base::FilePath> to_be_renamed = {android_old_path};
for (const auto& entry : expected_media_store_) {
if (android_old_path.IsParent(entry.first)) {
to_be_renamed.push_back(entry.first);
}
}
// Update media store index for all files under |old_path| to be under the
// |new_path|.
for (const auto& to_be_renamed_path : to_be_renamed) {
base::FilePath path = android_new_path;
android_old_path.AppendRelativePath(to_be_renamed_path, &path);
expected_media_store_[path] = expected_media_store_[to_be_renamed_path];
expected_media_store_.erase(to_be_renamed_path);
}
// Set the ctime accordingly.
file_util_.SetLastChangeTime(new_path, ctime);
ModifyDirectory(new_path.DirName(), ctime);
return Move(old_path, new_path);
}
bool DeleteFileRecursively(const base::FilePath& path, base::Time ctime) {
base::FilePath android_path =
GetAndroidPath(path, temp_dir_.GetPath(), android_dir_);
// Collect all files to be removed recursively under the |path|.
std::vector<base::FilePath> to_be_removed = {android_path};
for (const auto& entry : expected_media_store_) {
if (android_path.IsParent(entry.first)) {
to_be_removed.push_back(entry.first);
}
}
// Update media store index to remove the collected files.
for (const auto& to_be_removed_path : to_be_removed) {
expected_media_store_.erase(to_be_removed_path);
}
// Set the ctime accordingly.
ModifyDirectory(path.DirName(), ctime);
return base::DeleteFileRecursively(path);
}
std::unique_ptr<TestingProfileManager> profile_manager_;
ArcBridgeService arc_bridge_service_;
base::FilePath android_dir_;
base::ScopedTempDir temp_dir_;
std::unique_ptr<FileSystemScanner> file_system_scanner_;
content::BrowserTaskEnvironment task_environment_;
FakeFileSystemInstance file_system_instance_;
std::unique_ptr<ArcFileSystemBridge> arc_file_system_bridge_;
std::unique_ptr<TestingProfile> profile_;
FileUtil file_util_;
std::map<base::FilePath, base::Time> expected_media_store_;
};
TEST_F(ArcFileSystemScannerTest, ScheduleFullScan) {
file_system_scanner_->ScheduleFullScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanCreateTopLevelFile) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level files.
ASSERT_TRUE(CreateFile(dir.AppendASCII("foo.png"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanCreateTopLevelDirectory) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level directories.
ASSERT_TRUE(CreateDirectory(dir.AppendASCII("foo"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanModifyTopLevelFile) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level files.
ModifyFile(dir.AppendASCII("1.png"), now);
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanModifyTopLevelDirectory) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level files.
ModifyDirectory(dir.AppendASCII("a"), now);
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanRenameTopLevelFile) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level files.
ASSERT_TRUE(
RenameFile(dir.AppendASCII("1.png"), dir.AppendASCII("foo.jpg"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanRenameTopLevelDirectory) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level files.
ASSERT_TRUE(
RenameDirectory(dir.AppendASCII("a"), dir.AppendASCII("foo"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanDeleteTopLevelFile) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::FilePath dir = temp_dir_.GetPath();
base::Time now = base::Time::Now();
// Test if the scanner catches when top level files are deleted.
ASSERT_TRUE(DeleteFileRecursively(dir.AppendASCII("1.png"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanDeleteTopLevelDirectory) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::FilePath dir = temp_dir_.GetPath();
base::Time now = base::Time::Now();
// Test if the scanner catches when top level files are deleted.
ASSERT_TRUE(DeleteFileRecursively(dir.AppendASCII("a"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanCreateNestedFile) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level files.
ASSERT_TRUE(CreateFile(dir.AppendASCII("a/foo.png"), now));
ASSERT_TRUE(CreateFile(dir.AppendASCII("a/b/bar.png"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanCreateNestedDirectory) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level files.
ASSERT_TRUE(CreateDirectory(dir.AppendASCII("a/foo"), now));
ASSERT_TRUE(CreateDirectory(dir.AppendASCII("a/b/bar"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanRenameNestedFile) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level files.
ASSERT_TRUE(RenameFile(dir.AppendASCII("a/2.png"),
dir.AppendASCII("a/foo.jpg"), now));
ASSERT_TRUE(RenameFile(dir.AppendASCII("a/b/3.png"),
dir.AppendASCII("a/b/bar.jpg"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanRenameNestedDirectory) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level files.
ASSERT_TRUE(
RenameDirectory(dir.AppendASCII("a/b"), dir.AppendASCII("a/foo"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanModifyNestedFile) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level files.
ModifyFile(dir.AppendASCII("a/2.png"), now);
ModifyFile(dir.AppendASCII("a/b/3.png"), now);
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanModifyNestedDirectory) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
base::FilePath dir = temp_dir_.GetPath();
// Test if the scanner catches creation of top level files.
ModifyDirectory(dir.AppendASCII("a/b"), now);
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanDeleteNestedFile) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::FilePath dir = temp_dir_.GetPath();
base::Time now = base::Time::Now();
// Test if the scanner catches when top level files are deleted.
ASSERT_TRUE(DeleteFileRecursively(dir.AppendASCII("a/2.png"), now));
ASSERT_TRUE(DeleteFileRecursively(dir.AppendASCII("a/b/3.png"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanDeleteNestedDirectory) {
file_system_instance_.SetMediaStore(expected_media_store_);
// EXPECT_NE(expected_media_store_, file_system_instance_.GetMediaStore());
base::FilePath dir = temp_dir_.GetPath();
base::Time now = base::Time::Now();
// Test if the scanner catches when top level files are deleted.
ASSERT_TRUE(DeleteFileRecursively(dir.AppendASCII("a/b"), now));
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
TEST_F(ArcFileSystemScannerTest, ScheduleRegularScanNoChange) {
file_system_instance_.SetMediaStore(expected_media_store_);
base::Time now = base::Time::Now();
// Test if the scanner works as intended when there are no file system
// events.
file_system_scanner_->previous_scan_time_ = now;
file_system_scanner_->ScheduleRegularScan();
task_environment_.RunUntilIdle();
EXPECT_EQ(expected_media_store_, file_system_instance_.GetMediaStore());
}
} // namespace arc
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Next MinVersion: 15
// Next MinVersion: 14
module arc.mojom;
......@@ -278,7 +278,7 @@ interface FileSystemHost {
(FileSelectorElements elements);
};
// Next method ID: 21
// Next method ID: 19
interface FileSystemInstance {
// Notes about Android Documents Provider:
//
......@@ -409,13 +409,6 @@ interface FileSystemInstance {
// MediaProvider is removed.
RequestMediaScan@0(array<string> paths);
// Reloads and refreshes entries in MediaStore under |directory_path|.
[MinVersion=14] ReindexDirectory@19(string directory_path);
// Searches for the removed files/directories under the given
// |directory_paths| (non-recursively) in MediaStore and removes them.
[MinVersion=14] RequestFileRemovalScan@20(array<string> directory_paths);
// Opens URLs by sending an intent to the specified activity.
// Since this grants read/write URL permissions to the activity, callers must
// ensure that the user is correctly aware of the URLs and the activity they
......
......@@ -62,8 +62,6 @@ constexpr size_t kMaxBytesToReadFromPipe = 8 * 1024; // 8KB;
} // namespace
constexpr base::FilePath::CharType FakeFileSystemInstance::kFakeAndroidPath[];
FakeFileSystemInstance::File::File(const std::string& url,
const std::string& content,
const std::string& mime_type,
......@@ -176,23 +174,6 @@ void FakeFileSystemInstance::AddRoot(const Root& root) {
roots_.push_back(root);
}
void FakeFileSystemInstance::SetGetLastChangeTimeCallback(
GetLastChangeTimeCallback ctime_callback) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
ctime_callback_ = ctime_callback;
}
void FakeFileSystemInstance::SetCrosDir(const base::FilePath& cros_dir) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
cros_dir_ = cros_dir;
}
void FakeFileSystemInstance::SetMediaStore(
const std::map<base::FilePath, base::Time>& media_store) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
media_store_ = media_store;
}
void FakeFileSystemInstance::TriggerWatchers(
const std::string& authority,
const std::string& document_id,
......@@ -574,65 +555,10 @@ void FakeFileSystemInstance::RemoveWatcher(int64_t watcher_id,
FROM_HERE, base::BindOnce(std::move(callback), true));
}
// TODO(risan): "Added" directory might not be handled. Please double check
// this.
void FakeFileSystemInstance::RequestMediaScan(
const std::vector<std::string>& paths) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// TODO(risan): This is to prevent crashing other tests that expect nothing
// from RequestMediaScan, e.g., the following:
// FilesAppBrowserTest.Test/dirContextMenuDocumentsProvider_DocumentsProvider
if (cros_dir_.empty())
return;
for (const auto& path : paths) {
base::FilePath file_path = base::FilePath(path);
base::FilePath cros_path = GetCrosPath(file_path);
if (PathExists(cros_path)) {
// For each existing path, index itself and all parent directories of
// it.
base::Time ctime;
if (!DirectoryExists(cros_path))
ctime = ctime_callback_.Run(cros_path);
media_store_[file_path] = ctime;
file_path = file_path.DirName();
while (file_path != base::FilePath(kFakeAndroidPath).DirName()) {
media_store_[file_path] = base::Time();
file_path = file_path.DirName();
}
} else {
// When a file or directory does not exist, it means it has been
// deleted. So we need to erase its index entry in |media_store_|, and
// also the entries of all files/directories underneath it if it is a
// directory.
for (auto it = media_store_.begin(); it != media_store_.end();) {
if (it->first == file_path || file_path.IsParent(it->first))
media_store_.erase(it++);
else
++it;
}
}
}
}
void FakeFileSystemInstance::RequestFileRemovalScan(
const std::vector<std::string>& directory_paths) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
ReindexDirectory(kFakeAndroidPath);
}
void FakeFileSystemInstance::ReindexDirectory(
const std::string& directory_path) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
std::vector<std::string> paths = {directory_path};
base::FilePath directory_file_path(directory_path);
for (const auto& entry : media_store_) {
base::FilePath entry_path = entry.first;
if (!directory_file_path.IsParent(entry_path)) {
continue;
}
paths.push_back(entry_path.value());
}
RequestMediaScan(paths);
// Do nothing and pretend we scaned them.
}
void FakeFileSystemInstance::OpenUrlsWithPermission(
......@@ -711,12 +637,4 @@ base::ScopedFD FakeFileSystemInstance::CreateStreamFileDescriptorToWrite(
return fd_write;
}
base::FilePath FakeFileSystemInstance::GetCrosPath(
const base::FilePath& android_path) const {
base::FilePath cros_path(cros_dir_);
base::FilePath android_dir(kFakeAndroidPath);
android_dir.AppendRelativePath(android_path, &cros_path);
return cros_path;
}
} // namespace arc
......@@ -54,15 +54,6 @@ namespace arc {
class FakeFileSystemInstance : public mojom::FileSystemInstance {
public:
// Specification of a fake file available to content URL based methods.
using GetLastChangeTimeCallback =
base::RepeatingCallback<base::Time(const base::FilePath& path)>;
// TODO(risan): Consider if this is really needed, and whether we should just
// use the simpler option by using the original directory as is.
// The path for the top level directory considered in MediaStore.
static constexpr base::FilePath::CharType kFakeAndroidPath[] =
FILE_PATH_LITERAL("/android/path");
struct File {
enum class Seekable {
NO,
......@@ -190,20 +181,6 @@ class FakeFileSystemInstance : public mojom::FileSystemInstance {
// Adds a root accessible by document provider based methods.
void AddRoot(const Root& root);
// Fake the GetLastChangedTime implementation.
void SetGetLastChangeTimeCallback(GetLastChangeTimeCallback ctime_callback);
// Sets the path for the top level cros directory considered in MediaStore.
// TODO(risan): Consider to not use this (if we go without different android
// path approach). Another possibility to consider is probably to bind mount
// the directory in the caller test and do android_path conversion for the
// caller? In any case, this we don't want cros to be exposed to this instance
// that lives under Android.
void SetCrosDir(const base::FilePath& cros_dir);
// Sets the initial media store content.
void SetMediaStore(const std::map<base::FilePath, base::Time>& media_store);
// Triggers watchers installed to a document.
void TriggerWatchers(const std::string& authority,
const std::string& document_id,
......@@ -248,11 +225,6 @@ class FakeFileSystemInstance : public mojom::FileSystemInstance {
return handled_url_requests_;
}
// Returns the content of the fake media store index.
const std::map<base::FilePath, base::Time>& GetMediaStore() const {
return media_store_;
}
// mojom::FileSystemInstance:
void AddWatcher(const std::string& authority,
const std::string& document_id,
......@@ -301,9 +273,6 @@ class FakeFileSystemInstance : public mojom::FileSystemInstance {
void RemoveWatcher(int64_t watcher_id,
RemoveWatcherCallback callback) override;
void RequestMediaScan(const std::vector<std::string>& paths) override;
void RequestFileRemovalScan(
const std::vector<std::string>& directory_paths) override;
void ReindexDirectory(const std::string& directory_path) override;
void OpenUrlsWithPermission(mojom::OpenUrlsRequestPtr request,
OpenUrlsWithPermissionCallback callback) override;
......@@ -333,9 +302,6 @@ class FakeFileSystemInstance : public mojom::FileSystemInstance {
// read by calling GetFileContent() with the passed |url|.
base::ScopedFD CreateStreamFileDescriptorToWrite(const std::string& url);
// Returns the cros file path for |android_path|.
base::FilePath GetCrosPath(const base::FilePath& android_path) const;
THREAD_CHECKER(thread_checker_);
base::ScopedTempDir temp_dir_;
......@@ -374,15 +340,6 @@ class FakeFileSystemInstance : public mojom::FileSystemInstance {
// List of roots added by AddRoot().
std::vector<Root> roots_;
// Fake MediaStore database index.
std::map<base::FilePath, base::Time> media_store_;
// Fake GetLastChangedTime function callback.
GetLastChangeTimeCallback ctime_callback_;
// The path for the top level cros directory considered in MediaStore.
base::FilePath cros_dir_;
int64_t next_watcher_id_ = 1;
int get_child_documents_count_ = 0;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment