@@ -585,6 +585,145 @@ func unpackFile(bundlePath string, srcPath string) error {
585585 return os .Link (srcPath , bundlePath )
586586}
587587
588+ // UnpackFromLayers unpacks a model that was packaged using the layer-per-file approach.
589+ // Each file is stored as an individual layer with its filepath preserved in annotations.
590+ // This is the approach used by builder.FromDirectory and preserves nested directory structure.
591+ //
592+ // Unlike the standard Unpack function which uses model.GGUFPaths(), model.SafetensorsPaths(), etc.,
593+ // this function iterates directly over layers and uses their filepath annotations.
594+ func UnpackFromLayers (dir string , model types.ModelArtifact ) (* Bundle , error ) {
595+ bundle := & Bundle {
596+ dir : dir ,
597+ }
598+
599+ // Create model subdirectory upfront - all unpack operations will use it
600+ modelDir := filepath .Join (bundle .dir , ModelSubdir )
601+ if err := os .MkdirAll (modelDir , 0755 ); err != nil {
602+ return nil , fmt .Errorf ("create model directory: %w" , err )
603+ }
604+
605+ // Get all layers from the model
606+ layers , err := model .Layers ()
607+ if err != nil {
608+ return nil , fmt .Errorf ("get model layers: %w" , err )
609+ }
610+
611+ // Define the interface for getting descriptor with annotations
612+ type descriptorProvider interface {
613+ GetDescriptor () oci.Descriptor
614+ }
615+
616+ // Iterate through all layers and unpack using annotations
617+ for _ , layer := range layers {
618+ mediaType , err := layer .MediaType ()
619+ if err != nil {
620+ fmt .Printf ("Warning: error getting media type: %v\n " , err )
621+ continue
622+ }
623+
624+ // Get the filepath annotation
625+ dp , ok := layer .(descriptorProvider )
626+ if ! ok {
627+ fmt .Printf ("Warning: layer is not a descriptorProvider\n " )
628+ continue
629+ }
630+
631+ desc := dp .GetDescriptor ()
632+ relPath , exists := desc .Annotations [types .AnnotationFilePath ]
633+ if ! exists || relPath == "" {
634+ fmt .Printf ("Warning: layer missing filepath annotation\n " )
635+ continue
636+ }
637+
638+ // Validate the path to prevent directory traversal
639+ if err := validatePathWithinDirectory (modelDir , relPath ); err != nil {
640+ return nil , fmt .Errorf ("invalid filepath annotation %q: %w" , relPath , err )
641+ }
642+
643+ // Convert forward slashes to OS-specific separator
644+ relPath = filepath .FromSlash (relPath )
645+ destPath := filepath .Join (modelDir , relPath )
646+
647+ // Skip if file already exists
648+ if _ , err := os .Stat (destPath ); err == nil {
649+ continue
650+ }
651+
652+ // Create parent directories if needed
653+ if err := os .MkdirAll (filepath .Dir (destPath ), 0755 ); err != nil {
654+ return nil , fmt .Errorf ("create parent directory for %s: %w" , relPath , err )
655+ }
656+
657+ // Unpack the file
658+ if err := unpackLayerToFile (destPath , layer ); err != nil {
659+ return nil , fmt .Errorf ("unpack %s: %w" , relPath , err )
660+ }
661+
662+ // Update bundle tracking fields
663+ updateBundleFieldsFromLayer (bundle , mediaType , relPath )
664+ }
665+
666+ // Create the runtime config from the model
667+ cfg , err := model .Config ()
668+ if err != nil {
669+ return nil , fmt .Errorf ("get model config: %w" , err )
670+ }
671+
672+ // Write runtime config to bundle root
673+ f , err := os .Create (filepath .Join (bundle .dir , "config.json" ))
674+ if err != nil {
675+ return nil , fmt .Errorf ("create runtime config file: %w" , err )
676+ }
677+ defer f .Close ()
678+ if err := json .NewEncoder (f ).Encode (cfg ); err != nil {
679+ return nil , fmt .Errorf ("encode runtime config: %w" , err )
680+ }
681+ bundle .runtimeConfig = cfg
682+
683+ return bundle , nil
684+ }
685+
686+ // unpackLayerToFile unpacks a single layer to the destination path.
687+ // It tries to use hard linking for local layers, falling back to copying for remote layers.
688+ func unpackLayerToFile (destPath string , layer oci.Layer ) error {
689+ // Try to get the layer's local path for hard linking
690+ type pathProvider interface {
691+ GetPath () string
692+ }
693+
694+ if pp , ok := layer .(pathProvider ); ok {
695+ // Use hard link for local layers
696+ return unpackFile (destPath , pp .GetPath ())
697+ }
698+ return fmt .Errorf ("layer is not a path provider" )
699+ }
700+
701+ // updateBundleFieldsFromLayer updates the bundle tracking fields based on the unpacked layer.
702+ func updateBundleFieldsFromLayer (bundle * Bundle , mediaType oci.MediaType , relPath string ) {
703+ switch mediaType {
704+ case types .MediaTypeGGUF :
705+ if bundle .ggufFile == "" {
706+ bundle .ggufFile = relPath
707+ }
708+ case types .MediaTypeSafetensors :
709+ if bundle .safetensorsFile == "" {
710+ bundle .safetensorsFile = relPath
711+ }
712+ case types .MediaTypeDDUF :
713+ if bundle .ddufFile == "" {
714+ bundle .ddufFile = relPath
715+ }
716+ case types .MediaTypeMultimodalProjector :
717+ if bundle .mmprojPath == "" {
718+ bundle .mmprojPath = relPath
719+ }
720+ case types .MediaTypeChatTemplate :
721+ if bundle .chatTemplatePath == "" {
722+ bundle .chatTemplatePath = relPath
723+ }
724+ }
725+ }
726+
588727// unpackGenericFileLayers unpacks layers with MediaTypeModelFile using their filepath annotation.
589728// This supports the new format where each config file is packaged as an individual layer
590729// with its relative path preserved in the annotation.
0 commit comments