404d5bb6d8c0418d5f06535ef470e36c35d2a237
chmalee
  Thu Apr 16 15:57:56 2026 -0700
Add assembly hub support to hubSpace.

Users can upload a .2bit to create an assembly hub, optionally alongside
their own *.hub.txt (prefix names like araTha1.hub.txt are recognized)
and sibling track files. Uploads run in parallel; hub.txt mutations are
serialized per-hub via flock so arrival order does not matter.

- hubSpace table gains a hubType column ('trackHub' or 'assemblyHub');
ON DUPLICATE KEY UPDATE excludes it so a re-upload cannot revert an
upgraded hub.
- writeHubText can now emit an assembly stanza derived from the 2bit;
upgradeHubTxtForAssembly promotes an existing plain hub.txt in place
when a 2bit arrives after tracks.
- pre-finish decides synthesize vs upgrade vs leave-alone from server
state (existing rows, hub.txt on disk) plus a single client flag
(batchHasHubTxt); client-supplied hubType is no longer trusted.
- Client UI adds 2bit as a file type, locks the genome field when the
hub is authoritative (drilled-in or batch hub.txt), defaults new
uploads to an existing assembly hub at top level, and routes
hgTracks URLs through 'genome=' vs 'db=' by hubType.
- Fix pre-existing nested-path bug in hubPathFromParentDir
(*firstSlash = 0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

diff --git src/hg/lib/trackHub.c src/hg/lib/trackHub.c
index 6739172d149..8d6825c0e25 100644
--- src/hg/lib/trackHub.c
+++ src/hg/lib/trackHub.c
@@ -58,30 +58,49 @@
 #include "hgConfig.h"
 #include "cartTrackDb.h"
 
 #ifdef USE_HAL
 #include "halBlockViz.h"
 #endif
 
 struct grp *trackHubGrps = NULL;   // global with grps loaded from track hubs
 static struct hash *hubCladeHash;  // mapping of clade name to hub pointer
 static struct hash *hubAssemblyHash; // mapping of assembly name to genome struct
 static struct hash *hubAssemblyUndecoratedHash; // mapping of undecorated assembly name to genome struct
 static struct hash *hubOrgHash;   // mapping from organism name to hub pointer
 static struct trackHub *globalAssemblyHubList; // list of trackHubs in the user's cart
 static struct hash *trackHubHash;
 
+static boolean isValidSeqNameChar(char c)
+/* Return TRUE if c is a valid character for a sequence name: [A-Za-z0-9._-]. */
+{
+return isalnum((unsigned char)c) || c == '.' || c == '_' || c == '-';
+}
+
+boolean trackHubIsValidSeqName(char *name)
+/* Return TRUE if name is a valid sequence name: non-empty, starts with a
+ * letter or digit, and contains only [A-Za-z0-9._-]. */
+{
+if (!name || !name[0]) return FALSE;
+if (!isalnum((unsigned char)name[0])) return FALSE;
+char *p;
+for (p = name; *p; p++)
+    if (!isValidSeqNameChar(*p))
+        return FALSE;
+return TRUE;
+}
+
 static void tdbListAddHubToGroup(char *hubName, struct trackDb *tdbList)
 /* Prepend hub name to  group name for every tdb. */
 {
 struct trackDb *tdb;
 for (tdb = tdbList; tdb != NULL; tdb = tdb->next)
     {
     char buffer[4096];
 
     char *grp = trackDbSetting(tdb, "group");
     if (grp == NULL)
         tdb->grp = cloneString(hubName);
     else
         {
         safef(buffer, sizeof buffer, "%s_%s", hubName, grp);
         tdb->grp = cloneString(buffer);