// myGenomes provides functions to store a user's raw genome files // These are exported from sequencing companies like 23andMe and AncestryDNA package myGenomes import "seekia/internal/myDatastores/myMapList" import "seekia/internal/genetics/readRawGenomes" import "seekia/internal/localFilesystem" import "seekia/internal/helpers" import "seekia/internal/cryptography/blake3" import "path/filepath" import "time" import "sync" import "errors" import "strings" //TODO: Delete unused raw genome files // This will be locked anytime Genomes are being added/deleted var updatingMyGenomesMutex sync.Mutex var myGenomesMapListDatastore *myMapList.MyMapList // This function must be called whenever an app user signs in func CreateUserGenomesFolder() error{ userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return err } myGenomesFolderPath := filepath.Join(userDirectory, "MyGenomes") _, err = localFilesystem.CreateFolder(myGenomesFolderPath) if (err != nil) { return err } return nil } // This function must be called whenever an app user signs in func InitializeMyGenomeDatastore()error{ updatingMyGenomesMutex.Lock() defer updatingMyGenomesMutex.Unlock() newMyGenomesMapListDatastore, err := myMapList.CreateNewMapList("MyGenomes") if (err != nil) { return err } myGenomesMapListDatastore = newMyGenomesMapListDatastore return nil } //Outputs: // -[]map[string]string // -PersonIdentifier -> Identifier of Genome Person // -GenomeIdentifier -> Genome identifier (this is the name of the .txt file stored on disk) // -TimeExported -> Time the genome file was exported from company // -TimeImported -> Time the genome was imported into Seekia // -IsPhased -> "Yes"/"No" // -SNPCount -> Number of readable SNPs in file // -CompanyName -> Company name ("23andMe", "AncestryDNA") // -ImportVersion -> Import version for the company from which the metadata was retrieved // -FileHash -> 256 bits Blake3 hash of the genome file // -error func GetMyRawGenomesMetadataMapList()([]map[string]string, error){ myRawGenomesMapList, err := myGenomesMapListDatastore.GetMapList() if (err != nil) { return nil, err } return myRawGenomesMapList, nil } //Outputs: // -bool: File is valid // -bool: File already exists // -error func AddRawGenome(personIdentifier string, rawGenomeString string)(bool, bool, error){ isValid := helpers.VerifyHexString(15, personIdentifier) if (isValid == false) { return false, false, errors.New("AddRawGenome called with invalid personIdentifier: " + personIdentifier) } updatingMyGenomesMutex.Lock() defer updatingMyGenomesMutex.Unlock() currentFileHash, err := blake3.GetBlake3HashAsHexString(32, []byte(rawGenomeString)) if (err != nil) { return false, false, err } // We check to see if this file has already been imported for this Person lookupMap := map[string]string{ "PersonIdentifier": personIdentifier, "FileHash": currentFileHash, } anyItemFound, _, err := myGenomesMapListDatastore.GetMapListItems(lookupMap) if (err != nil) { return false, false, err } if (anyItemFound == true){ // Genome already exists return true, true, nil } // Genome is new. We will add it to the map list and copy the file to Seekia local storage rawGenomeReader := strings.NewReader(rawGenomeString) companyName, importVersion, timeFileWasGenerated, snpCount, genomeIsPhased, _, err := readRawGenomes.ReadRawGenomeFile(rawGenomeReader) if (err != nil){ return false, false, nil } genomeIdentifier, err := helpers.GetNewRandomHexString(16) if (err != nil) { return false, false, err } importVersionString := helpers.ConvertIntToString(importVersion) timeExported := helpers.ConvertInt64ToString(timeFileWasGenerated) timeImported := time.Now().Unix() timeImportedString := helpers.ConvertInt64ToString(timeImported) isPhasedString := helpers.ConvertBoolToYesOrNoString(genomeIsPhased) snpCountString := helpers.ConvertInt64ToString(snpCount) newGenomeMap := map[string]string{ "PersonIdentifier": personIdentifier, "GenomeIdentifier": genomeIdentifier, "TimeExported": timeExported, "TimeImported": timeImportedString, "IsPhased": isPhasedString, "SNPCount": snpCountString, "CompanyName": companyName, "ImportVersion": importVersionString, "FileHash": currentFileHash, } err = myGenomesMapListDatastore.AddMapListItem(newGenomeMap) if (err != nil) { return false, false, err } userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return false, false, err } myGenomesFolderPath := filepath.Join(userDirectory, "MyGenomes") genomeFileName := genomeIdentifier + ".txt" err = localFilesystem.CreateOrOverwriteFile([]byte(rawGenomeString), myGenomesFolderPath, genomeFileName) if (err != nil) { return false, false, err } return true, false, nil } func DeleteMyRawGenome(genomeIdentifier string)error{ isValid := helpers.VerifyHexString(16, genomeIdentifier) if (isValid == false){ return errors.New("DeleteMyRawGenome called with invalid genomeIdentifier: " + genomeIdentifier) } updatingMyGenomesMutex.Lock() defer updatingMyGenomesMutex.Unlock() mapToDelete := map[string]string{ "GenomeIdentifier": genomeIdentifier, } err := myGenomesMapListDatastore.DeleteMapListItems(mapToDelete) if (err != nil) { return err } userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return err } genomeFileName := genomeIdentifier + ".txt" genomeFilePath := filepath.Join(userDirectory, "MyGenomes", genomeFileName) _, err = localFilesystem.DeleteFileOrFolder(genomeFilePath) if (err != nil) { return err } return nil } //Outputs: // -bool: Genome found // -string: Person identifier // -int64: Time Genome was exported from company // -int64: Time genome was imported into Seekia // -bool: Is Phased // -int64: SNP Count // -string: CompanyName // -int: Import version // -string: FileHash // -error func GetMyRawGenomeMetadata(genomeIdentifier string)(bool, string, int64, int64, bool, int64, string, int, string, error){ if (genomeIdentifier == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" || genomeIdentifier == "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"){ // These are the genome identifiers we use for example reports // These are used to show the user what a genetic analysis would look like getPersonIdentifier := func()string{ if (genomeIdentifier == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"){ return "111111111111111111111111111111" } return "222222222222222222222222222222" } personIdentifier := getPersonIdentifier() return true, personIdentifier, 0, 0, false, 676720, "AncestryDNA", 1, "", nil } lookupMap := map[string]string{ "GenomeIdentifier": genomeIdentifier, } anyItemFound, foundItemsMapList, err := myGenomesMapListDatastore.GetMapListItems(lookupMap) if (anyItemFound == false){ return false, "", 0, 0, false, 0, "", 0, "", nil } if (len(foundItemsMapList) != 1){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Contains multiple entries for same GenomeIdentifier") } genomeMap := foundItemsMapList[0] personIdentifier, exists := genomeMap["PersonIdentifier"] if (exists == false){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing PersonIdentifier") } timeExported, exists := genomeMap["TimeExported"] if (exists == false){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing TimeExported") } timeImported, exists := genomeMap["TimeImported"] if (exists == false){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing TimeImported") } isPhased, exists := genomeMap["IsPhased"] if (exists == false){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing IsPhased") } snpCount, exists := genomeMap["SNPCount"] if (exists == false){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing SNPCount") } companyName, exists := genomeMap["CompanyName"] if (exists == false){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing CompanyName") } importVersion, exists := genomeMap["ImportVersion"] if (exists == false){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing ImportVersion") } fileHash, exists := genomeMap["FileHash"] if (exists == false){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing FileHash") } timeExportedInt64, err := helpers.ConvertStringToInt64(timeExported) if (err != nil){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item contains invalid TimeExported: " + timeExported) } timeImportedInt64, err := helpers.ConvertStringToInt64(timeImported) if (err != nil){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item contains invalid TimeImported: " + timeImported) } isPhasedBool, err := helpers.ConvertYesOrNoStringToBool(isPhased) if (err != nil) { return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item contains invalid isPhased: " + isPhased) } snpCountInt64, err := helpers.ConvertStringToInt64(snpCount) if (err != nil){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item contains invalid snpCount: " + snpCount) } importVersionInt, err := helpers.ConvertStringToInt(importVersion) if (err != nil){ return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item contains invalid ImportVersion: " + importVersion) } return true, personIdentifier, timeExportedInt64, timeImportedInt64, isPhasedBool, snpCountInt64, companyName, importVersionInt, fileHash, nil } // This function is used to refresh the genome metadata when a new import version is available func RefreshRawGenomeMetadata(genomeIdentifier string)error{ updatingMyGenomesMutex.Lock() defer updatingMyGenomesMutex.Unlock() myGenomesMapList, err := myGenomesMapListDatastore.GetMapList() if (err != nil) { return err } foundGenome := false for _, genomeMap := range myGenomesMapList{ currentGenomeIdentifier, exists := genomeMap["GenomeIdentifier"] if (exists == false){ return errors.New("myGenomesMapList item is malformed: item missing GenomeIdentifier.") } if (currentGenomeIdentifier != genomeIdentifier){ continue } if (foundGenome == true){ return errors.New("myGenomesMapList is malformed: Multiple entries for the same GenomeIdentifier exist.") } foundGenome = true rawGenomeString, err := GetGenomeRawDataString(genomeIdentifier) if (err != nil){ return err } rawGenomeReader := strings.NewReader(rawGenomeString) companyName, importVersion, timeFileWasGenerated, snpCount, genomeIsPhased, _, err := readRawGenomes.ReadRawGenomeFile(rawGenomeReader) if (err != nil){ // Could be that file was importable via old import version, but new import version rejects it. // That would still be bad undesireable behavior. return errors.New("Unable to import raw genome during RefreshRawGenomeMetadata: " + err.Error()) } importVersionString := helpers.ConvertIntToString(importVersion) timeExported := helpers.ConvertInt64ToString(timeFileWasGenerated) isPhasedString := helpers.ConvertBoolToYesOrNoString(genomeIsPhased) snpCountString := helpers.ConvertInt64ToString(snpCount) genomeMap["TimeExported"] = timeExported genomeMap["IsPhased"] = isPhasedString genomeMap["SNPCount"] = snpCountString genomeMap["CompanyName"] = companyName genomeMap["ImportVersion"] = importVersionString } if (foundGenome == false){ return errors.New("Genome not found during RefreshRawGenomeMetadata") } err = myGenomesMapListDatastore.OverwriteMapList(myGenomesMapList) if (err != nil) { return err } return nil } //Outputs: // -string: Genome raw data string // -error func GetGenomeRawDataString(genomeIdentifier string)(string, error){ userDirectory, err := localFilesystem.GetAppUserFolderPath() if (err != nil) { return "", err } genomeFileName := genomeIdentifier + ".txt" genomeFilePath := filepath.Join(userDirectory, "MyGenomes", genomeFileName) fileExists, fileBytes, err := localFilesystem.GetFileContents(genomeFilePath) if (err != nil) { return "", err } if (fileExists == false){ return "", errors.New("GetGenomeRawDataString called with genome whose file we cannot find.") } fileString := string(fileBytes) return fileString, nil } // Returns all genomes for a person func GetAllPersonGenomesMapList(personIdentifier string)([]map[string]string, error){ lookupMap := map[string]string{ "PersonIdentifier": personIdentifier, } anyItemsFound, matchingItemsMapList, err := myGenomesMapListDatastore.GetMapListItems(lookupMap) if (err != nil) { return nil, err } if (anyItemsFound == false){ emptyMapList := make([]map[string]string, 0) return emptyMapList, nil } return matchingItemsMapList, nil } // This will not include any calculated genome identifiers, which only exist within analyses func GetAllPersonRawGenomeIdentifiersList(personIdentifier string)([]string, error){ allPersonGenomesMapList, err := GetAllPersonGenomesMapList(personIdentifier) if (err != nil) { return nil, err } personGenomeIdentifiersList := make([]string, 0, len(allPersonGenomesMapList)) for _, genomeMap := range allPersonGenomesMapList{ genomeIdentifier, exists := genomeMap["GenomeIdentifier"] if (exists == false){ return nil, errors.New("Malformed myGenomesMapList: Item missing GenomeIdentifier") } personGenomeIdentifiersList = append(personGenomeIdentifiersList, genomeIdentifier) } return personGenomeIdentifiersList, nil } func DeleteAllPersonGenomes(personIdentifier string)error{ updatingMyGenomesMutex.Lock() defer updatingMyGenomesMutex.Unlock() mapToDelete := map[string]string{ "PersonIdentifier": personIdentifier, } err := myGenomesMapListDatastore.DeleteMapListItems(mapToDelete) if (err != nil) { return err } return nil }