const std = @import("std"); 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 DriveFile = struct { id: Uuid, path: []const u8, filename: []const u8, owner: FileOwner, size: usize, description: []const u8, content_type: []const u8, sensitive: bool, created_at: DateTime, updated_at: DateTime, }; const EntryType = enum { dir, file, }; pub const CreateFileArgs = struct { dir: []const u8, 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]; } 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 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 }); if (name.len == 0) return error.EmptyName; const id = Uuid.randV4(util.getThreadPrng()); const tx = try db.begin(); errdefer tx.rollback(); const parent = try lookupDirectory(tx, owner, dir, alloc); 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, }, alloc); } pub fn createFile(db: anytype, args: CreateFileArgs, data: []const u8, alloc: std.mem.Allocator) !void { const id = Uuid.randV4(util.getThreadPrng()); const now = DateTime.now(); { var tx = try db.begin(); errdefer tx.rollback(); const dir_id = try lookupDirectory(tx, args.owner, args.dir, alloc); try tx.insert("file_upload", .{ .id = id, .filename = args.filename, .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}); }; db.exec("DELETE FROM drive_entry WHERE ID = $1", .{id}, alloc) catch |err| { std.log.err("Unable to remove file record in DB: {}", .{err}); }; } try saveFile(id, data); } 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); } 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(); }