ObjFW  Check-in [486073790c]

Overview
Comment:Add utils/ofhttp

This is a small tool to download files via HTTP(S).

Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 486073790cc1d3080ee171f013377940215852410fabdc9c1ccd878ccae75aa1
User & Date: js on 2015-04-26 12:24:37
Other Links: manifest | tags
Context
2015-04-26
12:26
OFKernelEventObserverTests: Make old GCCs happy check-in: 8697161608 user: js tags: trunk
12:24
Add utils/ofhttp check-in: 486073790c user: js tags: trunk
10:42
OFKernelEventObserver: Keep FD -> object mapping check-in: f9ceddcb7d user: js tags: trunk
Changes

Modified .gitignore from [69b5130dd6] to [d6827ee9a0].

38
39
40
41
42
43
44


45
46
tests/tests.nds
tests/EBOOT.PBP
tests/PARAM.SFO
tests/objc_sync/objc_sync
utils/objfw-config
utils/ofhash/ofhash
utils/ofhash/ofhash.exe


utils/ofzip/ofzip
utils/ofzip/ofzip.exe







>
>


38
39
40
41
42
43
44
45
46
47
48
tests/tests.nds
tests/EBOOT.PBP
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

Modified configure.ac from [626f7189d6] to [92cc989460].

741
742
743
744
745
746
747




748
749
750
751
752
753
754
])

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}')





	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")

	AC_CHECK_HEADER(sys/socket.h, [
		AC_DEFINE(OF_HAVE_SYS_SOCKET_H, 1,







>
>
>
>







741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
])

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")

	AC_CHECK_HEADER(sys/socket.h, [
		AC_DEFINE(OF_HAVE_SYS_SOCKET_H, 1,

Modified extra.mk.in from [d9e9453b4a] to [020c04615c].

27
28
29
30
31
32
33

34
35
36
37
38
39
40
LOOKUP_ASM_A = @LOOKUP_ASM_A@
LOOKUP_ASM_LIB_A = @LOOKUP_ASM_LIB_A@
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@

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@
OFZIP = @OFZIP@
PROPERTIESTESTS_M = @PROPERTIESTESTS_M@







>







27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
LOOKUP_ASM_A = @LOOKUP_ASM_A@
LOOKUP_ASM_LIB_A = @LOOKUP_ASM_LIB_A@
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@
OFZIP = @OFZIP@
PROPERTIESTESTS_M = @PROPERTIESTESTS_M@

Modified utils/Makefile from [b16a509b18] to [66bf0f7ed1].

1
2
3

4
5
6
7
8
9
10
include ../extra.mk

SUBDIRS += ${OFHASH}	\

	   ${OFZIP}

include ../buildsys.mk

DISTCLEAN = objfw-config

install-extra: objfw-config objfw-compile



>







1
2
3
4
5
6
7
8
9
10
11
include ../extra.mk

SUBDIRS += ${OFHASH}	\
	   ${OFHTTP}	\
	   ${OFZIP}

include ../buildsys.mk

DISTCLEAN = objfw-config

install-extra: objfw-config objfw-compile

Added utils/ofhttp/Makefile version [07b3747125].





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
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 version [f40132b96b].



























































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
/*
 * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015
 *   Jonathan Schleifer <js@webkeks.org>
 *
 * 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 version [e0dcfb0362].







































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
 * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015
 *   Jonathan Schleifer <js@webkeks.org>
 *
 * 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 version [c13bb41aea].



































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/*
 * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015
 *   Jonathan Schleifer <js@webkeks.org>
 *
 * 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 <math.h>

#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