373b5c4187422a9584814dcfa324028fd2e50f14
chmalee
  Tue Aug 4 15:00:30 2026 -0700
Have the pre-finish hook return the rows it wrote, and fix the table display bugs that the real row data now lets us resolve, refs #37999

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

diff --git src/hg/lib/userdata.c src/hg/lib/userdata.c
index 73e5a1a1639..8f158a0d918 100644
--- src/hg/lib/userdata.c
+++ src/hg/lib/userdata.c
@@ -13,30 +13,31 @@
 #include "trashDir.h"
 #include "md5.h"
 #include "hgConfig.h"
 #include "dystring.h"
 #include "cheapcgi.h"
 #include "customFactory.h"
 #include "wikiLink.h"
 #include "userdata.h"
 #include "jksql.h"
 #include "hdb.h"
 #include "hubSpace.h"
 #include "hubSpaceQuotas.h"
 #include "errCatch.h"
 #include "twoBit.h"
 #include "trackHub.h"
+#include "jsonWrite.h"
 #include <limits.h>
 
 // how long a pre-finish hook waits for another upload to the same hub, and how
 // often it retries while waiting. A big batch into one hub queues on this lock,
 // so hg.conf can raise the wait without a rebuild
 #define HUB_LOCK_TIMEOUT_DEFAULT "300"
 #define HUB_LOCK_POLL_MS 100
 
 char *emailForUserName(char *userName)
 /* Fetch the email for this user from gbMembers hgcentral table */
 {
 struct sqlConnection *sc = hConnectCentral();
 struct dyString *query = sqlDyStringCreate("select email from gbMembers where userName = '%s'", userName);
 char *email = sqlQuickString(sc, dyStringCannibalize(&query));
 hDisconnectCentral(&sc);
@@ -874,40 +875,105 @@
 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
 }
 
+static struct dyString *hubSpaceQueryForUser(char *userName)
+/* Return a select of every hubSpace column the My Data table needs, restricted to
+ * one user. Callers narrow it further with sqlDyStringPrintf.
+ * Both times come back as seconds since the epoch so the browser can show them in
+ * the reader's own timezone. They need different conversions to get there:
+ * creationTime is mysql's CURRENT_TIMESTAMP, so UNIX_TIMESTAMP reads it correctly,
+ * while every writer of lastModified stores a GMT clock reading, which TIMESTAMPDIFF
+ * measures without applying the session timezone a second time */
+{
+return sqlDyStringCreate("select userName, fileName, fileSize, fileType, "
+    "UNIX_TIMESTAMP(creationTime) as creationTime, "
+    "TIMESTAMPDIFF(SECOND, '1970-01-01 00:00:00', lastModified) as lastModified, "
+    "db, location, md5sum, parentDir, hubType from hubSpace where userName='%s'", userName);
+}
+
 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 dyString *query = hubSpaceQueryForUser(userName);
+sqlDyStringPrintf(query, " order by location,creationTime");
 struct hubSpace *fileList = hubSpaceLoadByQuery(conn, dyStringCannibalize(&query));
 hDisconnectCentral(&conn);
 return fileList;
 }
 
+struct hubSpace *listFilesInHubDir(char *userName, char *hubName)
+/* Return the user's rows for one hub: the hub's own directory row plus every row
+ * underneath it */
+{
+if (isEmpty(hubName))
+    return NULL;
+struct hubSpace *hubList = NULL, *file, *next;
+struct dyString *prefix = dyStringCreate("%s/", hubName);
+// select the rows in C on the path stripDataDir gives, rather than on location in
+// the query. A row's location can carry either the hg.conf tusdDataDir or the
+// tusdMountPoint prefix, and stripDataDir is what knows about both
+for (file = listFilesForUser(userName); file != NULL; file = next)
+    {
+    next = file->next;
+    char *path = stripDataDir(file->location, userName);
+    if (sameOk(path, hubName) || (path && startsWith(dyStringContents(prefix), path)))
+        {
+        file->next = NULL;
+        slAddHead(&hubList, file);
+        }
+    }
+slReverse(&hubList);
+dyStringFree(&prefix);
+return hubList;
+}
+
+void hubSpaceWriteFileList(struct jsonWrite *jw, char *userName, struct hubSpace *fileList)
+/* Write fileList as the "fileList" array of jw, in the row shape the My Data table reads */
+{
+jsonWriteListStart(jw, "fileList");
+struct hubSpace *file;
+for (file = fileList; file != NULL; file = file->next)
+    {
+    jsonWriteObjectStart(jw, NULL);
+    jsonWriteString(jw, "fileName", file->fileName);
+    jsonWriteNumber(jw, "fileSize", file->fileSize);
+    jsonWriteString(jw, "fileType", file->fileType);
+    jsonWriteString(jw, "parentDir", file->parentDir);
+    jsonWriteString(jw, "genome", file->db);
+    jsonWriteNumber(jw, "lastModified", file->lastModified ? sqlLongLong(file->lastModified) : 0);
+    jsonWriteNumber(jw, "uploadTime", file->creationTime ? sqlLongLong(file->creationTime) : 0);
+    jsonWriteString(jw, "fullPath", stripDataDir(file->location, userName));
+    jsonWriteString(jw, "md5sum", file->md5sum);
+    jsonWriteString(jw, "hubType", file->hubType ? file->hubType : "trackHub");
+    jsonWriteObjectEnd(jw);
+    }
+jsonWriteListEnd(jw);
+}
+
 #define defaultHubName "defaultHub"
 char *defaultHubNameForUser(char *userName)
 /* Return a name to use as a default for a hub, starts with defaultHub, then defaultHub2, ... */
 {
 if (!userName)
     return defaultHubName;
 struct dyString *query = sqlDyStringCreate("select distinct(fileName) from hubSpace where parentDir='' and fileName like '%s%%' and userName='%s'", defaultHubName, userName);
 struct sqlConnection *conn = hConnectCentral();
 struct slName *hubNames = sqlQuickList(conn, dyStringCannibalize(&query));;
 hDisconnectCentral(&conn);
 if (hubNames == NULL)
     // user has no hubs created
     return defaultHubName;
 slSort(&hubNames,slNameCmpStringsWithEmbeddedNumbers);
 slReverse(&hubNames);