Serenity Operating System
at master 363 lines 13 kB view raw
1/* 2 * Copyright (c) 2021, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2021-2022, Sam Atkins <atkinssj@serenityos.org> 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include <AK/DeprecatedString.h> 9#include <AK/Format.h> 10#include <AK/LexicalPath.h> 11#include <AK/StringView.h> 12#include <LibCore/ArgsParser.h> 13#include <LibCore/DirIterator.h> 14#include <LibCore/File.h> 15#include <LibCore/System.h> 16#include <LibMain/Main.h> 17#include <sched.h> 18#include <sys/stat.h> 19#include <unistd.h> 20 21struct WorkItem { 22 enum class Type { 23 CreateDirectory, 24 DeleteDirectory, 25 CopyFile, 26 MoveFile, 27 DeleteFile, 28 }; 29 Type type; 30 DeprecatedString source; 31 DeprecatedString destination; 32 off_t size; 33}; 34 35static void report_warning(StringView message); 36static void report_error(StringView message); 37static ErrorOr<int> perform_copy(Vector<StringView> const& sources, DeprecatedString const& destination); 38static ErrorOr<int> perform_move(Vector<StringView> const& sources, DeprecatedString const& destination); 39static ErrorOr<int> perform_delete(Vector<StringView> const& sources); 40static ErrorOr<int> execute_work_items(Vector<WorkItem> const& items); 41static ErrorOr<NonnullOwnPtr<Core::File>> open_destination_file(DeprecatedString const& destination); 42static DeprecatedString deduplicate_destination_file_name(DeprecatedString const& destination); 43 44ErrorOr<int> serenity_main(Main::Arguments arguments) 45{ 46 DeprecatedString operation; 47 Vector<StringView> paths; 48 49 Core::ArgsParser args_parser; 50 args_parser.add_positional_argument(operation, "Operation: either 'Copy', 'Move' or 'Delete'", "operation", Core::ArgsParser::Required::Yes); 51 args_parser.add_positional_argument(paths, "Source paths, followed by a destination if applicable", "paths", Core::ArgsParser::Required::Yes); 52 args_parser.parse(arguments); 53 54 if (operation == "Delete") 55 return perform_delete(paths); 56 57 DeprecatedString destination = paths.take_last(); 58 if (paths.is_empty()) 59 return Error::from_string_literal("At least one source and destination are required"); 60 61 if (operation == "Copy") 62 return perform_copy(paths, destination); 63 if (operation == "Move") 64 return perform_move(paths, destination); 65 66 // FIXME: Return the formatted string directly. There is no way to do this right now without the temporary going out of scope and being destroyed. 67 report_error(DeprecatedString::formatted("Unknown operation '{}'", operation)); 68 return Error::from_string_literal("Unknown operation"); 69} 70 71static void report_warning(StringView message) 72{ 73 outln("WARN {}", message); 74} 75 76static void report_error(StringView message) 77{ 78 outln("ERROR {}", message); 79} 80 81static ErrorOr<int> collect_copy_work_items(DeprecatedString const& source, DeprecatedString const& destination, Vector<WorkItem>& items) 82{ 83 if (auto const st = TRY(Core::System::lstat(source)); !S_ISDIR(st.st_mode)) { 84 // It's a file. 85 items.append(WorkItem { 86 .type = WorkItem::Type::CopyFile, 87 .source = source, 88 .destination = LexicalPath::join(destination, LexicalPath::basename(source)).string(), 89 .size = st.st_size, 90 }); 91 return 0; 92 } 93 94 // It's a directory. 95 items.append(WorkItem { 96 .type = WorkItem::Type::CreateDirectory, 97 .source = {}, 98 .destination = LexicalPath::join(destination, LexicalPath::basename(source)).string(), 99 .size = 0, 100 }); 101 102 Core::DirIterator dt(source, Core::DirIterator::SkipParentAndBaseDir); 103 while (dt.has_next()) { 104 auto name = dt.next_path(); 105 TRY(collect_copy_work_items( 106 LexicalPath::join(source, name).string(), 107 LexicalPath::join(destination, LexicalPath::basename(source)).string(), 108 items)); 109 } 110 111 return 0; 112} 113 114ErrorOr<int> perform_copy(Vector<StringView> const& sources, DeprecatedString const& destination) 115{ 116 Vector<WorkItem> items; 117 118 for (auto& source : sources) { 119 TRY(collect_copy_work_items(source, destination, items)); 120 } 121 122 return execute_work_items(items); 123} 124 125static ErrorOr<int> collect_move_work_items(DeprecatedString const& source, DeprecatedString const& destination, Vector<WorkItem>& items) 126{ 127 if (auto const st = TRY(Core::System::lstat(source)); !S_ISDIR(st.st_mode)) { 128 // It's a file. 129 items.append(WorkItem { 130 .type = WorkItem::Type::MoveFile, 131 .source = source, 132 .destination = LexicalPath::join(destination, LexicalPath::basename(source)).string(), 133 .size = st.st_size, 134 }); 135 return 0; 136 } 137 138 // It's a directory. 139 items.append(WorkItem { 140 .type = WorkItem::Type::CreateDirectory, 141 .source = {}, 142 .destination = LexicalPath::join(destination, LexicalPath::basename(source)).string(), 143 .size = 0, 144 }); 145 146 Core::DirIterator dt(source, Core::DirIterator::SkipParentAndBaseDir); 147 while (dt.has_next()) { 148 auto name = dt.next_path(); 149 TRY(collect_move_work_items( 150 LexicalPath::join(source, name).string(), 151 LexicalPath::join(destination, LexicalPath::basename(source)).string(), 152 items)); 153 } 154 155 items.append(WorkItem { 156 .type = WorkItem::Type::DeleteDirectory, 157 .source = source, 158 .destination = {}, 159 .size = 0, 160 }); 161 162 return 0; 163} 164 165ErrorOr<int> perform_move(Vector<StringView> const& sources, DeprecatedString const& destination) 166{ 167 Vector<WorkItem> items; 168 169 for (auto& source : sources) { 170 TRY(collect_move_work_items(source, destination, items)); 171 } 172 173 return execute_work_items(items); 174} 175 176static ErrorOr<int> collect_delete_work_items(DeprecatedString const& source, Vector<WorkItem>& items) 177{ 178 if (auto const st = TRY(Core::System::lstat(source)); !S_ISDIR(st.st_mode)) { 179 // It's a file. 180 items.append(WorkItem { 181 .type = WorkItem::Type::DeleteFile, 182 .source = source, 183 .destination = {}, 184 .size = st.st_size, 185 }); 186 return 0; 187 } 188 189 // It's a directory. 190 Core::DirIterator dt(source, Core::DirIterator::SkipParentAndBaseDir); 191 while (dt.has_next()) { 192 auto name = dt.next_path(); 193 TRY(collect_delete_work_items(LexicalPath::join(source, name).string(), items)); 194 } 195 196 items.append(WorkItem { 197 .type = WorkItem::Type::DeleteDirectory, 198 .source = source, 199 .destination = {}, 200 .size = 0, 201 }); 202 203 return 0; 204} 205 206ErrorOr<int> perform_delete(Vector<StringView> const& sources) 207{ 208 Vector<WorkItem> items; 209 210 for (auto& source : sources) { 211 TRY(collect_delete_work_items(source, items)); 212 } 213 214 return execute_work_items(items); 215} 216 217ErrorOr<int> execute_work_items(Vector<WorkItem> const& items) 218{ 219 off_t total_work_bytes = 0; 220 for (auto& item : items) 221 total_work_bytes += item.size; 222 223 off_t executed_work_bytes = 0; 224 225 for (size_t i = 0; i < items.size(); ++i) { 226 auto& item = items[i]; 227 off_t item_done = 0; 228 auto print_progress = [&] { 229 outln("PROGRESS {} {} {} {} {} {} {}", i, items.size(), executed_work_bytes, total_work_bytes, item_done, item.size, item.source); 230 }; 231 232 auto copy_file = [&](DeprecatedString const& source, DeprecatedString const& destination) -> ErrorOr<int> { 233 auto source_file = TRY(Core::File::open(source, Core::File::OpenMode::Read)); 234 // FIXME: When the file already exists, let the user choose the next action instead of renaming it by default. 235 auto destination_file = TRY(open_destination_file(destination)); 236 auto buffer = TRY(ByteBuffer::create_zeroed(64 * KiB)); 237 238 while (true) { 239 print_progress(); 240 auto bytes_read = TRY(source_file->read_some(buffer.bytes())); 241 if (bytes_read.is_empty()) 242 break; 243 if (auto result = destination_file->write_until_depleted(bytes_read); result.is_error()) { 244 // FIXME: Return the formatted string directly. There is no way to do this right now without the temporary going out of scope and being destroyed. 245 report_warning(DeprecatedString::formatted("Failed to write to destination file: {}", result.error())); 246 return result.release_error(); 247 } 248 item_done += bytes_read.size(); 249 executed_work_bytes += bytes_read.size(); 250 print_progress(); 251 // FIXME: Remove this once the kernel is smart enough to schedule other threads 252 // while we're doing heavy I/O. Right now, copying a large file will totally 253 // starve the rest of the system. 254 sched_yield(); 255 } 256 print_progress(); 257 return 0; 258 }; 259 260 switch (item.type) { 261 262 case WorkItem::Type::CreateDirectory: { 263 outln("MKDIR {}", item.destination); 264 // FIXME: Support deduplication like open_destination_file() when the directory already exists. 265 if (mkdir(item.destination.characters(), 0755) < 0 && errno != EEXIST) 266 return Error::from_syscall("mkdir"sv, -errno); 267 break; 268 } 269 270 case WorkItem::Type::DeleteDirectory: { 271 TRY(Core::System::rmdir(item.source)); 272 break; 273 } 274 275 case WorkItem::Type::CopyFile: { 276 TRY(copy_file(item.source, item.destination)); 277 break; 278 } 279 280 case WorkItem::Type::MoveFile: { 281 DeprecatedString destination = item.destination; 282 while (true) { 283 if (rename(item.source.characters(), destination.characters()) == 0) { 284 item_done += item.size; 285 executed_work_bytes += item.size; 286 print_progress(); 287 break; 288 } 289 auto original_errno = errno; 290 291 if (original_errno == EEXIST) { 292 destination = deduplicate_destination_file_name(destination); 293 continue; 294 } 295 296 if (original_errno != EXDEV) { 297 // FIXME: Return the formatted string directly. There is no way to do this right now without the temporary going out of scope and being destroyed. 298 report_warning(DeprecatedString::formatted("Failed to move {}: {}", item.source, strerror(original_errno))); 299 return Error::from_errno(original_errno); 300 } 301 302 // EXDEV means we have to copy the file data and then remove the original 303 TRY(copy_file(item.source, item.destination)); 304 TRY(Core::System::unlink(item.source)); 305 break; 306 } 307 308 break; 309 } 310 311 case WorkItem::Type::DeleteFile: { 312 TRY(Core::System::unlink(item.source)); 313 314 item_done += item.size; 315 executed_work_bytes += item.size; 316 print_progress(); 317 318 break; 319 } 320 321 default: 322 VERIFY_NOT_REACHED(); 323 } 324 } 325 326 outln("FINISH"); 327 return 0; 328} 329 330ErrorOr<NonnullOwnPtr<Core::File>> open_destination_file(DeprecatedString const& destination) 331{ 332 auto destination_file_or_error = Core::File::open(destination, (Core::File::OpenMode)(Core::File::OpenMode::Write | Core::File::OpenMode::Truncate | Core::File::OpenMode::MustBeNew)); 333 if (destination_file_or_error.is_error() && destination_file_or_error.error().code() == EEXIST) { 334 return open_destination_file(deduplicate_destination_file_name(destination)); 335 } 336 return destination_file_or_error; 337} 338 339DeprecatedString deduplicate_destination_file_name(DeprecatedString const& destination) 340{ 341 LexicalPath destination_path(destination); 342 auto title_without_counter = destination_path.title(); 343 size_t next_counter = 1; 344 345 auto last_hyphen_index = title_without_counter.find_last('-'); 346 if (last_hyphen_index.has_value()) { 347 auto counter_string = title_without_counter.substring_view(*last_hyphen_index + 1); 348 auto last_counter = counter_string.to_uint(); 349 if (last_counter.has_value()) { 350 next_counter = *last_counter + 1; 351 title_without_counter = title_without_counter.substring_view(0, *last_hyphen_index); 352 } 353 } 354 355 StringBuilder basename; 356 basename.appendff("{}-{}", title_without_counter, next_counter); 357 if (!destination_path.extension().is_empty()) { 358 basename.append('.'); 359 basename.append(destination_path.extension()); 360 } 361 362 return LexicalPath::join(destination_path.dirname(), basename.to_deprecated_string()).string(); 363}