add initial backend web routes
This commit is contained in:
parent
080ea81ef5
commit
d2be071265
4 changed files with 213 additions and 52 deletions
|
@ -36,11 +36,17 @@ pub fn build(b: *std.Build) !void {
|
|||
// running `zig build`).
|
||||
b.installArtifact(lib);
|
||||
|
||||
const httpz = b.dependency("httpz", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const exe_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe_module.addImport("httpz", httpz.module("httpz"));
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "zetviel",
|
||||
|
@ -96,6 +102,7 @@ pub fn build(b: *std.Build) !void {
|
|||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe_test_module.addImport("httpz", httpz.module("httpz"));
|
||||
|
||||
const exe_unit_tests = b.addTest(.{
|
||||
.root_module = exe_test_module,
|
||||
|
|
|
@ -17,42 +17,11 @@
|
|||
// Once all dependencies are fetched, `zig build` no longer requires
|
||||
// internet connectivity.
|
||||
.dependencies = .{
|
||||
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
|
||||
//.example = .{
|
||||
// // When updating this field to a new URL, be sure to delete the corresponding
|
||||
// // `hash`, otherwise you are communicating that you expect to find the old hash at
|
||||
// // the new URL.
|
||||
// .url = "https://example.com/foo.tar.gz",
|
||||
//
|
||||
// // This is computed from the file contents of the directory of files that is
|
||||
// // obtained after fetching `url` and applying the inclusion rules given by
|
||||
// // `paths`.
|
||||
// //
|
||||
// // This field is the source of truth; packages do not come from a `url`; they
|
||||
// // come from a `hash`. `url` is just one of many possible mirrors for how to
|
||||
// // obtain a package matching this `hash`.
|
||||
// //
|
||||
// // Uses the [multihash](https://multiformats.io/multihash/) format.
|
||||
// .hash = "...",
|
||||
//
|
||||
// // When this is provided, the package is found in a directory relative to the
|
||||
// // build root. In this case the package's hash is irrelevant and therefore not
|
||||
// // computed. This field and `url` are mutually exclusive.
|
||||
// .path = "foo",
|
||||
|
||||
// // When this is set to `true`, a package is declared to be lazily
|
||||
// // fetched. This makes the dependency only get fetched if it is
|
||||
// // actually used.
|
||||
// .lazy = false,
|
||||
//},
|
||||
.httpz = .{
|
||||
.url = "git+https://github.com/karlseguin/http.zig?ref=master#5e5ab5f82477252fd85943bcb33db483bde6de86",
|
||||
.hash = "httpz-0.0.0-PNVzrLndBgBwKYPO0v3OFD-6741_9uKdWtU27sil2-df",
|
||||
},
|
||||
},
|
||||
|
||||
// Specifies the set of files and directories that are included in this package.
|
||||
// Only files and directories listed here are included in the `hash` that
|
||||
// is computed for this package.
|
||||
// Paths are relative to the build root. Use the empty string (`""`) to refer to
|
||||
// the build root itself.
|
||||
// A directory listed here means that all files within, recursively, are included.
|
||||
.paths = .{
|
||||
// This makes *all* files, recursively, included in this package. It is generally
|
||||
// better to explicitly list the files and directories instead, to insure that
|
||||
|
|
202
src/main.zig
202
src/main.zig
|
@ -1,23 +1,199 @@
|
|||
const std = @import("std");
|
||||
const httpz = @import("httpz");
|
||||
const root = @import("root.zig");
|
||||
|
||||
pub fn main() !void {
|
||||
// Prints to stderr (it's a shortcut based on `std.io.getStdErr()`)
|
||||
std.debug.print("All your {s} are belong to us. \n", .{"codebase"});
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
// stdout is for the actual output of your application, for example if you
|
||||
// are implementing gzip, then only the compressed bytes should be sent to
|
||||
// stdout, not any debugging messages.
|
||||
const stdout_file = std.fs.File{ .handle = std.posix.STDOUT_FILENO };
|
||||
try stdout_file.writeAll("Run `zig build test` to run the tests.\n");
|
||||
// Get notmuch database path from environment or use default
|
||||
const db_path = std.posix.getenv("NOTMUCH_PATH") orelse "mail";
|
||||
|
||||
// Example of using the root.zig functionality
|
||||
const allocator = std.heap.page_allocator;
|
||||
var db_result = root.openNotmuchDb(allocator, "mail", null) catch |err| {
|
||||
std.debug.print("Failed to open notmuch database: {}\n", .{err});
|
||||
// Open notmuch database
|
||||
var db = try root.openNotmuchDb(allocator, db_path, null);
|
||||
defer db.close();
|
||||
|
||||
std.debug.print("Zetviel starting on http://localhost:5000\n", .{});
|
||||
std.debug.print("Notmuch database: {s}\n", .{db.path});
|
||||
|
||||
// Create HTTP server
|
||||
var server = try httpz.Server(*root.NotmuchDb).init(allocator, .{
|
||||
.port = 5000,
|
||||
.address = "127.0.0.1",
|
||||
}, &db);
|
||||
defer server.deinit();
|
||||
|
||||
// API routes
|
||||
var router = try server.router(.{});
|
||||
router.get("/api/query/*", queryHandler, .{});
|
||||
router.get("/api/thread/:thread_id", threadHandler, .{});
|
||||
router.get("/api/message/:message_id", messageHandler, .{});
|
||||
router.get("/api/attachment/:message_id/:num", attachmentHandler, .{});
|
||||
|
||||
// TODO: Static file serving for frontend
|
||||
|
||||
try server.listen();
|
||||
}
|
||||
|
||||
fn queryHandler(db: *root.NotmuchDb, req: *httpz.Request, res: *httpz.Response) !void {
|
||||
const query = req.url.path[11..]; // Skip "/api/query/"
|
||||
if (query.len == 0) {
|
||||
res.status = 400;
|
||||
try res.json(.{ .@"error" = "Query parameter required" }, .{});
|
||||
return;
|
||||
}
|
||||
|
||||
var threads = db.search(query) catch |err| {
|
||||
res.status = 500;
|
||||
try res.json(.{ .@"error" = @errorName(err) }, .{});
|
||||
return;
|
||||
};
|
||||
defer db_result.close();
|
||||
defer threads.deinit();
|
||||
|
||||
std.debug.print("Successfully opened notmuch database at: {s}\n", .{db_result.path});
|
||||
try res.json(threads, .{});
|
||||
}
|
||||
|
||||
fn threadHandler(db: *root.NotmuchDb, req: *httpz.Request, res: *httpz.Response) !void {
|
||||
const thread_id = req.param("thread_id") orelse {
|
||||
res.status = 400;
|
||||
try res.json(.{ .@"error" = "Thread ID required" }, .{});
|
||||
return;
|
||||
};
|
||||
|
||||
var thread = db.getThread(thread_id) catch |err| {
|
||||
res.status = 404;
|
||||
try res.json(.{ .@"error" = @errorName(err) }, .{});
|
||||
return;
|
||||
};
|
||||
defer thread.deinit();
|
||||
|
||||
try res.json(thread, .{});
|
||||
}
|
||||
|
||||
fn messageHandler(db: *root.NotmuchDb, req: *httpz.Request, res: *httpz.Response) !void {
|
||||
const message_id = req.param("message_id") orelse {
|
||||
res.status = 400;
|
||||
try res.json(.{ .@"error" = "Message ID required" }, .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const msg = db.getMessage(message_id) catch |err| {
|
||||
res.status = 404;
|
||||
try res.json(.{ .@"error" = @errorName(err) }, .{});
|
||||
return;
|
||||
};
|
||||
defer msg.deinit(db.allocator);
|
||||
|
||||
try res.json(msg, .{});
|
||||
}
|
||||
|
||||
fn attachmentHandler(db: *root.NotmuchDb, req: *httpz.Request, res: *httpz.Response) !void {
|
||||
const message_id = req.param("message_id") orelse {
|
||||
res.status = 400;
|
||||
try res.json(.{ .@"error" = "Message ID required" }, .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const num_str = req.param("num") orelse {
|
||||
res.status = 400;
|
||||
try res.json(.{ .@"error" = "Attachment number required" }, .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const num = std.fmt.parseInt(usize, num_str, 10) catch {
|
||||
res.status = 400;
|
||||
try res.json(.{ .@"error" = "Invalid attachment number" }, .{});
|
||||
return;
|
||||
};
|
||||
|
||||
const msg = db.getMessage(message_id) catch |err| {
|
||||
res.status = 404;
|
||||
try res.json(.{ .@"error" = @errorName(err) }, .{});
|
||||
return;
|
||||
};
|
||||
defer msg.deinit(db.allocator);
|
||||
|
||||
if (num >= msg.attachments.len) {
|
||||
res.status = 404;
|
||||
try res.json(.{ .@"error" = "Attachment not found" }, .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const att = msg.attachments[num];
|
||||
res.header("Content-Type", att.content_type);
|
||||
res.header("Content-Disposition", try std.fmt.allocPrint(db.allocator, "attachment; filename=\"{s}\"", .{att.filename}));
|
||||
|
||||
// TODO: Actually retrieve and send attachment content
|
||||
// For now, just send metadata
|
||||
try res.json(.{ .filename = att.filename, .content_type = att.content_type }, .{});
|
||||
}
|
||||
|
||||
test "queryHandler with valid query" {
|
||||
const allocator = std.testing.allocator;
|
||||
var db = try root.openNotmuchDb(allocator, "mail", null);
|
||||
defer db.close();
|
||||
|
||||
var t = httpz.testing.init(.{});
|
||||
defer t.deinit();
|
||||
|
||||
t.url("/api/query/tag:inbox");
|
||||
try queryHandler(&db, t.req, t.res);
|
||||
try std.testing.expect(t.res.status != 400);
|
||||
}
|
||||
|
||||
test "queryHandler with empty query" {
|
||||
const allocator = std.testing.allocator;
|
||||
var db = try root.openNotmuchDb(allocator, "mail", null);
|
||||
defer db.close();
|
||||
|
||||
var t = httpz.testing.init(.{});
|
||||
defer t.deinit();
|
||||
|
||||
t.url("/api/query/");
|
||||
try queryHandler(&db, t.req, t.res);
|
||||
try std.testing.expectEqual(@as(u16, 400), t.res.status);
|
||||
}
|
||||
|
||||
test "messageHandler with valid message" {
|
||||
const allocator = std.testing.allocator;
|
||||
var db = try root.openNotmuchDb(allocator, "mail", null);
|
||||
defer db.close();
|
||||
|
||||
var threads = try db.search("*");
|
||||
defer threads.deinit();
|
||||
|
||||
var maybe_thread = (try threads.next()).?;
|
||||
defer maybe_thread.deinit();
|
||||
|
||||
var mi = try maybe_thread.thread.getMessages();
|
||||
const msg_id = mi.next().?.getMessageId();
|
||||
|
||||
var t = httpz.testing.init(.{});
|
||||
defer t.deinit();
|
||||
|
||||
t.param("message_id", msg_id);
|
||||
try messageHandler(&db, t.req, t.res);
|
||||
try std.testing.expect(t.res.status != 404);
|
||||
}
|
||||
|
||||
test "threadHandler with valid thread" {
|
||||
const allocator = std.testing.allocator;
|
||||
var db = try root.openNotmuchDb(allocator, "mail", null);
|
||||
defer db.close();
|
||||
|
||||
var threads = try db.search("*");
|
||||
defer threads.deinit();
|
||||
|
||||
var maybe_thread = (try threads.next()).?;
|
||||
defer maybe_thread.deinit();
|
||||
|
||||
const thread_id = maybe_thread.thread.getThreadId();
|
||||
|
||||
var t = httpz.testing.init(.{});
|
||||
defer t.deinit();
|
||||
|
||||
t.param("thread_id", thread_id);
|
||||
try threadHandler(&db, t.req, t.res);
|
||||
try std.testing.expect(t.res.status != 404);
|
||||
}
|
||||
|
|
17
src/root.zig
17
src/root.zig
|
@ -5,11 +5,16 @@ const Email = @import("Email.zig");
|
|||
pub const Thread = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
thread: *notmuch.Db.Thread,
|
||||
iterator: ?*notmuch.Db.ThreadIterator = null,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, t: *notmuch.Db.Thread) Thread {
|
||||
return .{ .allocator = allocator, .thread = t };
|
||||
}
|
||||
pub fn deinit(self: Thread) void {
|
||||
if (self.iterator) |iter| {
|
||||
iter.deinit();
|
||||
self.allocator.destroy(iter);
|
||||
}
|
||||
self.allocator.destroy(self.thread);
|
||||
}
|
||||
|
||||
|
@ -178,14 +183,18 @@ pub const NotmuchDb = struct {
|
|||
pub fn getThread(self: *NotmuchDb, thread_id: []const u8) !Thread {
|
||||
var query_buf: [1024:0]u8 = undefined;
|
||||
const query_z = try std.fmt.bufPrintZ(&query_buf, "thread:{s}", .{thread_id});
|
||||
var thread_iter = try self.db.searchThreads(query_z);
|
||||
defer thread_iter.deinit();
|
||||
const iter_ptr = try self.allocator.create(notmuch.Db.ThreadIterator);
|
||||
errdefer self.allocator.destroy(iter_ptr);
|
||||
iter_ptr.* = try self.db.searchThreads(query_z);
|
||||
errdefer iter_ptr.deinit();
|
||||
|
||||
const thread = thread_iter.next();
|
||||
const thread = iter_ptr.next();
|
||||
if (thread) |t| {
|
||||
const tptr = try self.allocator.create(notmuch.Db.Thread);
|
||||
tptr.* = t;
|
||||
return Thread.init(self.allocator, tptr);
|
||||
var result = Thread.init(self.allocator, tptr);
|
||||
result.iterator = iter_ptr;
|
||||
return result;
|
||||
}
|
||||
return error.ThreadNotFound;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue