File system api
This commit is contained in:
parent
b60b629b30
commit
6f4882794a
6 changed files with 281 additions and 69 deletions
|
@ -9,7 +9,8 @@ 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");
|
||||
const drive = @import("./services/drive.zig");
|
||||
const files = @import("./services/files.zig");
|
||||
const invites = @import("./services/invites.zig");
|
||||
const notes = @import("./services/notes.zig");
|
||||
const follows = @import("./services/follows.zig");
|
||||
|
@ -139,12 +140,25 @@ pub const FollowingQueryResult = FollowQueryResult;
|
|||
|
||||
pub const UploadFileArgs = struct {
|
||||
filename: []const u8,
|
||||
dir: ?[]const u8,
|
||||
dir: []const u8,
|
||||
description: ?[]const u8,
|
||||
content_type: []const u8,
|
||||
sensitive: bool,
|
||||
};
|
||||
|
||||
pub const DriveEntry = services.drive.DriveEntry;
|
||||
pub const FileUpload = services.files.FileUpload;
|
||||
pub const DriveGetResult = union(services.drive.Kind) {
|
||||
dir: struct {
|
||||
entry: DriveEntry,
|
||||
children: []DriveEntry,
|
||||
},
|
||||
file: struct {
|
||||
entry: DriveEntry,
|
||||
file: FileUpload,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn isAdminSetup(db: sql.Db) !bool {
|
||||
_ = services.communities.adminCommunityId(db) catch |err| switch (err) {
|
||||
error.NotFound => return false,
|
||||
|
@ -519,22 +533,88 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
);
|
||||
}
|
||||
|
||||
pub fn uploadFile(self: *Self, meta: UploadFileArgs, body: []const u8) !void {
|
||||
pub fn driveUpload(self: *Self, meta: UploadFileArgs, body: []const u8) !void {
|
||||
const user_id = self.user_id orelse return error.NoToken;
|
||||
return try services.drive.createFile(self.db, .{
|
||||
.dir = meta.dir orelse "/",
|
||||
const file_id = try services.files.create(self.db, user_id, .{
|
||||
.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);
|
||||
|
||||
errdefer services.files.delete(self.db, file_id, self.allocator) catch |err| {
|
||||
std.log.err("Unable to delete file {}: {}", .{ file_id, err });
|
||||
};
|
||||
|
||||
services.drive.create(self.db, user_id, meta.dir, meta.filename, file_id, self.allocator) catch |err| switch (err) {
|
||||
error.PathAlreadyExists => {
|
||||
var buf: [256]u8 = undefined;
|
||||
var split = std.mem.splitBackwards(u8, meta.filename, ".");
|
||||
const ext = split.first();
|
||||
const name = split.rest();
|
||||
const new_name = try std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ name, file_id, ext });
|
||||
|
||||
try services.drive.create(
|
||||
self.db,
|
||||
user_id,
|
||||
meta.dir,
|
||||
new_name,
|
||||
file_id,
|
||||
self.allocator,
|
||||
);
|
||||
},
|
||||
else => |e| return e,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
var split = std.mem.splitBackwards(u8, path, "/");
|
||||
std.log.debug("{s}", .{path});
|
||||
const base = split.first();
|
||||
const dir = split.rest();
|
||||
try services.drive.create(self.db, user_id, dir, base, null, self.allocator);
|
||||
}
|
||||
|
||||
pub fn driveDelete(self: *Self, path: []const u8) !void {
|
||||
const user_id = self.user_id orelse return error.NoToken;
|
||||
const entry = try services.drive.stat(self.db, user_id, path, self.allocator);
|
||||
defer util.deepFree(self.allocator, entry);
|
||||
try services.drive.delete(self.db, entry.id, self.allocator);
|
||||
if (entry.file_id) |file_id| try services.files.delete(self.db, file_id, self.allocator);
|
||||
}
|
||||
|
||||
pub fn driveMove(self: *Self, src: []const u8, dest: []const u8) !void {
|
||||
const user_id = self.user_id orelse return error.NoToken;
|
||||
try services.drive.move(self.db, user_id, src, dest, self.allocator);
|
||||
}
|
||||
|
||||
pub fn driveGet(self: *Self, path: []const u8) !DriveGetResult {
|
||||
const user_id = self.user_id orelse return error.NoToken;
|
||||
const entry = try services.drive.stat(self.db, user_id, path, self.allocator);
|
||||
errdefer util.deepFree(self.allocator, entry);
|
||||
|
||||
if (entry.file_id) |file_id| return .{
|
||||
.file = .{
|
||||
.entry = entry,
|
||||
.file = try services.files.get(self.db, file_id, self.allocator),
|
||||
},
|
||||
} else return .{
|
||||
.dir = .{
|
||||
.entry = entry,
|
||||
.children = try services.drive.list(self.db, entry.id, self.allocator),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn driveUpdate(self: *Self, path: []const u8, meta: services.files.PartialMeta) !void {
|
||||
const user_id = self.user_id orelse return error.NoToken;
|
||||
std.log.debug("{s}", .{path});
|
||||
const entry = try services.drive.stat(self.db, user_id, path, self.allocator);
|
||||
errdefer util.deepFree(self.allocator, entry);
|
||||
|
||||
std.log.debug("{}", .{entry.id});
|
||||
try services.files.update(self.db, entry.file_id orelse return error.NotAFile, meta, self.allocator);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -55,6 +55,10 @@ pub fn register(
|
|||
.hash = hash,
|
||||
.changed_at = DateTime.now(),
|
||||
}, alloc) catch return error.DatabaseFailure;
|
||||
tx.insert("drive_entry", .{
|
||||
.id = id,
|
||||
.owner_id = id,
|
||||
}, alloc) catch return error.DatabaseFailure;
|
||||
|
||||
tx.commitOrRelease() catch return error.DatabaseFailure;
|
||||
|
||||
|
|
129
src/api/services/drive.zig
Normal file
129
src/api/services/drive.zig
Normal file
|
@ -0,0 +1,129 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const sql = @import("sql");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
|
||||
pub const DriveOwner = union(enum) {
|
||||
user_id: Uuid,
|
||||
community_id: Uuid,
|
||||
};
|
||||
|
||||
pub const DriveEntry = struct {
|
||||
id: Uuid,
|
||||
owner_id: Uuid,
|
||||
name: ?[]const u8,
|
||||
path: []const u8,
|
||||
parent_directory_id: ?Uuid,
|
||||
file_id: ?Uuid,
|
||||
kind: Kind,
|
||||
};
|
||||
|
||||
pub const Kind = enum {
|
||||
dir,
|
||||
file,
|
||||
pub const jsonStringify = util.jsonSerializeEnumAsString;
|
||||
};
|
||||
|
||||
pub fn stat(db: anytype, owner: Uuid, path: []const u8, alloc: std.mem.Allocator) !DriveEntry {
|
||||
return (db.queryRow(DriveEntry,
|
||||
\\SELECT id, path, owner_id, name, file_id, kind, parent_directory_id
|
||||
\\FROM drive_entry_path
|
||||
\\WHERE owner_id = $1 AND path = ('/' || $2)
|
||||
\\LIMIT 1
|
||||
, .{
|
||||
owner,
|
||||
std.mem.trim(u8, path, "/"),
|
||||
}, alloc) catch |err| switch (err) {
|
||||
error.NoRows => return error.NotFound,
|
||||
else => |e| return e,
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates a file or directory
|
||||
pub fn create(db: anytype, owner: Uuid, dir: []const u8, name: []const u8, file_id: ?Uuid, alloc: std.mem.Allocator) !void {
|
||||
if (name.len == 0) return error.EmptyName;
|
||||
|
||||
const id = Uuid.randV4(util.getThreadPrng());
|
||||
|
||||
const tx = try db.begin();
|
||||
errdefer tx.rollback();
|
||||
|
||||
const parent = try stat(tx, owner, dir, alloc);
|
||||
defer util.deepFree(alloc, parent);
|
||||
|
||||
tx.insert("drive_entry", .{
|
||||
.id = id,
|
||||
.owner_id = owner,
|
||||
.name = name,
|
||||
.parent_directory_id = parent.id,
|
||||
.file_id = file_id,
|
||||
}, alloc) catch |err| switch (err) {
|
||||
error.UniqueViolation => return error.PathAlreadyExists,
|
||||
else => |e| return e,
|
||||
};
|
||||
|
||||
try tx.commit();
|
||||
}
|
||||
|
||||
pub fn delete(db: anytype, id: Uuid, alloc: std.mem.Allocator) !void {
|
||||
const tx = try db.beginOrSavepoint();
|
||||
errdefer tx.rollback();
|
||||
|
||||
if ((try tx.queryRow(
|
||||
std.meta.Tuple(&.{usize}),
|
||||
\\SELECT COUNT(1)
|
||||
\\FROM drive_entry
|
||||
\\WHERE parent_directory_id = $1
|
||||
,
|
||||
.{id},
|
||||
alloc,
|
||||
))[0] != 0) {
|
||||
return error.DirectoryNotEmpty;
|
||||
}
|
||||
|
||||
try tx.exec("DELETE FROM drive_entry WHERE id = $1", .{id}, alloc);
|
||||
|
||||
try tx.commitOrRelease();
|
||||
}
|
||||
|
||||
pub fn move(db: anytype, owner: Uuid, src: []const u8, dest: []const u8, alloc: std.mem.Allocator) !void {
|
||||
const tx = try db.beginOrSavepoint();
|
||||
errdefer tx.rollback();
|
||||
|
||||
const val = try stat(tx, owner, src, alloc);
|
||||
defer util.deepFree(alloc, val);
|
||||
|
||||
if (val.parent_directory_id == null) return error.RootDirectory;
|
||||
|
||||
var split = std.mem.splitBackwards(u8, std.mem.trim(u8, dest, "/"), "/");
|
||||
const name = split.first();
|
||||
const dir = split.rest();
|
||||
|
||||
const parent = try stat(tx, owner, dir, alloc);
|
||||
defer util.deepFree(alloc, parent);
|
||||
|
||||
try tx.exec(
|
||||
\\UPDATE drive_entry
|
||||
\\SET name = $1, parent_directory_id = $2
|
||||
\\WHERE id = $3
|
||||
,
|
||||
.{ name, parent.id, val.id },
|
||||
alloc,
|
||||
);
|
||||
|
||||
try tx.commitOrRelease();
|
||||
}
|
||||
|
||||
// TODO: paginate this
|
||||
pub fn list(db: anytype, id: Uuid, alloc: std.mem.Allocator) ![]DriveEntry {
|
||||
return (db.queryRows(DriveEntry,
|
||||
\\SELECT id, path, owner_id, name, file_id, kind, parent_directory_id
|
||||
\\FROM drive_entry_path
|
||||
\\WHERE parent_directory_id = $1
|
||||
, .{id}, null, alloc) catch |err| switch (err) {
|
||||
error.NoRows => return error.NotFound,
|
||||
else => |e| return e,
|
||||
});
|
||||
}
|
|
@ -10,12 +10,13 @@ pub const FileStatus = enum {
|
|||
uploaded,
|
||||
external,
|
||||
deleted,
|
||||
pub const jsonStringify = util.jsonSerializeEnumAsString;
|
||||
};
|
||||
|
||||
pub const FileUpload = struct {
|
||||
id: Uuid,
|
||||
|
||||
created_by: Uuid,
|
||||
owner_id: Uuid,
|
||||
size: usize,
|
||||
|
||||
filename: []const u8,
|
||||
|
@ -36,9 +37,33 @@ pub const FileMeta = struct {
|
|||
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]std.builtin.Type.StructField = undefined;
|
||||
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,
|
||||
|
@ -48,13 +73,13 @@ pub fn Partial(comptime T: type) type {
|
|||
};
|
||||
return @Type(.{ .Struct = .{
|
||||
.layout = .Auto,
|
||||
.fields = fields,
|
||||
.fields = &fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = false,
|
||||
} });
|
||||
}
|
||||
|
||||
pub fn update(db: anytype, id: Uuid, meta: Partial(FileMeta), alloc: std.mem.Allocator) !void {
|
||||
pub fn update(db: anytype, id: Uuid, meta: PartialMeta, alloc: std.mem.Allocator) !void {
|
||||
var builder = sql.QueryBuilder.init(alloc);
|
||||
defer builder.deinit();
|
||||
|
||||
|
@ -65,11 +90,11 @@ pub fn update(db: anytype, id: Uuid, meta: Partial(FileMeta), alloc: std.mem.All
|
|||
if (meta.content_type) |_| try builder.set("content_type", "$4");
|
||||
if (meta.sensitive) |_| try builder.set("sensitive", "$5");
|
||||
|
||||
if (meta.set_statements_appended == 0) return error.NoChange;
|
||||
if (builder.set_statements_appended == 0) return error.NoChange;
|
||||
|
||||
try builder.andWhere("id = $1");
|
||||
|
||||
try builder.appendSlice("\nLIMIT 1");
|
||||
std.log.debug("{any}", .{meta});
|
||||
|
||||
try db.exec(try builder.terminate(), .{
|
||||
id,
|
||||
|
@ -80,13 +105,13 @@ pub fn update(db: anytype, id: Uuid, meta: Partial(FileMeta), alloc: std.mem.All
|
|||
}, alloc);
|
||||
}
|
||||
|
||||
pub fn create(db: anytype, created_by: Uuid, meta: FileMeta, data: []const u8, alloc: std.mem.Allocator) !void {
|
||||
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,
|
||||
|
||||
.created_by = created_by,
|
||||
.owner_id = owner_id,
|
||||
.size = data.len,
|
||||
|
||||
.filename = meta.filename,
|
||||
|
@ -94,7 +119,7 @@ pub fn create(db: anytype, created_by: Uuid, meta: FileMeta, data: []const u8, a
|
|||
.content_type = meta.content_type,
|
||||
.sensitive = meta.sensitive,
|
||||
|
||||
.status = .uploading,
|
||||
.status = FileStatus.uploading,
|
||||
|
||||
.created_at = now,
|
||||
.updated_at = now,
|
||||
|
@ -102,7 +127,7 @@ pub fn create(db: anytype, created_by: Uuid, meta: FileMeta, data: []const u8, a
|
|||
|
||||
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});
|
||||
std.log.err("Unable to remove file {} record in DB: {}", .{ id, e });
|
||||
};
|
||||
return err;
|
||||
};
|
||||
|
@ -111,20 +136,20 @@ pub fn create(db: anytype, created_by: Uuid, meta: FileMeta, data: []const u8, a
|
|||
\\UPDATE file_upload
|
||||
\\SET status = 'uploaded'
|
||||
\\WHERE id = $1
|
||||
\\LIMIT 1
|
||||
, .{id}, 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 dir.deleteFile(&id.toCharArray());
|
||||
|
||||
try db.exec(
|
||||
\\DELETE FROM file_upload
|
||||
\\WHERE id = $1
|
||||
\\LIMIT 1
|
||||
, .{id}, alloc);
|
||||
}
|
||||
|
||||
|
|
|
@ -29,4 +29,8 @@ pub const routes = .{
|
|||
controllers.apiEndpoint(follows.query_following),
|
||||
controllers.apiEndpoint(drive.upload),
|
||||
controllers.apiEndpoint(drive.mkdir),
|
||||
controllers.apiEndpoint(drive.get),
|
||||
controllers.apiEndpoint(drive.delete),
|
||||
controllers.apiEndpoint(drive.move),
|
||||
controllers.apiEndpoint(drive.update),
|
||||
};
|
||||
|
|
|
@ -11,46 +11,15 @@ pub const DriveArgs = struct {
|
|||
path: []const u8,
|
||||
};
|
||||
|
||||
pub const query = struct {
|
||||
pub const get = struct {
|
||||
pub const method = .GET;
|
||||
pub const path = drive_path;
|
||||
pub const Args = DriveArgs;
|
||||
|
||||
pub const Query = struct {
|
||||
const OrderBy = enum {
|
||||
created_at,
|
||||
filename,
|
||||
};
|
||||
|
||||
max_items: usize = 20,
|
||||
|
||||
like: ?[]const u8 = null,
|
||||
|
||||
order_by: OrderBy = .created_at,
|
||||
direction: api.Direction = .descending,
|
||||
|
||||
prev: ?struct {
|
||||
id: Uuid,
|
||||
order_val: union(OrderBy) {
|
||||
created_at: DateTime,
|
||||
filename: []const u8,
|
||||
},
|
||||
} = null,
|
||||
|
||||
page_direction: api.PageDirection = .forward,
|
||||
};
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const result = srv.driveQuery(req.args.path, req.query) catch |err| switch (err) {
|
||||
error.NotADirectory => {
|
||||
const meta = try srv.getFile(path);
|
||||
try res.json(.ok, meta);
|
||||
return;
|
||||
},
|
||||
else => |e| return e,
|
||||
};
|
||||
const result = try srv.driveGet(req.args.path);
|
||||
|
||||
try controller_utils.paginate(result, res, req.allocator);
|
||||
try res.json(.ok, result);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -67,7 +36,7 @@ pub const upload = struct {
|
|||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const f = req.body.file;
|
||||
try srv.uploadFile(.{
|
||||
try srv.driveUpload(.{
|
||||
.dir = req.args.path,
|
||||
.filename = f.filename,
|
||||
.description = req.body.description,
|
||||
|
@ -86,11 +55,7 @@ pub const delete = struct {
|
|||
pub const Args = DriveArgs;
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const info = try srv.driveLookup(req.args.path);
|
||||
if (info == .dir)
|
||||
try srv.driveRmdir(req.args.path)
|
||||
else if (info == .file)
|
||||
try srv.deleteFile(req.args.path);
|
||||
try srv.driveDelete(req.args.path);
|
||||
|
||||
return res.json(.ok, .{});
|
||||
}
|
||||
|
@ -113,18 +78,23 @@ pub const update = struct {
|
|||
pub const path = drive_path;
|
||||
pub const Args = DriveArgs;
|
||||
|
||||
// TODO: Validate that unhandled fields are equivalent to ones in the object
|
||||
pub const allow_unknown_fields_in_body = true;
|
||||
pub const Body = struct {
|
||||
filename: ?[]const u8 = null,
|
||||
description: ?[]const u8 = null,
|
||||
content_type: ?[]const u8 = null,
|
||||
sensitive: ?bool = null,
|
||||
};
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const info = try srv.driveLookup(req.args.path);
|
||||
if (info != .file) return error.NotFile;
|
||||
|
||||
const new_info = try srv.updateFile(path, req.body);
|
||||
try res.json(.ok, new_info);
|
||||
try srv.driveUpdate(req.args.path, .{
|
||||
.filename = req.body.filename,
|
||||
.description = req.body.description,
|
||||
.content_type = req.body.content_type,
|
||||
.sensitive = req.body.sensitive,
|
||||
});
|
||||
try res.json(.ok, .{});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -134,11 +104,11 @@ pub const move = struct {
|
|||
pub const Args = DriveArgs;
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const destination = req.fields.get("Destination") orelse return error.NoDestination;
|
||||
const destination = req.headers.get("Destination") orelse return error.NoDestination;
|
||||
|
||||
try srv.driveMove(req.args.path, destination);
|
||||
|
||||
try res.fields.put("Location", destination);
|
||||
try srv.json(.created, .{});
|
||||
try res.headers.put("Location", destination);
|
||||
try res.json(.created, .{});
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue