Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

wispignore

nekomimi.pet 126cd693 0172a31f

verified
Changed files
+462 -164
.tangled
workflows
apps
main-app
cli
-1
.tangled/workflows/test.yml
···
# have to regenerate otherwise it wont install necessary dependencies to run
rm -rf bun.lock package-lock.json
-
bun install @oven/bun-linux-aarch64
bun install
- name: run all tests
+74
.wispignore.json
···
+
{
+
"version": "1.0.0",
+
"description": "Default ignore patterns for wisp.place uploads",
+
"patterns": [
+
".git",
+
".git/**",
+
".github",
+
".github/**",
+
".gitlab",
+
".gitlab/**",
+
".DS_Store",
+
".wisp.metadata.json",
+
".env",
+
".env.*",
+
"node_modules",
+
"node_modules/**",
+
"Thumbs.db",
+
"desktop.ini",
+
"._*",
+
".Spotlight-V100",
+
".Spotlight-V100/**",
+
".Trashes",
+
".Trashes/**",
+
".fseventsd",
+
".fseventsd/**",
+
".cache",
+
".cache/**",
+
".temp",
+
".temp/**",
+
".tmp",
+
".tmp/**",
+
"__pycache__",
+
"__pycache__/**",
+
"*.pyc",
+
".venv",
+
".venv/**",
+
"venv",
+
"venv/**",
+
"env",
+
"env/**",
+
"*.swp",
+
"*.swo",
+
"*~",
+
".tangled",
+
".tangled/**"
+
],
+
"comments": {
+
".git": "Version control - thousands of files",
+
".github": "GitHub workflows and configuration",
+
".gitlab": "GitLab CI/CD configuration",
+
".DS_Store": "macOS metadata - can leak info",
+
".wisp.metadata.json": "Wisp internal metadata",
+
".env": "Environment variables with secrets",
+
"node_modules": "Dependency folder - can be 100,000+ files",
+
"Thumbs.db": "Windows thumbnail cache",
+
"desktop.ini": "Windows folder config",
+
"._*": "macOS resource fork files",
+
".Spotlight-V100": "macOS Spotlight index",
+
".Trashes": "macOS trash folder",
+
".fseventsd": "macOS filesystem events",
+
".cache": "Cache directories",
+
".temp": "Temporary directories",
+
".tmp": "Temporary directories",
+
"__pycache__": "Python cache",
+
"*.pyc": "Python compiled files",
+
".venv": "Python virtual environments",
+
"venv": "Python virtual environments",
+
"env": "Python virtual environments",
+
"*.swp": "Vim swap files",
+
"*.swo": "Vim swap files",
+
"*~": "Editor backup files",
+
".tangled": "Tangled directory"
+
}
+
}
+9 -8
apps/main-app/package.json
···
"screenshot": "bun run scripts/screenshot-sites.ts"
},
"dependencies": {
-
"@wisp/lexicons": "workspace:*",
-
"@wisp/constants": "workspace:*",
-
"@wisp/observability": "workspace:*",
-
"@wisp/atproto-utils": "workspace:*",
-
"@wisp/database": "workspace:*",
-
"@wisp/fs-utils": "workspace:*",
"@atproto/api": "^0.17.3",
"@atproto/common-web": "^0.4.5",
"@atproto/jwk-jose": "^0.1.11",
···
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
"@tanstack/react-query": "^5.90.2",
+
"@wisp/atproto-utils": "workspace:*",
+
"@wisp/constants": "workspace:*",
+
"@wisp/database": "workspace:*",
+
"@wisp/fs-utils": "workspace:*",
+
"@wisp/lexicons": "workspace:*",
+
"@wisp/observability": "workspace:*",
"actor-typeahead": "^0.1.1",
"atproto-ui": "^0.11.3",
+
"bun-plugin-tailwind": "^0.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"elysia": "latest",
+
"ignore": "^7.0.5",
"iron-session": "^8.0.4",
"lucide-react": "^0.546.0",
"multiformats": "^13.4.1",
···
"tailwind-merge": "^3.3.1",
"tailwindcss": "4",
"tw-animate-css": "^1.4.0",
-
"zlib": "^1.0.5",
-
"bun-plugin-tailwind": "^0.1.2"
+
"zlib": "^1.0.5"
},
"devDependencies": {
"@types/react": "^19.2.2",
+127
apps/main-app/src/lib/ignore-patterns.ts
···
+
import ignore, { type Ignore } from 'ignore'
+
import { readFileSync, existsSync } from 'fs'
+
import { join } from 'path'
+
+
interface IgnoreConfig {
+
version: string
+
description: string
+
patterns: string[]
+
}
+
+
/**
+
* Load default ignore patterns from the .wispignore.json file in the monorepo root
+
*/
+
function loadDefaultPatterns(): string[] {
+
try {
+
// Path to the default ignore patterns JSON file (monorepo root, 3 levels up from this file)
+
const defaultJsonPath = join(__dirname, '../../../../.wispignore.json')
+
+
if (!existsSync(defaultJsonPath)) {
+
console.warn('⚠️ Default .wispignore.json not found, using hardcoded patterns')
+
return getHardcodedPatterns()
+
}
+
+
const contents = readFileSync(defaultJsonPath, 'utf-8')
+
const config: IgnoreConfig = JSON.parse(contents)
+
return config.patterns
+
} catch (error) {
+
console.error('Failed to load default ignore patterns:', error)
+
return getHardcodedPatterns()
+
}
+
}
+
+
/**
+
* Hardcoded fallback patterns (same as in .wispignore.json)
+
*/
+
function getHardcodedPatterns(): string[] {
+
return [
+
'.git',
+
'.git/**',
+
'.github',
+
'.github/**',
+
'.gitlab',
+
'.gitlab/**',
+
'.DS_Store',
+
'.wisp.metadata.json',
+
'.env',
+
'.env.*',
+
'node_modules',
+
'node_modules/**',
+
'Thumbs.db',
+
'desktop.ini',
+
'._*',
+
'.Spotlight-V100',
+
'.Spotlight-V100/**',
+
'.Trashes',
+
'.Trashes/**',
+
'.fseventsd',
+
'.fseventsd/**',
+
'.cache',
+
'.cache/**',
+
'.temp',
+
'.temp/**',
+
'.tmp',
+
'.tmp/**',
+
'__pycache__',
+
'__pycache__/**',
+
'*.pyc',
+
'.venv',
+
'.venv/**',
+
'venv',
+
'venv/**',
+
'env',
+
'env/**',
+
'*.swp',
+
'*.swo',
+
'*~',
+
'.tangled',
+
'.tangled/**',
+
]
+
}
+
+
/**
+
* Load custom ignore patterns from a .wispignore file
+
* @param wispignoreContent - Content of the .wispignore file (one pattern per line)
+
*/
+
function loadWispignorePatterns(wispignoreContent: string): string[] {
+
return wispignoreContent
+
.split('\n')
+
.map(line => line.trim())
+
.filter(line => line && !line.startsWith('#')) // Skip empty lines and comments
+
}
+
+
/**
+
* Create an ignore matcher
+
* @param customPatterns - Optional custom patterns from a .wispignore file
+
*/
+
export function createIgnoreMatcher(customPatterns?: string[]): Ignore {
+
const ig = ignore()
+
+
// Add default patterns
+
const defaultPatterns = loadDefaultPatterns()
+
ig.add(defaultPatterns)
+
+
// Add custom patterns if provided
+
if (customPatterns && customPatterns.length > 0) {
+
ig.add(customPatterns)
+
console.log(`Loaded ${customPatterns.length} custom patterns from .wispignore`)
+
}
+
+
return ig
+
}
+
+
/**
+
* Check if a file path should be ignored
+
* @param matcher - The ignore matcher
+
* @param filePath - The file path to check (relative to site root)
+
*/
+
export function shouldIgnore(matcher: Ignore, filePath: string): boolean {
+
return matcher.ignores(filePath)
+
}
+
+
/**
+
* Parse .wispignore content and return patterns
+
*/
+
export function parseWispignore(content: string): string[] {
+
return loadWispignorePatterns(content)
+
}
+22 -97
apps/main-app/src/routes/wisp.ts
···
failUploadJob,
addJobListener
} from '../lib/upload-jobs'
+
import { createIgnoreMatcher, shouldIgnore, parseWispignore } from '../lib/ignore-patterns'
+
import type { Ignore } from 'ignore'
const logger = createLogger('main-app')
···
}
}
+
// Check for .wispignore file in uploaded files
+
let customIgnorePatterns: string[] = [];
+
const wispignoreFile = fileArray.find(f => f && f.name && f.name.endsWith('.wispignore'));
+
if (wispignoreFile) {
+
try {
+
const content = await wispignoreFile.text();
+
customIgnorePatterns = parseWispignore(content);
+
console.log(`Found .wispignore file with ${customIgnorePatterns.length} custom patterns`);
+
} catch (err) {
+
console.warn('Failed to parse .wispignore file:', err);
+
}
+
}
+
+
// Create ignore matcher with default and custom patterns
+
const ignoreMatcher = createIgnoreMatcher(customIgnorePatterns);
+
// Convert File objects to UploadedFile format
const uploadedFiles: UploadedFile[] = [];
const skippedFiles: Array<{ name: string; reason: string }> = [];
···
currentFile: file.name
});
-
// Skip unwanted files and directories
+
// Skip files that match ignore patterns
const normalizedPath = file.name.replace(/^[^\/]*\//, '');
-
const fileName = normalizedPath.split('/').pop() || '';
-
const pathParts = normalizedPath.split('/');
-
// .git directory (version control - thousands of files)
-
if (normalizedPath.startsWith('.git/') || normalizedPath === '.git') {
-
console.log(`Skipping .git file: ${file.name}`);
-
skippedFiles.push({
-
name: file.name,
-
reason: '.git directory excluded'
-
});
-
continue;
-
}
-
-
// .DS_Store (macOS metadata - can leak info)
-
if (fileName === '.DS_Store') {
-
console.log(`Skipping .DS_Store file: ${file.name}`);
+
if (shouldIgnore(ignoreMatcher, normalizedPath)) {
+
console.log(`Skipping ignored file: ${file.name}`);
skippedFiles.push({
name: file.name,
-
reason: '.DS_Store file excluded'
-
});
-
continue;
-
}
-
-
// .env files (environment variables with secrets)
-
if (fileName.startsWith('.env')) {
-
console.log(`Skipping .env file: ${file.name}`);
-
skippedFiles.push({
-
name: file.name,
-
reason: 'environment files excluded for security'
-
});
-
continue;
-
}
-
-
// node_modules (dependency folder - can be 100,000+ files)
-
if (pathParts.includes('node_modules')) {
-
console.log(`Skipping node_modules file: ${file.name}`);
-
skippedFiles.push({
-
name: file.name,
-
reason: 'node_modules excluded'
-
});
-
continue;
-
}
-
-
// OS metadata files
-
if (fileName === 'Thumbs.db' || fileName === 'desktop.ini' || fileName.startsWith('._')) {
-
console.log(`Skipping OS metadata file: ${file.name}`);
-
skippedFiles.push({
-
name: file.name,
-
reason: 'OS metadata file excluded'
-
});
-
continue;
-
}
-
-
// macOS system directories
-
if (pathParts.includes('.Spotlight-V100') || pathParts.includes('.Trashes') || pathParts.includes('.fseventsd')) {
-
console.log(`Skipping macOS system file: ${file.name}`);
-
skippedFiles.push({
-
name: file.name,
-
reason: 'macOS system directory excluded'
-
});
-
continue;
-
}
-
-
// Cache and temp directories
-
if (pathParts.some(part => part === '.cache' || part === '.temp' || part === '.tmp')) {
-
console.log(`Skipping cache/temp file: ${file.name}`);
-
skippedFiles.push({
-
name: file.name,
-
reason: 'cache/temp directory excluded'
-
});
-
continue;
-
}
-
-
// Python cache
-
if (pathParts.includes('__pycache__') || fileName.endsWith('.pyc')) {
-
console.log(`Skipping Python cache file: ${file.name}`);
-
skippedFiles.push({
-
name: file.name,
-
reason: 'Python cache excluded'
-
});
-
continue;
-
}
-
-
// Python virtual environments
-
if (pathParts.some(part => part === '.venv' || part === 'venv' || part === 'env')) {
-
console.log(`Skipping Python venv file: ${file.name}`);
-
skippedFiles.push({
-
name: file.name,
-
reason: 'Python virtual environment excluded'
-
});
-
continue;
-
}
-
-
// Editor swap files
-
if (fileName.endsWith('.swp') || fileName.endsWith('.swo') || fileName.endsWith('~')) {
-
console.log(`Skipping editor swap file: ${file.name}`);
-
skippedFiles.push({
-
name: file.name,
-
reason: 'editor swap file excluded'
+
reason: 'matched ignore pattern'
});
continue;
}
+3
bun.lock
···
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"elysia": "latest",
+
"ignore": "^7.0.5",
"iron-session": "^8.0.4",
"lucide-react": "^0.546.0",
"multiformats": "^13.4.1",
···
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
+
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="],
+61 -1
cli/Cargo.lock
···
]
[[package]]
+
name = "bstr"
+
version = "1.12.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+
dependencies = [
+
"memchr",
+
"serde",
+
]
+
+
[[package]]
name = "btree-range-map"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+
dependencies = [
+
"crossbeam-utils",
+
]
+
+
[[package]]
+
name = "crossbeam-deque"
+
version = "0.8.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+
dependencies = [
+
"crossbeam-epoch",
+
"crossbeam-utils",
+
]
+
+
[[package]]
+
name = "crossbeam-epoch"
+
version = "0.9.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
···
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
+
name = "globset"
+
version = "0.4.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+
dependencies = [
+
"aho-corasick",
+
"bstr",
+
"log",
+
"regex-automata",
+
"regex-syntax",
+
]
+
+
[[package]]
name = "gloo-storage"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
dependencies = [
"icu_normalizer",
"icu_properties",
+
]
+
+
[[package]]
+
name = "ignore"
+
version = "0.4.25"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
+
dependencies = [
+
"crossbeam-deque",
+
"globset",
+
"log",
+
"memchr",
+
"regex-automata",
+
"same-file",
+
"walkdir",
+
"winapi-util",
[[package]]
···
[[package]]
name = "wisp-cli"
-
version = "0.4.1"
+
version = "0.4.2"
dependencies = [
"axum",
"base64 0.22.1",
···
"clap",
"flate2",
"futures",
+
"globset",
+
"ignore",
"jacquard",
"jacquard-api",
"jacquard-common",
+3 -1
cli/Cargo.toml
···
[package]
name = "wisp-cli"
-
version = "0.4.1"
+
version = "0.4.2"
edition = "2024"
[features]
···
chrono = "0.4"
url = "2.5"
regex = "1.11"
+
ignore = "0.4"
+
globset = "0.4"
+149
cli/src/ignore_patterns.rs
···
+
use globset::{Glob, GlobSet, GlobSetBuilder};
+
use serde::{Deserialize, Serialize};
+
use std::path::Path;
+
use miette::IntoDiagnostic;
+
+
#[derive(Debug, Deserialize, Serialize)]
+
struct IgnoreConfig {
+
patterns: Vec<String>,
+
}
+
+
/// Load ignore patterns from the default .wispignore.json file
+
fn load_default_patterns() -> miette::Result<Vec<String>> {
+
// Path to the default ignore patterns JSON file (in the monorepo root)
+
let default_json_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../.wispignore.json");
+
+
match std::fs::read_to_string(default_json_path) {
+
Ok(contents) => {
+
let config: IgnoreConfig = serde_json::from_str(&contents).into_diagnostic()?;
+
Ok(config.patterns)
+
}
+
Err(_) => {
+
// If the default file doesn't exist, return hardcoded patterns as fallback
+
eprintln!("⚠️ Default .wispignore.json not found, using hardcoded patterns");
+
Ok(get_hardcoded_patterns())
+
}
+
}
+
}
+
+
/// Hardcoded fallback patterns (same as in .wispignore.json)
+
fn get_hardcoded_patterns() -> Vec<String> {
+
vec![
+
".git".to_string(),
+
".git/**".to_string(),
+
".github".to_string(),
+
".github/**".to_string(),
+
".gitlab".to_string(),
+
".gitlab/**".to_string(),
+
".DS_Store".to_string(),
+
".wisp.metadata.json".to_string(),
+
".env".to_string(),
+
".env.*".to_string(),
+
"node_modules".to_string(),
+
"node_modules/**".to_string(),
+
"Thumbs.db".to_string(),
+
"desktop.ini".to_string(),
+
"._*".to_string(),
+
".Spotlight-V100".to_string(),
+
".Spotlight-V100/**".to_string(),
+
".Trashes".to_string(),
+
".Trashes/**".to_string(),
+
".fseventsd".to_string(),
+
".fseventsd/**".to_string(),
+
".cache".to_string(),
+
".cache/**".to_string(),
+
".temp".to_string(),
+
".temp/**".to_string(),
+
".tmp".to_string(),
+
".tmp/**".to_string(),
+
"__pycache__".to_string(),
+
"__pycache__/**".to_string(),
+
"*.pyc".to_string(),
+
".venv".to_string(),
+
".venv/**".to_string(),
+
"venv".to_string(),
+
"venv/**".to_string(),
+
"env".to_string(),
+
"env/**".to_string(),
+
"*.swp".to_string(),
+
"*.swo".to_string(),
+
"*~".to_string(),
+
".tangled".to_string(),
+
".tangled/**".to_string(),
+
]
+
}
+
+
/// Load custom ignore patterns from a .wispignore file in the given directory
+
fn load_wispignore_file(dir_path: &Path) -> miette::Result<Vec<String>> {
+
let wispignore_path = dir_path.join(".wispignore");
+
+
if !wispignore_path.exists() {
+
return Ok(Vec::new());
+
}
+
+
let contents = std::fs::read_to_string(&wispignore_path).into_diagnostic()?;
+
+
// Parse gitignore-style file (one pattern per line, # for comments)
+
let patterns: Vec<String> = contents
+
.lines()
+
.filter_map(|line| {
+
let line = line.trim();
+
// Skip empty lines and comments
+
if line.is_empty() || line.starts_with('#') {
+
None
+
} else {
+
Some(line.to_string())
+
}
+
})
+
.collect();
+
+
if !patterns.is_empty() {
+
println!("Loaded {} custom patterns from .wispignore", patterns.len());
+
}
+
+
Ok(patterns)
+
}
+
+
/// Build a GlobSet from a list of patterns
+
fn build_globset(patterns: Vec<String>) -> miette::Result<GlobSet> {
+
let mut builder = GlobSetBuilder::new();
+
+
for pattern in patterns {
+
let glob = Glob::new(&pattern).into_diagnostic()?;
+
builder.add(glob);
+
}
+
+
let globset = builder.build().into_diagnostic()?;
+
Ok(globset)
+
}
+
+
/// IgnoreMatcher handles checking if paths should be ignored
+
pub struct IgnoreMatcher {
+
globset: GlobSet,
+
}
+
+
impl IgnoreMatcher {
+
/// Create a new IgnoreMatcher for the given directory
+
/// Loads default patterns and any custom .wispignore file
+
pub fn new(dir_path: &Path) -> miette::Result<Self> {
+
let mut all_patterns = load_default_patterns()?;
+
+
// Load custom patterns from .wispignore
+
let custom_patterns = load_wispignore_file(dir_path)?;
+
all_patterns.extend(custom_patterns);
+
+
let globset = build_globset(all_patterns)?;
+
+
Ok(Self { globset })
+
}
+
+
/// Check if the given path (relative to site root) should be ignored
+
pub fn is_ignored(&self, path: &str) -> bool {
+
self.globset.is_match(path)
+
}
+
+
/// Check if a filename should be ignored (checks just the filename, not full path)
+
pub fn is_filename_ignored(&self, filename: &str) -> bool {
+
self.globset.is_match(filename)
+
}
+
}
+14 -56
cli/src/main.rs
···
mod serve;
mod subfs_utils;
mod redirects;
+
mod ignore_patterns;
use clap::{Parser, Subcommand};
use jacquard::CowStr;
···
}
};
-
// Build directory tree
-
let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new()).await?;
+
// Build directory tree with ignore patterns
+
let ignore_matcher = ignore_patterns::IgnoreMatcher::new(&path)?;
+
let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new(), &ignore_matcher).await?;
let uploaded_count = total_files - reused_count;
// Check if we need to split into subfs records
···
dir_path: &'a Path,
existing_blobs: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
current_path: String,
+
ignore_matcher: &'a ignore_patterns::IgnoreMatcher,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<(Directory<'static>, usize, usize)>> + 'a>>
{
Box::pin(async move {
···
.ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))?
.to_string();
-
// Skip unwanted files and directories
-
-
// .git directory (version control - thousands of files)
-
if name_str == ".git" {
-
continue;
-
}
-
-
// .DS_Store (macOS metadata - can leak info)
-
if name_str == ".DS_Store" {
-
continue;
-
}
-
-
// .wisp.metadata.json (wisp internal metadata - should not be uploaded)
-
if name_str == ".wisp.metadata.json" {
-
continue;
-
}
-
-
// .env files (environment variables with secrets)
-
if name_str.starts_with(".env") {
-
continue;
-
}
-
-
// node_modules (dependency folder - can be 100,000+ files)
-
if name_str == "node_modules" {
-
continue;
-
}
-
-
// OS metadata files
-
if name_str == "Thumbs.db" || name_str == "desktop.ini" || name_str.starts_with("._") {
-
continue;
-
}
+
// Construct full path for ignore checking
+
let full_path = if current_path.is_empty() {
+
name_str.clone()
+
} else {
+
format!("{}/{}", current_path, name_str)
+
};
-
// macOS system directories
-
if name_str == ".Spotlight-V100" || name_str == ".Trashes" || name_str == ".fseventsd" {
-
continue;
-
}
-
-
// Cache and temp directories
-
if name_str == ".cache" || name_str == ".temp" || name_str == ".tmp" {
-
continue;
-
}
-
-
// Python cache
-
if name_str == "__pycache__" || name_str.ends_with(".pyc") {
-
continue;
-
}
-
-
// Python virtual environments
-
if name_str == ".venv" || name_str == "venv" || name_str == "env" {
-
continue;
-
}
-
-
// Editor swap files
-
if name_str.ends_with(".swp") || name_str.ends_with(".swo") || name_str.ends_with("~") {
+
// Skip files/directories that match ignore patterns
+
if ignore_matcher.is_ignored(&full_path) || ignore_matcher.is_filename_ignored(&name_str) {
continue;
}
···
} else {
format!("{}/{}", current_path, name)
};
-
let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path).await?;
+
let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path, ignore_matcher).await?;
dir_entries.push(Entry::new()
.name(CowStr::from(name))
.node(EntryNode::Directory(Box::new(subdir)))