Something went wrong. Try again.
A Zig client for the Paperless-ngx REST API, with streaming uploads, task waiting and a command-line tool.
Something went wrong. Try again.
3.6 kB · 98 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The server's background work: consuming an upload, training the//! classifier, emptying the trash. The one a client most often cares about//! is the first, since a document does not exist until its upload's task has//! succeeded.
const std = @import("std");const Io = std.Io;
const Client = @import("Client.zig");const models = @import("models.zig");const query = @import("query.zig");const resource = @import("resource.zig");
const Id = models.Id;const Task = models.Task;const Parsed = std.json.Parsed;
const Tasks = @This();
client: *Client,
pub const Query = struct { page: ?u32 = null, page_size: ?u32 = null, ordering: ?[]const u8 = null, task_id: ?[]const u8 = null, status: ?models.TaskStatus = null, task_type: ?models.TaskType = null, acknowledged: ?bool = null, /// Done one way or the other. is_complete: ?bool = null, extra: []const query.Param = &.{},};
/// One page of tasks, newest first.pub fn list(self: Tasks, q: Query) Client.Error!Parsed(models.Page(Task)) { return self.client.getJson(models.Page(Task), "tasks/", q);}
pub fn pages(self: Tasks, q: Query) resource.Pager(Task, Query) { return .{ .client = self.client, .path = "tasks/", .query = q };}
/// A task by its Celery identifier -- what an upload hands back -- or null/// if the server has no record of it yet.pub fn byTaskId(self: Tasks, task_id: []const u8) Client.Error!?Parsed(Task) { var page = try self.list(.{ .task_id = task_id }); if (page.value.results.len == 0) { page.deinit(); return null; } return .{ .arena = page.arena, .value = page.value.results[0] };}
pub fn get(self: Tasks, id: Id) Client.Error!Parsed(Task) { var buffer: [32]u8 = undefined; return self.client.getJson(Task, std.fmt.bufPrint(&buffer, "tasks/{d}/", .{id}) catch unreachable, .{});}
/// Marks tasks as seen, which takes them out of the web interface's list of/// things to look at.pub fn acknowledge(self: Tasks, ids: []const Id) Client.Error!void { return self.client.sendJsonIgnore(.post, "tasks/acknowledge/", .{}, .{ .tasks = ids });}
pub const WaitOptions = struct { /// How long to sleep between asking. interval: Io.Duration = .fromSeconds(1), /// How long to keep asking before `error.Timeout`. Consuming a large /// scan -- OCR on every page -- can take minutes. timeout: Io.Duration = .fromSeconds(600),};
pub const WaitError = Client.Error || Io.Cancelable || error{Timeout};
/// Asks after a task until it has finished, and hands it back. Finished is/// not succeeded: check `status`, and `errorMessage` when it failed -- an/// upload of a file Paperless cannot read fails, for one. (A file it already/// has does not: Paperless 3 consumes it again and lists the original in/// the new document's `duplicate_documents`.)////// A task the server does not know of yet is waited for too, since an/// upload's task is recorded a moment after the upload is answered.pub fn wait(self: Tasks, task_id: []const u8, options: WaitOptions) WaitError!Parsed(Task) { const io = self.client.http.io; const deadline = Io.Clock.awake.now(io).addDuration(options.timeout); while (true) { if (try self.byTaskId(task_id)) |task| { if (task.value.status.isDone()) return task; task.deinit(); } if (Io.Clock.awake.now(io).addDuration(options.interval).nanoseconds > deadline.nanoseconds) return error.Timeout; try io.sleep(options.interval, .awake); }}