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) 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 // 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);