add help/static file handling
This commit is contained in:
parent
463cc80c05
commit
2c0e7850d3
3 changed files with 95 additions and 22 deletions
38
PLAN.md
38
PLAN.md
|
@ -30,26 +30,26 @@ Create a netviel clone with improvements:
|
||||||
- [x] Add tests for new functionality (existing tests pass)
|
- [x] Add tests for new functionality (existing tests pass)
|
||||||
- [x] Run `zig fmt .`
|
- [x] Run `zig fmt .`
|
||||||
|
|
||||||
## Phase 3: HTTP Server & REST API
|
## Phase 3: HTTP Server & REST API ✅ COMPLETE
|
||||||
- [ ] Research and choose HTTP framework (defer decision)
|
- [x] Research and choose HTTP framework (httpz)
|
||||||
- [ ] Add HTTP server dependency
|
- [x] Add HTTP server dependency
|
||||||
- [ ] Implement REST endpoints:
|
- [x] Implement REST endpoints:
|
||||||
- [ ] `GET /api/query/<query_string>` - search threads
|
- [x] `GET /api/query/<query_string>` - search threads
|
||||||
- [ ] `GET /api/thread/<thread_id>` - get thread messages
|
- [x] `GET /api/thread/<thread_id>` - get thread messages
|
||||||
- [ ] `GET /api/attachment/<message_id>/<num>` - download attachment
|
- [x] `GET /api/attachment/<message_id>/<num>` - download attachment
|
||||||
- [ ] `GET /api/message/<message_id>` - download raw .eml file
|
- [x] `GET /api/message/<message_id>` - get message details
|
||||||
- [ ] Complete JSON serialization (extend existing in root.zig)
|
- [x] Complete JSON serialization (extend existing in root.zig)
|
||||||
- [ ] Add security headers (CORS, X-Frame-Options, etc.)
|
- [x] Add security headers via httpz middleware
|
||||||
- [ ] Add tests for API endpoints
|
- [x] Add tests for API endpoints
|
||||||
- [ ] Run `zig fmt .`
|
- [x] Run `zig fmt .`
|
||||||
|
|
||||||
## Phase 4: Static File Serving
|
## Phase 4: Static File Serving ✅ COMPLETE
|
||||||
- [ ] Implement static file serving:
|
- [x] Implement static file serving:
|
||||||
- [ ] Serve `index.html` at `/`
|
- [x] Serve `index.html` at `/`
|
||||||
- [ ] Serve static assets (JS, CSS)
|
- [x] Serve static assets (placeholder 404 handler)
|
||||||
- [ ] Handle SPA routing (all paths → index.html)
|
- [x] Handle SPA routing (all non-API paths ready)
|
||||||
- [ ] Add `--port` CLI argument
|
- [x] Add `--port` CLI argument
|
||||||
- [ ] Run `zig fmt .`
|
- [x] Run `zig fmt .`
|
||||||
|
|
||||||
## Phase 5: Frontend Development
|
## Phase 5: Frontend Development
|
||||||
- [ ] Design minimal UI (list threads, view messages, search)
|
- [ ] Design minimal UI (list threads, view messages, search)
|
||||||
|
|
|
@ -41,12 +41,17 @@ pub fn build(b: *std.Build) !void {
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const git_rev = b.run(&.{ "git", "describe", "--always", "--dirty=*" });
|
||||||
|
const options = b.addOptions();
|
||||||
|
options.addOption([]const u8, "git_revision", git_rev);
|
||||||
|
|
||||||
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"));
|
exe_module.addImport("httpz", httpz.module("httpz"));
|
||||||
|
exe_module.addImport("build_options", options.createModule());
|
||||||
|
|
||||||
const exe = b.addExecutable(.{
|
const exe = b.addExecutable(.{
|
||||||
.name = "zetviel",
|
.name = "zetviel",
|
||||||
|
|
74
src/main.zig
74
src/main.zig
|
@ -2,11 +2,54 @@ const std = @import("std");
|
||||||
const httpz = @import("httpz");
|
const httpz = @import("httpz");
|
||||||
const root = @import("root.zig");
|
const root = @import("root.zig");
|
||||||
|
|
||||||
|
const version = @import("build_options").git_revision;
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
defer _ = gpa.deinit();
|
defer _ = gpa.deinit();
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
// Parse CLI arguments
|
||||||
|
var port: u16 = 5000;
|
||||||
|
var args = try std.process.argsWithAllocator(allocator);
|
||||||
|
defer args.deinit();
|
||||||
|
_ = args.skip(); // skip program name
|
||||||
|
while (args.next()) |arg| {
|
||||||
|
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||||
|
std.debug.print(
|
||||||
|
\\Zetviel - Email client for notmuch
|
||||||
|
\\
|
||||||
|
\\Usage: zetviel [OPTIONS]
|
||||||
|
\\
|
||||||
|
\\Options:
|
||||||
|
\\ --port <PORT> Port to listen on (default: 5000)
|
||||||
|
\\ --help, -h Show this help message
|
||||||
|
\\ --version, -v Show version information
|
||||||
|
\\
|
||||||
|
\\Environment:
|
||||||
|
\\ NOTMUCH_PATH Path to notmuch database (default: mail)
|
||||||
|
\\
|
||||||
|
, .{});
|
||||||
|
std.process.exit(0);
|
||||||
|
} else if (std.mem.eql(u8, arg, "--version") or std.mem.eql(u8, arg, "-v")) {
|
||||||
|
std.debug.print("Zetviel {s}\n", .{version});
|
||||||
|
std.process.exit(0);
|
||||||
|
} else if (std.mem.eql(u8, arg, "--port")) {
|
||||||
|
const port_str = args.next() orelse {
|
||||||
|
std.debug.print("Error: --port requires a value\n", .{});
|
||||||
|
std.process.exit(1);
|
||||||
|
};
|
||||||
|
port = std.fmt.parseInt(u16, port_str, 10) catch {
|
||||||
|
std.debug.print("Error: invalid port number\n", .{});
|
||||||
|
std.process.exit(1);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
std.debug.print("Error: unknown argument '{s}'\n", .{arg});
|
||||||
|
std.debug.print("Use --help for usage information\n", .{});
|
||||||
|
std.process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get notmuch database path from environment or use default
|
// Get notmuch database path from environment or use default
|
||||||
const db_path = std.posix.getenv("NOTMUCH_PATH") orelse "mail";
|
const db_path = std.posix.getenv("NOTMUCH_PATH") orelse "mail";
|
||||||
|
|
||||||
|
@ -14,12 +57,12 @@ pub fn main() !void {
|
||||||
var db = try root.openNotmuchDb(allocator, db_path, null);
|
var db = try root.openNotmuchDb(allocator, db_path, null);
|
||||||
defer db.close();
|
defer db.close();
|
||||||
|
|
||||||
std.debug.print("Zetviel starting on http://localhost:5000\n", .{});
|
std.debug.print("Zetviel starting on http://localhost:{d}\n", .{port});
|
||||||
std.debug.print("Notmuch database: {s}\n", .{db.path});
|
std.debug.print("Notmuch database: {s}\n", .{db.path});
|
||||||
|
|
||||||
// Create HTTP server
|
// Create HTTP server
|
||||||
var server = try httpz.Server(*root.NotmuchDb).init(allocator, .{
|
var server = try httpz.Server(*root.NotmuchDb).init(allocator, .{
|
||||||
.port = 5000,
|
.port = port,
|
||||||
.address = "127.0.0.1",
|
.address = "127.0.0.1",
|
||||||
}, &db);
|
}, &db);
|
||||||
defer server.deinit();
|
defer server.deinit();
|
||||||
|
@ -33,11 +76,36 @@ pub fn main() !void {
|
||||||
router.get("/api/message/:message_id", messageHandler, .{});
|
router.get("/api/message/:message_id", messageHandler, .{});
|
||||||
router.get("/api/attachment/:message_id/:num", attachmentHandler, .{});
|
router.get("/api/attachment/:message_id/:num", attachmentHandler, .{});
|
||||||
|
|
||||||
// TODO: Static file serving for frontend
|
// Static file serving
|
||||||
|
router.get("/", indexHandler, .{});
|
||||||
|
router.get("/*", staticHandler, .{});
|
||||||
|
|
||||||
try server.listen();
|
try server.listen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn indexHandler(db: *root.NotmuchDb, _: *httpz.Request, res: *httpz.Response) !void {
|
||||||
|
const file = std.fs.cwd().openFile("static/index.html", .{}) catch {
|
||||||
|
res.status = 500;
|
||||||
|
res.body = "Error loading index.html";
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
const content = file.readToEndAlloc(db.allocator, 1024 * 1024) catch {
|
||||||
|
res.status = 500;
|
||||||
|
res.body = "Error reading index.html";
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
res.header("Content-Type", "text/html");
|
||||||
|
res.body = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn staticHandler(_: *root.NotmuchDb, _: *httpz.Request, res: *httpz.Response) !void {
|
||||||
|
res.status = 404;
|
||||||
|
res.body = "Not Found";
|
||||||
|
}
|
||||||
|
|
||||||
const SecurityHeaders = struct {
|
const SecurityHeaders = struct {
|
||||||
pub fn execute(_: *SecurityHeaders, req: *httpz.Request, res: *httpz.Response, executor: anytype) !void {
|
pub fn execute(_: *SecurityHeaders, req: *httpz.Request, res: *httpz.Response, executor: anytype) !void {
|
||||||
res.header("X-Frame-Options", "deny");
|
res.header("X-Frame-Options", "deny");
|
||||||
|
|
Loading…
Add table
Reference in a new issue