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; +} +