32cd2100d9dd11140705c848850e86c4f79191c4 max Tue Sep 1 07:02:04 2026 -0700 Reusable "view snapshot" sessions for durable, minimal Share-a-link links "Share a link" could only share the whole cart: it saved a session holding every track, setting and position, which bloated the central db and leaked the sharer's unrelated tracks to whoever opened the link. Anonymous share sessions (under the reserved user "l") were also never reaped, so they accumulated forever, and their 8-char names were generated client-side with no uniqueness check, so two shares could collide and silently overwrite. Adds a lightweight "snapshot session" facility (lib/snapshotSession.c): a snapshot stores only the handful of cart variables a feature declares (a registered snapshotType, e.g. "blat" -> {db, blatLastBigBed}), moves only those variables' trash files into durable sessionData storage, and is saved under a "__"-prefixed name. The "__" marks it machine-made: hidden from the My Sessions list by default and eligible for reaping. For the anonymous "l" owner the durable files fan out over two extra hash levels so one directory never fills with millions of entries. Every anonymous link now shares one server-side name generator (snapshotNewName): a unique (db-checked), crypto-strong, "__"-prefixed token, so tokens never collide. The hgSession doSaveSessionJson endpoint gained hgS_snapshotType (save a minimal snapshot rather than the whole cart) and hgS_doAnonName (reserve a unique anonymous name without saving, so the top-right dialog can preview the exact link before it is created). Wired three callers to the facility: - hgc htcBlatAlign "Share a link": a minimal "blat" snapshot instead of a full-cart anonymous session. - hgBlat results "Share a link": creates a "blat" snapshot on click and reveals its ?u=&s= reopen link (rebuilt from the durable bigPsl by the existing doShareReopen), replacing the trash-only reveal. - top-right "Share a link": anonymous links use the reserved server name; logged-in named shares are unchanged. snapshotReaper (hg/utils) garbage-collects abandoned anonymous snapshots: it deletes user "l" "__" rows whose lastUse is older than the TTL (hg.conf snapshot.ttlDays, default ~4 years) and removes their durable files. lastUse is bumped on every open by the existing session load, so a link stays alive as long as it is used. Meant to run from the trash-cleaner cron. Also folds in the recent Share-dialog work in these files: the auto share name is a short "_" prefix instead of "share_", the dialog previews the link and creates it only when the button is clicked (no orphan session just from opening the dialog), an optional name field with an overwrite warning, and the anonymous save path forces the reap-eligible "__" name. refs #38197 diff --git src/hg/inc/snapshotSession.h src/hg/inc/snapshotSession.h new file mode 100644 index 00000000000..5b6d57c3309 --- /dev/null +++ src/hg/inc/snapshotSession.h @@ -0,0 +1,73 @@ +/* snapshotSession - lightweight, shareable "view snapshot" sessions. + * + * A snapshot is a minimal named session in the central namedSessionDb. Unlike a normal saved + * session (which stores the whole cart), a snapshot stores ONLY the handful of cart variables a + * feature needs to reconstruct one specific view - e.g. a single BLAT alignment - and moves only + * those variables' backing trash files into durable sessionData storage. This keeps the row tiny + * and, crucially, avoids leaking the sharer's unrelated tracks/position to whoever opens the link. + * + * Snapshots are always shared-by-link. Their session names are prefixed "__" so the My Sessions + * list can hide them by default and a reaper can garbage-collect abandoned anonymous ones. Each + * feature that wants durable shareable links registers a snapshotType naming its variables; the + * feature reconstructs its view from those variables and calls snapshotTouchLastUse() on every open + * so popular links stay alive under the reaper's "durable while used" policy. + * + * Copyright (C) 2026 The Regents of the University of California + * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ + +#ifndef SNAPSHOTSESSION_H +#define SNAPSHOTSESSION_H + +#include "cart.h" +#include "jksql.h" + +/* All snapshot session names start with this marker: hidden from the session list by default and + * eligible for TTL reaping. A single leading '_' is reserved for real, user-visible auto-named + * quick shares; the double '__' means "machine-made, not a normal loadable session". */ +#define snapshotNamePrefix "__" + +/* Reserved userName for logged-out (anonymous) snapshots, matching doSaveSessionJson's convention + * and the /s/l/ short link. */ +#define snapshotAnonUser "l" + +/* Default reaper TTL: an anonymous snapshot not opened within this many days is garbage-collected. + * 4 years ~ the length of a typical PhD, so a link in a thesis keeps working for its author's degree. + * Override with the hg.conf setting "snapshot.ttlDays". */ +#define snapshotDefaultTtlDays (4 * 365) + +struct snapshotType +/* A registered kind of shareable view snapshot: the cart variables a given feature needs to + * reconstruct one of its views. Register one per feature and keep the list minimal. */ + { + char *name; /* type key sent by the client, e.g. "blat" */ + char **vars; /* NULL-terminated cart variable names to persist (besides "db") */ + }; + +struct snapshotType *snapshotTypeFind(char *name); +/* Return the registered snapshot type, or NULL if name is not a known type. */ + +boolean snapshotIsSnapshotName(char *sessionName); +/* Return TRUE if sessionName is a snapshot name (starts with the "__" prefix). */ + +char *snapshotNewName(struct sqlConnection *conn, char *encUserName); +/* Alloc and return a fresh "__"-prefixed snapshot name, server-generated and checked against + * namedSessionDb so it is guaranteed unique for encUserName (share tokens must never collide and + * overwrite each other). The token is long (128 bits) and URL-safe, so it needs no CGI-encoding. */ + +int saveSnapshotSession(struct sqlConnection *conn, char *snapshotTypeName, + char *encUserName, char *encSessionName, struct cart *cart); +/* Save a minimal shared-by-link session named encSessionName (which must already start with "__") + * under encUserName, holding only the variables declared by snapshotTypeName (plus "db"), and moving + * just those variables' trash files into durable sessionData storage when it is configured. + * Overwrites any existing row of that name, preserving its firstUse/useCount. errAborts on an + * unknown type or a name lacking the "__" prefix. Returns the (post-increment) useCount. */ + +/* Note: no explicit "touch lastUse" is needed - cartLoadUserSession() already bumps lastUse (via + * sessionTouchLastUse) on every session open, so a link stays alive as long as it is used. */ + +int snapshotReapAnon(struct sqlConnection *conn, int ttlDays, boolean dryRun); +/* Delete anonymous ("l") snapshot rows whose lastUse is older than ttlDays, and remove their durable + * sessionData directories. Never touches non-anonymous or non-snapshot rows. Returns the count + * reaped (or that would be reaped, when dryRun). */ + +#endif /* SNAPSHOTSESSION_H */