From 4205038c45ac195ebaa4dada71e0d67e535945d3 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Mon, 12 Apr 2021 20:29:57 +0200 Subject: [PATCH] FileOperation: Add a new helper program for out-of-process file ops This is a helper program for FileManager that performs a file operation in a separate process and reports progress on standard out. This initial implementation only supports the "Copy" operation and does not do any detailed error handling. --- Userland/Services/CMakeLists.txt | 1 + .../Services/FileOperation/CMakeLists.txt | 6 + Userland/Services/FileOperation/main.cpp | 168 ++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 Userland/Services/FileOperation/CMakeLists.txt create mode 100644 Userland/Services/FileOperation/main.cpp diff --git a/Userland/Services/CMakeLists.txt b/Userland/Services/CMakeLists.txt index 2a1a478860..6a40b4d5c2 100644 --- a/Userland/Services/CMakeLists.txt +++ b/Userland/Services/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory(Clipboard) add_subdirectory(CrashDaemon) add_subdirectory(DHCPClient) add_subdirectory(EchoServer) +add_subdirectory(FileOperation) add_subdirectory(ImageDecoder) add_subdirectory(LaunchServer) add_subdirectory(LookupServer) diff --git a/Userland/Services/FileOperation/CMakeLists.txt b/Userland/Services/FileOperation/CMakeLists.txt new file mode 100644 index 0000000000..6373889dc5 --- /dev/null +++ b/Userland/Services/FileOperation/CMakeLists.txt @@ -0,0 +1,6 @@ +set(SOURCES + main.cpp +) + +serenity_bin(FileOperation) +target_link_libraries(FileOperation LibCore) diff --git a/Userland/Services/FileOperation/main.cpp b/Userland/Services/FileOperation/main.cpp new file mode 100644 index 0000000000..719905d84f --- /dev/null +++ b/Userland/Services/FileOperation/main.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2021, Andreas Kling + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include + +static int perform_copy(const String& source, const String& destination); + +int main(int argc, char** argv) +{ + String operation; + String source; + String destination; + + Core::ArgsParser args_parser; + args_parser.add_positional_argument(operation, "Operation", "operation", Core::ArgsParser::Required::Yes); + args_parser.add_positional_argument(source, "Source", "source", Core::ArgsParser::Required::Yes); + args_parser.add_positional_argument(destination, "Destination", "destination", Core::ArgsParser::Required::Yes); + args_parser.parse(argc, argv); + + if (operation == "Copy") + return perform_copy(source, destination); + + warnln("Unknown operation '{}'", operation); + return 0; +} + +struct WorkItem { + enum class Type { + CreateDirectory, + CopyFile, + }; + Type type; + String source; + String destination; + off_t size; +}; + +static bool collect_work_items(const String& source, const String& destination, Vector& items) +{ + struct stat st = {}; + if (stat(source.characters(), &st) < 0) { + perror("stat"); + return false; + } + + if (!S_ISDIR(st.st_mode)) { + // It's a file. + items.append(WorkItem { + .type = WorkItem::Type::CopyFile, + .source = source, + .destination = String::formatted("{}/{}", destination, LexicalPath(source).basename()), + .size = st.st_size, + }); + return true; + } + + // It's a directory. + items.append(WorkItem { + .type = WorkItem::Type::CreateDirectory, + .source = {}, + .destination = String::formatted("{}/{}", destination, LexicalPath(source).basename()), + .size = 0, + }); + + Core::DirIterator dt(source, Core::DirIterator::SkipParentAndBaseDir); + while (dt.has_next()) { + auto name = dt.next_path(); + if (!collect_work_items( + String::formatted("{}/{}", source, name), + String::formatted("{}/{}", destination, LexicalPath(source).basename()), + items)) { + return false; + } + } + + return true; +} + +int perform_copy(const String& source, const String& destination) +{ + Vector items; + + if (!collect_work_items(source, destination, items)) + return 1; + + off_t total_bytes_to_copy = 0; + for (auto& item : items) + total_bytes_to_copy += item.size; + + off_t bytes_copied_so_far = 0; + + for (size_t i = 0; i < items.size(); ++i) { + auto& item = items[i]; + auto print_progress = [&] { + outln("PROGRESS {} {} {} {} {}", i, items.size(), bytes_copied_so_far, total_bytes_to_copy, item.source); + }; + if (item.type == WorkItem::Type::CreateDirectory) { + outln("MKDIR {}", item.destination); + if (mkdir(item.destination.characters(), 0755) < 0 && errno != EEXIST) { + perror("mkdir"); + return 1; + } + continue; + } + VERIFY(item.type == WorkItem::Type::CopyFile); + auto source_file_or_error = Core::File::open(item.source, Core::File::ReadOnly); + if (source_file_or_error.is_error()) { + warnln("Failed to open {} for reading: {}", item.source, source_file_or_error.error()); + return 1; + } + auto destination_file_or_error = Core::File::open(item.destination, (Core::IODevice::OpenMode)(Core::File::WriteOnly | Core::File::Truncate)); + if (destination_file_or_error.is_error()) { + warnln("Failed to open {} for write: {}", item.destination, destination_file_or_error.error()); + return 1; + } + auto& source_file = *source_file_or_error.value(); + auto& destination_file = *destination_file_or_error.value(); + + while (true) { + auto buffer = source_file.read(65536); + if (buffer.is_null()) + break; + if (!destination_file.write(buffer)) { + warnln("Failed to write to destination file: {}", destination_file.error_string()); + return 1; + } + bytes_copied_so_far += buffer.size(); + print_progress(); + // FIXME: Remove this once the kernel is smart enough to schedule other threads + // while we're doing heavy I/O. Right now, copying a large file will totally + // starve the rest of the system. + sched_yield(); + } + print_progress(); + } + + outln("FINISH"); + return 0; +}