2c8a873e7286d797e88549d5a2825f0ea6fe3f04
chmalee
  Fri May 15 15:07:52 2026 -0700
HubSpace now allows tracks hubs to be defined via hub.txt+genomes.txt+trackDb.txt files, refs Baihe email and #37566

diff --git src/hg/lib/userdata.c src/hg/lib/userdata.c
index 9c36f9f7d12..2cb2abf7f61 100644
--- src/hg/lib/userdata.c
+++ src/hg/lib/userdata.c
@@ -336,31 +336,32 @@
     if (sameString(subdir, "."))
         continue;
     if (!subdir)
         errAbort("error: empty subdirectory components for parentDir string '%s'", parentDirStr);
     if (lastChar(dyStringContents(currLocation)) != '/')
         dyStringAppendC(currLocation, '/');
     dyStringAppend(currLocation, subdir);
     struct hubSpace *row = NULL;
     AllocVar(row);
     row->userName = userName;
     row->fileName = subdir;
     row->fileSize = 0;
     row->fileType = "dir";
     row->creationTime = NULL;
     row->lastModified = sqlUnixTimeToDate(&lastModified, TRUE);
-    row->db = db;
+    // Leaf-only db; ancestors sit above per-genome subdirs.
+    row->db = (i == foundSlashes - 1) ? db : "";
     row->location = cloneString(dyStringContents(currLocation));
     row->md5sum = "";
     row->parentDir = i > 0 ? components[i-1] : "";
     row->hubType = hubType ? hubType : "trackHub";
     // only insert a row for this parentDir if it's unique to the table
     if (!checkHubSpaceRowExists(row))
         addHubSpaceRowForFile(row);
     }
 }
 
 static char *defaultPosFromTwoBit(char *twoBitPath)
 /* Open the 2bit, pick the first sequence and return "chrom:1-min(size,1000)".
  * Returns NULL if the 2bit cannot be opened, has no sequences, or the first
  * sequence name would inject content into hub.txt. */
 {
@@ -738,64 +739,72 @@
 hubTextRow->hubType = rowForFile->hubType ? rowForFile->hubType : "trackHub";
 if (!checkHubSpaceRowExists(hubTextRow))
     addHubSpaceRowForFile(hubTextRow);
 }
 
 static void deleteHubSpaceRow(char *fname, char *userName)
 /* Deletes a row from the hubspace table for a given fname */
 {
 struct sqlConnection *conn = hConnectCentral();
 struct dyString *deleteQuery = sqlDyStringCreate("delete from hubSpace where location='%s' and userName='%s'", fname, userName);
 sqlUpdate(conn, dyStringCannibalize(&deleteQuery));
 hDisconnectCentral(&conn);
 }
 
 void removeFileForUser(char *fname, char *userName)
-/* Remove a file for this user if it exists */
+/* Remove a file (or recursively, a directory) for this user if it exists */
 {
 // The file to remove must be prefixed by the hg.conf userDataDir
 char canonicalPath[PATH_MAX];
-realpath(fname, canonicalPath);
+if (realpath(fname, canonicalPath) == NULL)
+    return;
 if (!startsWith(getDataDir(userName), canonicalPath))
     return;
-if (fileExists(canonicalPath))
-    {
-    // If removing a hub directory, clean up the per-hub flock file so
-    // rmdir doesn't fail with ENOTEMPTY. The .hub.lock file is a backend
-    // artifact, never recorded as a hubSpace row.
+if (!fileExists(canonicalPath))
+    return;
+
 if (isDirectory(canonicalPath))
     {
+    // Clean up the per-hub flock file (a backend artifact, not in hubSpace).
     struct dyString *lockPath = dyStringCreate("%s%s.hub.lock",
         canonicalPath, endsWith(canonicalPath, "/") ? "" : "/");
     if (fileExists(dyStringContents(lockPath)))
-            unlink(dyStringContents(lockPath));
+        mustRemove(dyStringContents(lockPath));
     dyStringFree(&lockPath);
+
+    // Recurse into children so rmdir succeeds; listDirX("*") skips
+    // dotfiles, and .hub.lock was already removed above.
+    struct fileInfo *entries = listDirX(canonicalPath, "*", TRUE);
+    struct fileInfo *e;
+    for (e = entries; e != NULL; e = e->next)
+        removeFileForUser(e->name, userName);
+    slFreeList(&entries);
     }
-    // delete the actual file
+
+// delete the file (or now-empty directory)
 mustRemove(canonicalPath);
 
 // delete the table row, which probably has the location based
 // on the other filesystem
 if (checkHubSpaceLocationExists(userName, canonicalPath))
     deleteHubSpaceRow(canonicalPath, userName);
 else
     {
     char *unswapped = unswapDataDir(userName, canonicalPath);
     if (checkHubSpaceLocationExists(userName, unswapped))
         deleteHubSpaceRow(unswapped, userName);
     }
-    }
 // TODO: we should also modify the hub.txt associated with this file
 }
 
 struct hubSpace *listFilesForUser(char *userName)
 /* Return the files the user has uploaded */
 {
 struct sqlConnection *conn = hConnectCentral();
 struct dyString *query = sqlDyStringCreate("select userName, fileName, fileSize, fileType, creationTime, DATE_FORMAT(lastModified, '%%c/%%d/%%Y, %%l:%%i:%%s %%p') as lastModified, db, location, md5sum, parentDir, hubType from hubSpace where userName='%s' order by location,creationTime", userName);
 struct hubSpace *fileList = hubSpaceLoadByQuery(conn, dyStringCannibalize(&query));
 hDisconnectCentral(&conn);
 return fileList;
 }
 
 #define defaultHubName "defaultHub"
 char *defaultHubNameForUser(char *userName)