main.cpp 13 KB

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