From 0016aaa197697ec5ff38dfb3f63ac8b6f74b48e0 Mon Sep 17 00:00:00 2001 From: Nathan Reiner Date: Sat, 15 Nov 2025 15:12:19 +0100 Subject: backend: add image upload --- flake.nix | 1 - src/routes/api/image/root.zig | 3 +++ src/routes/api/image/upload.zig | 15 ++++++++++++++ src/routes/api/root.zig | 1 + src/storage/image.zig | 39 +++++++++++++++++++++++++++++++++++++ src/storage/root.zig | 1 + static/api/images.js | 43 +++++++++++++++++++++++++++++++++++++++++ static/index.js | 26 +++++++------------------ static/pages/settings/index.js | 4 +--- 9 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 src/routes/api/image/root.zig create mode 100644 src/routes/api/image/upload.zig create mode 100644 src/storage/image.zig diff --git a/flake.nix b/flake.nix index ce7acb6..b0a941a 100644 --- a/flake.nix +++ b/flake.nix @@ -13,7 +13,6 @@ devShells.x86_64-linux.default = pkgs.mkShell { packages = [ pkgs.zig - pkgs.http-server ]; }; }; diff --git a/src/routes/api/image/root.zig b/src/routes/api/image/root.zig new file mode 100644 index 0000000..0ad1960 --- /dev/null +++ b/src/routes/api/image/root.zig @@ -0,0 +1,3 @@ +const HandlerInfo = @import("../../handler-info.zig"); + +pub const upload: HandlerInfo = .from_type(@import("upload.zig")); diff --git a/src/routes/api/image/upload.zig b/src/routes/api/image/upload.zig new file mode 100644 index 0000000..7cd5cf1 --- /dev/null +++ b/src/routes/api/image/upload.zig @@ -0,0 +1,15 @@ +const std = @import("std"); +const Context = @import("../../context.zig"); +const Storage = @import("../../../storage/root.zig"); + +const log = std.log.scoped(.image_upload); + +pub const access = .everyone; + +pub fn post(ctx: *Context) !void { + if (ctx.request.head.content_length) |length| { + var buffer: [1024]u8 = undefined; + const reader = try ctx.request.readerExpectContinue(&buffer); + try Storage.Image.new(ctx.storage, reader, length); + } +} diff --git a/src/routes/api/root.zig b/src/routes/api/root.zig index e3c5d6f..6d5745f 100644 --- a/src/routes/api/root.zig +++ b/src/routes/api/root.zig @@ -1,2 +1,3 @@ pub const auth = @import("auth/root.zig"); pub const session = @import("session/root.zig"); +pub const image = @import("image/root.zig"); diff --git a/src/storage/image.zig b/src/storage/image.zig new file mode 100644 index 0000000..73bca84 --- /dev/null +++ b/src/storage/image.zig @@ -0,0 +1,39 @@ +const std = @import("std"); +const Storage = @import("root.zig"); + +const id_size = 32; + +fn new_id() [id_size]u8 { + var buffer: [id_size]u8 = undefined; + var raw_buffer: [id_size / 2]u8 = undefined; + std.crypto.random.bytes(&raw_buffer); + + var writer = std.Io.Writer.fixed(buffer[0..]); + writer.print("{x}", .{raw_buffer}) catch unreachable; + + return buffer[0..].*; +} + +pub fn new( + storage: *Storage, + reader: *std.Io.Reader, + size: usize, +) !void { + var dir = storage.dir.openDir("image", .{}) catch blk: { + try storage.dir.makeDir("image"); + break :blk try storage.dir.openDir("image", .{}); + }; + defer dir.close(); + + var file_name: [id_size+4]u8 = undefined; + @memcpy(file_name[0..id_size], &new_id()); + @memcpy(file_name[id_size..], ".jpg"); + + var file = try dir.createFile(&file_name, .{}); + defer file.close(); + + var buffer: [1024]u8 = undefined; + var file_writer = file.writer(&buffer); + + try reader.streamExact(&file_writer.interface, size); +} diff --git a/src/storage/root.zig b/src/storage/root.zig index f1a753c..c121ca9 100644 --- a/src/storage/root.zig +++ b/src/storage/root.zig @@ -3,6 +3,7 @@ const config = @import("../config.zig"); const prompt = @import("../prompt.zig"); pub const User = @import("user.zig"); +pub const Image = @import("image.zig"); pub const SessionManager = @import("session-manager/root.zig"); pub const Session = SessionManager.Session; diff --git a/static/api/images.js b/static/api/images.js index 915aae6..4a41a3c 100644 --- a/static/api/images.js +++ b/static/api/images.js @@ -1,6 +1,43 @@ import * as sfw from 'sfw'; const { Input } = sfw.element.native; +class FileUploader { + constructor(url) { + this.onprogress = () => {} + this.ondone = () => {} + this.url = url; + this.sessions = []; + } + + send(...files) { + let count = 0; + this.sessions = this.sessions.concat(files.map( + file => new Promise((resolve) => { + const xhr = new XMLHttpRequest(); + xhr.upload.addEventListener("progress", (event) => { + if (event.lengthComputable) { + this.onprogress(file, event.loaded, event.total) + } + }); + + xhr.addEventListener("loadend", () => { + count += 1; + + resolve(xhr.readyState === 4 && xhr.status === 200); + + if (count == files.length) { + this.ondone(); + } + }); + + xhr.open("POST", this.url, true); + xhr.setRequestHeader("Content-Type", "application/octet-stream"); + xhr.send(file.slice()); + }) + )); + } +} + export async function upload_to_timeline() { const input = Input.new({ type: 'file', @@ -8,6 +45,12 @@ export async function upload_to_timeline() { accept: 'image/jpeg', }) input.click(); + + const uploader = new FileUploader('/api/image/upload'); + + input.onchange = async () => { + uploader.send(...input.files); + } } export async function upload_to_profile() { diff --git a/static/index.js b/static/index.js index e9ef859..90b5ffd 100644 --- a/static/index.js +++ b/static/index.js @@ -16,20 +16,6 @@ sfw.theme.add_css(await sfw.css(import.meta.url, './index.css')); const image_viewer = ImageViewer.new(); -[ - '/images/0001.jpg', - '/images/0002.jpg', - '/images/0003.jpg', - '/images/0004.jpg', - '/images/0005.jpg', - '/images/0006.jpg', - '/images/0007.jpg', - '/images/0008.jpg', - '/images/0009.jpg', - '/images/0010.jpg', -].forEach(url => image_viewer.add(url)) - - const login = LoginView.new({ onlogin: async (user, password) => { if (await api.auth.login(user, password)) { @@ -102,8 +88,10 @@ document.body.append( month_select, ); -if (await api.session.is_valid()) { - login.hide(); -} else { - login.focus(); -} + +login.hide(); +//if (await api.session.is_valid()) { +// login.hide(); +//} else { +// login.focus(); +//} diff --git a/static/pages/settings/index.js b/static/pages/settings/index.js index 6a0e231..c11a3e9 100644 --- a/static/pages/settings/index.js +++ b/static/pages/settings/index.js @@ -23,9 +23,7 @@ export default class SettingsView extends sfw.element.Container { Div.new({ id: 'image-container', children: [ - Img.new({ - src: '/images/0010.jpg', - }), + Img.new(), ] }), Div.new({ -- cgit v1.2.3-70-g09d2