// // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2021 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // #include "td/telegram/files/FileLoaderUtils.h" #include "td/telegram/files/FileLocation.h" #include "td/telegram/Global.h" #include "td/telegram/TdDb.h" #include "td/telegram/TdParameters.h" #include "td/utils/common.h" #include "td/utils/filesystem.h" #include "td/utils/format.h" #include "td/utils/logging.h" #include "td/utils/misc.h" #include "td/utils/PathView.h" #include "td/utils/port/Clocks.h" #include "td/utils/port/FileFd.h" #include "td/utils/port/path.h" #include "td/utils/port/Stat.h" #include "td/utils/Random.h" #include "td/utils/SliceBuilder.h" #include "td/utils/StringBuilder.h" #include namespace td { int VERBOSITY_NAME(file_loader) = VERBOSITY_NAME(DEBUG) + 2; namespace { Result> try_create_new_file(CSlice name) { LOG(DEBUG) << "Trying to create new file " << name; TRY_RESULT(fd, FileFd::open(name, FileFd::Read | FileFd::Write | FileFd::CreateNew, 0640)); return std::make_pair(std::move(fd), name.str()); } Result> try_open_file(CSlice name) { LOG(DEBUG) << "Trying to open file " << name; TRY_RESULT(fd, FileFd::open(name, FileFd::Read, 0640)); return std::make_pair(std::move(fd), name.str()); } struct RandSuff { int len; }; StringBuilder &operator<<(StringBuilder &sb, const RandSuff &) { for (int i = 0; i < 6; i++) { sb << format::hex_digit(Random::fast(0, 15)); } return sb; } struct Ext { Slice ext; }; StringBuilder &operator<<(StringBuilder &sb, const Ext &ext) { if (ext.ext.empty()) { return sb; } return sb << "." << ext.ext; } } // namespace Result> open_temp_file(FileType file_type) { auto pmc = G()->td_db()->get_binlog_pmc(); // TODO: CAS? auto file_id = to_integer(pmc->get("tmp_file_id")); pmc->set("tmp_file_id", to_string(file_id + 1)); auto temp_dir = get_files_temp_dir(file_type); auto res = try_create_new_file(PSLICE() << temp_dir << file_id); if (res.is_error()) { res = try_create_new_file(PSLICE() << temp_dir << file_id << "_" << RandSuff{6}); } return res; } template bool for_suggested_file_name(CSlice name, bool use_pmc, bool use_random, F &&callback) { auto cleaned_name = clean_filename(name); PathView path_view(cleaned_name); auto stem = path_view.file_stem(); auto ext = path_view.extension(); bool active = true; if (!stem.empty() && !G()->parameters().ignore_file_names) { active = callback(PSLICE() << stem << Ext{ext}); for (int i = 0; active && i < 10; i++) { active = callback(PSLICE() << stem << "_(" << i << ")" << Ext{ext}); } for (int i = 2; active && i < 12 && use_random; i++) { active = callback(PSLICE() << stem << "_(" << RandSuff{i} << ")" << Ext{ext}); } } else if (use_pmc) { auto pmc = G()->td_db()->get_binlog_pmc(); auto file_id = to_integer(pmc->get("perm_file_id")); pmc->set("perm_file_id", to_string(file_id + 1)); active = callback(PSLICE() << "file_" << file_id << Ext{ext}); if (active) { active = callback(PSLICE() << "file_" << file_id << "_" << RandSuff{6} << Ext{ext}); } } return active; } Result create_from_temp(CSlice temp_path, CSlice dir, CSlice name) { LOG(INFO) << "Create file in directory " << dir << " with suggested name " << name << " from temporary file " << temp_path; Result> res = Status::Error(500, "Can't find suitable file name"); for_suggested_file_name(name, true, true, [&](CSlice suggested_name) { res = try_create_new_file(PSLICE() << dir << suggested_name); return res.is_error(); }); TRY_RESULT(tmp, std::move(res)); tmp.first.close(); auto perm_path = std::move(tmp.second); TRY_STATUS(rename(temp_path, perm_path)); return perm_path; } Result search_file(CSlice dir, CSlice name, int64 expected_size) { Result res = Status::Error(500, "Can't find suitable file name"); for_suggested_file_name(name, false, false, [&](CSlice suggested_name) { auto r_pair = try_open_file(PSLICE() << dir << suggested_name); if (r_pair.is_error()) { return false; } FileFd fd; std::string path; std::tie(fd, path) = r_pair.move_as_ok(); auto r_size = fd.get_size(); if (r_size.is_error() || r_size.ok() != expected_size) { return true; } fd.close(); res = std::move(path); return false; }); return res; } Result get_suggested_file_name(CSlice directory, Slice file_name) { string cleaned_name = clean_filename(file_name.str()); file_name = cleaned_name; if (directory.empty()) { directory = CSlice("./"); } auto dir_stat = stat(directory); if (dir_stat.is_error() || !dir_stat.ok().is_dir_) { return cleaned_name; } PathView path_view(file_name); auto stem = path_view.file_stem(); auto ext = path_view.extension(); if (stem.empty()) { return cleaned_name; } Slice directory_slice = directory; while (directory_slice.size() > 1 && (directory_slice.back() == '/' || directory_slice.back() == '\\')) { directory_slice.remove_suffix(1); } auto check_file_name = [directory_slice](Slice name) { return stat(PSLICE() << directory_slice << TD_DIR_SLASH << name).is_error(); // in case of success, the name is bad }; string checked_name = PSTRING() << stem << Ext{ext}; if (check_file_name(checked_name)) { return checked_name; } for (int i = 1; i < 100; i++) { checked_name = PSTRING() << stem << " (" << i << ")" << Ext{ext}; if (check_file_name(checked_name)) { return checked_name; } } return PSTRING() << stem << " - " << StringBuilder::FixedDouble(Clocks::system(), 3) << Ext{ext}; } Result save_file_bytes(FileType type, BufferSlice bytes, CSlice file_name) { auto r_old_path = search_file(get_files_dir(type), file_name, bytes.size()); if (r_old_path.is_ok()) { auto r_old_bytes = read_file(r_old_path.ok()); if (r_old_bytes.is_ok() && r_old_bytes.ok().as_slice() == bytes.as_slice()) { LOG(INFO) << "Found previous file with the same name " << r_old_path.ok(); return FullLocalFileLocation(type, r_old_path.ok(), 0); } } TRY_RESULT(fd_path, open_temp_file(type)); FileFd fd = std::move(fd_path.first); string path = std::move(fd_path.second); TRY_RESULT(size, fd.write(bytes.as_slice())); fd.close(); if (size != bytes.size()) { return Status::Error("Failed to write bytes to the file"); } auto dir = get_files_dir(type); TRY_RESULT(perm_path, create_from_temp(path, dir, file_name)); return FullLocalFileLocation(type, std::move(perm_path), 0); } static Slice get_file_base_dir(const FileDirType &file_dir_type) { switch (file_dir_type) { case FileDirType::Secure: return G()->get_secure_files_dir(); case FileDirType::Common: return G()->get_files_dir(); default: UNREACHABLE(); return Slice(); } } Slice get_files_base_dir(FileType file_type) { return get_file_base_dir(get_file_dir_type(file_type)); } string get_files_temp_dir(FileType file_type) { return PSTRING() << get_files_base_dir(file_type) << "temp" << TD_DIR_SLASH; } string get_files_dir(FileType file_type) { return PSTRING() << get_files_base_dir(file_type) << get_file_type_name(file_type) << TD_DIR_SLASH; } } // namespace td