feat: initial impl of file proxy

This commit is contained in:
2025-10-21 23:45:04 +00:00
parent 6eded27121
commit 6234c5efd3
24 changed files with 420 additions and 125 deletions

View File

@@ -0,0 +1,36 @@
import type { Doc, Id } from "../_generated/dataModel"
import type {
AuthenticatedMutationCtx,
AuthenticatedQueryCtx,
} from "../functions"
import * as FileShare from "./fileshare"
const PREVIEW_FILE_SHARE_VALID_FOR_MS = 1000 * 60 * 10 // 10 minutes
export async function find(
ctx: AuthenticatedMutationCtx | AuthenticatedQueryCtx,
{ storageId }: { storageId: Id<"_storage"> },
) {
return await FileShare.findByStorageId(ctx, { storageId })
}
export async function create(
ctx: AuthenticatedMutationCtx,
{ storageId }: { storageId: Id<"_storage"> },
) {
return await FileShare.create(ctx, {
shareToken: crypto.randomUUID(),
storageId,
expiresAt: new Date(Date.now() + PREVIEW_FILE_SHARE_VALID_FOR_MS),
})
}
export async function extend(
ctx: AuthenticatedMutationCtx,
{ doc }: { doc: Doc<"fileShares"> },
) {
return await FileShare.updateExpiry(ctx, {
doc,
expiresAt: new Date(Date.now() + PREVIEW_FILE_SHARE_VALID_FOR_MS),
})
}

View File

@@ -0,0 +1,83 @@
import type { Doc, Id } from "../_generated/dataModel"
import type { MutationCtx } from "../_generated/server"
import type {
ApiKeyAuthenticatedQueryCtx,
AuthenticatedMutationCtx,
AuthenticatedQueryCtx,
} from "../functions"
import * as Err from "../shared/error"
export async function create(
ctx: MutationCtx,
{
shareToken,
storageId,
expiresAt,
}: { shareToken: string; storageId: Id<"_storage">; expiresAt?: Date },
) {
const id = await ctx.db.insert("fileShares", {
shareToken,
storageId,
expiresAt: expiresAt?.getTime(),
})
const doc = await ctx.db.get(id)
if (!doc) {
throw Err.create(Err.Code.Internal, "Failed to create file share")
}
return doc
}
export async function remove(
ctx: AuthenticatedMutationCtx,
{ doc }: { doc: Doc<"fileShares"> },
) {
return await ctx.db.delete(doc._id)
}
export async function find(
ctx:
| AuthenticatedMutationCtx
| AuthenticatedQueryCtx
| ApiKeyAuthenticatedQueryCtx,
{ shareToken }: { shareToken: string },
) {
const doc = await ctx.db
.query("fileShares")
.withIndex("byShareToken", (q) => q.eq("shareToken", shareToken))
.first()
if (!doc) {
throw Err.create(Err.Code.NotFound, "File share not found")
}
if (hasExpired(doc)) {
throw Err.create(Err.Code.NotFound, "File share not found")
}
return doc
}
export async function findByStorageId(
ctx: AuthenticatedMutationCtx | AuthenticatedQueryCtx,
{ storageId }: { storageId: Id<"_storage"> },
) {
return await ctx.db
.query("fileShares")
.withIndex("byStorageId", (q) => q.eq("storageId", storageId))
.first()
}
export function hasExpired(doc: Doc<"fileShares">) {
if (!doc.expiresAt) {
return false
}
return doc.expiresAt < Date.now()
}
export async function updateExpiry(
ctx: AuthenticatedMutationCtx,
{ doc, expiresAt }: { doc: Doc<"fileShares">; expiresAt: Date },
) {
return await ctx.db.patch(doc._id, {
expiresAt: expiresAt.getTime(),
})
}

View File

@@ -17,7 +17,9 @@ import {
newFileHandle,
} from "../shared/filesystem"
import * as Directories from "./directories"
import * as FilePreview from "./filepreview"
import * as Files from "./files"
import * as FileShare from "./fileshare"
export const VDirectoryHandle = v.object({
kind: v.literal(FileType.Directory),
@@ -228,3 +230,33 @@ export async function fetchFileUrl(
return url
}
export async function openFile(
ctx: AuthenticatedMutationCtx,
{ fileId }: { fileId: Id<"files"> },
) {
const file = await authorizedGet(ctx, fileId)
if (!file) {
throw Err.create(Err.Code.NotFound, "file not found")
}
const fileShare = await FilePreview.find(ctx, {
storageId: file.storageId,
})
if (fileShare && !FileShare.hasExpired(fileShare)) {
await FilePreview.extend(ctx, { doc: fileShare })
return {
file,
shareToken: fileShare.shareToken,
}
}
const newFileShare = await FilePreview.create(ctx, {
storageId: file.storageId,
})
return {
file,
shareToken: newFileShare.shareToken,
}
}