blob: 65a899cd8bc8f010b95907c2cad3c744d30a5634 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/file_manager/trash_auto_cleanup.h"
#include <vector>
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/i18n/time_formatting.h"
#include "base/rand_util.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/mock_callback.h"
#include "base/test/test_future.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/trash_io_task.h"
#include "chrome/browser/ash/file_manager/trash_unittest_base.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/browser/test/test_file_system_context.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace file_manager::trash {
inline constexpr size_t kTestFileSize = 32;
class TrashAutoCleanupTest : public file_manager::io_task::TrashBaseTest {
public:
TrashAutoCleanupTest() = default;
TrashAutoCleanupTest(const TrashAutoCleanupTest&) = delete;
TrashAutoCleanupTest& operator=(const TrashAutoCleanupTest&) = delete;
void SetUp() override {
file_manager::io_task::TrashBaseTest::SetUp();
trash_auto_cleanup_ = base::WrapUnique(new TrashAutoCleanup(profile()));
}
void TearDown() override {
trash_auto_cleanup_.reset();
file_manager::io_task::TrashBaseTest::TearDown();
}
void SetupFileTrashedFromDownloads(const std::string& file_name) {
// Create file in Downloads.
const base::FilePath file_path = downloads_dir_.Append(file_name);
ASSERT_TRUE(
base::WriteFile(file_path, base::RandBytesAsString(kTestFileSize)));
// Create the FileSystemURL for the file to trash.
std::string relative_path = file_path.value();
EXPECT_TRUE(file_manager::util::ReplacePrefix(
&relative_path, temp_dir_.GetPath().AsEndingWithSeparator().value(),
""));
std::vector<storage::FileSystemURL> source_urls = {
file_system_context_->CreateCrackedFileSystemURL(
kTestStorageKey, storage::kFileSystemTypeTest,
base::FilePath::FromUTF8Unsafe(relative_path)),
};
// Trash the file.
base::RunLoop run_loop;
base::MockRepeatingCallback<void(const io_task::ProgressStatus&)>
progress_callback;
base::MockOnceCallback<void(io_task::ProgressStatus)> complete_callback;
EXPECT_CALL(complete_callback,
Run(::testing::Field(&io_task::ProgressStatus::state,
io_task::State::kSuccess)))
.WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
io_task::TrashIOTask task(source_urls, profile(), file_system_context_,
temp_dir_.GetPath());
task.Execute(progress_callback.Get(), complete_callback.Get());
run_loop.Run();
// Check that the file has been trashed.
const base::FilePath trashed_file_path = GetTrashedFilePath(file_name);
const base::FilePath info_file_path = GetInfoFilePath(file_name);
ASSERT_FALSE(base::PathExists(file_path));
ASSERT_TRUE(base::PathExists(trashed_file_path));
ASSERT_TRUE(base::PathExists(info_file_path));
// Override last_modified time of the trashinfo file to match the task
// environment's mocked clock.
base::Time now = base::Time::Now();
base::TouchFile(info_file_path, now, now);
}
void CheckFileExistsInTrash(const std::string& file_name, bool should_exist) {
ASSERT_EQ(base::PathExists(GetInfoFilePath(file_name)), should_exist);
ASSERT_EQ(base::PathExists(GetTrashedFilePath(file_name)), should_exist);
}
void SetCleanupDoneCallbackForTest(
base::OnceCallback<void(AutoCleanupResult result)> callback) {
trash_auto_cleanup_->SetCleanupDoneCallbackForTest(std::move(callback));
}
void StartCleanup() { trash_auto_cleanup_->StartCleanup(); }
void InitAutoCleanup() { trash_auto_cleanup_->Init(); }
std::string GenerateTrashInfoContents(const base::Time& deletion_time) {
return base::StrCat(
{"[Trash Info]\nPath=", "/Downloads/bar/", "original_file.txt",
"\nDeletionDate=", base::TimeFormatAsIso8601(deletion_time)});
}
base::FilePath GetInfoFilePath(const std::string& file_name) {
return downloads_dir_.Append(trash::kTrashFolderName)
.Append(trash::kInfoFolderName)
.Append(base::StrCat({file_name, ".trashinfo"}));
}
base::FilePath GetTrashedFilePath(const std::string& file_name) {
return downloads_dir_.Append(trash::kTrashFolderName)
.Append(trash::kFilesFolderName)
.Append(file_name);
}
Profile* profile() const { return profile_.get(); }
content::BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
std::unique_ptr<TrashAutoCleanup> trash_auto_cleanup_;
};
TEST_F(TrashAutoCleanupTest, CleanupIteration) {
EnsureTrashDirectorySetup(downloads_dir_);
// Setup files in trash directory.
const std::string first_file_name = "first_file.txt";
const std::string second_file_name = "second_file.txt";
// Setup trashed files. The first file is trashed at T = 0, the second file is
// trashed at T = 1 day.
SetupFileTrashedFromDownloads(first_file_name);
task_environment_.FastForwardBy(base::Days(1));
SetupFileTrashedFromDownloads(second_file_name);
// At T = 29 days, no file should be removed.
task_environment_.FastForwardBy(base::Days(28));
// Start auto cleanup.
base::test::TestFuture<AutoCleanupResult> future;
SetCleanupDoneCallbackForTest(future.GetCallback<AutoCleanupResult>());
StartCleanup();
ASSERT_TRUE(future.Wait());
// Check that no file has been removed.
ASSERT_EQ(future.Take(), AutoCleanupResult::kNoOldFilesToCleanup);
CheckFileExistsInTrash(first_file_name, /*should_exist=*/true);
CheckFileExistsInTrash(second_file_name, /*should_exist=*/true);
// At T = 30 days, only the first file should be removed.
task_environment_.FastForwardBy(base::Days(1));
// Start auto cleanup.
SetCleanupDoneCallbackForTest(future.GetCallback<AutoCleanupResult>());
StartCleanup();
ASSERT_TRUE(future.Wait());
// Check that the first file has been removed.
ASSERT_EQ(future.Take(), AutoCleanupResult::kCleanupSuccessful);
CheckFileExistsInTrash(first_file_name, /*should_exist=*/false);
CheckFileExistsInTrash(second_file_name, /*should_exist=*/true);
// At T = 31 days, the second file should also be removed.
task_environment_.FastForwardBy(base::Days(1));
// Start auto cleanup.
SetCleanupDoneCallbackForTest(future.GetCallback<AutoCleanupResult>());
StartCleanup();
ASSERT_TRUE(future.Wait());
// Check that the second file has been removed.
ASSERT_EQ(future.Take(), AutoCleanupResult::kCleanupSuccessful);
CheckFileExistsInTrash(first_file_name, /*should_exist=*/false);
CheckFileExistsInTrash(second_file_name, /*should_exist=*/false);
}
TEST_F(TrashAutoCleanupTest, PeriodicCleanup) {
// Setup files in trash directory.
const std::string first_file_name = "first_file.txt";
const std::string second_file_name = "second_file.txt";
// Setup trashed files. The first file is trashed at T = 0, the second file is
// trashed a bit later at T = 10 minutes.
SetupFileTrashedFromDownloads(first_file_name);
task_environment_.FastForwardBy(base::Minutes(10));
SetupFileTrashedFromDownloads(second_file_name);
// Init the autocleanup process at T = 30 days (minus kCleanupCheckInterval,
// the delay before the initial cleanup iteration). Only the first file should
// be removed.
task_environment_.FastForwardBy(base::Minutes(50) + base::Hours(23) +
base::Days(29) - kCleanupCheckInterval);
// Init periodic cleanup.
base::test::TestFuture<AutoCleanupResult> future;
SetCleanupDoneCallbackForTest(future.GetCallback<AutoCleanupResult>());
InitAutoCleanup();
ASSERT_TRUE(future.Wait());
// Check that the first file has been removed.
ASSERT_EQ(future.Take(), AutoCleanupResult::kCleanupSuccessful);
CheckFileExistsInTrash(first_file_name, /*should_exist=*/false);
CheckFileExistsInTrash(second_file_name, /*should_exist=*/true);
// The second file should persist while we are within `kCleanupInterval` of
// the last cleanup iteration.
// Note: the mocked time stays constant until `future.Wait()` is called, so
// `last_cleanup_iteration` is accurate.
const base::Time last_cleanup_iteration = base::Time::Now();
task_environment_.FastForwardBy(kCleanupInterval - kCleanupCheckInterval);
CheckFileExistsInTrash(second_file_name, /*should_exist=*/true);
// The next cleanup check should happen `kCleanupInterval` after the last
// iteration. Check that the second file has now been removed.
SetCleanupDoneCallbackForTest(future.GetCallback<AutoCleanupResult>());
ASSERT_TRUE(future.Wait());
ASSERT_EQ(base::Time::Now() - last_cleanup_iteration, kCleanupInterval);
ASSERT_EQ(future.Take(), AutoCleanupResult::kCleanupSuccessful);
CheckFileExistsInTrash(second_file_name, /*should_exist=*/false);
}
TEST_F(TrashAutoCleanupTest, MultipleBatchesCleanup) {
// Setup a number of files in trash directory that equals more than twice the
// batch size.
std::vector<std::string> file_names;
const int n_files = 2.5 * kMaxBatchSize;
for (int i = 0; i < n_files; ++i) {
const std::string file_name = "file" + base::NumberToString(i);
file_names.emplace_back(file_name);
SetupFileTrashedFromDownloads(file_name);
}
task_environment_.FastForwardBy(base::Days(30));
base::test::TestFuture<AutoCleanupResult> future;
SetCleanupDoneCallbackForTest(future.GetCallback<AutoCleanupResult>());
InitAutoCleanup();
ASSERT_TRUE(future.Wait());
base::Time last_cleanup_iteration = base::Time::Now();
// Check that the first batch of files has been removed.
ASSERT_EQ(future.Take(), AutoCleanupResult::kCleanupSuccessful);
// Check that `kMaxBatchSize` files have been removed.
int n_remaining_files = 0;
for (int i = 0; i < n_files; ++i) {
const std::string file_name = "file" + base::NumberToString(i);
if (!base::PathExists(GetTrashedFilePath(file_name))) {
ASSERT_FALSE(base::PathExists(GetInfoFilePath(file_name)));
} else {
ASSERT_TRUE(base::PathExists(GetInfoFilePath(file_name)));
++n_remaining_files;
}
}
ASSERT_EQ(n_remaining_files, n_files - kMaxBatchSize);
// After this iteration, the next batch of files should be processed.
SetCleanupDoneCallbackForTest(future.GetCallback<AutoCleanupResult>());
ASSERT_TRUE(future.Wait());
// This iteration should happen `kCleanupCheckInterval` after the preview one,
// instead of the next day, since not all the batches have been processed
// yet.
ASSERT_EQ(base::Time::Now() - last_cleanup_iteration, kCleanupCheckInterval);
last_cleanup_iteration = base::Time::Now();
// Check that the second batch of files has been removed.
ASSERT_EQ(future.Take(), AutoCleanupResult::kCleanupSuccessful);
// Check that `2 * kMaxBatchSize` files have been removed.
n_remaining_files = 0;
for (int i = 0; i < n_files; ++i) {
const std::string file_name = "file" + base::NumberToString(i);
if (!base::PathExists(GetTrashedFilePath(file_name))) {
ASSERT_FALSE(base::PathExists(GetInfoFilePath(file_name)));
} else {
ASSERT_TRUE(base::PathExists(GetInfoFilePath(file_name)));
++n_remaining_files;
}
}
ASSERT_EQ(n_remaining_files, n_files - 2 * kMaxBatchSize);
// After the last iteration, no file should be remaining.
SetCleanupDoneCallbackForTest(future.GetCallback<AutoCleanupResult>());
ASSERT_TRUE(future.Wait());
ASSERT_EQ(base::Time::Now() - last_cleanup_iteration, kCleanupCheckInterval);
// Check that all files have been removed.
ASSERT_EQ(future.Take(), AutoCleanupResult::kCleanupSuccessful);
for (int i = 0; i < n_files; ++i) {
const std::string file_name = "file" + base::NumberToString(i);
CheckFileExistsInTrash(file_name, /*should_exist=*/false);
}
}
} // namespace file_manager::trash