diff --git a/src/api/services/files.zig b/src/api/services/files.zig index 147d049..ae45771 100644 --- a/src/api/services/files.zig +++ b/src/api/services/files.zig @@ -1,203 +1,131 @@ const std = @import("std"); +const sql = @import("sql"); const util = @import("util"); const Uuid = util.Uuid; const DateTime = util.DateTime; -pub const FileOwner = union(enum) { - user_id: Uuid, - community_id: Uuid, +pub const FileStatus = enum { + uploading, + uploaded, + external, + deleted, }; -pub const DriveFile = struct { +pub const FileUpload = struct { id: Uuid, - path: []const u8, - filename: []const u8, - - owner: FileOwner, - + created_by: Uuid, size: usize, - description: []const u8, - content_type: []const u8, + filename: []const u8, + description: ?[]const u8, + content_type: ?[]const u8, sensitive: bool, + status: FileStatus, + created_at: DateTime, updated_at: DateTime, }; -const EntryType = enum { - dir, - file, -}; - -pub const CreateFileArgs = struct { - dir: []const u8, +pub const FileMeta = struct { filename: []const u8, - owner: FileOwner, - created_by: Uuid, description: ?[]const u8, content_type: ?[]const u8, sensitive: bool, }; -fn lookupDirectory(db: anytype, owner: FileOwner, path: []const u8, alloc: std.mem.Allocator) !Uuid { - return (try db.queryRow( - std.meta.Tuple( - &.{util.Uuid}, - ), - \\SELECT id - \\FROM drive_entry_path - \\WHERE - \\ path = (CASE WHEN LENGTH($1) = 0 THEN '/' ELSE '/' || $1 || '/' END) - \\ AND account_owner_id IS NOT DISTINCT FROM $2 - \\ AND community_owner_id IS NOT DISTINCT FROM $3 - \\ AND kind = 'dir' - \\LIMIT 1 - , - .{ - std.mem.trim(u8, path, "/"), - if (owner == .user_id) owner.user_id else null, - if (owner == .community_id) owner.community_id else null, - }, - alloc, - ))[0]; +pub fn Partial(comptime T: type) type { + const t_fields = std.meta.fields(T); + var fields: [t_fields]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, + } }); } -fn lookup(db: anytype, owner: FileOwner, path: []const u8, alloc: std.mem.Allocator) !Uuid { - return (try db.queryRow( - std.meta.Tuple( - &.{util.Uuid}, - ), - \\SELECT id - \\FROM drive_entry_path - \\WHERE - \\ path = (CASE WHEN LENGTH($1) = 0 THEN '/' ELSE '/' || $1 || '/' END) - \\ AND account_owner_id IS NOT DISTINCT FROM $2 - \\ AND community_owner_id IS NOT DISTINCT FROM $3 - \\LIMIT 1 - , - .{ - std.mem.trim(u8, path, "/"), - if (owner == .user_id) owner.user_id else null, - if (owner == .community_id) owner.community_id else null, - }, - alloc, - ))[0]; -} +pub fn update(db: anytype, id: Uuid, meta: Partial(FileMeta), alloc: std.mem.Allocator) !void { + var builder = sql.QueryBuilder.init(alloc); + defer builder.deinit(); -pub fn mkdir(db: anytype, owner: FileOwner, path: []const u8, alloc: std.mem.Allocator) !void { - var split = std.mem.splitBackwards(u8, std.mem.trim(u8, path, "/"), "/"); - const name = split.first(); - const dir = split.rest(); - std.log.debug("'{s}' / '{s}'", .{ name, dir }); + try builder.appendSlice("UPDATE file_upload"); - if (name.len == 0) return error.EmptyName; + 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"); - const id = Uuid.randV4(util.getThreadPrng()); + if (meta.set_statements_appended == 0) return error.NoChange; - const tx = try db.begin(); - errdefer tx.rollback(); + try builder.andWhere("id = $1"); - const parent = try lookupDirectory(tx, owner, dir, alloc); + try builder.appendSlice("\nLIMIT 1"); - try tx.insert("drive_entry", .{ - .id = id, - - .account_owner_id = if (owner == .user_id) owner.user_id else null, - .community_owner_id = if (owner == .community_id) owner.community_id else null, - - .name = name, - .parent_directory_id = parent, - }, alloc); - try tx.commit(); -} - -pub fn rmdir(db: anytype, owner: FileOwner, path: []const u8, alloc: std.mem.Allocator) !void { - const tx = try db.begin(); - errdefer tx.rollback(); - - const id = try lookupDirectory(tx, owner, path, alloc); - try tx.exec("DELETE FROM drive_directory WHERE id = $1", .{id}, alloc); - try tx.commit(); -} - -fn insertFileRow(tx: anytype, id: Uuid, filename: []const u8, owner: FileOwner, dir: Uuid, alloc: std.mem.Allocator) !void { - try tx.insert("drive_entry", .{ - .id = id, - - .account_owner_id = if (owner == .user_id) owner.user_id else null, - .community_owner_id = if (owner == .community_id) owner.community_id else null, - - .parent_directory_id = dir, - .name = filename, - - .file_id = id, + try db.exec(try builder.terminate(), .{ + id, + meta.filename orelse null, + meta.description orelse null, + meta.content_type orelse null, + meta.sensitive orelse null, }, alloc); } -pub fn createFile(db: anytype, args: CreateFileArgs, data: []const u8, alloc: std.mem.Allocator) !void { +pub fn create(db: anytype, created_by: Uuid, meta: FileMeta, data: []const u8, alloc: std.mem.Allocator) !void { const id = Uuid.randV4(util.getThreadPrng()); const now = DateTime.now(); + try db.insert("file_upload", .{ + .id = id, - { - var tx = try db.begin(); - errdefer tx.rollback(); + .created_by = created_by, + .size = data.len, - const dir_id = try lookupDirectory(tx, args.owner, args.dir, alloc); + .filename = meta.filename, + .description = meta.description, + .content_type = meta.content_type, + .sensitive = meta.sensitive, - try tx.insert("file_upload", .{ - .id = id, + .status = .uploading, - .filename = args.filename, + .created_at = now, + .updated_at = now, + }, alloc); - .created_by = args.created_by, - .size = data.len, - - .description = args.description, - .content_type = args.content_type, - .sensitive = args.sensitive, - - .is_deleted = false, - - .created_at = now, - .updated_at = now, - }, alloc); - - var sub_tx = try tx.savepoint(); - if (insertFileRow(sub_tx, id, args.filename, args.owner, dir_id, alloc)) |_| { - try sub_tx.release(); - } else |err| { - std.log.debug("{}", .{err}); - switch (err) { - error.UniqueViolation => { - try sub_tx.rollbackSavepoint(); - // Rename the file before trying again - var split = std.mem.split(u8, args.filename, "."); - const name = split.first(); - const ext = split.rest(); - var buf: [256]u8 = undefined; - const drive_filename = try std.fmt.bufPrint(&buf, "{s}.{}.{s}", .{ name, id, ext }); - try insertFileRow(tx, id, drive_filename, args.owner, dir_id, alloc); - }, - else => return error.DatabaseFailure, - } - } - - try tx.commit(); - } - - errdefer { - db.exec("DELETE FROM file_upload WHERE ID = $1", .{id}, alloc) catch |err| { - std.log.err("Unable to remove file record in DB: {}", .{err}); + 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: {}", .{e}); }; - db.exec("DELETE FROM drive_entry WHERE ID = $1", .{id}, alloc) catch |err| { - std.log.err("Unable to remove file record in DB: {}", .{err}); - }; - } + return err; + }; - try saveFile(id, data); + try db.exec( + \\UPDATE file_upload + \\SET status = 'uploaded' + \\WHERE id = $1 + \\LIMIT 1 + , .{id}, alloc); +} + +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 + \\LIMIT 1 + , .{id}, alloc); } const data_root = "./files"; @@ -218,17 +146,3 @@ pub fn deref(alloc: std.mem.Allocator, id: Uuid) ![]const u8 { return dir.readFileAlloc(alloc, &id.toCharArray(), 1 << 32); } - -pub fn deleteFile(db: anytype, alloc: std.mem.Allocator, id: Uuid) !void { - var dir = try std.fs.cwd().openDir(data_root, .{}); - defer dir.close(); - - try dir.deleteFile(id.toCharArray()); - - const tx = try db.beginOrSavepoint(); - errdefer tx.rollback(); - - tx.exec("DELETE FROM drive_entry WHERE ID = $1", .{id}, alloc) catch return error.DatabaseFailure; - tx.exec("DELETE FROM file_upload WHERE ID = $1", .{id}, alloc) catch return error.DatabaseFailure; - try tx.commitOrRelease(); -}