Skip to content

Commit 3f8b58b

Browse files
committed
feat(unpack): implement UnpackFromLayers function for layer-per-file model unpacking
1 parent beb05f5 commit 3f8b58b

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

pkg/distribution/internal/bundle/unpack.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)