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

wispignore

nekomimi.pet 126cd693 0172a31f

verified
Changed files
+462 -164
.tangled
workflows
apps
main-app
cli
-1
.tangled/workflows/test.yml
··· 17 17 18 18 # have to regenerate otherwise it wont install necessary dependencies to run 19 19 rm -rf bun.lock package-lock.json 20 - bun install @oven/bun-linux-aarch64 21 20 bun install 22 21 23 22 - name: run all tests
+74
.wispignore.json
··· 1 + { 2 + "version": "1.0.0", 3 + "description": "Default ignore patterns for wisp.place uploads", 4 + "patterns": [ 5 + ".git", 6 + ".git/**", 7 + ".github", 8 + ".github/**", 9 + ".gitlab", 10 + ".gitlab/**", 11 + ".DS_Store", 12 + ".wisp.metadata.json", 13 + ".env", 14 + ".env.*", 15 + "node_modules", 16 + "node_modules/**", 17 + "Thumbs.db", 18 + "desktop.ini", 19 + "._*", 20 + ".Spotlight-V100", 21 + ".Spotlight-V100/**", 22 + ".Trashes", 23 + ".Trashes/**", 24 + ".fseventsd", 25 + ".fseventsd/**", 26 + ".cache", 27 + ".cache/**", 28 + ".temp", 29 + ".temp/**", 30 + ".tmp", 31 + ".tmp/**", 32 + "__pycache__", 33 + "__pycache__/**", 34 + "*.pyc", 35 + ".venv", 36 + ".venv/**", 37 + "venv", 38 + "venv/**", 39 + "env", 40 + "env/**", 41 + "*.swp", 42 + "*.swo", 43 + "*~", 44 + ".tangled", 45 + ".tangled/**" 46 + ], 47 + "comments": { 48 + ".git": "Version control - thousands of files", 49 + ".github": "GitHub workflows and configuration", 50 + ".gitlab": "GitLab CI/CD configuration", 51 + ".DS_Store": "macOS metadata - can leak info", 52 + ".wisp.metadata.json": "Wisp internal metadata", 53 + ".env": "Environment variables with secrets", 54 + "node_modules": "Dependency folder - can be 100,000+ files", 55 + "Thumbs.db": "Windows thumbnail cache", 56 + "desktop.ini": "Windows folder config", 57 + "._*": "macOS resource fork files", 58 + ".Spotlight-V100": "macOS Spotlight index", 59 + ".Trashes": "macOS trash folder", 60 + ".fseventsd": "macOS filesystem events", 61 + ".cache": "Cache directories", 62 + ".temp": "Temporary directories", 63 + ".tmp": "Temporary directories", 64 + "__pycache__": "Python cache", 65 + "*.pyc": "Python compiled files", 66 + ".venv": "Python virtual environments", 67 + "venv": "Python virtual environments", 68 + "env": "Python virtual environments", 69 + "*.swp": "Vim swap files", 70 + "*.swo": "Vim swap files", 71 + "*~": "Editor backup files", 72 + ".tangled": "Tangled directory" 73 + } 74 + }
+9 -8
apps/main-app/package.json
··· 10 10 "screenshot": "bun run scripts/screenshot-sites.ts" 11 11 }, 12 12 "dependencies": { 13 - "@wisp/lexicons": "workspace:*", 14 - "@wisp/constants": "workspace:*", 15 - "@wisp/observability": "workspace:*", 16 - "@wisp/atproto-utils": "workspace:*", 17 - "@wisp/database": "workspace:*", 18 - "@wisp/fs-utils": "workspace:*", 19 13 "@atproto/api": "^0.17.3", 20 14 "@atproto/common-web": "^0.4.5", 21 15 "@atproto/jwk-jose": "^0.1.11", ··· 34 28 "@radix-ui/react-slot": "^1.2.3", 35 29 "@radix-ui/react-tabs": "^1.1.13", 36 30 "@tanstack/react-query": "^5.90.2", 31 + "@wisp/atproto-utils": "workspace:*", 32 + "@wisp/constants": "workspace:*", 33 + "@wisp/database": "workspace:*", 34 + "@wisp/fs-utils": "workspace:*", 35 + "@wisp/lexicons": "workspace:*", 36 + "@wisp/observability": "workspace:*", 37 37 "actor-typeahead": "^0.1.1", 38 38 "atproto-ui": "^0.11.3", 39 + "bun-plugin-tailwind": "^0.1.2", 39 40 "class-variance-authority": "^0.7.1", 40 41 "clsx": "^2.1.1", 41 42 "elysia": "latest", 43 + "ignore": "^7.0.5", 42 44 "iron-session": "^8.0.4", 43 45 "lucide-react": "^0.546.0", 44 46 "multiformats": "^13.4.1", ··· 48 50 "tailwind-merge": "^3.3.1", 49 51 "tailwindcss": "4", 50 52 "tw-animate-css": "^1.4.0", 51 - "zlib": "^1.0.5", 52 - "bun-plugin-tailwind": "^0.1.2" 53 + "zlib": "^1.0.5" 53 54 }, 54 55 "devDependencies": { 55 56 "@types/react": "^19.2.2",
+127
apps/main-app/src/lib/ignore-patterns.ts
··· 1 + import ignore, { type Ignore } from 'ignore' 2 + import { readFileSync, existsSync } from 'fs' 3 + import { join } from 'path' 4 + 5 + interface IgnoreConfig { 6 + version: string 7 + description: string 8 + patterns: string[] 9 + } 10 + 11 + /** 12 + * Load default ignore patterns from the .wispignore.json file in the monorepo root 13 + */ 14 + function loadDefaultPatterns(): string[] { 15 + try { 16 + // Path to the default ignore patterns JSON file (monorepo root, 3 levels up from this file) 17 + const defaultJsonPath = join(__dirname, '../../../../.wispignore.json') 18 + 19 + if (!existsSync(defaultJsonPath)) { 20 + console.warn('⚠️ Default .wispignore.json not found, using hardcoded patterns') 21 + return getHardcodedPatterns() 22 + } 23 + 24 + const contents = readFileSync(defaultJsonPath, 'utf-8') 25 + const config: IgnoreConfig = JSON.parse(contents) 26 + return config.patterns 27 + } catch (error) { 28 + console.error('Failed to load default ignore patterns:', error) 29 + return getHardcodedPatterns() 30 + } 31 + } 32 + 33 + /** 34 + * Hardcoded fallback patterns (same as in .wispignore.json) 35 + */ 36 + function getHardcodedPatterns(): string[] { 37 + return [ 38 + '.git', 39 + '.git/**', 40 + '.github', 41 + '.github/**', 42 + '.gitlab', 43 + '.gitlab/**', 44 + '.DS_Store', 45 + '.wisp.metadata.json', 46 + '.env', 47 + '.env.*', 48 + 'node_modules', 49 + 'node_modules/**', 50 + 'Thumbs.db', 51 + 'desktop.ini', 52 + '._*', 53 + '.Spotlight-V100', 54 + '.Spotlight-V100/**', 55 + '.Trashes', 56 + '.Trashes/**', 57 + '.fseventsd', 58 + '.fseventsd/**', 59 + '.cache', 60 + '.cache/**', 61 + '.temp', 62 + '.temp/**', 63 + '.tmp', 64 + '.tmp/**', 65 + '__pycache__', 66 + '__pycache__/**', 67 + '*.pyc', 68 + '.venv', 69 + '.venv/**', 70 + 'venv', 71 + 'venv/**', 72 + 'env', 73 + 'env/**', 74 + '*.swp', 75 + '*.swo', 76 + '*~', 77 + '.tangled', 78 + '.tangled/**', 79 + ] 80 + } 81 + 82 + /** 83 + * Load custom ignore patterns from a .wispignore file 84 + * @param wispignoreContent - Content of the .wispignore file (one pattern per line) 85 + */ 86 + function loadWispignorePatterns(wispignoreContent: string): string[] { 87 + return wispignoreContent 88 + .split('\n') 89 + .map(line => line.trim()) 90 + .filter(line => line && !line.startsWith('#')) // Skip empty lines and comments 91 + } 92 + 93 + /** 94 + * Create an ignore matcher 95 + * @param customPatterns - Optional custom patterns from a .wispignore file 96 + */ 97 + export function createIgnoreMatcher(customPatterns?: string[]): Ignore { 98 + const ig = ignore() 99 + 100 + // Add default patterns 101 + const defaultPatterns = loadDefaultPatterns() 102 + ig.add(defaultPatterns) 103 + 104 + // Add custom patterns if provided 105 + if (customPatterns && customPatterns.length > 0) { 106 + ig.add(customPatterns) 107 + console.log(`Loaded ${customPatterns.length} custom patterns from .wispignore`) 108 + } 109 + 110 + return ig 111 + } 112 + 113 + /** 114 + * Check if a file path should be ignored 115 + * @param matcher - The ignore matcher 116 + * @param filePath - The file path to check (relative to site root) 117 + */ 118 + export function shouldIgnore(matcher: Ignore, filePath: string): boolean { 119 + return matcher.ignores(filePath) 120 + } 121 + 122 + /** 123 + * Parse .wispignore content and return patterns 124 + */ 125 + export function parseWispignore(content: string): string[] { 126 + return loadWispignorePatterns(content) 127 + }
+22 -97
apps/main-app/src/routes/wisp.ts
··· 34 34 failUploadJob, 35 35 addJobListener 36 36 } from '../lib/upload-jobs' 37 + import { createIgnoreMatcher, shouldIgnore, parseWispignore } from '../lib/ignore-patterns' 38 + import type { Ignore } from 'ignore' 37 39 38 40 const logger = createLogger('main-app') 39 41 ··· 145 147 } 146 148 } 147 149 150 + // Check for .wispignore file in uploaded files 151 + let customIgnorePatterns: string[] = []; 152 + const wispignoreFile = fileArray.find(f => f && f.name && f.name.endsWith('.wispignore')); 153 + if (wispignoreFile) { 154 + try { 155 + const content = await wispignoreFile.text(); 156 + customIgnorePatterns = parseWispignore(content); 157 + console.log(`Found .wispignore file with ${customIgnorePatterns.length} custom patterns`); 158 + } catch (err) { 159 + console.warn('Failed to parse .wispignore file:', err); 160 + } 161 + } 162 + 163 + // Create ignore matcher with default and custom patterns 164 + const ignoreMatcher = createIgnoreMatcher(customIgnorePatterns); 165 + 148 166 // Convert File objects to UploadedFile format 149 167 const uploadedFiles: UploadedFile[] = []; 150 168 const skippedFiles: Array<{ name: string; reason: string }> = []; ··· 171 189 currentFile: file.name 172 190 }); 173 191 174 - // Skip unwanted files and directories 192 + // Skip files that match ignore patterns 175 193 const normalizedPath = file.name.replace(/^[^\/]*\//, ''); 176 - const fileName = normalizedPath.split('/').pop() || ''; 177 - const pathParts = normalizedPath.split('/'); 178 194 179 - // .git directory (version control - thousands of files) 180 - if (normalizedPath.startsWith('.git/') || normalizedPath === '.git') { 181 - console.log(`Skipping .git file: ${file.name}`); 182 - skippedFiles.push({ 183 - name: file.name, 184 - reason: '.git directory excluded' 185 - }); 186 - continue; 187 - } 188 - 189 - // .DS_Store (macOS metadata - can leak info) 190 - if (fileName === '.DS_Store') { 191 - console.log(`Skipping .DS_Store file: ${file.name}`); 195 + if (shouldIgnore(ignoreMatcher, normalizedPath)) { 196 + console.log(`Skipping ignored file: ${file.name}`); 192 197 skippedFiles.push({ 193 198 name: file.name, 194 - reason: '.DS_Store file excluded' 195 - }); 196 - continue; 197 - } 198 - 199 - // .env files (environment variables with secrets) 200 - if (fileName.startsWith('.env')) { 201 - console.log(`Skipping .env file: ${file.name}`); 202 - skippedFiles.push({ 203 - name: file.name, 204 - reason: 'environment files excluded for security' 205 - }); 206 - continue; 207 - } 208 - 209 - // node_modules (dependency folder - can be 100,000+ files) 210 - if (pathParts.includes('node_modules')) { 211 - console.log(`Skipping node_modules file: ${file.name}`); 212 - skippedFiles.push({ 213 - name: file.name, 214 - reason: 'node_modules excluded' 215 - }); 216 - continue; 217 - } 218 - 219 - // OS metadata files 220 - if (fileName === 'Thumbs.db' || fileName === 'desktop.ini' || fileName.startsWith('._')) { 221 - console.log(`Skipping OS metadata file: ${file.name}`); 222 - skippedFiles.push({ 223 - name: file.name, 224 - reason: 'OS metadata file excluded' 225 - }); 226 - continue; 227 - } 228 - 229 - // macOS system directories 230 - if (pathParts.includes('.Spotlight-V100') || pathParts.includes('.Trashes') || pathParts.includes('.fseventsd')) { 231 - console.log(`Skipping macOS system file: ${file.name}`); 232 - skippedFiles.push({ 233 - name: file.name, 234 - reason: 'macOS system directory excluded' 235 - }); 236 - continue; 237 - } 238 - 239 - // Cache and temp directories 240 - if (pathParts.some(part => part === '.cache' || part === '.temp' || part === '.tmp')) { 241 - console.log(`Skipping cache/temp file: ${file.name}`); 242 - skippedFiles.push({ 243 - name: file.name, 244 - reason: 'cache/temp directory excluded' 245 - }); 246 - continue; 247 - } 248 - 249 - // Python cache 250 - if (pathParts.includes('__pycache__') || fileName.endsWith('.pyc')) { 251 - console.log(`Skipping Python cache file: ${file.name}`); 252 - skippedFiles.push({ 253 - name: file.name, 254 - reason: 'Python cache excluded' 255 - }); 256 - continue; 257 - } 258 - 259 - // Python virtual environments 260 - if (pathParts.some(part => part === '.venv' || part === 'venv' || part === 'env')) { 261 - console.log(`Skipping Python venv file: ${file.name}`); 262 - skippedFiles.push({ 263 - name: file.name, 264 - reason: 'Python virtual environment excluded' 265 - }); 266 - continue; 267 - } 268 - 269 - // Editor swap files 270 - if (fileName.endsWith('.swp') || fileName.endsWith('.swo') || fileName.endsWith('~')) { 271 - console.log(`Skipping editor swap file: ${file.name}`); 272 - skippedFiles.push({ 273 - name: file.name, 274 - reason: 'editor swap file excluded' 199 + reason: 'matched ignore pattern' 275 200 }); 276 201 continue; 277 202 }
+3
bun.lock
··· 73 73 "class-variance-authority": "^0.7.1", 74 74 "clsx": "^2.1.1", 75 75 "elysia": "latest", 76 + "ignore": "^7.0.5", 76 77 "iron-session": "^8.0.4", 77 78 "lucide-react": "^0.546.0", 78 79 "multiformats": "^13.4.1", ··· 768 769 "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], 769 770 770 771 "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], 772 + 773 + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], 771 774 772 775 "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=="], 773 776
+61 -1
cli/Cargo.lock
··· 360 360 ] 361 361 362 362 [[package]] 363 + name = "bstr" 364 + version = "1.12.1" 365 + source = "registry+https://github.com/rust-lang/crates.io-index" 366 + checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" 367 + dependencies = [ 368 + "memchr", 369 + "serde", 370 + ] 371 + 372 + [[package]] 363 373 name = "btree-range-map" 364 374 version = "0.7.2" 365 375 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 674 684 version = "0.5.15" 675 685 source = "registry+https://github.com/rust-lang/crates.io-index" 676 686 checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 687 + dependencies = [ 688 + "crossbeam-utils", 689 + ] 690 + 691 + [[package]] 692 + name = "crossbeam-deque" 693 + version = "0.8.6" 694 + source = "registry+https://github.com/rust-lang/crates.io-index" 695 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 696 + dependencies = [ 697 + "crossbeam-epoch", 698 + "crossbeam-utils", 699 + ] 700 + 701 + [[package]] 702 + name = "crossbeam-epoch" 703 + version = "0.9.18" 704 + source = "registry+https://github.com/rust-lang/crates.io-index" 705 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 677 706 dependencies = [ 678 707 "crossbeam-utils", 679 708 ] ··· 1209 1238 checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 1210 1239 1211 1240 [[package]] 1241 + name = "globset" 1242 + version = "0.4.18" 1243 + source = "registry+https://github.com/rust-lang/crates.io-index" 1244 + checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" 1245 + dependencies = [ 1246 + "aho-corasick", 1247 + "bstr", 1248 + "log", 1249 + "regex-automata", 1250 + "regex-syntax", 1251 + ] 1252 + 1253 + [[package]] 1212 1254 name = "gloo-storage" 1213 1255 version = "0.3.0" 1214 1256 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1649 1691 dependencies = [ 1650 1692 "icu_normalizer", 1651 1693 "icu_properties", 1694 + ] 1695 + 1696 + [[package]] 1697 + name = "ignore" 1698 + version = "0.4.25" 1699 + source = "registry+https://github.com/rust-lang/crates.io-index" 1700 + checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" 1701 + dependencies = [ 1702 + "crossbeam-deque", 1703 + "globset", 1704 + "log", 1705 + "memchr", 1706 + "regex-automata", 1707 + "same-file", 1708 + "walkdir", 1709 + "winapi-util", 1652 1710 ] 1653 1711 1654 1712 [[package]] ··· 4939 4997 4940 4998 [[package]] 4941 4999 name = "wisp-cli" 4942 - version = "0.4.1" 5000 + version = "0.4.2" 4943 5001 dependencies = [ 4944 5002 "axum", 4945 5003 "base64 0.22.1", ··· 4948 5006 "clap", 4949 5007 "flate2", 4950 5008 "futures", 5009 + "globset", 5010 + "ignore", 4951 5011 "jacquard", 4952 5012 "jacquard-api", 4953 5013 "jacquard-common",
+3 -1
cli/Cargo.toml
··· 1 1 [package] 2 2 name = "wisp-cli" 3 - version = "0.4.1" 3 + version = "0.4.2" 4 4 edition = "2024" 5 5 6 6 [features] ··· 40 40 chrono = "0.4" 41 41 url = "2.5" 42 42 regex = "1.11" 43 + ignore = "0.4" 44 + globset = "0.4"
+149
cli/src/ignore_patterns.rs
··· 1 + use globset::{Glob, GlobSet, GlobSetBuilder}; 2 + use serde::{Deserialize, Serialize}; 3 + use std::path::Path; 4 + use miette::IntoDiagnostic; 5 + 6 + #[derive(Debug, Deserialize, Serialize)] 7 + struct IgnoreConfig { 8 + patterns: Vec<String>, 9 + } 10 + 11 + /// Load ignore patterns from the default .wispignore.json file 12 + fn load_default_patterns() -> miette::Result<Vec<String>> { 13 + // Path to the default ignore patterns JSON file (in the monorepo root) 14 + let default_json_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../.wispignore.json"); 15 + 16 + match std::fs::read_to_string(default_json_path) { 17 + Ok(contents) => { 18 + let config: IgnoreConfig = serde_json::from_str(&contents).into_diagnostic()?; 19 + Ok(config.patterns) 20 + } 21 + Err(_) => { 22 + // If the default file doesn't exist, return hardcoded patterns as fallback 23 + eprintln!("⚠️ Default .wispignore.json not found, using hardcoded patterns"); 24 + Ok(get_hardcoded_patterns()) 25 + } 26 + } 27 + } 28 + 29 + /// Hardcoded fallback patterns (same as in .wispignore.json) 30 + fn get_hardcoded_patterns() -> Vec<String> { 31 + vec![ 32 + ".git".to_string(), 33 + ".git/**".to_string(), 34 + ".github".to_string(), 35 + ".github/**".to_string(), 36 + ".gitlab".to_string(), 37 + ".gitlab/**".to_string(), 38 + ".DS_Store".to_string(), 39 + ".wisp.metadata.json".to_string(), 40 + ".env".to_string(), 41 + ".env.*".to_string(), 42 + "node_modules".to_string(), 43 + "node_modules/**".to_string(), 44 + "Thumbs.db".to_string(), 45 + "desktop.ini".to_string(), 46 + "._*".to_string(), 47 + ".Spotlight-V100".to_string(), 48 + ".Spotlight-V100/**".to_string(), 49 + ".Trashes".to_string(), 50 + ".Trashes/**".to_string(), 51 + ".fseventsd".to_string(), 52 + ".fseventsd/**".to_string(), 53 + ".cache".to_string(), 54 + ".cache/**".to_string(), 55 + ".temp".to_string(), 56 + ".temp/**".to_string(), 57 + ".tmp".to_string(), 58 + ".tmp/**".to_string(), 59 + "__pycache__".to_string(), 60 + "__pycache__/**".to_string(), 61 + "*.pyc".to_string(), 62 + ".venv".to_string(), 63 + ".venv/**".to_string(), 64 + "venv".to_string(), 65 + "venv/**".to_string(), 66 + "env".to_string(), 67 + "env/**".to_string(), 68 + "*.swp".to_string(), 69 + "*.swo".to_string(), 70 + "*~".to_string(), 71 + ".tangled".to_string(), 72 + ".tangled/**".to_string(), 73 + ] 74 + } 75 + 76 + /// Load custom ignore patterns from a .wispignore file in the given directory 77 + fn load_wispignore_file(dir_path: &Path) -> miette::Result<Vec<String>> { 78 + let wispignore_path = dir_path.join(".wispignore"); 79 + 80 + if !wispignore_path.exists() { 81 + return Ok(Vec::new()); 82 + } 83 + 84 + let contents = std::fs::read_to_string(&wispignore_path).into_diagnostic()?; 85 + 86 + // Parse gitignore-style file (one pattern per line, # for comments) 87 + let patterns: Vec<String> = contents 88 + .lines() 89 + .filter_map(|line| { 90 + let line = line.trim(); 91 + // Skip empty lines and comments 92 + if line.is_empty() || line.starts_with('#') { 93 + None 94 + } else { 95 + Some(line.to_string()) 96 + } 97 + }) 98 + .collect(); 99 + 100 + if !patterns.is_empty() { 101 + println!("Loaded {} custom patterns from .wispignore", patterns.len()); 102 + } 103 + 104 + Ok(patterns) 105 + } 106 + 107 + /// Build a GlobSet from a list of patterns 108 + fn build_globset(patterns: Vec<String>) -> miette::Result<GlobSet> { 109 + let mut builder = GlobSetBuilder::new(); 110 + 111 + for pattern in patterns { 112 + let glob = Glob::new(&pattern).into_diagnostic()?; 113 + builder.add(glob); 114 + } 115 + 116 + let globset = builder.build().into_diagnostic()?; 117 + Ok(globset) 118 + } 119 + 120 + /// IgnoreMatcher handles checking if paths should be ignored 121 + pub struct IgnoreMatcher { 122 + globset: GlobSet, 123 + } 124 + 125 + impl IgnoreMatcher { 126 + /// Create a new IgnoreMatcher for the given directory 127 + /// Loads default patterns and any custom .wispignore file 128 + pub fn new(dir_path: &Path) -> miette::Result<Self> { 129 + let mut all_patterns = load_default_patterns()?; 130 + 131 + // Load custom patterns from .wispignore 132 + let custom_patterns = load_wispignore_file(dir_path)?; 133 + all_patterns.extend(custom_patterns); 134 + 135 + let globset = build_globset(all_patterns)?; 136 + 137 + Ok(Self { globset }) 138 + } 139 + 140 + /// Check if the given path (relative to site root) should be ignored 141 + pub fn is_ignored(&self, path: &str) -> bool { 142 + self.globset.is_match(path) 143 + } 144 + 145 + /// Check if a filename should be ignored (checks just the filename, not full path) 146 + pub fn is_filename_ignored(&self, filename: &str) -> bool { 147 + self.globset.is_match(filename) 148 + } 149 + }
+14 -56
cli/src/main.rs
··· 8 8 mod serve; 9 9 mod subfs_utils; 10 10 mod redirects; 11 + mod ignore_patterns; 11 12 12 13 use clap::{Parser, Subcommand}; 13 14 use jacquard::CowStr; ··· 323 324 } 324 325 }; 325 326 326 - // Build directory tree 327 - let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new()).await?; 327 + // Build directory tree with ignore patterns 328 + let ignore_matcher = ignore_patterns::IgnoreMatcher::new(&path)?; 329 + let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new(), &ignore_matcher).await?; 328 330 let uploaded_count = total_files - reused_count; 329 331 330 332 // Check if we need to split into subfs records ··· 603 605 dir_path: &'a Path, 604 606 existing_blobs: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>, 605 607 current_path: String, 608 + ignore_matcher: &'a ignore_patterns::IgnoreMatcher, 606 609 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<(Directory<'static>, usize, usize)>> + 'a>> 607 610 { 608 611 Box::pin(async move { ··· 623 626 .ok_or_else(|| miette::miette!("Invalid filename: {:?}", name))? 624 627 .to_string(); 625 628 626 - // Skip unwanted files and directories 627 - 628 - // .git directory (version control - thousands of files) 629 - if name_str == ".git" { 630 - continue; 631 - } 632 - 633 - // .DS_Store (macOS metadata - can leak info) 634 - if name_str == ".DS_Store" { 635 - continue; 636 - } 637 - 638 - // .wisp.metadata.json (wisp internal metadata - should not be uploaded) 639 - if name_str == ".wisp.metadata.json" { 640 - continue; 641 - } 642 - 643 - // .env files (environment variables with secrets) 644 - if name_str.starts_with(".env") { 645 - continue; 646 - } 647 - 648 - // node_modules (dependency folder - can be 100,000+ files) 649 - if name_str == "node_modules" { 650 - continue; 651 - } 652 - 653 - // OS metadata files 654 - if name_str == "Thumbs.db" || name_str == "desktop.ini" || name_str.starts_with("._") { 655 - continue; 656 - } 629 + // Construct full path for ignore checking 630 + let full_path = if current_path.is_empty() { 631 + name_str.clone() 632 + } else { 633 + format!("{}/{}", current_path, name_str) 634 + }; 657 635 658 - // macOS system directories 659 - if name_str == ".Spotlight-V100" || name_str == ".Trashes" || name_str == ".fseventsd" { 660 - continue; 661 - } 662 - 663 - // Cache and temp directories 664 - if name_str == ".cache" || name_str == ".temp" || name_str == ".tmp" { 665 - continue; 666 - } 667 - 668 - // Python cache 669 - if name_str == "__pycache__" || name_str.ends_with(".pyc") { 670 - continue; 671 - } 672 - 673 - // Python virtual environments 674 - if name_str == ".venv" || name_str == "venv" || name_str == "env" { 675 - continue; 676 - } 677 - 678 - // Editor swap files 679 - if name_str.ends_with(".swp") || name_str.ends_with(".swo") || name_str.ends_with("~") { 636 + // Skip files/directories that match ignore patterns 637 + if ignore_matcher.is_ignored(&full_path) || ignore_matcher.is_filename_ignored(&name_str) { 680 638 continue; 681 639 } 682 640 ··· 732 690 } else { 733 691 format!("{}/{}", current_path, name) 734 692 }; 735 - let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path).await?; 693 + let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path, ignore_matcher).await?; 736 694 dir_entries.push(Entry::new() 737 695 .name(CowStr::from(name)) 738 696 .node(EntryNode::Directory(Box::new(subdir)))