package imgcache import ( "context" "errors" "fmt" "io" "io/fs" "net/http" "strings" ) // MockFetcher implements the Fetcher interface using an embedded filesystem. // Files are organized as: hostname/path/to/file.ext // URLs like https://example.com/images/photo.jpg map to example.com/images/photo.jpg type MockFetcher struct { fs fs.FS } // NewMockFetcher creates a new mock fetcher backed by the given filesystem. func NewMockFetcher(fsys fs.FS) *MockFetcher { return &MockFetcher{fs: fsys} } // Fetch retrieves content from the mock filesystem. func (m *MockFetcher) Fetch(ctx context.Context, url string) (*FetchResult, error) { // Check context cancellation select { case <-ctx.Done(): return nil, ctx.Err() default: } // Parse URL to get filesystem path path, err := urlToFSPath(url) if err != nil { return nil, err } // Open the file f, err := m.fs.Open(path) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("%w: status 404", ErrUpstreamError) } return nil, fmt.Errorf("failed to open mock file: %w", err) } // Get file info for content length stat, err := f.Stat() if err != nil { _ = f.Close() return nil, fmt.Errorf("failed to stat mock file: %w", err) } // Detect content type from extension contentType := detectContentTypeFromPath(path) return &FetchResult{ Content: f.(io.ReadCloser), ContentLength: stat.Size(), ContentType: contentType, Headers: make(http.Header), }, nil } // urlToFSPath converts a URL to a filesystem path. // https://example.com/images/photo.jpg -> example.com/images/photo.jpg func urlToFSPath(rawURL string) (string, error) { // Strip scheme url := rawURL if idx := strings.Index(url, "://"); idx != -1 { url = url[idx+3:] } // Remove query string if idx := strings.Index(url, "?"); idx != -1 { url = url[:idx] } // Remove fragment if idx := strings.Index(url, "#"); idx != -1 { url = url[:idx] } if url == "" { return "", errors.New("empty URL path") } return url, nil } // detectContentTypeFromPath returns the MIME type based on file extension. func detectContentTypeFromPath(path string) string { path = strings.ToLower(path) switch { case strings.HasSuffix(path, ".jpg"), strings.HasSuffix(path, ".jpeg"): return "image/jpeg" case strings.HasSuffix(path, ".png"): return "image/png" case strings.HasSuffix(path, ".gif"): return "image/gif" case strings.HasSuffix(path, ".webp"): return "image/webp" case strings.HasSuffix(path, ".avif"): return "image/avif" case strings.HasSuffix(path, ".svg"): return "image/svg+xml" default: return "application/octet-stream" } }