Drive - Uploads & dirs
This commit is contained in:
parent
31f676580d
commit
208007c0f7
4 changed files with 238 additions and 46 deletions
|
@ -9,7 +9,7 @@ const services = struct {
|
||||||
const communities = @import("./services/communities.zig");
|
const communities = @import("./services/communities.zig");
|
||||||
const actors = @import("./services/actors.zig");
|
const actors = @import("./services/actors.zig");
|
||||||
const auth = @import("./services/auth.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 invites = @import("./services/invites.zig");
|
||||||
const notes = @import("./services/notes.zig");
|
const notes = @import("./services/notes.zig");
|
||||||
const follows = @import("./services/follows.zig");
|
const follows = @import("./services/follows.zig");
|
||||||
|
@ -137,6 +137,14 @@ pub const FollowerQueryResult = FollowQueryResult;
|
||||||
pub const FollowingQueryArgs = FollowQueryArgs;
|
pub const FollowingQueryArgs = FollowQueryArgs;
|
||||||
pub const FollowingQueryResult = FollowQueryResult;
|
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 {
|
pub fn isAdminSetup(db: sql.Db) !bool {
|
||||||
_ = services.communities.adminCommunityId(db) catch |err| switch (err) {
|
_ = services.communities.adminCommunityId(db) catch |err| switch (err) {
|
||||||
error.NotFound => return false,
|
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;
|
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,38 +11,197 @@ pub const FileOwner = union(enum) {
|
||||||
|
|
||||||
pub const DriveFile = struct {
|
pub const DriveFile = struct {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
|
|
||||||
|
path: []const u8,
|
||||||
filename: []const u8,
|
filename: []const u8,
|
||||||
|
|
||||||
owner: FileOwner,
|
owner: FileOwner,
|
||||||
|
|
||||||
size: usize,
|
size: usize,
|
||||||
|
|
||||||
|
description: []const u8,
|
||||||
|
content_type: []const u8,
|
||||||
|
sensitive: bool,
|
||||||
|
|
||||||
created_at: DateTime,
|
created_at: DateTime,
|
||||||
|
updated_at: DateTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const files = struct {
|
const EntryType = enum {
|
||||||
pub fn create(db: anytype, owner: FileOwner, filename: []const u8, data: []const u8, alloc: std.mem.Allocator) !void {
|
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 id = Uuid.randV4(util.getThreadPrng());
|
||||||
const now = DateTime.now();
|
const now = DateTime.now();
|
||||||
|
|
||||||
// TODO: assert we're not in a transaction
|
{
|
||||||
db.insert("drive_file", .{
|
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,
|
.id = id,
|
||||||
.filename = filename,
|
|
||||||
.account_owner_id = if (owner == .user_id) owner.user_id else null,
|
.filename = args.filename,
|
||||||
.community_owner_id = if (owner == .community_id) owner.community_id else null,
|
|
||||||
.created_at = now,
|
.created_by = args.created_by,
|
||||||
.size = data.len,
|
.size = data.len,
|
||||||
}, alloc) catch return error.DatabaseFailure;
|
|
||||||
// Assume the previous statement succeeded and is not stuck in a transaction
|
.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 {
|
errdefer {
|
||||||
db.exec("DELETE FROM drive_file WHERE ID = $1", .{id}, alloc) catch |err| {
|
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});
|
std.log.err("Unable to remove file record in DB: {}", .{err});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try saveFile(id, data);
|
try saveFile(id, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data_root = "./files";
|
const data_root = "./files";
|
||||||
fn saveFile(id: Uuid, data: []const u8) !void {
|
fn saveFile(id: Uuid, data: []const u8) !void {
|
||||||
var dir = try std.fs.cwd().openDir(data_root, .{});
|
var dir = try std.fs.cwd().openDir(data_root, .{});
|
||||||
defer dir.close();
|
defer dir.close();
|
||||||
|
|
||||||
|
@ -51,21 +210,25 @@ pub const files = struct {
|
||||||
|
|
||||||
try file.writer().writeAll(data);
|
try file.writer().writeAll(data);
|
||||||
try file.sync();
|
try file.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deref(alloc: std.mem.Allocator, id: Uuid) ![]const u8 {
|
pub fn deref(alloc: std.mem.Allocator, id: Uuid) ![]const u8 {
|
||||||
var dir = try std.fs.cwd().openDir(data_root, .{});
|
var dir = try std.fs.cwd().openDir(data_root, .{});
|
||||||
defer dir.close();
|
defer dir.close();
|
||||||
|
|
||||||
return dir.readFileAlloc(alloc, &id.toCharArray(), 1 << 32);
|
return dir.readFileAlloc(alloc, &id.toCharArray(), 1 << 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(db: anytype, alloc: std.mem.Allocator, id: Uuid) !void {
|
pub fn deleteFile(db: anytype, alloc: std.mem.Allocator, id: Uuid) !void {
|
||||||
var dir = try std.fs.cwd().openDir(data_root, .{});
|
var dir = try std.fs.cwd().openDir(data_root, .{});
|
||||||
defer dir.close();
|
defer dir.close();
|
||||||
|
|
||||||
try dir.deleteFile(id.toCharArray());
|
try dir.deleteFile(id.toCharArray());
|
||||||
|
|
||||||
db.exec("DELETE FROM drive_file WHERE ID = $1", .{id}, alloc) catch return error.DatabaseFailure;
|
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();
|
||||||
|
}
|
||||||
|
|
|
@ -28,4 +28,5 @@ pub const routes = .{
|
||||||
controllers.apiEndpoint(follows.query_followers),
|
controllers.apiEndpoint(follows.query_followers),
|
||||||
controllers.apiEndpoint(follows.query_following),
|
controllers.apiEndpoint(follows.query_following),
|
||||||
controllers.apiEndpoint(drive.upload),
|
controllers.apiEndpoint(drive.upload),
|
||||||
|
controllers.apiEndpoint(drive.mkdir),
|
||||||
};
|
};
|
||||||
|
|
|
@ -67,9 +67,16 @@ pub const upload = struct {
|
||||||
|
|
||||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||||
const f = req.body.file;
|
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, .{});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue