c3788110e8d865774fff8197cf5b47350472ba64 chmalee Tue Aug 4 11:26:55 2026 -0700 Fix hubSpace hook error handling and nested hub path handling, refs #37964 Co-Authored-By: Claude Opus 5 (1M context) diff --git src/hg/lib/userdata.c src/hg/lib/userdata.c index 2cb2abf7f61..73e5a1a1639 100644 --- src/hg/lib/userdata.c +++ src/hg/lib/userdata.c @@ -15,30 +15,36 @@ #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 +// 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); // this should be freeMem'd: return email; } char *getEncodedUserNamePath(char *userName) /* Compute the path for just the userName part of the users upload */ { struct dyString *ret = dyStringNew(0); @@ -374,42 +380,84 @@ { char *firstName = tbf->indexList->name; int size = twoBitSeqSize(tbf, firstName); int end = (size < 1000) ? size : 1000; struct dyString *ds = dyStringCreate("%s:1-%d", firstName, end); result = dyStringCannibalize(&ds); } if (tbf) twoBitClose(&tbf); } errCatchEnd(errCatch); errCatchFree(&errCatch); return result; } -static char *hubPathFromParentDir(char *parentDir, char *userDataDir) -/* Assume parentDir does not have leading '/' or '.', parse out the first dir component - * and add it to the users directory*/ +char *hubRootFromParentDir(char *parentDir) +/* Return the first '/' separated component of parentDir, which is the hub itself. + * The hub.txt and the hubSpace dir row for a hub both live at that level, while + * hubNameFromPath gives the immediately containing directory, which for a nested + * parentDir like 'myHub/hg38' is a subdirectory of the hub */ { char *copy = cloneString(parentDir); char *firstSlash = strchr(copy, '/'); -if (!firstSlash) - { +if (firstSlash) + *firstSlash = 0; return copy; } -*firstSlash = 0; -return catTwoStrings(userDataDir, copy); + +char *hubPathFromParentDir(char *parentDir, char *userDataDir) +/* Return the directory holding this hub's hub.txt, that is the user's directory + * plus the hub component of parentDir */ +{ +char *hubRoot = hubRootFromParentDir(parentDir); +char *hubPath = catTwoStrings(userDataDir, hubRoot); +freeMem(hubRoot); +return hubPath; +} + +static char *hubSubPathFromParentDir(char *parentDir) +/* Return the part of parentDir below the hub component, keeping its trailing '/', + * or "" when the file sits directly in the hub. This is the prefix a bigDataUrl + * needs, since hub.txt is at the hub and the file may be in a subdirectory */ +{ +char *firstSlash = strchr(parentDir, '/'); +if (firstSlash && firstSlash[1] != '\0') + return cloneString(firstSlash + 1); +return cloneString(""); +} + +static char *pathRelativeToHubDir(char *filePath, char *hubDir) +/* Return the part of filePath below hubDir. twoBitPath and bigDataUrl are relative + * to the hub.txt, so a file in a subdirectory needs that subdirectory in its setting. + * Fall back to the file name if filePath is not under hubDir */ +{ +// the '/' test keeps a hub from matching a sibling whose name it is a prefix of +if (startsWith(hubDir, filePath) && filePath[strlen(hubDir)] == '/') + { + char *rel = filePath + strlen(hubDir); + while (*rel == '/') + rel++; + if (*rel != '\0') + return cloneString(rel); + } +// only reachable if the file is not under the hub, which means the two paths +// disagree about symlinks. The setting we fall back to will not resolve +fprintf(stderr, "warning: '%s' is not under hub dir '%s', hub.txt setting " + "will use the file name alone\n", filePath, hubDir); +char *lastSlash = strrchr(filePath, '/'); +return cloneString(lastSlash ? lastSlash + 1 : filePath); } static void upgradeHubTxtForAssembly(char *hubFile, char *db, char *twoBitFileName) /* If hubFile exists but lacks a twoBitPath line, rewrite it to insert an * assembly-hub stanza (twoBitPath + stub organism/scientificName/description/ * defaultPos) immediately after the 'genome' line, and replace that line's * db value with the 2bit's assembly name. Called when a 2bit arrives after * a plain track-hub hub.txt has already been synthesized for this hub. * No-op if hubFile doesn't exist or already has twoBitPath. */ { if (!fileExists(hubFile)) return; // Collect all lines, matching directives against skipLeadingSpaces so that // indented (tab/space) stanzas are handled the same as column-0 ones. @@ -430,244 +478,281 @@ return; } if (genomeIdx < 0 && startsWith("genome ", trimmed)) genomeIdx = i; slAddHead(&lines, slNameNew(line)); i++; } lineFileClose(&lf); slReverse(&lines); if (genomeIdx < 0) { slNameFreeList(&lines); return; } -char *twoBitBase = strrchr(twoBitFileName, '/'); -twoBitBase = twoBitBase ? twoBitBase + 1 : twoBitFileName; +char *hubDir = cloneString(hubFile); +char *lastSlash = strrchr(hubDir, '/'); +if (lastSlash) + *lastSlash = 0; +else + { + freeMem(hubDir); + hubDir = cloneString("."); + } +char *twoBitBase = pathRelativeToHubDir(twoBitFileName, hubDir); char *defaultPos = defaultPosFromTwoBit(twoBitFileName); struct dyString *out = dyStringNew(1024); struct slName *ln; for (ln = lines, i = 0; ln; ln = ln->next, i++) { if (i == genomeIdx) { dyStringPrintf(out, "genome %s\n" "twoBitPath %s\n" "organism %s\n" "scientificName %s\n" "description %s\n" "defaultPos %s\n", db, twoBitBase, db, db, db, defaultPos ? defaultPos : "chr1:1-1000"); } else { dyStringAppend(out, ln->name); dyStringAppendC(out, '\n'); } } // Write to a sibling temp file and rename into place so a partial write // (ENOSPC, SIGKILL, etc.) cannot leave the user's hub.txt truncated. -char *hubDir = cloneString(hubFile); -char *lastSlash = strrchr(hubDir, '/'); -if (lastSlash) - *lastSlash = 0; -else - strcpy(hubDir, "."); char *tmpFile = cloneString(rTempName(hubDir, "hub", ".txt")); FILE *f = mustOpen(tmpFile, "w"); mustWrite(f, out->string, out->stringSize); carefulClose(&f); mustRename(tmpFile, hubFile); freeMem(tmpFile); freeMem(hubDir); freez(&defaultPos); dyStringFree(&out); slNameFreeList(&lines); } +static void refreshHubTextRow(char *userName, char *hubFile, char *hubName) +/* Bring the hubSpace row for a hub.txt back in line with the file on disk. Called + * after rewriting a hub.txt that already has a row, so My Data and the quota do not + * keep reporting the size and checksum from before the rewrite */ +{ +if (!fileExists(hubFile)) + return; +time_t modTime = fileModTime(hubFile); +struct sqlConnection *conn = hConnectCentral(); +struct dyString *q = sqlDyStringCreate( + "update hubSpace set fileSize=%lld, md5sum='%s', lastModified='%s' " + "where userName='%s' and fileName='hub.txt' and parentDir='%s'", + (long long)fileSize(hubFile), md5HexForFile(hubFile), + sqlUnixTimeToDate(&modTime, TRUE), userName, hubName); +sqlUpdate(conn, dyStringCannibalize(&q)); +hDisconnectCentral(&conn); +} + static void setAssemblyHubTypeForDir(char *userName, char *parentDir) /* Flip this user's hub (dir row + direct-child files) to hubType='assemblyHub'. * Does not recurse into nested parentDirs like hubName/tracks; only the * hubtools-then-UI promotion flow can produce those. */ { if (!userName || !parentDir || parentDir[0] == '\0') return; struct sqlConnection *conn = hConnectCentral(); struct dyString *q = sqlDyStringCreate( "update hubSpace set hubType='assemblyHub' " "where userName='%s' and (parentDir='%s' or (fileName='%s' and parentDir=''))", userName, parentDir, parentDir); sqlUpdate(conn, dyStringCannibalize(&q)); hDisconnectCentral(&conn); } int lockHubDir(char *hubDir) /* Acquire an exclusive flock on hubDir/.hub.lock, creating the lock file * if necessary. Returns a file descriptor; pass to unlockHubDir to release. * Serializes hub.txt read-modify-write across parallel pre-finish processes. */ { struct dyString *lockPath = dyStringCreate("%s%s.hub.lock", hubDir, endsWith(hubDir, "/") ? "" : "/"); -int fd = open(dyStringContents(lockPath), O_RDWR | O_CREAT, 0666); +// O_CLOEXEC so the md5sum children we fork under this lock do not inherit it. +// An inherited fd outliving a killed hook would hold the lock with no owner left +// to release it, and every later upload to the hub would time out +int fd = open(dyStringContents(lockPath), O_RDWR | O_CREAT | O_CLOEXEC, 0666); if (fd < 0) errnoAbort("could not open hub lock %s", dyStringContents(lockPath)); -if (flock(fd, LOCK_EX) < 0) +// poll rather than block, so one stuck upload cannot hold up every other upload +// to this hub indefinitely +// clamp so a mistyped hg.conf value cannot turn into a negative wait +int timeoutSeconds = atoi(cfgOptionDefault("hubSpaceLockTimeout", HUB_LOCK_TIMEOUT_DEFAULT)); +if (timeoutSeconds < 1 || timeoutSeconds > 3600) + timeoutSeconds = atoi(HUB_LOCK_TIMEOUT_DEFAULT); +int timeoutMs = timeoutSeconds * 1000; +int waitedMs = 0; +while (flock(fd, LOCK_EX | LOCK_NB) < 0) + { + if (errno != EWOULDBLOCK) errnoAbort("could not acquire hub lock on %s", dyStringContents(lockPath)); + if (waitedMs >= timeoutMs) + errAbort("Timed out waiting for another upload to this hub to finish. " + "Please try again."); + sleep1000(HUB_LOCK_POLL_MS); + waitedMs += HUB_LOCK_POLL_MS; + } dyStringFree(&lockPath); return fd; } void unlockHubDir(int fd) /* Release an exclusive hub lock acquired by lockHubDir. Closing the fd * releases the flock automatically on Linux. */ { if (fd >= 0) close(fd); } -boolean literalHubTxtExistsOnDisk(char *parentDir, char *userDataDir) -/* Return TRUE if path/hub.txt is a real file on disk. Used by pre-finish to - * decide between synthesize-fresh vs upgrade-in-place. */ -{ -if (!parentDir || !parentDir[0]) return FALSE; -char *hubDir = hubPathFromParentDir(parentDir, userDataDir); -struct dyString *hubFileDy = dyStringCreate("%s%shub.txt", - hubDir, endsWith(hubDir, "/") ? "" : "/"); -char *hubFile = dyStringCannibalize(&hubFileDy); -boolean exists = fileExists(hubFile); -freeMem(hubFile); -return exists; -} - void upgradeExistingHubToAssembly(struct hubSpace *rowForFile, char *userDataDir, char *encodedParentDir) /* When a 2bit lands in a hub, add the assembly stanza to hub.txt (if the * backend owns it) and flip every row for this hub to hubType='assemblyHub'. * No-op unless rowForFile is a 2bit. */ { if (!sameOk(rowForFile->fileType, "2bit")) return; char *hubDir = hubPathFromParentDir(rowForFile->parentDir, userDataDir); struct dyString *hubFileDy = dyStringCreate("%s%shub.txt", hubDir, endsWith(hubDir, "/") ? "" : "/"); char *hubFile = dyStringCannibalize(&hubFileDy); upgradeHubTxtForAssembly(hubFile, rowForFile->db, rowForFile->location); -char *hubNameOnly = encodedParentDir ? hubNameFromPath(encodedParentDir) : NULL; +// setAssemblyHubTypeForDir looks up the hub's row at the top level, so give it the +// hub component of parentDir rather than a nested subdirectory +char *hubNameOnly = encodedParentDir ? hubRootFromParentDir(encodedParentDir) : NULL; if (hubNameOnly && hubNameOnly[0]) + { setAssemblyHubTypeForDir(rowForFile->userName, hubNameOnly); + // hub.txt just changed on disk, so the row's size and md5 are out of date + refreshHubTextRow(rowForFile->userName, hubFile, hubNameOnly); + } freeMem(hubFile); } char *writeHubText(char *path, char *userName, char *db, char *twoBitFileName) /* Create a hub.txt file, optionally creating the directory holding it. * If twoBitFileName is non-NULL, write an assembly hub stanza referencing it * (with stub organism / scientificName / description / defaultPos derived from * the 2bit). For convenience, return the file name of the created hub, which * can be freed. */ { +// an empty path would put the hub.txt at the root of the filesystem +if (isEmpty(path)) + errAbort("no directory given for the hub, cannot create hub.txt"); int oldUmask = 00; oldUmask = umask(0); makeDirsOnPath(path); // restore umask umask(oldUmask); // now make the hub.txt with some basic information char *hubFile = NULL; struct dyString *hubFileDy = dyStringCreate("%s%shub.txt", path, endsWith(path, "/") ? "" : "/"); hubFile = dyStringCannibalize(&hubFileDy); if (fileExists(hubFile)) return hubFile; char *hubName = hubNameFromPath(path); FILE *f = mustOpen(hubFile, "w"); fprintf(f, "hub %s\n" "email %s\n" "shortLabel %s\n" "longLabel %s\n" "useOneFile on\n" "\n" "genome %s\n", hubName, emailForUserName(userName), hubName, hubName, db); if (twoBitFileName) { // Assembly hub: write twoBitPath plus stub fields the user can edit later. // The bigDataUrl/twoBitPath is relative to the hub.txt location. - char *twoBitBase = strrchr(twoBitFileName, '/'); - twoBitBase = twoBitBase ? twoBitBase + 1 : twoBitFileName; + char *twoBitBase = pathRelativeToHubDir(twoBitFileName, path); char *defaultPos = defaultPosFromTwoBit(twoBitFileName); fprintf(f, "twoBitPath %s\n" "organism %s\n" "scientificName %s\n" "description %s\n" "defaultPos %s\n", twoBitBase, db, db, db, defaultPos ? defaultPos : "chr1:1-1000"); freez(&defaultPos); } fprintf(f, "\n"); carefulClose(&f); return hubFile; } -static boolean bigDataUrlExistsInHub(char *hubFileName, char *fileName) -/* Check if a bigDataUrl line already references this file in the hub.txt. +static boolean trackExistsInHub(char *hubFileName, char *track, char *bigDataUrl) +/* Check if this track is already in the hub.txt, either by name or by the file it + * points at. Track names have to be unique within a hub, so that is the key that + * matters, but a user's own hub.txt may reference the file under a different name. * Simple line-by-line check - not a full trackDb parser. */ { -if (!hubFileName || !fileName) +if (!hubFileName) return FALSE; struct lineFile *lf = lineFileMayOpen(hubFileName, TRUE); if (!lf) return FALSE; +boolean found = FALSE; char *line; -while (lineFileNext(lf, &line, NULL)) +while (!found && lineFileNext(lf, &line, NULL)) { char *trimmedLine = skipLeadingSpaces(line); - if (startsWith("bigDataUrl ", trimmedLine)) - { - char *url = trimmedLine + 11; // skip "bigDataUrl " - url = skipLeadingSpaces(url); - if (isEmpty(url)) - continue; - // Check if the URL ends with this filename (handles relative paths) - if (endsWith(url, fileName) || sameString(url, fileName)) + if (track && startsWith("track ", trimmedLine)) { - lineFileClose(&lf); - return TRUE; + char *name = trimSpaces(skipLeadingSpaces(trimmedLine + 6)); + found = sameString(name, track); } + else if (bigDataUrl && startsWith("bigDataUrl ", trimmedLine)) + { + char *url = trimSpaces(skipLeadingSpaces(trimmedLine + 11)); + // compare whole paths: a bare 'track.bb' is a different file from + // 'hg38/track.bb', and 'mydata.bb' is not 'data.bb' + if (startsWith("./", url)) + url += 2; + found = sameString(url, bigDataUrl); } } lineFileClose(&lf); -return FALSE; +return found; } static void writeTrackStanza(char *hubFileName, char *track, char *bigDataUrl, char *type, char *label, char *bigFileLocation) { if ( (sameString(type, "bamIndex") || sameString(type, "tabixIndex") || sameString(type, "text")) ) // don't need to make track stanzas for these supporting files return; // Skip if this file is already referenced in hub.txt (e.g., user uploaded their own hub.txt) -if (bigDataUrlExistsInHub(hubFileName, bigDataUrl)) +if (trackExistsInHub(hubFileName, track, bigDataUrl)) return; FILE *f = mustOpen(hubFileName, "a"); // Always add a leading newline to ensure separation from previous content fprintf(f, "\n"); char *trackDbType = type; if (sameString(type, "bigBed")) { // don't errAbort if the file is actually not a bigBed struct errCatch *errCatch = errCatchNew(); if (errCatchStart(errCatch)) { // figure out the type based on the bbiFile header struct bbiFile *bbi = bigBedFileOpen(bigFileLocation); char tdbType[32]; @@ -679,60 +764,65 @@ errCatchFree(&errCatch); // NOTE: if the file was not actually a bigBed (and so bigBedFileOpen errAborted), we // just want to prevent the errAbort, not prevent creating the stanza itself, as that // would be majorly confusing to the user, so just continue on here } fprintf(f, "track %s\n" "bigDataUrl %s\n" "type %s\n" "shortLabel %s\n" "longLabel %s\n" "\n", track, bigDataUrl, trackDbType, label, label); carefulClose(&f); } -static char *writeHubStanzasForFile(struct hubSpace *rowForFile, char *userDataDir, char *parentDir) +static char *writeHubStanzasForFile(struct hubSpace *rowForFile, char *userDataDir) /* Create a hub.txt (if necessary) and add track stanzas for the file described by rowForFile. * If the file is a 2bit, write the assembly-hub genome stanza instead of a track stanza. * Returns the path to the hub.txt */ { char *hubFileName = NULL; char *hubDir = hubPathFromParentDir(rowForFile->parentDir, userDataDir); boolean isAssemblyHub = sameOk(rowForFile->fileType, "2bit"); char *twoBitForHubText = isAssemblyHub ? rowForFile->location : NULL; hubFileName = writeHubText(hubDir, rowForFile->userName, rowForFile->db, twoBitForHubText); if (!isAssemblyHub) { // NOTE: even though rowForFile->fileName was already cgiEncoded by the pre-finish hook, // we still must cgiEncode again to make the bigDataUrl setting work, as apache needs // to look for a literal '%' in a filename if there was a character encoded. For example, // if the filename from tus was &.bb, tus encodes this to "\u0026.bb", which we write to // disk as %5Cu0026.bb, and apache needs to find at: // https://url/hash/userName/%25Cu0026.bb in order to work in hgTracks - writeTrackStanza(hubFileName, rowForFile->fileName, cgiEncodeFull(rowForFile->fileName), rowForFile->fileType, rowForFile->fileName, rowForFile->location); + // The subdirectory part needs no such encoding: isValidParentDir limits those + // components to letters, digits, '.' and '_'. + char *subPath = hubSubPathFromParentDir(rowForFile->parentDir); + struct dyString *bigDataUrl = dyStringCreate("%s%s", subPath, cgiEncodeFull(rowForFile->fileName)); + writeTrackStanza(hubFileName, rowForFile->fileName, dyStringCannibalize(&bigDataUrl), rowForFile->fileType, rowForFile->fileName, rowForFile->location); + freeMem(subPath); } return hubFileName; } -void createNewTempHubForUpload(char *requestId, struct hubSpace *rowForFile, char *userDataDir, char *parentDir) +void createNewTempHubForUpload(char *requestId, struct hubSpace *rowForFile, char *userDataDir) /* Creates a hub.txt for this upload, and updates the hubSpace table for the * hub.txt and any parentDirs we need to create. */ { // first create the hub.txt if necessary and write the stanza for this track -char *hubPath = writeHubStanzasForFile(rowForFile, userDataDir, parentDir); +char *hubPath = writeHubStanzasForFile(rowForFile, userDataDir); // update the mysql table with a record of the hub.txt: struct hubSpace *hubTextRow = NULL; AllocVar(hubTextRow); hubTextRow->userName = rowForFile->userName; hubTextRow->fileName = "hub.txt"; hubTextRow->fileSize = fileSize(hubPath); hubTextRow->fileType = "hub.txt"; hubTextRow->creationTime = NULL; time_t lastModTime = fileModTime(hubPath); hubTextRow->lastModified = sqlUnixTimeToDate(&lastModTime, TRUE); hubTextRow->db = rowForFile->db; hubTextRow->location = hubPath; hubTextRow->md5sum = md5HexForFile(hubPath); hubTextRow->parentDir = hubNameFromPath(hubPath);