Skip to content

Commit

Permalink
feat: support multiple part (#1)
Browse files Browse the repository at this point in the history
* init multiple part

* add mp in request

* refactor mp

* add data_cb comments
  • Loading branch information
jiacai2050 authored Sep 20, 2023
1 parent a7a93ea commit 1d7daca
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 34 deletions.
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%2Fgithub.com%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());
}

0 comments on commit 1d7daca

Please sign in to comment.