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/hgHubConnect/trackHubWizard.c src/hg/hgHubConnect/trackHubWizard.c index 9cc153d4c85..546d15e9fc0 100644 --- src/hg/hgHubConnect/trackHubWizard.c +++ src/hg/hgHubConnect/trackHubWizard.c @@ -1,264 +1,265 @@ /* trackHubWizard -- a user interface for creating a track hubs configuration files */ /* Copyright (C) 2019 The Regents of the University of California * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ #include "common.h" #include "cart.h" #include "cheapcgi.h" #include "hdb.h" #include "hgConfig.h" #include "md5.h" #include "trashDir.h" #include "hgHubConnect.h" #include "jsHelper.h" #include "web.h" #include "wikiLink.h" #include "customTrack.h" #include "userdata.h" #include "jsonWrite.h" #include "cartJson.h" #include "hubSpace.h" #include "hubSpaceKeys.h" #include "hubConnect.h" #include "trackHub.h" #include "htmshell.h" #include <limits.h> #include "errCatch.h" void removeOneFile(char *userName, char *cgiFileName, char *fullPath, char *db, char *fileType) /* Remove one single file for userName */ { // prefixUserFile returns a canonicalized path, or NULL if the // canonicalized path does not begin with the hg.conf specified userDataDir // TODO: make the debug information from stderr go to stdout so the user // can know there is a mistake somewhere, and only print the debug // information in the event that the filename actually begins with the // userDataDir so we don't tell hackers what files do and do not exist char *fileName = prefixUserFile(userName, fullPath, NULL); if (fileName) { if (fileExists(fileName)) { fprintf(stderr, "deleting file: '%s'\n", fileName); removeFileForUser(fileName, userName); fflush(stderr); } else { fprintf(stderr, "file '%s' does not exist\n", fileName); fflush(stderr); } } } int pathDepth(char *path) { // replace multiple occurences of '/' with just a single one to get a canonical path // as path///to/file and path/to/file are the same path on Linux char *deduped = replaceChars(path, "//", "/"); return countChars(deduped, '/'); } int sortByFullPathCmp(const void *va, const void *vb) /* Compare two fullPaths */ { struct jsonElement *a = (struct jsonElement *)(*(struct slRef **)va)->val; struct jsonElement *b = (struct jsonElement *)(*(struct slRef **)vb)->val; char *aFullpath = jsonStringField(a, "fullPath"); char *bFullpath = jsonStringField(b, "fullPath"); int aDepth = pathDepth(aFullpath); int bDepth = pathDepth(bFullpath); // ensure subdirectories order before their parents: if (aDepth != bDepth) return bDepth - aDepth; // if equal depth than lexicographic sort is fine return strcmp(jsonStringField(a,"fullPath"), jsonStringField(b, "fullPath")); } void sortByFullPath(struct jsonElement *listJson) { slSort(&(listJson->val.jeList), sortByFullPathCmp); } void doRemoveFile(struct cartJson *cj, struct hash *paramHash) /* Process the request to remove a file */ { char *userName = getUserName(); if (userName) { // our array of objects, each object represents a track file struct jsonElement *deleteJson = hashFindVal(paramHash, "fileList"); struct slRef *copy, *f, *fileList = deleteJson->val.jeList; struct jsonElement *dirListJsonEle = newJsonList(NULL); jsonWriteListStart(cj->jw, "deletedList"); for (f = fileList; f != NULL; ) { struct jsonElement *fileObj = (struct jsonElement *)f->val; char *fileName = jsonStringField(fileObj, "fileName"); char *fileType = jsonStringField(fileObj, "fileType"); char *db = jsonStringField(fileObj, "genome"); char *fullPath = jsonStringField(fileObj, "fullPath"); copy = f->next; if (sameString(fileType, "dir")) { f->next = NULL; jsonListAdd(dirListJsonEle, fileObj); } else { if (sameString(fileType, "hub.txt")) { // disconnect this hub from the cart if it exists char *hubUrl = urlForFile(userName, fullPath); char *hubId = hubNameFromUrl(hubUrl); if (hubId) { /* remove the cart variable */ hubId += 4; // skip past the hub_ part char buffer[1024]; safef(buffer, sizeof buffer, "hgHubConnect.hub.%s", hubId); cartRemove(cj->cart, buffer); } } removeOneFile(userName, fileName, fullPath, db, fileType); // write out the fullPath so the DataTable can remove the correct row: jsonWriteString(cj->jw, NULL, fullPath); } f = copy; } // now attempt to delete any requested directories, but don't die if they still have contents sortByFullPath(dirListJsonEle); struct slRef *dir = NULL; for (dir = dirListJsonEle->val.jeList; dir != NULL; dir = dir->next) { struct jsonElement *fileObj = (struct jsonElement *)dir->val; char *fileName = jsonStringField(fileObj, "fileName"); char *fileType = jsonStringField(fileObj, "fileType"); char *db = jsonStringField(fileObj, "genome"); char *fullPath = jsonStringField(fileObj, "fullPath"); removeOneFile(userName, fileName, fullPath, db, fileType); // write out the fullPath so the DataTable can remove the correct row: jsonWriteString(cj->jw, NULL, fullPath); } jsonWriteListEnd(cj->jw); } } void doMoveFile(struct cartJson *cj, struct hash *paramHash) /* Move a file to a new hub */ { } static void outUiDataForUser(struct jsonWrite *jw) /* List out the currently stored files for the user as well as other data * needed to create the hubSpace table */ { char *userName = getUserName(); jsonWriteObjectStart(jw, "userFiles"); if (userName) { // the url for this user: jsonWriteString(jw, "userUrl", webDataDir(userName)); jsonWriteListStart(jw, "fileList"); struct hubSpace *file, *fileList = listFilesForUser(userName); 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); jsonWriteString(jw, "lastModified", file->lastModified); jsonWriteString(jw, "uploadTime", file->creationTime); jsonWriteString(jw, "fullPath", stripDataDir(file->location, userName)); jsonWriteString(jw, "md5sum", file->md5sum); + jsonWriteString(jw, "hubType", file->hubType ? file->hubType : "trackHub"); jsonWriteObjectEnd(jw); } jsonWriteListEnd(jw); } jsonWriteBoolean(jw, "isLoggedIn", getUserName() ? TRUE : FALSE); jsonWriteString(jw, "hubNameDefault", defaultHubNameForUser(getUserName())); // if the user is not logged, the 0 for the quota is ignored jsonWriteNumber(jw, "userQuota", getUserName() ? checkUserQuota(getUserName()) : 0); jsonWriteNumber(jw, "maxQuota", getUserName() ? getMaxUserQuota(getUserName()) : HUB_SPACE_DEFAULT_QUOTA); jsonWriteObjectEnd(jw); } void getHubSpaceUIState(struct cartJson *cj, struct hash *paramHash) /* Get all the data we need to make a users hubSpace UI table. The cartJson library * deals with printing the json */ { outUiDataForUser(cj->jw); } void cjRevokeApiKey(struct cartJson *cj, struct hash *paramHash) /* Wrapper for cartJson to call lib function revokeApiKey, removes any api keys for the user */ { struct errCatch *errCatch = errCatchNew(); if (errCatchStart(errCatch)) { char *userName = getUserName(); hubSpaceRevokeApiKey(userName); } errCatchEnd(errCatch); if (!(errCatch->gotError)) jsonWriteString(cj->jw, "revoke", "true"); else jsonWriteStringf(cj->jw, "error", "revokeApiKey() error: '%s'", errCatch->message->string); errCatchFree(&errCatch); } void cjGenerateApiKey(struct cartJson *cj, struct hash *paramHash) /* Wrapper for cartJson to call lib function generateApiKey, makes a random (but not crypto-secure api key for use of hubtools to upload to hubspace, or for skipping cloudflare */ { struct errCatch *errCatch = errCatchNew(); char *apiKey = NULL; if (errCatchStart(errCatch)) { char *userName = getUserName(); apiKey = hubSpaceGenerateApiKey(userName); } errCatchEnd(errCatch); if (apiKey) jsonWriteString(cj->jw, "apiKey", apiKey); else if (errCatch->gotError) jsonWriteStringf(cj->jw, "error", "generateApiKey() error: '%s'", errCatch->message->string); errCatchFree(&errCatch); } void doTrackHubWizard(char *database) /* Offer an upload form so users can upload all their hub files */ { jsIncludeFile("utils.js", NULL); jsIncludeFile("ajax.js", NULL); jsIncludeFile("lodash.3.10.0.compat.min.js", NULL); jsIncludeFile("cart.js", NULL); jsIncludeFile("autocompleteCat.js",NULL); webIncludeResourceFile("font-awesome.min.css"); webIncludeResourceFile("dataTables-2.2.2.min.css"); jsIncludeFile("dataTables-2.2.2.min.js", NULL); webIncludeResourceFile("dataTables.buttons-3.2.2.min.css"); jsIncludeFile("dataTables.buttons-3.2.2.min.js", NULL); webIncludeResourceFile("dataTables.select-3.0.0.min.css"); jsIncludeFile("dataTables.select-3.0.0.min.js", NULL); puts("<link href=\"https://releases.transloadit.com/uppy/v4.5.0/uppy.min.css\" rel=\"stylesheet\">"); puts("<script type=\"text/javascript\" src=\"https://releases.transloadit.com/uppy/v4.5.0/uppy.min.js\"></script>"); jsIncludeFile("hgMyData.js", NULL); // the skeleton HTML: webIncludeFile("inc/hgMyData.html"); webIncludeResourceFile("hgMyData.css"); jsInlineF("\nvar isLoggedIn = %s;\n", getUserName() ? "true" : "false"); jsInlineF("\nvar cartDb=\"%s %s\";\n", trackHubSkipHubName(hGenome(database)), database); jsInlineF("\nvar tusdEndpoint=\"%s\";\n", cfgOptionDefault("hubSpaceTusdEndpoint", NULL)); jsInlineF("\nvar fileListEndpoint=\"%shgHubConnect\";\n", hLoginHostCgiBinUrl()); jsInlineF("\nvar loginHost=\"http%s://%s\";\n", loginUseHttps() ? "s" : "", wikiLinkHost()); jsInline("$(document).ready(function() {\nhubCreate.init();\n})"); puts("</div>"); }