vidzig/src/routes.zig

332 lines
9.3 KiB
Zig

const std = @import("std");
const c = @import("ffi.zig").c;
const responseWriter = @import("response_writer.zig").writer;
const DownloadQueue = @import("DownloadQueue.zig");
const State = @import("State.zig");
const inferred_html =
\\<span class="gray">&lt;inferred&gt;</span>
;
const audio_only_prefix_html =
\\<span class="audio_only_txt">Audio Only </span>
;
pub fn routeCb(comptime routeFn: anytype, comptime has_state: bool) *anyopaque {
return @intToPtr(*anyopaque, @ptrToInt(&struct {
fn f(
udata: ?*anyopaque,
req: *c.onion_request,
res: *c.onion_response,
) callconv(.C) c_int {
return (if (has_state)
routeFn(
@ptrCast(*State, @alignCast(@alignOf(*State), udata)),
req,
res,
)
else
routeFn(req, res)) catch |e| {
std.log.warn("Error occured while processing request: {s}", .{@errorName(e)});
return c.OCS_INTERNAL_ERROR;
};
}
}.f));
}
pub fn comptimeStatic(comptime data: []const u8, comptime mime: ?[:0]const u8) *anyopaque {
return routeCb(struct {
fn route(
req: *c.onion_request,
res: *c.onion_response,
) !c_int {
_ = req;
const w = responseWriter(res);
if (mime) |m| {
c.onion_response_set_header(res, "Content-Type", m.ptr);
}
try w.writeAll(data);
return c.OCS_PROCESSED;
}
}.route, false);
}
pub fn indexRoute(
state: *State,
req: *c.onion_request,
res: *c.onion_response,
) !c_int {
_ = req;
const w = responseWriter(res);
try w.writeAll(
\\<!DOCTYPE html>
\\<html>
\\<head>
\\ <title>Vidzig index</title>
\\ <meta charset="UTF-8">
\\ <meta name="viewport" content="width=device-width, initial-scale=1">
\\ <link rel="stylesheet" href="static/index.css">
\\</head>
\\<body>
);
var paused = false;
{
state.downloads.m.lock();
defer state.downloads.m.unlock();
paused = state.downloads.paused;
if (state.downloads.active_task) |active| {
try w.print(
\\<p>Active task: <b>{s}{s}</b> -> <b>{s}</b></p>
\\<hr>
,
.{
if (active.audio_only) audio_only_prefix_html else "",
active.url,
active.outname orelse inferred_html,
},
);
}
if (state.downloads.tasks.len > 0) {
try w.writeAll(
\\<table id="tasks_tbl"><tbody>
);
var task = state.downloads.tasks.last;
while (task) |t| {
try w.print(
\\<tr><td>{s}{s}</td><td>{s}</td></tr>
,
.{
if (t.data.audio_only) audio_only_prefix_html else "",
t.data.url,
t.data.outname orelse inferred_html,
},
);
task = t.prev;
}
try w.writeAll(
\\</table></tbody>
\\<hr>
);
}
}
try w.print(
\\<script>paused = {}</script>
,
.{paused},
);
try w.writeAll(
\\<form id="enqueue_form">
\\ <input type="text" placeholder="URL" id="url_inp"><br>
\\ <input type="text" placeholder="output name" id="outname_inp"><br>
\\ <input type="checkbox" id="prepend_switch">Prepend<br>
\\ <input type="checkbox" id="audio_only_switch">Audio Only<br>
\\ <input type="submit" id="enqueue_btn" value="Enqueue">
\\</form>
\\<button id="pause_btn"></button>
\\<hr>
\\<ul>
);
var vids_dir = try std.fs.cwd().openIterableDir(state.vids_dir, .{});
defer vids_dir.close();
var iter = vids_dir.iterate();
while (try iter.next()) |entry| {
if (entry.kind != .File)
continue;
const path_z = try std.heap.c_allocator.dupeZ(u8, entry.name);
defer std.heap.c_allocator.free(path_z);
const quote = c.onion_quote_new(path_z) orelse return error.OutOfMemory;
defer c.free(quote);
try w.print(
\\<li>
\\<button class="vid_del_btn">del</button>
\\<a href="vids/{s}">{s}</a>
\\</li>
,
.{ quote, entry.name },
);
}
try w.writeAll(
\\</ul>
\\<script src="static/index.js"></script>
\\</body>
\\</html>
);
return c.OCS_PROCESSED;
}
pub fn indexJsonRoute(
state: *State,
req: *c.onion_request,
res: *c.onion_response,
) !c_int {
_ = req;
const Response = struct {
active_task: ?DownloadQueue.Task,
tasks: []DownloadQueue.Task,
vids: [][]u8,
paused: bool,
};
var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
defer arena.deinit();
const alloc = arena.allocator();
const tasks = try alloc.alloc(DownloadQueue.Task, state.downloads.tasks.len);
var cur_task = state.downloads.tasks.last;
var i: usize = 0;
while (cur_task) |t| {
tasks[i] = t.data;
i += 1;
cur_task = t.prev;
}
var vids_dir = try std.fs.cwd().openIterableDir(state.vids_dir, .{});
defer vids_dir.close();
var iter = vids_dir.iterate();
var vids = std.ArrayList([]u8).init(std.heap.c_allocator);
defer vids.deinit();
while (try iter.next()) |ent| {
if (ent.kind != .File)
continue;
const path_z = try alloc.dupeZ(u8, ent.name);
const quote = c.onion_quote_new(path_z) orelse return error.OutOfMemory;
defer c.free(quote);
try vids.append(try std.fmt.allocPrint(alloc, "{s}/vids/{s}", .{ state.base_url, quote }));
}
c.onion_response_set_header(res, "Content-Type", "application/json");
const data = Response{
.active_task = state.downloads.active_task,
.tasks = tasks,
.vids = vids.items,
.paused = state.downloads.paused,
};
const writer = responseWriter(res);
try std.json.stringify(data, .{}, writer);
return c.OCS_PROCESSED;
}
pub const ApiTask = struct {
url: []const u8,
outname: ?[]const u8,
audio_only: bool = false,
prepend: bool = false,
};
pub fn vidsRoute(
state: *State,
req: *c.onion_request,
res: *c.onion_response,
) !c_int {
const flags = c.onion_request_get_flags(req);
switch (flags & c.OR_METHODS) {
c.OR_GET, c.OR_HEAD => {
const basepath = std.fs.path.basename(std.mem.span(c.onion_request_get_path(req)));
const filepath = try std.fs.path.resolve(std.heap.c_allocator, &.{ state.vids_dir, basepath });
defer std.heap.c_allocator.free(filepath);
if (std.fs.path.isAbsolute(filepath) or std.mem.startsWith(u8, filepath, ".."))
return c.OCS_FORBIDDEN;
const filepath_z = try std.heap.c_allocator.dupeZ(u8, filepath);
defer std.heap.c_allocator.free(filepath_z);
return c.onion_shortcut_response_file(filepath_z.ptr, req, res);
},
c.OR_POST => {
const data = c.onion_request_get_data(req) orelse return c.OCS_FORBIDDEN;
var ts = std.json.TokenStream.init(data.*.data[0..@intCast(usize, data.*.size)]);
const task = try std.json.parse(
ApiTask,
&ts,
.{ .allocator = std.heap.c_allocator },
);
defer std.json.parseFree(ApiTask, task, .{ .allocator = std.heap.c_allocator });
if (task.url.len == 0)
return c.OCS_FORBIDDEN;
std.log.info("adding {s} to queue", .{task.url});
try state.downloads.pushTask(
.{ .url = task.url, .outname = task.outname, .audio_only = task.audio_only },
task.prepend,
);
return c.OCS_PROCESSED;
},
c.OR_DELETE => {
const basepath = std.fs.path.basename(std.mem.span(c.onion_request_get_path(req)));
const filepath = try std.fs.path.resolve(std.heap.c_allocator, &.{ state.vids_dir, basepath });
defer std.heap.c_allocator.free(filepath);
if (std.mem.startsWith(u8, filepath, ".."))
return c.OCS_FORBIDDEN;
std.log.info("deleting vid {s}", .{filepath});
try std.fs.cwd().deleteFile(filepath);
return c.OCS_PROCESSED;
},
else => return c.OCS_FORBIDDEN,
}
}
pub fn setPausedRoute(
state: *State,
req: *c.onion_request,
res: *c.onion_response,
) !c_int {
_ = res;
if (c.onion_request_get_flags(req) & c.OR_METHODS != c.OR_POST)
return c.OCS_FORBIDDEN;
const data_block = c.onion_request_get_data(req) orelse return c.OCS_FORBIDDEN;
const data = data_block.*.data[0..@intCast(usize, data_block.*.size)];
const paused = if (std.mem.eql(u8, data, "true"))
true
else if (std.mem.eql(u8, data, "false"))
false
else
return c.OCS_FORBIDDEN;
std.log.info("setting paused state to {}", .{paused});
state.downloads.setPaused(paused);
return c.OCS_PROCESSED;
}