Index: .gitignore ================================================================== --- .gitignore +++ .gitignore @@ -40,7 +40,9 @@ tests/PARAM.SFO tests/objc_sync/objc_sync utils/objfw-config utils/ofhash/ofhash utils/ofhash/ofhash.exe +utils/ofhttp/ofhttp +utils/ofhttp/ofhttp.exe utils/ofzip/ofzip utils/ofzip/ofzip.exe Index: configure.ac ================================================================== --- configure.ac +++ configure.ac @@ -743,10 +743,14 @@ AC_ARG_ENABLE(sockets, AS_HELP_STRING([--disable-sockets], [disable socket support])) AS_IF([test x"$enable_sockets" != x"no"], [ AC_DEFINE(OF_HAVE_SOCKETS, 1, [Whether we have sockets]) AC_SUBST(USE_SRCS_SOCKETS, '${SRCS_SOCKETS}') + + AS_IF([test x"$enable_files" != x"no"], [ + AC_SUBST(OFHTTP, "ofhttp") + ]) AC_CHECK_LIB(socket, socket, LIBS="$LIBS -lsocket") AC_CHECK_LIB(network, socket, LIBS="$LIBS -lnetwork") AC_CHECK_LIB(ws2_32, main, LIBS="$LIBS -lws2_32") Index: extra.mk.in ================================================================== --- extra.mk.in +++ extra.mk.in @@ -29,10 +29,11 @@ LOOKUP_ASM_LOOKUP_ASM_A = @LOOKUP_ASM_LOOKUP_ASM_A@ LOOKUP_ASM_LOOKUP_ASM_LIB_A = @LOOKUP_ASM_LOOKUP_ASM_LIB_A@ MAP_LDFLAGS = @MAP_LDFLAGS@ OFBLOCKTESTS_M = @OFBLOCKTESTS_M@ OFHASH = @OFHASH@ +OFHTTP = @OFHTTP@ OFHTTPCLIENTTESTS_M = @OFHTTPCLIENTTESTS_M@ OFPROCESS_M = @OFPROCESS_M@ OFKERNELEVENTOBSERVER_KQUEUE_M = @OFKERNELEVENTOBSERVER_KQUEUE_M@ OFKERNELEVENTOBSERVER_POLL_M = @OFKERNELEVENTOBSERVER_POLL_M@ OFKERNELEVENTOBSERVER_SELECT_M = @OFKERNELEVENTOBSERVER_SELECT_M@ Index: utils/Makefile ================================================================== --- utils/Makefile +++ utils/Makefile @@ -1,8 +1,9 @@ include ../extra.mk SUBDIRS += ${OFHASH} \ + ${OFHTTP} \ ${OFZIP} include ../buildsys.mk DISTCLEAN = objfw-config ADDED utils/ofhttp/Makefile Index: utils/ofhttp/Makefile ================================================================== --- utils/ofhttp/Makefile +++ utils/ofhttp/Makefile @@ -0,0 +1,14 @@ +include ../../extra.mk + +PROG = ofhttp${PROG_SUFFIX} +SRCS = OFHTTP.m \ + ProgressBar.m + +include ../../buildsys.mk + +${PROG}: ${LIBOBJFW_DEP_LVL2} + +CPPFLAGS += -I../../src -I../../src/runtime -I../../src/exceptions -I../.. +LIBS := -L../../src -lobjfw ${LIBS} +LD = ${OBJC} +LDFLAGS += ${LDFLAGS_RPATH} ADDED utils/ofhttp/OFHTTP.m Index: utils/ofhttp/OFHTTP.m ================================================================== --- utils/ofhttp/OFHTTP.m +++ utils/ofhttp/OFHTTP.m @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015 + * Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#include "config.h" + +#import "OFApplication.h" +#import "OFArray.h" +#import "OFDictionary.h" +#import "OFFile.h" +#import "OFHTTPClient.h" +#import "OFHTTPRequest.h" +#import "OFHTTPResponse.h" +#import "OFOptionsParser.h" +#import "OFStdIOStream.h" +#import "OFSystemInfo.h" +#import "OFURL.h" + +#import "OFHTTPRequestFailedException.h" +#import "OFInvalidFormatException.h" +#import "OFOpenItemFailedException.h" +#import "OFUnsupportedProtocolException.h" + +#import "ProgressBar.h" + +#define GIBIBYTE (1024 * 1024 * 1024) +#define MEBIBYTE (1024 * 1024) +#define KIBIBYTE (1024) + +@interface OFHTTP: OFObject +{ + OFArray *_URLs; + size_t _URLIndex; + int _errorCode; + OFString *_outputPath; + bool _continue, _quiet; + OFHTTPClient *_HTTPClient; + char *_buffer; + OFStream *_output; + intmax_t _received, _length; + ProgressBar *_progressBar; +} + +- (void)downloadNextURL; +@end + +OF_APPLICATION_DELEGATE(OFHTTP) + +static void +help(OFStream *stream, bool full, int status) +{ + [of_stderr writeFormat: + @"Usage: %@ -[hoq] url1 [url2 ...]\n", + [OFApplication programName]]; + + if (full) + [stream writeString: + @"\nOptions:\n" + @" -h Show this help\n" + @" -o Output filename\n" + @" -q Quiet mode (no output, except errors)\n"]; + + [OFApplication terminateWithStatus: status]; +} + +@implementation OFHTTP +- init +{ + self = [super init]; + + @try { + _HTTPClient = [[OFHTTPClient alloc] init]; + _buffer = [self allocMemoryWithSize: [OFSystemInfo pageSize]]; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)applicationDidFinishLaunching +{ + OFOptionsParser *optionsParser = + [OFOptionsParser parserWithOptions: @"ho:q"]; + of_unichar_t option; + + while ((option = [optionsParser nextOption]) != '\0') { + switch (option) { + case 'h': + help(of_stdout, true, 0); + break; + case 'o': + [_outputPath release]; + _outputPath = [[optionsParser argument] retain]; + break; + case 'q': + _quiet = true; + break; + case ':': + [of_stderr writeFormat: @"%@: Argument for option -%C " + @"missing\n", + [OFApplication programName], + [optionsParser lastOption]]; + [OFApplication terminateWithStatus: 1]; + default: + [of_stderr writeFormat: @"%@: Unknown option: -%C\n", + [OFApplication programName], + [optionsParser lastOption]]; + [OFApplication terminateWithStatus: 1]; + } + } + + _URLs = [[optionsParser remainingArguments] retain]; + + if ([_URLs count] < 1) + help(of_stderr, false, 1); + + if (_outputPath != nil && [_URLs count] > 1) { + [of_stderr writeFormat: @"%@: Cannot use -o when more than " + @"one URL has been specified!\n", + [OFApplication programName]]; + [OFApplication terminateWithStatus: 1]; + } + + [self performSelector: @selector(downloadNextURL) + afterDelay: 0]; +} + +- (bool)stream: (OFHTTPResponse*)response + didReadIntoBuffer: (void*)buffer + length: (size_t)length + exception: (OFException*)e +{ + if (e != nil) { + OFURL *URL; + + [_progressBar stop]; + [_progressBar draw]; + [_progressBar release]; + _progressBar = nil; + + if (!_quiet) + [of_stdout writeString: @"\n Error!\n"]; + + URL = [_URLs objectAtIndex: _URLIndex - 1]; + [of_stderr writeFormat: @"%@: Failed to download <%@>: %@\n", + [OFApplication programName], + [URL string], e]; + + _errorCode = 1; + goto next; + } + + _received += length; + + [_output writeBuffer: buffer + length: length]; + + [_progressBar setReceived: _received]; + + if ([response isAtEndOfStream] || + (_length >= 0 && _received >= _length)) { + [_progressBar stop]; + [_progressBar draw]; + [_progressBar release]; + _progressBar = nil; + + if (!_quiet) + [of_stdout writeString: @"\n Done!\n"]; + + goto next; + } + + return true; + +next: + [self performSelector: @selector(downloadNextURL) + afterDelay: 0]; + return false; +} + +- (void)downloadNextURL +{ + OFString *URLString = nil; + OFURL *URL; + OFHTTPRequest *request; + OFHTTPResponse *response; + OFDictionary *headers; + OFString *fileName, *lengthString, *type; + + _length = -1; + _received = 0; + + if (_output != of_stdout) + [_output release]; + _output = nil; + + if (_URLIndex >= [_URLs count]) + [OFApplication terminateWithStatus: _errorCode]; + + @try { + URLString = [_URLs objectAtIndex: _URLIndex++]; + URL = [OFURL URLWithString: URLString]; + } @catch (OFInvalidFormatException *e) { + [of_stderr writeFormat: @"%@: Invalid URL: <%@>!\n", + [OFApplication programName], + URLString]; + + _errorCode = 1; + goto next; + } + + if (![[URL scheme] isEqual: @"http"] && + ![[URL scheme] isEqual: @"https"]) { + [of_stderr writeFormat: @"%@: Invalid scheme: <%@:>!\n", + [OFApplication programName], + URLString]; + + _errorCode = 1; + goto next; + } + + if (!_quiet) + [of_stdout writeFormat: @"⇣ %@", [URL string]]; + + request = [OFHTTPRequest requestWithURL: URL]; + + @try { + response = [_HTTPClient performRequest: request]; + } @catch (OFHTTPRequestFailedException *e) { + if (!_quiet) + [of_stdout writeFormat: @" ➜ %d\n", + [[e response] statusCode]]; + + [of_stderr writeFormat: @"%@: Failed to download <%@>!\n", + [OFApplication programName], + [URL string]]; + + _errorCode = 1; + goto next; + } @catch (OFUnsupportedProtocolException *e) { + if (!_quiet) + [of_stdout writeString: @"\n"]; + + [of_stderr writeFormat: @"%@: No SSL library loaded!\n" + @" In order to download via https, " + @"you need to preload an SSL library " + @"for ObjFW\n such as ObjOpenSSL!\n", + [OFApplication programName]]; + + _errorCode = 1; + goto next; + } + + if (!_quiet) + [of_stdout writeFormat: @" ➜ %d\n", [response statusCode]]; + + headers = [response headers]; + lengthString = [headers objectForKey: @"Content-Length"]; + type = [headers objectForKey: @"Content-Type"]; + + if (_outputPath != nil) + fileName = _outputPath; + else + fileName = [[URL path] lastPathComponent]; + + if (lengthString != nil) + _length = [lengthString decimalValue]; + + if (!_quiet) { + if (type == nil) + type = @"unknown"; + + if (lengthString != nil) { + if (_length >= GIBIBYTE) + lengthString = [OFString stringWithFormat: + @"%.2f GiB", (float)_length / GIBIBYTE]; + else if (_length >= MEBIBYTE) + lengthString = [OFString stringWithFormat: + @"%.2f MiB", (float)_length / MEBIBYTE]; + else if (_length >= KIBIBYTE) + lengthString = [OFString stringWithFormat: + @"%.2f KiB", (float)_length / KIBIBYTE]; + else + lengthString = [OFString stringWithFormat: + @"%jd bytes", _length]; + } else + lengthString = @"unknown"; + + [of_stdout writeFormat: @" Name: %@\n", fileName]; + [of_stdout writeFormat: @" Type: %@\n", type]; + [of_stdout writeFormat: @" Size: %@\n", lengthString]; + } + + if ([_outputPath isEqual: @"-"]) + _output = of_stdout; + else { + if ([OFFile fileExistsAtPath: fileName]) { + [of_stderr writeFormat: + @"%@: File %@ already exists!\n", + [OFApplication programName], fileName]; + + _errorCode = 1; + goto next; + } + + @try { + _output = [[OFFile alloc] initWithPath: fileName + mode: @"wb"]; + } @catch (OFOpenItemFailedException *e) { + [of_stderr writeFormat: + @"%@: Failed to open file %@!\n", + [OFApplication programName], fileName]; + + _errorCode = 1; + goto next; + } + } + + if (!_quiet) { + _progressBar = [[ProgressBar alloc] initWithLength: _length]; + [_progressBar draw]; + } + + [response asyncReadIntoBuffer: _buffer + length: [OFSystemInfo pageSize] + target: self + selector: @selector(stream:didReadIntoBuffer: + length:exception:)]; + return; + +next: + [self performSelector: @selector(downloadNextURL) + afterDelay: 0]; +} +@end ADDED utils/ofhttp/ProgressBar.h Index: utils/ofhttp/ProgressBar.h ================================================================== --- utils/ofhttp/ProgressBar.h +++ utils/ofhttp/ProgressBar.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015 + * Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#import "OFObject.h" + +@class OFDate; +@class OFTimer; + +@interface ProgressBar: OFObject +{ + intmax_t _length, _received, _lastReceived; + OFDate *_startDate; + double _lastDrawn; + OFTimer *_timer; + bool _stopped; +} + +- initWithLength: (intmax_t)length; +- (void)setReceived: (intmax_t)received; +- (void)draw; +- (void)stop; +@end ADDED utils/ofhttp/ProgressBar.m Index: utils/ofhttp/ProgressBar.m ================================================================== --- utils/ofhttp/ProgressBar.m +++ utils/ofhttp/ProgressBar.m @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015 + * Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#include "config.h" + +#include + +#import "OFDate.h" +#import "OFStdIOStream.h" +#import "OFTimer.h" + +#import "ProgressBar.h" + +#define GIBIBYTE (1024 * 1024 * 1024) +#define MEBIBYTE (1024 * 1024) +#define KIBIBYTE (1024) + +#define BAR_WIDTH 52 +#define UPDATE_INTERVAL 0.1 + +@implementation ProgressBar +- initWithLength: (intmax_t)length +{ + self = [super init]; + + @try { + void *pool = objc_autoreleasePoolPush(); + + _length = length; + _startDate = [[OFDate alloc] init]; + _timer = [[OFTimer + scheduledTimerWithTimeInterval: UPDATE_INTERVAL + target: self + selector: @selector(draw) + repeats: true] retain]; + + objc_autoreleasePoolPop(pool); + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + [self stop]; + + [_timer release]; + + [super dealloc]; +} + +- (void)setReceived: (intmax_t)received +{ + _received = received; +} + +- (void)_drawProgress +{ + uint_fast8_t i; + float bars, percent, bps; + + bars = (float)_received / (float)_length * BAR_WIDTH; + percent = (float)_received / (float)_length * 100; + + [of_stdout writeString: @"\r ▕"]; + + for (i = 0; i < (uint_fast8_t)bars; i++) + [of_stdout writeString: @"█"]; + if (bars < BAR_WIDTH) { + float rest = bars - floorf(bars); + + if (rest >= 0.875) + [of_stdout writeString: @"▉"]; + else if (rest >= 0.75) + [of_stdout writeString: @"▊"]; + else if (rest >= 0.625) + [of_stdout writeString: @"▋"]; + else if (rest >= 0.5) + [of_stdout writeString: @"▌"]; + else if (rest >= 0.375) + [of_stdout writeString: @"▍"]; + else if (rest >= 0.25) + [of_stdout writeString: @"▎"]; + else if (rest >= 0.125) + [of_stdout writeString: @"▏"]; + else + [of_stdout writeString: @" "]; + + for (i = 0; i < BAR_WIDTH - (uint_fast8_t)bars - 1; i++) + [of_stdout writeString: @" "]; + } + + [of_stdout writeFormat: @"▏ %6.2f%% ", percent]; + + if (percent == 100) + bps = (float)_received / -[_startDate timeIntervalSinceNow]; + else + bps = (float)(_received - _lastReceived) / UPDATE_INTERVAL; + + if (bps >= GIBIBYTE) + [of_stdout writeFormat: @"%7.2f GiB/s", bps / GIBIBYTE]; + else if (bps >= MEBIBYTE) + [of_stdout writeFormat: @"%7.2f MiB/s", bps / MEBIBYTE]; + else if (bps >= KIBIBYTE) + [of_stdout writeFormat: @"%7.2f KiB/s", bps / KIBIBYTE]; + else + [of_stdout writeFormat: @"%7.2f B/s ", bps]; + + _lastDrawn = [[OFDate date] timeIntervalSince1970]; + _lastReceived = _received; +} + +- (void)_drawReceived +{ + float bps; + + if (_received >= GIBIBYTE) + [of_stdout writeFormat: @"\r %7.2f GiB (", + (float)_received / GIBIBYTE]; + else if (_received >= MEBIBYTE) + [of_stdout writeFormat: @"\r %7.2f MiB ", + (float)_received / MEBIBYTE]; + else if (_received >= KIBIBYTE) + [of_stdout writeFormat: @"\r %7.2f KiB ", + (float)_received / KIBIBYTE]; + else + [of_stdout writeFormat: @"\r %jd bytes ", _received]; + + if (_stopped) + bps = (float)_received / -[_startDate timeIntervalSinceNow]; + else + bps = (float)(_received - _lastReceived) / UPDATE_INTERVAL; + + if (bps >= GIBIBYTE) + [of_stdout writeFormat: @"%7.2f GiB/s", bps / GIBIBYTE]; + else if (bps >= MEBIBYTE) + [of_stdout writeFormat: @"%7.2f MiB/s", bps / MEBIBYTE]; + else if (bps >= KIBIBYTE) + [of_stdout writeFormat: @"%7.2f KiB/s", bps / KIBIBYTE]; + else + [of_stdout writeFormat: @"%7.2f B/s ", bps]; + + _lastDrawn = [[OFDate date] timeIntervalSince1970]; + _lastReceived = _received; +} + +- (void)draw +{ + if (_length > 0) + [self _drawProgress]; + else + [self _drawReceived]; +} + +- (void)stop +{ + [_timer invalidate]; + + _stopped = true; +} +@end