drop-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev b2e053fd98cb
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 80ddb8c5df822e290afdd3c7
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/lib/items.ts
// What the sender offers: single files as they are, and folders zipped on this device while they
// are sent (nothing is zipped ahead of time, so a folder of any size starts sending at once).
import { makeZip, predictLength } from "client-zip";
import { sha256 } from "@noble/hashes/sha2";
import { bytesToHex } from "@noble/hashes/utils";
import { CHUNK, type FileInfo } from "./protocol";

/** One thing the person picked: a file, or a folder with the paths of its files inside it. */
export interface Pick {
  name: string;
  folder: boolean;
  files: { path: string; file: File }[];
}

export interface Outgoing {
  info: FileInfo;
  pick: Pick;
  sum?: Promise<string>;
}

/** Files from a file input. A folder input gives paths like "Photos/2026/a.jpg": one pick per top folder. */
export function picksFromInput(list: FileList | File[]): Pick[] {
  const picks: Pick[] = [];
  const folders = new Map<string, Pick>();
  for (const file of Array.from(list)) {
    const rel = (file as File & { webkitRelativePath?: string }).webkitRelativePath || "";
    const top = rel.includes("/") ? rel.split("/")[0] : "";
    if (!top) { picks.push({ name: file.name, folder: false, files: [{ path: file.name, file }] }); continue; }
    let p = folders.get(top);
    if (!p) { p = { name: top, folder: true, files: [] }; folders.set(top, p); picks.push(p); }
    p.files.push({ path: rel, file });
  }
  return picks;
}

type Entry = { isFile: boolean; isDirectory: boolean; name: string; fullPath: string };
type FileEntry = Entry & { file(ok: (f: File) => void, fail: (e: unknown) => void): void };
type DirEntry = Entry & { createReader(): { readEntries(ok: (e: Entry[]) => void, fail: (e: unknown) => void): void } };

/** Files and folders dropped on the page. Entries must be taken before the first await. */
export function picksFromDrop(dt: DataTransfer): Promise<Pick[]> {
  const entries = Array.from(dt.items || [])
    .filter((it) => it.kind === "file")
    .map((it) => (it as DataTransferItem & { webkitGetAsEntry?: () => Entry | null }).webkitGetAsEntry?.() || null);
  const plain = Array.from(dt.files || []);
  if (!entries.length || entries.some((e) => !e)) return Promise.resolve(picksFromInput(plain));
  return Promise.all((entries as Entry[]).map(async (entry, n): Promise<Pick> => {
    if (entry.isFile) {
      const file = await fileOf(entry as FileEntry).catch(() => plain[n]);
      return { name: file.name, folder: false, files: [{ path: file.name, file }] };
    }
    const files: Pick["files"] = [];
    await walk(entry as DirEntry, files);
    return { name: entry.name, folder: true, files };
  }));
}

function fileOf(entry: FileEntry): Promise<File> {
  return new Promise((ok, fail) => entry.file(ok, fail));
}

async function walk(dir: DirEntry, out: Pick["files"]) {
  const reader = dir.createReader();
  for (;;) {
    const batch = await new Promise<Entry[]>((ok, fail) => reader.readEntries(ok, fail));
    if (!batch.length) return;
    for (const e of batch) {
      if (e.isDirectory) await walk(e as DirEntry, out);
      else out.push({ path: e.fullPath.replace(/^\//, ""), file: await fileOf(e as FileEntry) });
    }
  }
}

/** Turns picks into numbered items, the first one numbered `start`. */
export function toOutgoing(picks: Pick[], start: number): Outgoing[] {
  return picks.filter((p) => p.files.length).map((pick, n) => {
    const i = start + n;
    if (!pick.folder) {
      const f = pick.files[0].file;
      return { pick, info: { i, name: f.name, size: f.size, type: f.type || "application/octet-stream" } };
    }
    const size = Number(predictLength(pick.files.map((e) => ({ name: e.path, size: e.file.size }))));
    return { pick, info: { i, name: `${pick.name}.zip`, size, type: "application/zip", zip: true } };
  });
}

const BLOCK = 1024 * 1024;

/** The item's bytes from offset `from`, in pieces of at most CHUNK bytes. */
export async function* read(item: Outgoing, from: number): AsyncGenerator<Uint8Array> {
  if (!item.info.zip) {
    const f = item.pick.files[0].file;
    for (let pos = from; pos < f.size;) {
      const end = Math.min(f.size, pos + BLOCK);
      const buf = new Uint8Array(await f.slice(pos, end).arrayBuffer());
      for (let o = 0; o < buf.length; o += CHUNK) yield buf.subarray(o, o + CHUNK);
      pos = end;
    }
    return;
  }
  // The same files in the same order make the same zip, byte for byte, so a dropped connection
  // can pick up from an offset and the checksum matches.
  const zip = makeZip(item.pick.files.map((e) => ({ name: e.path, lastModified: new Date(e.file.lastModified), input: e.file })), { buffersAreUTF8: true });
  const reader = zip.getReader();
  let pos = 0;
  try {
    for (;;) {
      const { done, value } = await reader.read();
      if (done) return;
      let chunk = value;
      if (pos + chunk.length <= from) { pos += chunk.length; continue; }
      if (pos < from) { chunk = chunk.subarray(from - pos); pos = from; }
      for (let o = 0; o < chunk.length; o += CHUNK) yield chunk.subarray(o, o + CHUNK);
      pos += chunk.length;
    }
  } finally {
    void reader.cancel().catch(() => null);
  }
}

/** SHA-256 of the whole item, worked out once and shared by every receiver. */
export function sumOf(item: Outgoing): Promise<string> {
  item.sum ??= (async () => {
    const h = sha256.create();
    for await (const chunk of read(item, 0)) h.update(chunk);
    return bytesToHex(h.digest());
  })();
  item.sum.catch(() => { item.sum = undefined; });
  return item.sum;
}

Change history

Point OpenApps links to openapps.free
b2e053fd98 · 17 hours ago
Bind agent address in policy
ca424dd814 · 19 hours ago
Retry release capture
ae07f1a3fd · 19 hours ago
Use OpenApps service URLs and add contribution strip
0452ce136f · 19 hours ago
Bounded source sync
ca1bc00aa4 · 1 day ago
Bounded source sync
d4982911a0 · 1 day ago
Bounded source sync
18c97fe4ca · 1 day ago