feat: v0.4.0 — File sharing + multi-target messages
Some checks failed
Some checks failed
Files: MinIO-backed file sharing built into the broker. share_file for persistent mesh files, send_message(file:) for ephemeral attachments. Presigned URLs for download, access tracking per peer. Broker infra: MinIO in docker-compose, internal network. HTTP POST /upload endpoint. WS handlers for get_file, list_files, file_status, delete_file. Multi-target: send_message(to:) accepts string or array. Targets deduplicated before delivery. Targeted views: MCP instructions teach Claude to send tailored messages per audience instead of generic broadcasts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
28
packages/db/migrations/0009_add-file-tables.sql
Normal file
28
packages/db/migrations/0009_add-file-tables.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE "mesh"."file" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"mesh_id" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"size_bytes" integer NOT NULL,
|
||||
"mime_type" text,
|
||||
"minio_key" text NOT NULL,
|
||||
"tags" text[] DEFAULT '{}',
|
||||
"persistent" boolean DEFAULT true NOT NULL,
|
||||
"uploaded_by_name" text,
|
||||
"uploaded_by_member" text,
|
||||
"target_spec" text,
|
||||
"uploaded_at" timestamp DEFAULT now() NOT NULL,
|
||||
"expires_at" timestamp,
|
||||
"deleted_at" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "mesh"."file_access" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"file_id" text NOT NULL,
|
||||
"peer_session_pubkey" text,
|
||||
"peer_name" text,
|
||||
"accessed_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "mesh"."file" ADD CONSTRAINT "file_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "mesh"."file" ADD CONSTRAINT "file_uploaded_by_member_member_id_fk" FOREIGN KEY ("uploaded_by_member") REFERENCES "mesh"."member"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "mesh"."file_access" ADD CONSTRAINT "file_access_file_id_file_id_fk" FOREIGN KEY ("file_id") REFERENCES "mesh"."file"("id") ON DELETE cascade ON UPDATE no action;
|
||||
3237
packages/db/migrations/meta/0009_snapshot.json
Normal file
3237
packages/db/migrations/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,13 @@
|
||||
"when": 1775477883426,
|
||||
"tag": "0008_add-state-and-memory",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "7",
|
||||
"when": 1775480008546,
|
||||
"tag": "0009_add-file-tables",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgSchema,
|
||||
@@ -289,6 +290,43 @@ export const meshMemory = meshSchema.table("memory", {
|
||||
forgottenAt: timestamp(),
|
||||
});
|
||||
|
||||
/**
|
||||
* File metadata for shared files in a mesh. Actual bytes live in MinIO;
|
||||
* this table tracks ownership, access control, and soft-deletion.
|
||||
*/
|
||||
export const meshFile = meshSchema.table("file", {
|
||||
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||
meshId: text()
|
||||
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
|
||||
.notNull(),
|
||||
name: text().notNull(),
|
||||
sizeBytes: integer().notNull(),
|
||||
mimeType: text(),
|
||||
minioKey: text().notNull(),
|
||||
tags: text().array().default([]),
|
||||
persistent: boolean().notNull().default(true),
|
||||
uploadedByName: text(),
|
||||
uploadedByMember: text().references(() => meshMember.id),
|
||||
targetSpec: text(), // null = entire mesh
|
||||
uploadedAt: timestamp().defaultNow().notNull(),
|
||||
expiresAt: timestamp(),
|
||||
deletedAt: timestamp(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Access log for file downloads. Tracks which peer accessed which file
|
||||
* and when, for auditability and read-receipt semantics.
|
||||
*/
|
||||
export const meshFileAccess = meshSchema.table("file_access", {
|
||||
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||
fileId: text()
|
||||
.references(() => meshFile.id, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
peerSessionPubkey: text(),
|
||||
peerName: text(),
|
||||
accessedAt: timestamp().defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const meshRelations = relations(mesh, ({ one, many }) => ({
|
||||
owner: one(user, {
|
||||
fields: [mesh.ownerUserId],
|
||||
@@ -367,6 +405,25 @@ export const meshMemoryRelations = relations(meshMemory, ({ one }) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
export const meshFileRelations = relations(meshFile, ({ one, many }) => ({
|
||||
mesh: one(mesh, {
|
||||
fields: [meshFile.meshId],
|
||||
references: [mesh.id],
|
||||
}),
|
||||
uploader: one(meshMember, {
|
||||
fields: [meshFile.uploadedByMember],
|
||||
references: [meshMember.id],
|
||||
}),
|
||||
accesses: many(meshFileAccess),
|
||||
}));
|
||||
|
||||
export const meshFileAccessRelations = relations(meshFileAccess, ({ one }) => ({
|
||||
file: one(meshFile, {
|
||||
fields: [meshFileAccess.fileId],
|
||||
references: [meshFile.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const selectMeshSchema = createSelectSchema(mesh);
|
||||
export const insertMeshSchema = createInsertSchema(mesh);
|
||||
export const selectMemberSchema = createSelectSchema(meshMember);
|
||||
@@ -404,3 +461,11 @@ export type SelectMeshState = typeof meshState.$inferSelect;
|
||||
export type InsertMeshState = typeof meshState.$inferInsert;
|
||||
export type SelectMeshMemory = typeof meshMemory.$inferSelect;
|
||||
export type InsertMeshMemory = typeof meshMemory.$inferInsert;
|
||||
export const selectMeshFileSchema = createSelectSchema(meshFile);
|
||||
export const insertMeshFileSchema = createInsertSchema(meshFile);
|
||||
export const selectMeshFileAccessSchema = createSelectSchema(meshFileAccess);
|
||||
export const insertMeshFileAccessSchema = createInsertSchema(meshFileAccess);
|
||||
export type SelectMeshFile = typeof meshFile.$inferSelect;
|
||||
export type InsertMeshFile = typeof meshFile.$inferInsert;
|
||||
export type SelectMeshFileAccess = typeof meshFileAccess.$inferSelect;
|
||||
export type InsertMeshFileAccess = typeof meshFileAccess.$inferInsert;
|
||||
|
||||
Reference in New Issue
Block a user