| // Copyright 2018 The Chromium OS 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 "arc/apk-cache/cache_cleaner.h" |
| |
| #include <fnmatch.h> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include <base/files/file.h> |
| #include <base/files/file_enumerator.h> |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/json/json_reader.h> |
| #include <base/logging.h> |
| #include <base/time/time.h> |
| #include <base/values.h> |
| |
| namespace apk_cache { |
| |
| const char kAttrJson[] = "attr.json"; |
| const char kApkExtension[] = ".apk"; |
| const char kObbExtension[] = ".obb"; |
| const char kMainObbPrefix[] = "main."; |
| const char kPatchObbPrefix[] = "patch."; |
| const base::TimeDelta kValidityPeriod = base::TimeDelta::FromDays(30); |
| |
| namespace { |
| |
| constexpr char kKeyAttributesAtime[] = "attributes.atime"; |
| |
| // Removes all the files (if any) from cache root. Does not remove |
| // directories. Returns true if all the intended files were deleted. |
| bool RemoveUnexpectedFilesFromCacheRoot(const base::FilePath& cache_root) { |
| bool success = true; |
| base::FileEnumerator unexpected_files( |
| cache_root, false /* recursive */, |
| base::FileEnumerator::FILES | base::FileEnumerator::SHOW_SYM_LINKS); |
| |
| for (base::FilePath unexpected_file_path = unexpected_files.Next(); |
| !unexpected_file_path.empty(); |
| unexpected_file_path = unexpected_files.Next()) { |
| LOG(INFO) << "Deleting file " << unexpected_file_path.value(); |
| if (!base::DeleteFile(unexpected_file_path, false /* recursive */)) { |
| LOG(ERROR) << "Could not delete file " << unexpected_file_path.value(); |
| success = false; |
| } |
| } |
| |
| return success; |
| } |
| |
| // Returns if |file_name| matches the |pattern|. |
| bool IsMatch(const std::string& file_name, const std::string& pattern) { |
| return fnmatch(pattern.c_str(), file_name.c_str(), FNM_NOESCAPE) == 0; |
| } |
| |
| // Parses contents of the attributes JSON file and verifies that last access |
| // time of the package was at most 30 days ago. |
| bool IsAccessTimeValid(const base::StringPiece& json_message) { |
| std::string error_msg; |
| int error_code = 0; |
| const std::unique_ptr<base::Value> root(base::JSONReader::ReadAndReturnError( |
| json_message, base::JSON_PARSE_RFC, &error_code, &error_msg)); |
| if (!root.get()) { |
| LOG(ERROR) << "Reading attributes JSON failed (error code: " << error_code |
| << "; error message: " << error_msg << ")."; |
| return false; |
| } |
| |
| base::DictionaryValue* root_dict = nullptr; |
| if (!root->GetAsDictionary(&root_dict)) { |
| LOG(ERROR) << "Could not interpret the JSON as a dictionary."; |
| return false; |
| } |
| |
| std::string atime_str; |
| if (!root_dict->GetString(kKeyAttributesAtime, &atime_str)) { |
| LOG(ERROR) << "Could not read the value of the access time with the " |
| << kKeyAttributesAtime << " key."; |
| return false; |
| } |
| |
| base::Time atime; |
| if (!base::Time::FromString(atime_str.c_str(), &atime)) { |
| LOG(ERROR) << "Can not parse the date: " << atime_str; |
| return false; |
| } |
| |
| const base::TimeDelta age = base::Time::Now() - atime; |
| return age < kValidityPeriod; |
| } |
| |
| // Verifies that package directory contains all the necessary files, does |
| // not contain any extra files/directories and was accessed within last 30 days. |
| // Returns true if the package is valid and should be kept in the cache. |
| bool IsPackageValid(const base::FilePath& package_path) { |
| // Package directory must contain: |
| // 1. One .apk file |
| // 2. One attr.json file |
| // 3. No or one main .obb file. |
| // 4. No or one patch .obb file. |
| // 5. No other files or directories. |
| base::FileEnumerator files(package_path, false /* recursive */, |
| base::FileEnumerator::DIRECTORIES | |
| base::FileEnumerator::FILES | |
| base::FileEnumerator::SHOW_SYM_LINKS); |
| int apk_count = 0; |
| int attr_count = 0; |
| int main_obb_count = 0; |
| int patch_obb_count = 0; |
| |
| const std::string package_name = package_path.BaseName().value(); |
| const std::string apk_file_name = package_name + kApkExtension; |
| const std::string main_obb_file_name = |
| std::string(kMainObbPrefix) + "*" + package_name + kObbExtension; |
| const std::string patch_obb_file_name = |
| std::string(kPatchObbPrefix) + "*" + package_name + kObbExtension; |
| |
| for (base::FilePath file_path = files.Next(); !file_path.empty(); |
| file_path = files.Next()) { |
| if (base::DirectoryExists(file_path)) { |
| LOG(INFO) << "There are directories in " << package_path.value(); |
| return false; |
| } |
| |
| const std::string file_name = file_path.BaseName().value(); |
| if (IsMatch(file_name, apk_file_name)) { |
| apk_count++; |
| } else if (IsMatch(file_name, kAttrJson)) { |
| attr_count++; |
| } else if (IsMatch(file_name, main_obb_file_name)) { |
| main_obb_count++; |
| } else if (IsMatch(file_name, patch_obb_file_name)) { |
| patch_obb_count++; |
| } else { |
| LOG(INFO) << package_name << " contains unnecessary files."; |
| return false; |
| } |
| } |
| |
| if (apk_count != 1) { |
| LOG(INFO) << "Number of APK files is not equal to 1 in " << package_name; |
| return false; |
| } |
| |
| if (attr_count != 1) { |
| LOG(INFO) << "Number of JSON attributes files is " << attr_count |
| << " which not equal to 1 in " << package_name; |
| return false; |
| } |
| |
| if (main_obb_count > 1) { |
| LOG(INFO) << "Number of patch OBB files is " << main_obb_count |
| << ", which greater then 1 in " << package_name; |
| return false; |
| } |
| |
| if (patch_obb_count > 1) { |
| LOG(INFO) << "Number of patch OBB files is " << patch_obb_count |
| << ", which greater then 1 in " << package_name; |
| return false; |
| } |
| |
| const base::FilePath attr_file_path = package_path.Append(kAttrJson); |
| std::string attr_json_contents; |
| if (!base::ReadFileToString(attr_file_path, &attr_json_contents)) { |
| LOG(ERROR) << "Could not read the attributes file."; |
| return false; |
| } |
| |
| return IsAccessTimeValid(attr_json_contents); |
| } |
| |
| } // namespace |
| |
| bool Clean(const base::FilePath& cache_path) { |
| if (!base::DirectoryExists(cache_path)) { |
| LOG(ERROR) << "APK cache directory " << cache_path.value() |
| << " does not exist"; |
| return false; |
| } |
| |
| bool success = RemoveUnexpectedFilesFromCacheRoot(cache_path); |
| |
| base::FileEnumerator packages( |
| cache_path, false /* recursive */, |
| base::FileEnumerator::DIRECTORIES | base::FileEnumerator::SHOW_SYM_LINKS); |
| |
| for (base::FilePath package_path = packages.Next(); !package_path.empty(); |
| package_path = packages.Next()) { |
| if (!IsPackageValid(package_path)) { |
| if (!base::DeleteFile(package_path, true /* recursive */)) { |
| LOG(ERROR) << "Error deletion path " << package_path.value(); |
| success = false; |
| } |
| } else { |
| LOG(INFO) << "Package " << package_path.value() << " looks OK."; |
| } |
| } |
| |
| return success; |
| } |
| |
| } // namespace apk_cache |