const std = @import("std"); const sql = @import("sql"); const util = @import("util"); const Uuid = util.Uuid; const DateTime = util.DateTime; pub const FileStatus = enum { uploading, uploaded, external, deleted, pub const jsonStringify = util.jsonSerializeEnumAsString; }; pub const FileUpload = struct { id: Uuid, owner_id: Uuid, size: usize, filename: []const u8, description: ?[]const u8, content_type: ?[]const u8, sensitive: bool, status: FileStatus, created_at: DateTime, updated_at: DateTime, }; pub const FileMeta = struct { filename: []const u8, description: ?[]const u8, content_type: ?[]const u8, sensitive: bool, }; pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !FileUpload { return try db.queryRow( FileUpload, \\SELECT \\ id, \\ owner_id, \\ size, \\ filename, \\ description, \\ content_type, \\ sensitive, \\ status, \\ created_at, \\ updated_at \\FROM file_upload \\WHERE id = $1 \\LIMIT 1 , .{id}, alloc, ); } pub const PartialMeta = Partial(FileMeta); pub fn Partial(comptime T: type) type { const t_fields = std.meta.fields(T); var fields: [t_fields.len]std.builtin.Type.StructField = undefined; for (std.meta.fields(T)) |f, i| fields[i] = .{ .name = f.name, .field_type = ?f.field_type, .default_value = &@as(?f.field_type, null), .is_comptime = false, .alignment = @alignOf(?f.field_type), }; return @Type(.{ .Struct = .{ .layout = .Auto, .fields = &fields, .decls = &.{}, .is_tuple = false, } }); } pub fn update(db: anytype, id: Uuid, meta: PartialMeta, alloc: std.mem.Allocator) !void { var builder = sql.QueryBuilder.init(alloc); defer builder.deinit(); try builder.appendSlice("UPDATE file_upload"); if (meta.filename) |_| try builder.set("filename", "$2"); if (meta.description) |_| try builder.set("description", "$3"); if (meta.content_type) |_| try builder.set("content_type", "$4"); if (meta.sensitive) |_| try builder.set("sensitive", "$5"); if (builder.set_statements_appended == 0) return error.NoChange; try builder.set("updated_at", "$6"); try builder.andWhere("id = $1"); try db.exec(try builder.terminate(), .{ id, meta.filename orelse null, meta.description orelse null, meta.content_type orelse null, meta.sensitive orelse null, DateTime.now(), }, alloc); } pub fn create(db: anytype, owner_id: Uuid, meta: FileMeta, data: []const u8, alloc: std.mem.Allocator) !Uuid { const id = Uuid.randV4(util.getThreadPrng()); const now = DateTime.now(); try db.insert("file_upload", .{ .id = id, .owner_id = owner_id, .size = data.len, .filename = meta.filename, .description = meta.description, .content_type = meta.content_type, .sensitive = meta.sensitive, .status = FileStatus.uploading, .created_at = now, .updated_at = now, }, alloc); saveFile(id, data) catch |err| { db.exec("DELETE FROM file_upload WHERE ID = $1", .{id}, alloc) catch |e| { std.log.err("Unable to remove file {} record in DB: {}", .{ id, e }); }; return err; }; try db.exec( \\UPDATE file_upload \\SET status = 'uploaded', updated_at = $2 \\WHERE id = $1 , .{ id, DateTime.now() }, alloc); return id; } pub fn delete(db: anytype, id: Uuid, alloc: std.mem.Allocator) !void { var dir = try std.fs.cwd().openDir(data_root, .{}); defer dir.close(); try dir.deleteFile(&id.toCharArray()); try db.exec( \\DELETE FROM file_upload \\WHERE id = $1 , .{id}, alloc); } const data_root = "./files"; fn saveFile(id: Uuid, data: []const u8) !void { var dir = try std.fs.cwd().openDir(data_root, .{}); defer dir.close(); var file = try dir.createFile(&id.toCharArray(), .{ .exclusive = true }); defer file.close(); try file.writer().writeAll(data); try file.sync(); } pub fn deref(alloc: std.mem.Allocator, id: Uuid) ![]const u8 { var dir = try std.fs.cwd().openDir(data_root, .{}); defer dir.close(); return dir.readFileAlloc(alloc, &id.toCharArray(), 1 << 32); }