b53a97d26633a1f174a7031559a69d37e393379f
chmalee
  Fri May 1 14:19:19 2026 -0700
Allow myVariants tracks to be shared similar to google docs, read only and read+edit access by username or for all. Last write wins. Currently no history of changes, leaving that for the next version of the feature

diff --git src/hg/lib/myVariantsShare.c src/hg/lib/myVariantsShare.c
new file mode 100644
index 00000000000..252c589d3ce
--- /dev/null
+++ src/hg/lib/myVariantsShare.c
@@ -0,0 +1,369 @@
+/* myVariantsShare.c was originally generated by the autoSql program, which also 
+ * generated myVariantsShare.h and myVariantsShare.sql.  This module links the database and
+ * the RAM representation of objects. */
+
+#include "common.h"
+#include "linefile.h"
+#include "dystring.h"
+#include "jksql.h"
+#include "myVariantsShare.h"
+#include "htmshell.h"
+#include "jsonParse.h"
+#include "jsonWrite.h"
+#include "errCatch.h"
+
+
+
+char *myVariantsShareCommaSepFieldNames = "id,ownerUser,shareToken,project,db,permission,targetUser,label,createdAt";
+
+void myVariantsShareStaticLoad(char **row, struct myVariantsShare *ret)
+/* Load a row from myVariantsShare table into ret.  The contents of ret will
+ * be replaced at the next call to this function. */
+{
+
+ret->id = sqlUnsigned(row[0]);
+ret->ownerUser = row[1];
+ret->shareToken = row[2];
+ret->project = row[3];
+ret->db = row[4];
+ret->permission = sqlUnsigned(row[5]);
+ret->targetUser = row[6];
+ret->label = row[7];
+ret->createdAt = row[8];
+}
+
+struct myVariantsShare *myVariantsShareLoadByQuery(struct sqlConnection *conn, char *query)
+/* Load all myVariantsShare from table that satisfy the query given.  
+ * Where query is of the form 'select * from example where something=something'
+ * or 'select example.* from example, anotherTable where example.something = 
+ * anotherTable.something'.
+ * Dispose of this with myVariantsShareFreeList(). */
+{
+struct myVariantsShare *list = NULL, *el;
+struct sqlResult *sr;
+char **row;
+
+sr = sqlGetResult(conn, query);
+while ((row = sqlNextRow(sr)) != NULL)
+    {
+    el = myVariantsShareLoad(row);
+    slAddHead(&list, el);
+    }
+slReverse(&list);
+sqlFreeResult(&sr);
+return list;
+}
+
+void myVariantsShareSaveToDb(struct sqlConnection *conn, struct myVariantsShare *el, char *tableName, int updateSize)
+/* Save myVariantsShare as a row to the table specified by tableName. 
+ * As blob fields may be arbitrary size updateSize specifies the approx size
+ * of a string that would contain the entire query. Arrays of native types are
+ * converted to comma separated strings and loaded as such, User defined types are
+ * inserted as NULL. This function automatically escapes quoted strings for mysql. */
+{
+struct dyString *update = dyStringNew(updateSize);
+sqlDyStringPrintf(update, "insert into %s values ( %u,'%s','%s','%s','%s',%u,'%s','%s','%s')", 
+	tableName,  el->id,  el->ownerUser,  el->shareToken,  el->project,  el->db,  el->permission,  el->targetUser,  el->label,  el->createdAt);
+sqlUpdate(conn, update->string);
+dyStringFree(&update);
+}
+
+struct myVariantsShare *myVariantsShareLoad(char **row)
+/* Load a myVariantsShare from row fetched with select * from myVariantsShare
+ * from database.  Dispose of this with myVariantsShareFree(). */
+{
+struct myVariantsShare *ret;
+
+AllocVar(ret);
+ret->id = sqlUnsigned(row[0]);
+ret->ownerUser = cloneString(row[1]);
+ret->shareToken = cloneString(row[2]);
+ret->project = cloneString(row[3]);
+ret->db = cloneString(row[4]);
+ret->permission = sqlUnsigned(row[5]);
+ret->targetUser = cloneString(row[6]);
+ret->label = cloneString(row[7]);
+ret->createdAt = cloneString(row[8]);
+return ret;
+}
+
+struct myVariantsShare *myVariantsShareLoadAll(char *fileName) 
+/* Load all myVariantsShare from a whitespace-separated file.
+ * Dispose of this with myVariantsShareFreeList(). */
+{
+struct myVariantsShare *list = NULL, *el;
+struct lineFile *lf = lineFileOpen(fileName, TRUE);
+char *row[9];
+
+while (lineFileRow(lf, row))
+    {
+    el = myVariantsShareLoad(row);
+    slAddHead(&list, el);
+    }
+lineFileClose(&lf);
+slReverse(&list);
+return list;
+}
+
+struct myVariantsShare *myVariantsShareLoadAllByChar(char *fileName, char chopper) 
+/* Load all myVariantsShare from a chopper separated file.
+ * Dispose of this with myVariantsShareFreeList(). */
+{
+struct myVariantsShare *list = NULL, *el;
+struct lineFile *lf = lineFileOpen(fileName, TRUE);
+char *row[9];
+
+while (lineFileNextCharRow(lf, chopper, row, ArraySize(row)))
+    {
+    el = myVariantsShareLoad(row);
+    slAddHead(&list, el);
+    }
+lineFileClose(&lf);
+slReverse(&list);
+return list;
+}
+
+struct myVariantsShare *myVariantsShareCommaIn(char **pS, struct myVariantsShare *ret)
+/* Create a myVariantsShare out of a comma separated string. 
+ * This will fill in ret if non-null, otherwise will
+ * return a new myVariantsShare */
+{
+char *s = *pS;
+
+if (ret == NULL)
+    AllocVar(ret);
+ret->id = sqlUnsignedComma(&s);
+ret->ownerUser = sqlStringComma(&s);
+ret->shareToken = sqlStringComma(&s);
+ret->project = sqlStringComma(&s);
+ret->db = sqlStringComma(&s);
+ret->permission = sqlUnsignedComma(&s);
+ret->targetUser = sqlStringComma(&s);
+ret->label = sqlStringComma(&s);
+ret->createdAt = sqlStringComma(&s);
+*pS = s;
+return ret;
+}
+
+void myVariantsShareFree(struct myVariantsShare **pEl)
+/* Free a single dynamically allocated myVariantsShare such as created
+ * with myVariantsShareLoad(). */
+{
+struct myVariantsShare *el;
+
+if ((el = *pEl) == NULL) return;
+freeMem(el->ownerUser);
+freeMem(el->shareToken);
+freeMem(el->project);
+freeMem(el->db);
+freeMem(el->targetUser);
+freeMem(el->label);
+freeMem(el->createdAt);
+freez(pEl);
+}
+
+void myVariantsShareFreeList(struct myVariantsShare **pList)
+/* Free a list of dynamically allocated myVariantsShare's */
+{
+struct myVariantsShare *el, *next;
+
+for (el = *pList; el != NULL; el = next)
+    {
+    next = el->next;
+    myVariantsShareFree(&el);
+    }
+*pList = NULL;
+}
+
+void myVariantsShareOutput(struct myVariantsShare *el, FILE *f, char sep, char lastSep) 
+/* Print out myVariantsShare.  Separate fields with sep. Follow last field with lastSep. */
+{
+fprintf(f, "%u", el->id);
+fputc(sep,f);
+if (sep == ',') fputc('"',f);
+fprintf(f, "%s", el->ownerUser);
+if (sep == ',') fputc('"',f);
+fputc(sep,f);
+if (sep == ',') fputc('"',f);
+fprintf(f, "%s", el->shareToken);
+if (sep == ',') fputc('"',f);
+fputc(sep,f);
+if (sep == ',') fputc('"',f);
+fprintf(f, "%s", el->project);
+if (sep == ',') fputc('"',f);
+fputc(sep,f);
+if (sep == ',') fputc('"',f);
+fprintf(f, "%s", el->db);
+if (sep == ',') fputc('"',f);
+fputc(sep,f);
+fprintf(f, "%u", el->permission);
+fputc(sep,f);
+if (sep == ',') fputc('"',f);
+fprintf(f, "%s", el->targetUser);
+if (sep == ',') fputc('"',f);
+fputc(sep,f);
+if (sep == ',') fputc('"',f);
+fprintf(f, "%s", el->label);
+if (sep == ',') fputc('"',f);
+fputc(sep,f);
+if (sep == ',') fputc('"',f);
+fprintf(f, "%s", el->createdAt);
+if (sep == ',') fputc('"',f);
+fputc(lastSep,f);
+}
+
+/* -------------------------------- End autoSql Generated Code -------------------------------- */
+
+struct myVariantsShare *myVariantsCreateShare(struct sqlConnection *conn,
+    char *ownerUser, char *project, char *db, int permission,
+    char *targetUser, char *label)
+/* Create a new share record. Returns the share with token and id filled in.
+ * Free with myVariantsShareFree. */
+{
+char *token = makeRandomKey(288);  /* 288 bits -> 48 base64 URL-safe chars */
+struct dyString *dy = sqlDyStringCreate(
+    "INSERT INTO myVariantsShares (ownerUser, shareToken, project, db, permission, targetUser, label)"
+    " VALUES ('%s', '%s', '%s', '%s', %d,",
+    ownerUser, token, project, db, permission);
+if (targetUser != NULL)
+    sqlDyStringPrintf(dy, " '%s',", targetUser);
+else
+    sqlDyStringPrintf(dy, " NULL,");
+if (label != NULL)
+    sqlDyStringPrintf(dy, " '%s')", label);
+else
+    sqlDyStringPrintf(dy, " NULL)");
+sqlUpdate(conn, dy->string);
+dyStringFree(&dy);
+
+struct myVariantsShare *share;
+AllocVar(share);
+share->id = sqlLastAutoId(conn);
+share->ownerUser = cloneString(ownerUser);
+share->shareToken = token;
+share->project = cloneString(project);
+share->db = cloneString(db);
+share->permission = permission;
+share->targetUser = cloneString(targetUser);
+share->label = cloneString(label);
+share->createdAt = cloneString("");
+return share;
+}
+
+struct myVariantsShare *myVariantsGetShareByToken(struct sqlConnection *conn,
+    char *token)
+/* Look up a single share by token. Returns NULL if not found or token is malformed. */
+{
+if (token == NULL || strlen(token) != MYVAR_TOKEN_LENGTH)
+    return NULL;
+char query[512];
+sqlSafef(query, sizeof(query),
+    "SELECT * FROM myVariantsShares WHERE shareToken='%s'", token);
+struct sqlResult *sr = sqlGetResult(conn, query);
+char **row = sqlNextRow(sr);
+struct myVariantsShare *share = NULL;
+if (row != NULL)
+    share = myVariantsShareLoad(row);
+sqlFreeResult(&sr);
+return share;
+}
+
+struct myVariantsShare *myVariantsGetSharesForOwner(struct sqlConnection *conn,
+    char *ownerUser, char *db)
+/* Get all shares created by this user for the given assembly. */
+{
+char query[512];
+sqlSafef(query, sizeof(query),
+    "SELECT * FROM myVariantsShares WHERE ownerUser='%s' AND db='%s'"
+    " ORDER BY createdAt DESC", ownerUser, db);
+return myVariantsShareLoadByQuery(conn, query);
+}
+
+struct myVariantsShare *myVariantsGetSharesForUser(struct sqlConnection *conn,
+    char *targetUser, char *db)
+/* Get all shares targeted at this user for the given assembly. */
+{
+char query[512];
+sqlSafef(query, sizeof(query),
+    "SELECT * FROM myVariantsShares WHERE targetUser='%s' AND db='%s'"
+    " ORDER BY createdAt DESC", targetUser, db);
+return myVariantsShareLoadByQuery(conn, query);
+}
+
+boolean myVariantsRevokeShare(struct sqlConnection *conn,
+    char *shareToken, char *ownerUser)
+/* Delete a share record. ownerUser must match the share's owner.
+ * Returns TRUE if a row was deleted, FALSE if not found or not owner. */
+{
+char query[512];
+sqlSafef(query, sizeof(query),
+    "SELECT count(*) FROM myVariantsShares WHERE shareToken='%s' AND ownerUser='%s'",
+    shareToken, ownerUser);
+int count = sqlQuickNum(conn, query);
+if (count == 0)
+    return FALSE;
+sqlSafef(query, sizeof(query),
+    "DELETE FROM myVariantsShares WHERE shareToken='%s' AND ownerUser='%s'",
+    shareToken, ownerUser);
+sqlUpdate(conn, query);
+return TRUE;
+}
+
+char *myVariantsShareCartValue(struct myVariantsShare *share)
+/* Build JSON cart value string from a share record.
+ * Caller must freeMem the result. */
+{
+struct jsonWrite *jw = jsonWriteNew();
+jsonWriteObjectStart(jw, NULL);
+jsonWriteString(jw, "owner", share->ownerUser);
+jsonWriteString(jw, "project", share->project);
+jsonWriteString(jw, "db", share->db);
+jsonWriteNumber(jw, "permission", share->permission);
+if (isNotEmpty(share->label))
+    jsonWriteString(jw, "label", share->label);
+jsonWriteObjectEnd(jw);
+char *result = cloneString(jw->dy->string);
+jsonWriteFree(&jw);
+return result;
+}
+
+boolean myVariantsParseShareCartValue(char *val, char **retOwner,
+    char **retProject, char **retDb, int *retPermission, char **retLabel)
+/* Parse a JSON cart value string back into components. Returns FALSE on bad format.
+ * retOwner, retProject, retDb, retLabel are cloneString'd; caller must free.
+ * retLabel may be NULL; pass NULL to skip. */
+{
+if (isEmpty(val))
+    return FALSE;
+/* jsonParse errAborts on malformed input; catch so a malicious cart value
+ * can't take down a session-load or page render. */
+struct jsonElement *json = NULL;
+struct errCatch *ec = errCatchNew();
+if (errCatchStart(ec))
+    json = jsonParse(val);
+errCatchEnd(ec);
+boolean parseFailed = ec->gotError;
+errCatchFree(&ec);
+if (parseFailed || json == NULL)
+    return FALSE;
+char *owner = jsonOptionalStringField(json, "owner", NULL);
+char *project = jsonOptionalStringField(json, "project", NULL);
+char *db = jsonOptionalStringField(json, "db", NULL);
+if (owner == NULL || project == NULL || db == NULL)
+    return FALSE;
+if (retOwner)
+    *retOwner = cloneString(owner);
+if (retProject)
+    *retProject = cloneString(project);
+if (retDb)
+    *retDb = cloneString(db);
+if (retPermission)
+    {
+    struct jsonElement *permEl = jsonFindNamedField(json, "", "permission");
+    *retPermission = (permEl != NULL) ? (int)jsonNumberVal(permEl, "permission") : 0;
+    }
+if (retLabel)
+    *retLabel = cloneString(jsonOptionalStringField(json, "label", ""));
+return TRUE;
+}
+