185 lines · 7.6 KB
import { env } from "cloudflare:workers";
import { MAX_IN_LIST, all, batch, chunks, first, placeholders, run, stmt } from "../db";
import { HttpError } from "../errors";
import { randomId } from "../crypto";
import { hasPermission, loadRepoById, repoPermission } from "../repos";
import type { RepoRow, Viewer } from "~/lib/types";

/** Forks read LFS objects from their parents; bound the chain so lookups stay O(1) in D1 calls. */
const MAX_FORK_DEPTH = 3;
/** Unreferenced blobs survive this long, so an upload racing the sweeper never loses its data. */
const ORPHAN_GRACE_MS = 60 * 60 * 1000;
const SWEEP_BATCH = 100;

type ObjectRow = { repo_id: string; size: number };

type ChainRepo = Pick<RepoRow, "id" | "fork_of_id" | "owner_id" | "visibility">;

/**
 * Whether readers of `repo` may see objects stored in the private `ancestor`: only for a private fork,
 * and only while the fork's owner (the account whose collaborators can read it) can still read the
 * ancestor, so a removed collaborator's fork stops seeing new objects.
 */
async function ownerCanRead(repo: ChainRepo, ancestorId: string): Promise<boolean> {
  if (repo.visibility === "public") return false;
  const ancestor = await loadRepoById(ancestorId);
  if (!ancestor) return false;
  if (ancestor.owner_id === repo.owner_id) return true;
  const owner: Viewer = { id: repo.owner_id, name: "", displayName: null, email: "", isAdmin: false };
  return hasPermission(await repoPermission(ancestor, owner), "read");
}

/**
 * The repo plus the ancestors whose objects its readers may see: public ancestors, or private ones the
 * fork's owner can still read when the fork is private too. Stops at the first ancestor that fails.
 */
async function repoChain(repo: ChainRepo): Promise<string[]> {
  const chain = [repo.id];
  let parent = repo.fork_of_id;
  while (parent && chain.length < MAX_FORK_DEPTH) {
    const row = await first<{ fork_of_id: string | null; visibility: string }>(