-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathe6_file_downloader.zig
More file actions
160 lines (128 loc) · 4.8 KB
/
e6_file_downloader.zig
File metadata and controls
160 lines (128 loc) · 4.8 KB
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
const std = @import("std");
const chilli = @import("chilli");
const DownloadContext = struct {
client: std.http.Client,
allocator: std.mem.Allocator,
};
fn rootExec(ctx: chilli.CommandContext) !void {
try ctx.command.printHelp();
}
fn downloadExec(ctx: chilli.CommandContext) !void {
const url = try ctx.getArg("url", []const u8);
const output_path_arg = try ctx.getArg("output", []const u8);
const verbose = try ctx.getFlag("verbose", bool);
var download_ctx = ctx.getContextData(DownloadContext).?;
const output_path = if (output_path_arg.len > 0)
try download_ctx.allocator.dupe(u8, output_path_arg)
else
try getFilenameFromUrl(download_ctx.allocator, url);
defer download_ctx.allocator.free(output_path);
if (verbose) {
std.debug.print("Downloading: {s}\n", .{url});
std.debug.print("Output file: {s}\n", .{output_path});
}
// Parse URI and create request
const uri = try std.Uri.parse(url);
var req = try download_ctx.client.request(.GET, uri, .{});
defer req.deinit();
// Send request
try req.sendBodiless();
// Receive response headers
var redirect_buf: [1024]u8 = undefined;
var response = try req.receiveHead(&redirect_buf);
if (response.head.status != .ok) {
std.debug.print("Error: HTTP {d} - {s}\n", .{ @intFromEnum(response.head.status), @tagName(response.head.status) });
return error.HttpRequestFailed;
}
// Read response body
var buf: [8192]u8 = undefined;
var body_reader = response.reader(&buf);
var response_body: std.ArrayList(u8) = .empty;
defer response_body.deinit(ctx.app_allocator);
try body_reader.appendRemainingUnlimited(ctx.app_allocator, &response_body);
if (verbose) {
std.debug.print("Downloaded {d} bytes\n", .{response_body.items.len});
}
// Write to file
const io = std.Options.debug_io;
const file = try std.Io.Dir.cwd().createFile(io, output_path, .{});
defer file.close(io);
var file_buf: [8192]u8 = undefined;
var file_writer = file.writer(io, &file_buf);
try file_writer.interface.writeAll(response_body.items);
try file_writer.flush();
std.debug.print("Download complete: {s} ({d} bytes)\n", .{ output_path, response_body.items.len });
}
fn getFilenameFromUrl(allocator: std.mem.Allocator, url: []const u8) ![]const u8 {
const uri = std.Uri.parse(url) catch
return allocator.dupe(u8, "downloaded_file");
const path_str = switch (uri.path) {
.raw => |raw| raw,
.percent_encoded => |encoded| encoded,
};
const filename = if (std.mem.lastIndexOfScalar(u8, path_str, '/')) |idx|
path_str[idx + 1 ..]
else
path_str;
if (filename.len == 0) {
return allocator.dupe(u8, "downloaded_file");
}
return allocator.dupe(u8, filename);
}
pub fn main(init: std.process.Init.Minimal) !void {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var client = std.http.Client{ .allocator = allocator, .io = std.Options.debug_io };
defer client.deinit();
var download_ctx = DownloadContext{
.client = client,
.allocator = allocator,
};
var root_cmd = try chilli.Command.init(allocator, .{
.name = "downloader",
.description = "A simple file downloader using HTTP.",
.exec = rootExec,
});
defer root_cmd.deinit();
try root_cmd.addFlag(.{
.name = "verbose",
.shortcut = 'v',
.description = "Enable verbose output",
.type = .Bool,
.default_value = .{ .Bool = false },
});
var download_cmd = try chilli.Command.init(allocator, .{
.name = "download",
.description = "Download a file from a URL.",
.exec = downloadExec,
});
try download_cmd.addPositional(.{
.name = "url",
.description = "The URL to download from.",
.is_required = true,
});
try download_cmd.addPositional(.{
.name = "output",
.description = "Output filename (auto-detected if not provided).",
.is_required = false,
.default_value = .{ .String = "" },
});
try root_cmd.addSubcommand(download_cmd);
try root_cmd.run(init.args, &download_ctx);
}
// Example Invocations
//
// 1. Build the example executable:
// zig build e6_file_downloader
//
// 2. Run with different arguments:
//
// // Show the help message
// ./zig-out/bin/e6_file_downloader --help
//
// // Download a file, letting the program determine the output filename
// ./zig-out/bin/e6_file_downloader download https://ziglang.org/zig-logo.svg
//
// // Download a file with verbose logging and a specified output filename
// ./zig-out/bin/e6_file_downloader -v download https://ziglang.org/documentation/master/std/std.zig zig_std.zig