332 lines
9.3 KiB
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"><inferred></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;
|
|
}
|