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`).
|
// running `zig build`).
|
||||||
b.installArtifact(lib);
|
b.installArtifact(lib);
|
||||||
|
|
||||||
|
const httpz = b.dependency("httpz", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
const exe_module = b.createModule(.{
|
const exe_module = b.createModule(.{
|
||||||
.root_source_file = b.path("src/main.zig"),
|
.root_source_file = b.path("src/main.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
|
exe_module.addImport("httpz", httpz.module("httpz"));
|
||||||
|
|
||||||
const exe = b.addExecutable(.{
|
const exe = b.addExecutable(.{
|
||||||
.name = "zetviel",
|
.name = "zetviel",
|
||||||
|
|
@ -96,6 +102,7 @@ pub fn build(b: *std.Build) !void {
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
|
exe_test_module.addImport("httpz", httpz.module("httpz"));
|
||||||
|
|
||||||
const exe_unit_tests = b.addTest(.{
|
const exe_unit_tests = b.addTest(.{
|
||||||
.root_module = exe_test_module,
|
.root_module = exe_test_module,
|
||||||
|
|
|
||||||
|
|
@ -17,42 +17,11 @@
|
||||||
// Once all dependencies are fetched, `zig build` no longer requires
|
// Once all dependencies are fetched, `zig build` no longer requires
|
||||||
// internet connectivity.
|
// internet connectivity.
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
|
.httpz = .{
|
||||||
//.example = .{
|
.url = "git+https://github.com/karlseguin/http.zig?ref=master#5e5ab5f82477252fd85943bcb33db483bde6de86",
|
||||||
// // When updating this field to a new URL, be sure to delete the corresponding
|
.hash = "httpz-0.0.0-PNVzrLndBgBwKYPO0v3OFD-6741_9uKdWtU27sil2-df",
|
||||||
// // `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,
|
|
||||||
//},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 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 = .{
|
.paths = .{
|
||||||
// This makes *all* files, recursively, included in this package. It is generally
|
// This makes *all* files, recursively, included in this package. It is generally
|
||||||
// better to explicitly list the files and directories instead, to insure that
|
// 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 std = @import("std");
|
||||||
|
const httpz = @import("httpz");
|
||||||
const root = @import("root.zig");
|
const root = @import("root.zig");
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
// Prints to stderr (it's a shortcut based on `std.io.getStdErr()`)
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
std.debug.print("All your {s} are belong to us. \n", .{"codebase"});
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
// stdout is for the actual output of your application, for example if you
|
// Get notmuch database path from environment or use default
|
||||||
// are implementing gzip, then only the compressed bytes should be sent to
|
const db_path = std.posix.getenv("NOTMUCH_PATH") orelse "mail";
|
||||||
// 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");
|
|
||||||
|
|
||||||
// Example of using the root.zig functionality
|
// Open notmuch database
|
||||||
const allocator = std.heap.page_allocator;
|
var db = try root.openNotmuchDb(allocator, db_path, null);
|
||||||
var db_result = root.openNotmuchDb(allocator, "mail", null) catch |err| {
|
defer db.close();
|
||||||
std.debug.print("Failed to open notmuch database: {}\n", .{err});
|
|
||||||
|
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;
|
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 {
|
pub const Thread = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
thread: *notmuch.Db.Thread,
|
thread: *notmuch.Db.Thread,
|
||||||
|
iterator: ?*notmuch.Db.ThreadIterator = null,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, t: *notmuch.Db.Thread) Thread {
|
pub fn init(allocator: std.mem.Allocator, t: *notmuch.Db.Thread) Thread {
|
||||||
return .{ .allocator = allocator, .thread = t };
|
return .{ .allocator = allocator, .thread = t };
|
||||||
}
|
}
|
||||||
pub fn deinit(self: Thread) void {
|
pub fn deinit(self: Thread) void {
|
||||||
|
if (self.iterator) |iter| {
|
||||||
|
iter.deinit();
|
||||||
|
self.allocator.destroy(iter);
|
||||||
|
}
|
||||||
self.allocator.destroy(self.thread);
|
self.allocator.destroy(self.thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,14 +183,18 @@ pub const NotmuchDb = struct {
|
||||||
pub fn getThread(self: *NotmuchDb, thread_id: []const u8) !Thread {
|
pub fn getThread(self: *NotmuchDb, thread_id: []const u8) !Thread {
|
||||||
var query_buf: [1024:0]u8 = undefined;
|
var query_buf: [1024:0]u8 = undefined;
|
||||||
const query_z = try std.fmt.bufPrintZ(&query_buf, "thread:{s}", .{thread_id});
|
const query_z = try std.fmt.bufPrintZ(&query_buf, "thread:{s}", .{thread_id});
|
||||||
var thread_iter = try self.db.searchThreads(query_z);
|
const iter_ptr = try self.allocator.create(notmuch.Db.ThreadIterator);
|
||||||
defer thread_iter.deinit();
|
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| {
|
if (thread) |t| {
|
||||||
const tptr = try self.allocator.create(notmuch.Db.Thread);
|
const tptr = try self.allocator.create(notmuch.Db.Thread);
|
||||||
tptr.* = t;
|
tptr.* = t;
|
||||||
return Thread.init(self.allocator, tptr);
|
var result = Thread.init(self.allocator, tptr);
|
||||||
|
result.iterator = iter_ptr;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
return error.ThreadNotFound;
|
return error.ThreadNotFound;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue