diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f4b06c7..cc49082 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -22,23 +22,27 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest] + os: [ubuntu-latest, macos-latest] zig-version: [master, 0.12.0] steps: - uses: actions/checkout@v4 - uses: goto-bus-stop/setup-zig@v2 with: version: ${{ matrix.zig-version }} - - name: Install deps - run: | - sudo apt update && sudo apt install -y valgrind - name: Run tests run: | make test - name: Run examples run: | + make serve & + sleep 5 make run + - name: Install deps + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt update && sudo apt install -y valgrind - name: Memory leak detect + if: matrix.os == 'ubuntu-latest' run: | zig build -Dcpu=baseline --verbose @@ -47,20 +51,3 @@ jobs: valgrind --leak-check=full --tool=memcheck \ --show-leak-kinds=all --error-exitcode=1 ${bin} done - - test-macos: - timeout-minutes: 10 - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [macos-latest] - zig-version: [master, 0.12.0] - steps: - - uses: actions/checkout@v4 - - uses: goto-bus-stop/setup-zig@v1 - with: - version: ${{ matrix.zig-version }} - - name: Run examples - run: | - make run diff --git a/Makefile b/Makefile index 9c433e9..5946acc 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ prepare: clean: rm -rf zig-cache zig-out +serve: + cd server && go run main.go + run: zig build run-basic -freference-trace zig build run-advanced -freference-trace diff --git a/examples/basic.zig b/examples/basic.zig index 369d722..f85a162 100644 --- a/examples/basic.zig +++ b/examples/basic.zig @@ -5,6 +5,8 @@ const Allocator = mem.Allocator; const curl = @import("curl"); const Easy = curl.Easy; +const LOCAL_SERVER_ADDR = "http://localhost:8182"; + fn get(allocator: Allocator, easy: Easy) !void { try easy.setVerbose(true); const resp = try easy.get("https://httpbin.org/anything"); @@ -66,6 +68,27 @@ fn post(allocator: Allocator, easy: Easy) !void { }); } +fn upload(allocator: Allocator, easy: Easy) !void { + const path = "LICENSE"; + const resp = try easy.upload(LOCAL_SERVER_ADDR ++ "/anything", path); + const Response = struct { + method: []const u8, + body_len: usize, + }; + const parsed = try std.json.parseFromSlice(Response, allocator, resp.body.?.items, .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + + try std.testing.expectEqualDeep(parsed.value, Response{ + .body_len = 1086, + .method = "PUT", + }); + + std.debug.print("Status code: {d}\nBody: {s}\n", .{ + resp.status_code, + resp.body.?.items, + }); +} + pub fn main() !void { const allocator = std.heap.page_allocator; @@ -80,5 +103,10 @@ pub fn main() !void { try get(allocator, easy); println("POST demo"); + easy.reset(); try post(allocator, easy); + + println("Upload demo"); + easy.reset(); + try upload(allocator, easy); } diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..c1e6927 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,3 @@ +module server + +go 1.20.0 diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..c4e50ad --- /dev/null +++ b/server/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "fmt" + "html" + "io" + "log" + "net/http" +) + +type Response struct { + Method string `json:"method"` + Path string `json:"path"` + Body string `json:"body"` + BodyLen int `json:"body_len"` + Headers map[string]string `json:"headers"` +} + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) + }) + + http.HandleFunc("/anything", func(w http.ResponseWriter, r *http.Request) { + bs, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer r.Body.Close() + + headers := map[string]string{} + for k, v := range r.Header { + headers[k] = v[0] + } + + ret := Response{ + Method: r.Method, + Path: r.URL.Path, + Body: string(bs), + BodyLen: len(bs), + Headers: headers, + } + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + log.Println("Listening on :8182") + log.Fatal(http.ListenAndServe(":8182", nil)) +} diff --git a/src/Easy.zig b/src/Easy.zig index b5cbed0..5e9c4d4 100644 --- a/src/Easy.zig +++ b/src/Easy.zig @@ -134,6 +134,33 @@ pub const MultiPart = struct { } }; +pub const Upload = struct { + file: std.fs.File, + file_len: u64, + + pub fn init(path: []const u8) !Upload { + const file = try std.fs.cwd().openFile(path, .{}); + const md = try file.metadata(); + return .{ .file = file, .file_len = md.size() }; + } + + pub fn deinit(self: Upload) void { + self.file.close(); + } + + pub fn readFunction(ptr: [*c]c_char, size: c_uint, nmemb: c_uint, user_data: *anyopaque) callconv(.C) c_uint { + const up: *Upload = @alignCast(@ptrCast(user_data)); + const max_length = @min(size * nmemb, up.file_len); + var buf: [*]u8 = @ptrCast(ptr); + const n = up.file.read(buf[0..max_length]) catch |e| { + std.log.err("Upload read file failed, err:{any}\n", .{e}); + return c.CURL_READFUNC_ABORT; + }; + + return @intCast(n); + } +}; + /// Init options for Easy handle pub const Options = struct { // Note that the vendored libcurl is compiled with mbedtls and does not include a CA bundle, @@ -201,6 +228,12 @@ pub fn setMultiPart(self: Self, multi_part: MultiPart) !void { try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MIMEPOST, multi_part.mime_handle)); } +pub fn setUpload(self: Self, up: *Upload) !void { + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_UPLOAD, @as(c_int, 1))); + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_READFUNCTION, Upload.readFunction)); + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_READDATA, up)); +} + pub fn reset(self: Self) void { c.curl_easy_reset(self.handle); } @@ -257,7 +290,7 @@ pub fn head(self: Self, url: [:0]const u8) !Response { return self.perform(); } -// /// Post issues a POST to the specified URL. +/// Post issues a POST to the specified URL. pub fn post(self: Self, url: [:0]const u8, content_type: []const u8, body: []const u8) !Response { var buf = Buffer.init(self.allocator); try self.setWritefunction(bufferWriteCallback); @@ -275,6 +308,21 @@ pub fn post(self: Self, url: [:0]const u8, content_type: []const u8, body: []con return resp; } +/// Upload issues a PUT request to upload file. +pub fn upload(self: Self, url: [:0]const u8, path: []const u8) !Response { + var up = try Upload.init(path); + defer up.deinit(); + + try self.setUpload(&up); + var buf = Buffer.init(self.allocator); + try self.setWritefunction(bufferWriteCallback); + try self.setWritedata(&buf); + try self.setUrl(url); + var resp = try self.perform(); + resp.body = buf; + return resp; +} + /// Used for write response via `Buffer` type. // https://curl.se/libcurl/c/CURLOPT_WRITEFUNCTION.html // size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata);