Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support multiple part #1

Merged
merged 4 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
- name: Install deps
run: |
sudo apt update && sudo apt install -y valgrind libcurl4-openssl-dev
- name: Run tests
run: |
make test
- name: Run examples
run: |
make run-examples
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
run-examples:
zig build run-basic -freference-trace
zig build run-advanced -freference-trace

test:
zig build test

.PHONY: test run-examples
3 changes: 3 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub fn build(b: *std.Build) void {
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
});
main_tests.addModule(MODULE_NAME, module);
main_tests.linkSystemLibrary("curl");
main_tests.linkLibC();

const run_main_tests = b.addRunArtifact(main_tests);
const test_step = b.step("test", "Run library tests");
Expand Down
22 changes: 22 additions & 0 deletions examples/advanced.zig
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fn put_with_custom_header(allocator: Allocator, easy: Easy) !void {
var req = curl.Request(@TypeOf(body)).init("http://httpbin.org/anything/zig-curl", body, .{
.method = .PUT,
.header = header,
.verbose = true,
});
defer req.deinit();

Expand Down Expand Up @@ -81,6 +82,26 @@ fn put_with_custom_header(allocator: Allocator, easy: Easy) !void {
}
}

fn post_mutli_part(easy: Easy) !void {
const multi_part = try easy.add_multi_part();
try multi_part.add_part("foo", .{ .data = "hello foo" });
try multi_part.add_part("bar", .{ .data = "hello bar" });
try multi_part.add_part("build.zig", .{ .file = "build.zig" });
try multi_part.add_part("readme", .{ .file = "README.org" });

var req = curl.Request(void).init("http://httpbin.org/anything/mp", {}, .{
.method = .PUT,
.multi_part = multi_part,
.verbose = true,
});
defer req.deinit();

const resp = try easy.do(req);
defer resp.deinit();

std.debug.print("resp:{s}\n", .{resp.body.items});
}

pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() != .ok) @panic("leak");
Expand All @@ -93,4 +114,5 @@ pub fn main() !void {

println("PUT with custom header demo");
try put_with_custom_header(allocator, easy);
try post_mutli_part(easy);
}
41 changes: 30 additions & 11 deletions src/c.zig
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,38 @@ pub fn print_libcurl_version() void {
}
}

pub fn polyfill_struct_curl_header() type {
if (has_parse_header_support()) {
return *c.struct_curl_header;
} else {
// return a dummy struct to make it compile on old version.
return struct {
value: [:0]const u8,
};
}
}

pub fn has_parse_header_support() bool {
// `curl_header` is officially supported since 7.84.0.
// https://curl.se/libcurl/c/curl_easy_header.html
return c.CURL_AT_LEAST_VERSION(7, 84, 0);
}

comptime {
// `curl_easy_reset` is only available since 7.12.0
if (!c.CURL_AT_LEAST_VERSION(7, 12, 0)) {
@compileError("Libcurl version must at least 7.12.0");
}
}

pub fn url_encode(string: []const u8) ?[]const u8 {
const r = c.curl_easy_escape(null, string.ptr, @intCast(string.len));
return std.mem.sliceTo(r.?, 0);
}

test "url encode" {
inline for (.{
.{
"https://github.com/",
"https%3A%2F%2Fgit.luolix.top%2F",
},
.{
"https://httpbin.org/anything/你好",
"https%3A%2F%2Fhttpbin.org%2Fanything%2F%E4%BD%A0%E5%A5%BD",
},
}) |case| {
const input = case.@"0";
const expected = case.@"1";
const actual = url_encode(input);
try std.testing.expectEqualStrings(expected, actual.?);
}
}
105 changes: 86 additions & 19 deletions src/easy.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ const fmt = std.fmt;
const Allocator = mem.Allocator;
const checkCode = errors.checkCode;

const has_curl_header = @import("c.zig").has_parse_header_support;
const polyfill_struct_curl_header = @import("c.zig").polyfill_struct_curl_header;
const has_parse_header_support = @import("c.zig").has_parse_header_support;

const Self = @This();

Expand Down Expand Up @@ -90,6 +89,7 @@ pub const RequestHeader = struct {
pub const RequestArgs = struct {
method: Method = .GET,
header: ?RequestHeader = null,
multi_part: ?MultiPart = null,
verbose: bool = false,
/// Redirection limit, 0 refuse any redirect, -1 for an infinite number of redirects.
redirects: i32 = 10,
Expand All @@ -116,6 +116,9 @@ pub fn Request(comptime ReaderType: type) type {
if (self.args.header) |*h| {
h.deinit();
}
if (self.args.multi_part) |mp| {
mp.deinit();
}
}

fn getVerbose(self: @This()) c_long {
Expand Down Expand Up @@ -157,7 +160,7 @@ pub const Response = struct {

/// Gets the header associated with the given name.
pub fn get_header(self: @This(), name: []const u8) errors.HeaderError!?Header {
if (comptime !has_curl_header()) {
if (comptime !has_parse_header_support()) {
return error.NoCurlHeaderSupport;
}

Expand All @@ -179,56 +182,109 @@ pub const Response = struct {
}
};

pub fn init(allocator: Allocator) !Self {
const handle = c.curl_easy_init();
if (handle == null) {
return error.Init;
}
pub const MultiPart = struct {
mime_handle: *c.curl_mime,
allocator: Allocator,

return .{
.allocator = allocator,
.handle = handle.?,
pub const DataSource = union(enum) {
/// Set a mime part's body content from memory data.
/// Data will get copied when send request.
/// Setting large data is memory consuming: one might consider using `data_callback` in such a case.
data: []const u8,
/// Set a mime part's body data from a file contents.
file: []const u8,
// TODO: https://curl.se/libcurl/c/curl_mime_data_cb.html
// data_callback: u8,
};

pub fn deinit(self: @This()) void {
c.curl_mime_free(self.mime_handle);
}

pub fn add_part(self: @This(), name: []const u8, source: DataSource) !void {
const part = if (c.curl_mime_addpart(self.mime_handle)) |part| part else return error.MimeAddPart;

const namez = try fmt.allocPrintZ(self.allocator, "{s}", .{name});
defer self.allocator.free(namez);

try checkCode(c.curl_mime_name(part, namez));
switch (source) {
.data => |slice| {
try checkCode(c.curl_mime_data(part, slice.ptr, slice.len));
},
.file => |filepath| {
const filepathz = try std.fmt.allocPrintZ(self.allocator, "{s}", .{filepath});
defer self.allocator.free(filepathz);

try checkCode(c.curl_mime_filedata(part, filepathz));
},
}
}
};

pub fn init(allocator: Allocator) !Self {
return if (c.curl_easy_init()) |h|
.{
.allocator = allocator,
.handle = h,
}
else
error.CurlInit;
}

pub fn deinit(self: Self) void {
c.curl_easy_cleanup(self.handle);
}

pub fn add_multi_part(self: Self) !MultiPart {
return if (c.curl_mime_init(self.handle)) |h|
.{
.allocator = self.allocator,
.mime_handle = h,
}
else
error.MimeInit;
}

/// Do sends an HTTP request and returns an HTTP response.
pub fn do(self: Self, req: anytype) !Response {
try self.set_common_opts();
// Re-initializes all options previously set on a specified CURL handle to the default values.
defer c.curl_easy_reset(self.handle);

try self.set_common_opts();
const url = try fmt.allocPrintZ(self.allocator, "{s}", .{req.url});
defer self.allocator.free(url);
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_URL, url.ptr));

try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MAXREDIRS, @as(c_long, req.args.redirects)));
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_CUSTOMREQUEST, req.args.method.asString().ptr));
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_VERBOSE, req.getVerbose()));

const body = try req.getBody(self.allocator);
defer if (body) |b| self.allocator.free(b);

defer if (body) |b| {
self.allocator.free(b);
};
if (body) |b| {
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDS, b.ptr));
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDSIZE, b.len));
} else {
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDSIZE, @as(c_long, 0)));
}

var mime_handle: ?*c.curl_mime = null;
if (req.args.multi_part) |mp| {
mime_handle = mp.mime_handle;
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MIMEPOST, mime_handle));
}

var header: ?*c.struct_curl_slist = null;
if (req.args.header) |h| {
header = try h.asCHeader(self.default_user_agent);
}
defer if (header) |h| RequestHeader.freeCHeader(h);

try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_HTTPHEADER, header));
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_WRITEFUNCTION, write_callback));

var resp_buffer = Buffer.init(self.allocator);
errdefer resp_buffer.deinit();
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_WRITEDATA, &resp_buffer));
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_WRITEFUNCTION, write_callback));

try checkCode(c.curl_easy_perform(self.handle));

Expand Down Expand Up @@ -291,3 +347,14 @@ fn write_callback(ptr: [*c]c_char, size: c_uint, nmemb: c_uint, user_data: *anyo
fn set_common_opts(self: Self) !void {
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_TIMEOUT_MS, self.timeout_ms));
}

pub fn polyfill_struct_curl_header() type {
if (has_parse_header_support()) {
return *c.struct_curl_header;
} else {
// return a dummy struct to make it compile on old version.
return struct {
value: [:0]const u8,
};
}
}
11 changes: 7 additions & 4 deletions src/main.zig
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
const std = @import("std");
const c = @import("c.zig").c;
const checkCode = @import("errors.zig").checkCode;

pub const Easy = @import("easy.zig");
pub usingnamespace Easy;

pub const print_libcurl_version = @import("c.zig").print_libcurl_version;
pub const has_parse_header_support = @import("c.zig").has_parse_header_support;
pub usingnamespace @import("c.zig");

/// This function sets up the program environment that libcurl needs.
/// Since this function is not thread safe before libcurl 7.84.0, this function
/// must be called before the program calls any other function in libcurl.
/// A common place is in the beginning of the program. More see:
/// https://curl.se/libcurl/c/curl_global_init.html
pub fn global_init() !void {
checkCode(c.curl_global_init(c.CURL_GLOBAL_ALL));
try checkCode(c.curl_global_init(c.CURL_GLOBAL_ALL));
}

/// This function releases resources acquired by curl_global_init.
pub fn global_deinit() void {
c.curl_global_cleanup();
}

test {
std.testing.refAllDecls(@This());
}
Loading