pro.cpp 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. /*
  2. * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
  3. * All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. *
  8. * 1. Redistributions of source code must retain the above copyright notice, this
  9. * list of conditions and the following disclaimer.
  10. *
  11. * 2. Redistributions in binary form must reproduce the above copyright notice,
  12. * this list of conditions and the following disclaimer in the documentation
  13. * and/or other materials provided with the distribution.
  14. *
  15. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  16. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  17. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  18. * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  19. * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  20. * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  21. * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  22. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  23. * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  24. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  25. */
  26. #include <AK/GenericLexer.h>
  27. #include <AK/LexicalPath.h>
  28. #include <AK/NumberFormat.h>
  29. #include <AK/SharedBuffer.h>
  30. #include <AK/URL.h>
  31. #include <LibCore/ArgsParser.h>
  32. #include <LibCore/EventLoop.h>
  33. #include <LibCore/File.h>
  34. #include <LibProtocol/Client.h>
  35. #include <LibProtocol/Download.h>
  36. #include <ctype.h>
  37. #include <stdio.h>
  38. // FIXME: Move this somewhere else when it's needed (e.g. in the Browser)
  39. class ContentDispositionParser {
  40. public:
  41. ContentDispositionParser(const StringView& value)
  42. {
  43. GenericLexer lexer(value);
  44. lexer.ignore_while(isspace);
  45. if (lexer.consume_specific("inline")) {
  46. m_kind = Kind::Inline;
  47. if (!lexer.is_eof())
  48. m_might_be_wrong = true;
  49. return;
  50. }
  51. if (lexer.consume_specific("attachment")) {
  52. m_kind = Kind::Attachment;
  53. if (lexer.consume_specific(";")) {
  54. lexer.ignore_while(isspace);
  55. if (lexer.consume_specific("filename=")) {
  56. // RFC 2183: "A short (length <= 78 characters)
  57. // parameter value containing only non-`tspecials' characters SHOULD be
  58. // represented as a single `token'."
  59. // Some people seem to take this as generic advice of "if it doesn't have special characters,
  60. // it's safe to specify as a single token"
  61. // So let's just be as lenient as possible.
  62. if (lexer.next_is('"'))
  63. m_filename = lexer.consume_quoted_string();
  64. else
  65. m_filename = lexer.consume_until(is_any_of("()<>@,;:\\\"/[]?= "));
  66. } else {
  67. m_might_be_wrong = true;
  68. }
  69. }
  70. return;
  71. }
  72. if (lexer.consume_specific("form-data")) {
  73. m_kind = Kind::FormData;
  74. while (lexer.consume_specific(";")) {
  75. lexer.ignore_while(isspace);
  76. if (lexer.consume_specific("name=")) {
  77. m_name = lexer.consume_quoted_string();
  78. } else if (lexer.consume_specific("filename=")) {
  79. if (lexer.next_is('"'))
  80. m_filename = lexer.consume_quoted_string();
  81. else
  82. m_filename = lexer.consume_until(is_any_of("()<>@,;:\\\"/[]?= "));
  83. } else {
  84. m_might_be_wrong = true;
  85. }
  86. }
  87. return;
  88. }
  89. // FIXME: Support 'filename*'
  90. m_might_be_wrong = true;
  91. }
  92. enum class Kind {
  93. Inline,
  94. Attachment,
  95. FormData,
  96. };
  97. const StringView& filename() const { return m_filename; }
  98. const StringView& name() const { return m_name; }
  99. Kind kind() const { return m_kind; }
  100. bool might_be_wrong() const { return m_might_be_wrong; }
  101. private:
  102. StringView m_filename;
  103. StringView m_name;
  104. Kind m_kind { Kind::Inline };
  105. bool m_might_be_wrong { false };
  106. };
  107. static void do_write(const ByteBuffer& payload)
  108. {
  109. size_t length_remaining = payload.size();
  110. size_t length_written = 0;
  111. while (length_remaining > 0) {
  112. auto nwritten = fwrite(payload.offset_pointer(length_written), sizeof(char), length_remaining, stdout);
  113. if (nwritten > 0) {
  114. length_remaining -= nwritten;
  115. length_written += nwritten;
  116. continue;
  117. }
  118. if (feof(stdout)) {
  119. fprintf(stderr, "pro: unexpected eof while writing\n");
  120. return;
  121. }
  122. if (ferror(stdout)) {
  123. fprintf(stderr, "pro: error while writing\n");
  124. return;
  125. }
  126. }
  127. }
  128. int main(int argc, char** argv)
  129. {
  130. const char* url_str = nullptr;
  131. bool save_at_provided_name = false;
  132. Core::ArgsParser args_parser;
  133. args_parser.add_option(save_at_provided_name, "Write to a file named as the remote file", nullptr, 'O');
  134. args_parser.add_positional_argument(url_str, "URL to download from", "url");
  135. args_parser.parse(argc, argv);
  136. URL url(url_str);
  137. if (!url.is_valid()) {
  138. fprintf(stderr, "'%s' is not a valid URL\n", url_str);
  139. return 1;
  140. }
  141. Core::EventLoop loop;
  142. auto protocol_client = Protocol::Client::construct();
  143. auto download = protocol_client->start_download("GET", url.to_string());
  144. if (!download) {
  145. fprintf(stderr, "Failed to start download for '%s'\n", url_str);
  146. return 1;
  147. }
  148. u32 previous_downloaded_size { 0 };
  149. timeval prev_time, current_time, time_diff;
  150. gettimeofday(&prev_time, nullptr);
  151. download->on_progress = [&](Optional<u32> maybe_total_size, u32 downloaded_size) {
  152. fprintf(stderr, "\r\033[2K");
  153. if (maybe_total_size.has_value()) {
  154. fprintf(stderr, "\033]9;%d;%d;\033\\", downloaded_size, maybe_total_size.value());
  155. fprintf(stderr, "Download progress: %s / %s", human_readable_size(downloaded_size).characters(), human_readable_size(maybe_total_size.value()).characters());
  156. } else {
  157. fprintf(stderr, "Download progress: %s / ???", human_readable_size(downloaded_size).characters());
  158. }
  159. gettimeofday(&current_time, nullptr);
  160. timersub(&current_time, &prev_time, &time_diff);
  161. auto time_diff_ms = time_diff.tv_sec * 1000 + time_diff.tv_usec / 1000;
  162. auto size_diff = downloaded_size - previous_downloaded_size;
  163. fprintf(stderr, " at %s/s", human_readable_size(((float)size_diff / (float)time_diff_ms) * 1000).characters());
  164. previous_downloaded_size = downloaded_size;
  165. prev_time = current_time;
  166. };
  167. download->on_finish = [&](bool success, auto& payload, auto, auto& response_headers, auto) {
  168. fprintf(stderr, "\033]9;-1;\033\\");
  169. fprintf(stderr, "\n");
  170. if (success && save_at_provided_name) {
  171. String output_name;
  172. if (auto content_disposition = response_headers.get("Content-Disposition"); content_disposition.has_value()) {
  173. auto& value = content_disposition.value();
  174. ContentDispositionParser parser(value);
  175. output_name = parser.filename();
  176. }
  177. if (output_name.is_empty())
  178. output_name = url.path();
  179. LexicalPath path { output_name };
  180. output_name = path.basename();
  181. // The URL didn't have a name component, e.g. 'serenityos.org'
  182. if (output_name.is_empty() || output_name == "/") {
  183. int i = -1;
  184. do {
  185. output_name = url.host();
  186. if (i > -1)
  187. output_name = String::format("%s.%d", output_name.characters(), i);
  188. ++i;
  189. } while (Core::File::exists(output_name));
  190. }
  191. if (freopen(output_name.characters(), "w", stdout) == nullptr) {
  192. perror("freopen");
  193. success = false; // oops!
  194. loop.quit(1);
  195. }
  196. }
  197. if (success)
  198. do_write(payload);
  199. else
  200. fprintf(stderr, "Download failed :(\n");
  201. loop.quit(0);
  202. };
  203. dbgprintf("started download with id %d\n", download->id());
  204. return loop.exec();
  205. }