From 208007c0f716d7b8847e8fff25972cf7379b0d33 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sat, 3 Dec 2022 07:09:29 -0800 Subject: [PATCH] Drive - Uploads & dirs --- src/api/lib.zig | 27 +++- src/api/services/files.zig | 245 ++++++++++++++++++++++++----- src/main/controllers/api.zig | 1 + src/main/controllers/api/drive.zig | 11 +- 4 files changed, 238 insertions(+), 46 deletions(-) diff --git a/src/api/lib.zig b/src/api/lib.zig index 49c174e..065fdc7 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -9,7 +9,7 @@ const services = struct { const communities = @import("./services/communities.zig"); const actors = @import("./services/actors.zig"); const auth = @import("./services/auth.zig"); - const drive = @import("./services/files.zig").files; + const drive = @import("./services/files.zig"); const invites = @import("./services/invites.zig"); const notes = @import("./services/notes.zig"); const follows = @import("./services/follows.zig"); @@ -137,6 +137,14 @@ pub const FollowerQueryResult = FollowQueryResult; pub const FollowingQueryArgs = FollowQueryArgs; pub const FollowingQueryResult = FollowQueryResult; +pub const UploadFileArgs = struct { + filename: []const u8, + dir: ?[]const u8, + description: ?[]const u8, + content_type: []const u8, + sensitive: bool, +}; + pub fn isAdminSetup(db: sql.Db) !bool { _ = services.communities.adminCommunityId(db) catch |err| switch (err) { error.NotFound => return false, @@ -511,9 +519,22 @@ fn ApiConn(comptime DbConn: type) type { ); } - pub fn uploadFile(self: *Self, filename: []const u8, body: []const u8) !void { + pub fn uploadFile(self: *Self, meta: UploadFileArgs, body: []const u8) !void { const user_id = self.user_id orelse return error.NoToken; - try services.drive.create(self.db, .{ .user_id = user_id }, filename, body, self.allocator); + return try services.drive.createFile(self.db, .{ + .dir = meta.dir orelse "/", + .filename = meta.filename, + .owner = .{ .user_id = user_id }, + .created_by = user_id, + .description = meta.description, + .content_type = meta.content_type, + .sensitive = meta.sensitive, + }, body, self.allocator); + } + + pub fn driveMkdir(self: *Self, path: []const u8) !void { + const user_id = self.user_id orelse return error.NoToken; + try services.drive.mkdir(self.db, .{ .user_id = user_id }, path, self.allocator); } }; } diff --git a/src/api/services/files.zig b/src/api/services/files.zig index fa3b0a1..147d049 100644 --- a/src/api/services/files.zig +++ b/src/api/services/files.zig @@ -11,61 +11,224 @@ pub const FileOwner = union(enum) { 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, - size: usize, - created_at: DateTime, + created_by: Uuid, + description: ?[]const u8, + content_type: ?[]const u8, + sensitive: bool, }; -pub const files = struct { - pub fn create(db: anytype, owner: FileOwner, filename: []const u8, data: []const u8, alloc: std.mem.Allocator) !void { - const id = Uuid.randV4(util.getThreadPrng()); - const now = DateTime.now(); +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]; +} - // TODO: assert we're not in a transaction - db.insert("drive_file", .{ +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 = filename, - .account_owner_id = if (owner == .user_id) owner.user_id else null, - .community_owner_id = if (owner == .community_id) owner.community_id else null, - .created_at = now, + + .filename = args.filename, + + .created_by = args.created_by, .size = data.len, - }, alloc) catch return error.DatabaseFailure; - // Assume the previous statement succeeded and is not stuck in a transaction - errdefer { - db.exec("DELETE FROM drive_file WHERE ID = $1", .{id}, alloc) catch |err| { - std.log.err("Unable to remove file record in DB: {}", .{err}); - }; + + .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 saveFile(id, data); + try tx.commit(); } - 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(); + 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}); + }; } - pub fn deref(alloc: std.mem.Allocator, id: Uuid) ![]const u8 { - var dir = try std.fs.cwd().openDir(data_root, .{}); - defer dir.close(); + try saveFile(id, data); +} - return dir.readFileAlloc(alloc, &id.toCharArray(), 1 << 32); - } +const data_root = "./files"; +fn saveFile(id: Uuid, data: []const u8) !void { + var dir = try std.fs.cwd().openDir(data_root, .{}); + defer dir.close(); - pub fn delete(db: anytype, alloc: std.mem.Allocator, id: Uuid) !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 dir.deleteFile(id.toCharArray()); + try file.writer().writeAll(data); + try file.sync(); +} - db.exec("DELETE FROM drive_file WHERE ID = $1", .{id}, alloc) catch return error.DatabaseFailure; - } -}; +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(); +} diff --git a/src/main/controllers/api.zig b/src/main/controllers/api.zig index 08e4f1d..9a76c91 100644 --- a/src/main/controllers/api.zig +++ b/src/main/controllers/api.zig @@ -28,4 +28,5 @@ pub const routes = .{ controllers.apiEndpoint(follows.query_followers), controllers.apiEndpoint(follows.query_following), controllers.apiEndpoint(drive.upload), + controllers.apiEndpoint(drive.mkdir), }; diff --git a/src/main/controllers/api/drive.zig b/src/main/controllers/api/drive.zig index c89f357..f617898 100644 --- a/src/main/controllers/api/drive.zig +++ b/src/main/controllers/api/drive.zig @@ -67,9 +67,16 @@ pub const upload = struct { pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const f = req.body.file; - const meta = try srv.createFile(f.filename, f.content_type, f.data); + try srv.uploadFile(.{ + .dir = req.args.path, + .filename = f.filename, + .description = req.body.description, + .content_type = f.content_type, + .sensitive = req.body.sensitive, + }, f.data); - try res.json(.created, meta); + // TODO: print meta + try res.json(.created, .{}); } };