2026-02-05 14:57:12 +01:00
package obikmer
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/pelletier/go-toml/v2"
"gopkg.in/yaml.v3"
)
// MetadataFormat représente le format de sérialisation des métadonnées
type MetadataFormat int
const (
FormatTOML MetadataFormat = iota
FormatYAML
FormatJSON
)
// String retourne l'extension de fichier pour le format
func ( f MetadataFormat ) String ( ) string {
switch f {
case FormatTOML :
return "toml"
case FormatYAML :
return "yaml"
case FormatJSON :
return "json"
default :
return "toml"
}
}
// KmerSetMetadata contient les métadonnées d'un KmerSet ou KmerSetGroup
type KmerSetMetadata struct {
2026-02-05 15:32:19 +01:00
ID string ` toml:"id,omitempty" yaml:"id,omitempty" json:"id,omitempty" ` // Identifiant unique
K int ` toml:"k" yaml:"k" json:"k" ` // Taille des k-mers
Type string ` toml:"type" yaml:"type" json:"type" ` // "KmerSet" ou "KmerSetGroup"
Size int ` toml:"size" yaml:"size" json:"size" ` // 1 pour KmerSet, n pour KmerSetGroup
Files [ ] string ` toml:"files" yaml:"files" json:"files" ` // Liste des fichiers .roaring
SetsIDs [ ] string ` toml:"sets_ids,omitempty" yaml:"sets_ids,omitempty" json:"sets_ids,omitempty" ` // IDs des KmerSet individuels
UserMetadata map [ string ] interface { } ` toml:"user_metadata,omitempty" yaml:"user_metadata,omitempty" json:"user_metadata,omitempty" ` // Métadonnées KmerSet ou KmerSetGroup
SetsMetadata [ ] map [ string ] interface { } ` toml:"sets_metadata,omitempty" yaml:"sets_metadata,omitempty" json:"sets_metadata,omitempty" ` // Métadonnées des KmerSet individuels dans un KmerSetGroup
2026-02-05 14:57:12 +01:00
}
// SaveKmerSet sauvegarde un KmerSet dans un répertoire
// Format: directory/metadata.{toml,yaml,json} + directory/set_0.roaring
func ( ks * KmerSet ) Save ( directory string , format MetadataFormat ) error {
// Créer le répertoire si nécessaire
if err := os . MkdirAll ( directory , 0755 ) ; err != nil {
return fmt . Errorf ( "failed to create directory %s: %w" , directory , err )
}
// Métadonnées
metadata := KmerSetMetadata {
2026-02-05 15:32:19 +01:00
ID : ks . id ,
K : ks . k ,
2026-02-05 15:02:27 +01:00
Type : "KmerSet" ,
Size : 1 ,
Files : [ ] string { "set_0.roaring" } ,
UserMetadata : ks . Metadata , // Sauvegarder les métadonnées utilisateur
2026-02-05 14:57:12 +01:00
}
// Sauvegarder les métadonnées
if err := saveMetadata ( filepath . Join ( directory , "metadata." + format . String ( ) ) , metadata , format ) ; err != nil {
return err
}
// Sauvegarder le bitmap
bitmapPath := filepath . Join ( directory , "set_0.roaring" )
file , err := os . Create ( bitmapPath )
if err != nil {
return fmt . Errorf ( "failed to create bitmap file %s: %w" , bitmapPath , err )
}
defer file . Close ( )
if _ , err := ks . bitmap . WriteTo ( file ) ; err != nil {
return fmt . Errorf ( "failed to write bitmap: %w" , err )
}
return nil
}
// LoadKmerSet charge un KmerSet depuis un répertoire
func LoadKmerSet ( directory string ) ( * KmerSet , error ) {
// Lire les métadonnées (essayer tous les formats)
metadata , err := loadMetadata ( directory )
if err != nil {
return nil , err
}
// Vérifier le type
if metadata . Type != "KmerSet" {
return nil , fmt . Errorf ( "invalid type: expected KmerSet, got %s" , metadata . Type )
}
// Vérifier qu'il n'y a qu'un seul fichier
if metadata . Size != 1 || len ( metadata . Files ) != 1 {
return nil , fmt . Errorf ( "KmerSet must have exactly 1 bitmap file, got %d" , len ( metadata . Files ) )
}
// Charger le bitmap
bitmapPath := filepath . Join ( directory , metadata . Files [ 0 ] )
file , err := os . Open ( bitmapPath )
if err != nil {
return nil , fmt . Errorf ( "failed to open bitmap file %s: %w" , bitmapPath , err )
}
defer file . Close ( )
ks := NewKmerSet ( metadata . K )
2026-02-05 15:02:27 +01:00
2026-02-05 15:32:19 +01:00
// Charger l'ID
ks . id = metadata . ID
2026-02-05 15:02:27 +01:00
// Charger les métadonnées utilisateur
if metadata . UserMetadata != nil {
ks . Metadata = metadata . UserMetadata
}
2026-02-05 14:57:12 +01:00
if _ , err := ks . bitmap . ReadFrom ( file ) ; err != nil {
return nil , fmt . Errorf ( "failed to read bitmap: %w" , err )
}
return ks , nil
}
// SaveKmerSetGroup sauvegarde un KmerSetGroup dans un répertoire
// Format: directory/metadata.{toml,yaml,json} + directory/set_0.roaring, set_1.roaring, ...
func ( ksg * KmerSetGroup ) Save ( directory string , format MetadataFormat ) error {
// Créer le répertoire si nécessaire
if err := os . MkdirAll ( directory , 0755 ) ; err != nil {
return fmt . Errorf ( "failed to create directory %s: %w" , directory , err )
}
// Métadonnées
files := make ( [ ] string , len ( ksg . sets ) )
for i := range ksg . sets {
files [ i ] = fmt . Sprintf ( "set_%d.roaring" , i )
}
2026-02-05 15:32:19 +01:00
// Collecter les IDs et métadonnées de chaque KmerSet individuel
setsIDs := make ( [ ] string , len ( ksg . sets ) )
setsMetadata := make ( [ ] map [ string ] interface { } , len ( ksg . sets ) )
for i , ks := range ksg . sets {
setsIDs [ i ] = ks . id
setsMetadata [ i ] = ks . Metadata
}
2026-02-05 14:57:12 +01:00
metadata := KmerSetMetadata {
2026-02-05 15:32:19 +01:00
ID : ksg . id ,
K : ksg . k ,
2026-02-05 15:02:27 +01:00
Type : "KmerSetGroup" ,
Size : len ( ksg . sets ) ,
Files : files ,
2026-02-05 15:32:19 +01:00
SetsIDs : setsIDs , // IDs de chaque set
UserMetadata : ksg . Metadata , // Métadonnées du groupe
SetsMetadata : setsMetadata , // Métadonnées de chaque set
2026-02-05 14:57:12 +01:00
}
// Sauvegarder les métadonnées
if err := saveMetadata ( filepath . Join ( directory , "metadata." + format . String ( ) ) , metadata , format ) ; err != nil {
return err
}
// Sauvegarder chaque bitmap
for i , ks := range ksg . sets {
bitmapPath := filepath . Join ( directory , files [ i ] )
file , err := os . Create ( bitmapPath )
if err != nil {
return fmt . Errorf ( "failed to create bitmap file %s: %w" , bitmapPath , err )
}
if _ , err := ks . bitmap . WriteTo ( file ) ; err != nil {
file . Close ( )
return fmt . Errorf ( "failed to write bitmap %d: %w" , i , err )
}
file . Close ( )
}
return nil
}
// LoadKmerSetGroup charge un KmerSetGroup depuis un répertoire
func LoadKmerSetGroup ( directory string ) ( * KmerSetGroup , error ) {
// Lire les métadonnées (essayer tous les formats)
metadata , err := loadMetadata ( directory )
if err != nil {
return nil , err
}
// Vérifier le type
if metadata . Type != "KmerSetGroup" {
return nil , fmt . Errorf ( "invalid type: expected KmerSetGroup, got %s" , metadata . Type )
}
// Vérifier la cohérence
if metadata . Size != len ( metadata . Files ) {
return nil , fmt . Errorf ( "size mismatch: size=%d but %d files listed" , metadata . Size , len ( metadata . Files ) )
}
// Créer le groupe
ksg := NewKmerSetGroup ( metadata . K , metadata . Size )
2026-02-05 15:32:19 +01:00
// Charger l'ID du groupe
ksg . id = metadata . ID
// Charger les métadonnées du groupe
if metadata . UserMetadata != nil {
ksg . Metadata = metadata . UserMetadata
}
// Charger les IDs de chaque KmerSet
if metadata . SetsIDs != nil && len ( metadata . SetsIDs ) == metadata . Size {
for i := range ksg . sets {
ksg . sets [ i ] . id = metadata . SetsIDs [ i ]
}
}
// Charger les métadonnées de chaque KmerSet individuel
2026-02-05 15:02:27 +01:00
if metadata . SetsMetadata != nil {
if len ( metadata . SetsMetadata ) != metadata . Size {
2026-02-05 15:32:19 +01:00
return nil , fmt . Errorf ( "sets metadata size mismatch: expected %d, got %d" , metadata . Size , len ( metadata . SetsMetadata ) )
}
for i := range ksg . sets {
ksg . sets [ i ] . Metadata = metadata . SetsMetadata [ i ]
2026-02-05 15:02:27 +01:00
}
}
2026-02-05 14:57:12 +01:00
// Charger chaque bitmap
for i , filename := range metadata . Files {
bitmapPath := filepath . Join ( directory , filename )
file , err := os . Open ( bitmapPath )
if err != nil {
return nil , fmt . Errorf ( "failed to open bitmap file %s: %w" , bitmapPath , err )
}
if _ , err := ksg . sets [ i ] . bitmap . ReadFrom ( file ) ; err != nil {
file . Close ( )
return nil , fmt . Errorf ( "failed to read bitmap %d: %w" , i , err )
}
file . Close ( )
}
return ksg , nil
}
// saveMetadata sauvegarde les métadonnées dans le format spécifié
func saveMetadata ( path string , metadata KmerSetMetadata , format MetadataFormat ) error {
file , err := os . Create ( path )
if err != nil {
return fmt . Errorf ( "failed to create metadata file %s: %w" , path , err )
}
defer file . Close ( )
var encoder interface { Encode ( interface { } ) error }
switch format {
case FormatTOML :
encoder = toml . NewEncoder ( file )
case FormatYAML :
encoder = yaml . NewEncoder ( file )
case FormatJSON :
jsonEncoder := json . NewEncoder ( file )
jsonEncoder . SetIndent ( "" , " " )
encoder = jsonEncoder
default :
return fmt . Errorf ( "unsupported format: %v" , format )
}
if err := encoder . Encode ( metadata ) ; err != nil {
return fmt . Errorf ( "failed to encode metadata: %w" , err )
}
return nil
}
// loadMetadata charge les métadonnées depuis un répertoire
// Essaie tous les formats (TOML, YAML, JSON) dans l'ordre
func loadMetadata ( directory string ) ( * KmerSetMetadata , error ) {
formats := [ ] MetadataFormat { FormatTOML , FormatYAML , FormatJSON }
var lastErr error
for _ , format := range formats {
path := filepath . Join ( directory , "metadata." + format . String ( ) )
// Vérifier si le fichier existe
if _ , err := os . Stat ( path ) ; os . IsNotExist ( err ) {
continue
}
metadata , err := loadMetadataFromFile ( path , format )
if err != nil {
lastErr = err
continue
}
return metadata , nil
}
if lastErr != nil {
return nil , fmt . Errorf ( "failed to load metadata: %w" , lastErr )
}
return nil , fmt . Errorf ( "no metadata file found in %s (tried .toml, .yaml, .json)" , directory )
}
// loadMetadataFromFile charge les métadonnées depuis un fichier spécifique
func loadMetadataFromFile ( path string , format MetadataFormat ) ( * KmerSetMetadata , error ) {
file , err := os . Open ( path )
if err != nil {
return nil , fmt . Errorf ( "failed to open metadata file %s: %w" , path , err )
}
defer file . Close ( )
var metadata KmerSetMetadata
var decoder interface { Decode ( interface { } ) error }
switch format {
case FormatTOML :
decoder = toml . NewDecoder ( file )
case FormatYAML :
decoder = yaml . NewDecoder ( file )
case FormatJSON :
decoder = json . NewDecoder ( file )
default :
return nil , fmt . Errorf ( "unsupported format: %v" , format )
}
if err := decoder . Decode ( & metadata ) ; err != nil {
return nil , fmt . Errorf ( "failed to decode metadata: %w" , err )
}
return & metadata , nil
}
// DetectFormat détecte le format des métadonnées dans un répertoire
func DetectFormat ( directory string ) ( MetadataFormat , error ) {
formats := [ ] MetadataFormat { FormatTOML , FormatYAML , FormatJSON }
for _ , format := range formats {
path := filepath . Join ( directory , "metadata." + format . String ( ) )
if _ , err := os . Stat ( path ) ; err == nil {
return format , nil
}
}
return FormatTOML , fmt . Errorf ( "no metadata file found in %s" , directory )
}
// IsKmerSetDirectory vérifie si un répertoire contient un KmerSet ou KmerSetGroup
func IsKmerSetDirectory ( directory string ) ( bool , string , error ) {
metadata , err := loadMetadata ( directory )
if err != nil {
return false , "" , err
}
return true , metadata . Type , nil
}
// ListBitmapFiles liste tous les fichiers .roaring dans un répertoire
func ListBitmapFiles ( directory string ) ( [ ] string , error ) {
entries , err := os . ReadDir ( directory )
if err != nil {
return nil , fmt . Errorf ( "failed to read directory %s: %w" , directory , err )
}
var files [ ] string
for _ , entry := range entries {
if ! entry . IsDir ( ) && strings . HasSuffix ( entry . Name ( ) , ".roaring" ) {
files = append ( files , entry . Name ( ) )
}
}
return files , nil
}