Skip to content

Commit beb05f5

Browse files
committed
feat(builder): add support for exclusions in FromDirectory function
1 parent 60f24da commit beb05f5

2 files changed

Lines changed: 375 additions & 7 deletions

File tree

pkg/distribution/builder/from_directory.go

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,35 @@ import (
1414
"github.com/docker/model-runner/pkg/distribution/types"
1515
)
1616

17+
// DirectoryOptions configures the behavior of FromDirectory.
18+
type DirectoryOptions struct {
19+
// Exclusions is a list of patterns to exclude from packaging.
20+
// Patterns can be:
21+
// - Directory names (e.g., ".git", "__pycache__") - excludes the entire directory
22+
// - File names (e.g., "README.md") - excludes files with this exact name
23+
// - Glob patterns (e.g., "*.log", "*.tmp") - excludes files matching the pattern
24+
// - Paths with slashes (e.g., "logs/debug.log") - excludes specific paths
25+
Exclusions []string
26+
}
27+
28+
// DirectoryOption is a functional option for configuring FromDirectory.
29+
type DirectoryOption func(*DirectoryOptions)
30+
31+
// WithExclusions specifies patterns to exclude from packaging.
32+
// Patterns can be directory names, file names, glob patterns, or specific paths.
33+
//
34+
// Examples:
35+
//
36+
// WithExclusions(".git", "__pycache__") // Exclude directories
37+
// WithExclusions("README.md", "CHANGELOG.md") // Exclude specific files
38+
// WithExclusions("*.log", "*.tmp") // Exclude by pattern
39+
// WithExclusions("logs/", "cache/") // Exclude directories (trailing slash)
40+
func WithExclusions(patterns ...string) DirectoryOption {
41+
return func(opts *DirectoryOptions) {
42+
opts.Exclusions = append(opts.Exclusions, patterns...)
43+
}
44+
}
45+
1746
// FromDirectory creates a Builder from a directory containing model files.
1847
// It recursively scans the directory and adds each non-hidden file as a separate layer.
1948
// Each layer's filepath annotation preserves the relative path from the directory root.
@@ -29,7 +58,17 @@ import (
2958
// text_encoder/
3059
// config.json -> layer with annotation "text_encoder/config.json"
3160
// model.safetensors -> layer with annotation "text_encoder/model.safetensors"
32-
func FromDirectory(dirPath string) (*Builder, error) {
61+
//
62+
// Example with exclusions:
63+
//
64+
// builder.FromDirectory(dirPath, builder.WithExclusions(".git", "__pycache__", "*.log"))
65+
func FromDirectory(dirPath string, opts ...DirectoryOption) (*Builder, error) {
66+
// Apply options
67+
options := &DirectoryOptions{}
68+
for _, opt := range opts {
69+
opt(options)
70+
}
71+
3372
// Verify directory exists
3473
info, err := os.Stat(dirPath)
3574
if err != nil {
@@ -63,6 +102,20 @@ func FromDirectory(dirPath string) (*Builder, error) {
63102
return nil
64103
}
65104

105+
// Calculate relative path from the directory root
106+
relPath, err := filepath.Rel(dirPath, path)
107+
if err != nil {
108+
return fmt.Errorf("compute relative path: %w", err)
109+
}
110+
111+
// Check exclusions
112+
if shouldExclude(info, relPath, options.Exclusions) {
113+
if info.IsDir() {
114+
return filepath.SkipDir
115+
}
116+
return nil
117+
}
118+
66119
// Skip directories (but continue walking into them)
67120
if info.IsDir() {
68121
return nil
@@ -73,12 +126,6 @@ func FromDirectory(dirPath string) (*Builder, error) {
73126
return nil
74127
}
75128

76-
// Calculate relative path from the directory root
77-
relPath, err := filepath.Rel(dirPath, path)
78-
if err != nil {
79-
return fmt.Errorf("compute relative path: %w", err)
80-
}
81-
82129
// Classify the file to determine media type
83130
fileType := files.Classify(path)
84131
mediaType := fileTypeToMediaType(fileType)
@@ -100,6 +147,10 @@ func FromDirectory(dirPath string) (*Builder, error) {
100147
detectedFormat = types.FormatDiffusers
101148
}
102149
weightFiles = append(weightFiles, path)
150+
case files.FileTypeUnknown:
151+
case files.FileTypeConfig:
152+
case files.FileTypeLicense:
153+
case files.FileTypeChatTemplate:
103154
}
104155

105156
// Create layer with relative path annotation
@@ -160,6 +211,71 @@ func FromDirectory(dirPath string) (*Builder, error) {
160211
}, nil
161212
}
162213

214+
// shouldExclude checks if a file or directory should be excluded based on the exclusion patterns.
215+
func shouldExclude(info os.FileInfo, relPath string, exclusions []string) bool {
216+
if len(exclusions) == 0 {
217+
return false
218+
}
219+
220+
name := info.Name()
221+
// Normalize path separators for cross-platform matching
222+
normalizedRelPath := filepath.ToSlash(relPath)
223+
224+
for _, pattern := range exclusions {
225+
// Normalize the pattern
226+
pattern = filepath.ToSlash(pattern)
227+
228+
// Pattern ends with "/" - match directories only
229+
if strings.HasSuffix(pattern, "/") {
230+
if info.IsDir() {
231+
dirPattern := strings.TrimSuffix(pattern, "/")
232+
// Match directory name
233+
if name == dirPattern {
234+
return true
235+
}
236+
// Match full path
237+
if normalizedRelPath == dirPattern || strings.HasPrefix(normalizedRelPath, dirPattern+"/") {
238+
return true
239+
}
240+
}
241+
continue
242+
}
243+
244+
// Pattern contains "/" - treat as path match
245+
if strings.Contains(pattern, "/") {
246+
// Exact path match
247+
if normalizedRelPath == pattern {
248+
return true
249+
}
250+
// Directory path prefix match
251+
if info.IsDir() && strings.HasPrefix(normalizedRelPath+"/", pattern+"/") {
252+
return true
253+
}
254+
// File inside excluded directory
255+
if strings.HasPrefix(normalizedRelPath, pattern+"/") {
256+
return true
257+
}
258+
continue
259+
}
260+
261+
// Pattern contains glob characters - use glob matching
262+
if strings.ContainsAny(pattern, "*?[]") {
263+
matched, err := filepath.Match(pattern, name)
264+
if err == nil && matched {
265+
return true
266+
}
267+
continue
268+
}
269+
270+
// Simple name match (works for both files and directories)
271+
if name == pattern {
272+
return true
273+
}
274+
}
275+
276+
return false
277+
}
278+
163279
// fileTypeToMediaType converts a FileType to the corresponding OCI MediaType
164280
func fileTypeToMediaType(ft files.FileType) oci.MediaType {
165281
switch ft {

0 commit comments

Comments
 (0)